From c109bd240585631fd9c5dbb866b8ed6fda751596 Mon Sep 17 00:00:00 2001
From: Alan Agius <17563226+alan-agius4@users.noreply.github.com>
Date: Tue, 1 Apr 2025 09:20:57 +0000
Subject: [PATCH] fix(@angular-devkit/schematics): properly resolve relative
 schematics when executed from a nested directory

Ensure that relative schematic paths are correctly resolved when invoked from within a nested directory.

Closes: #29978
---
 .../tools/node-module-engine-host.ts          | 10 +---
 .../schematics-collections-relative.ts        | 53 +++++++++++++++++++
 2 files changed, 55 insertions(+), 8 deletions(-)
 create mode 100644 tests/legacy-cli/e2e/tests/generate/schematics-collections-relative.ts

diff --git a/packages/angular_devkit/schematics/tools/node-module-engine-host.ts b/packages/angular_devkit/schematics/tools/node-module-engine-host.ts
index c24599cf345e..0bc269840c18 100644
--- a/packages/angular_devkit/schematics/tools/node-module-engine-host.ts
+++ b/packages/angular_devkit/schematics/tools/node-module-engine-host.ts
@@ -7,7 +7,7 @@
  */
 
 import { BaseException } from '@angular-devkit/core';
-import { dirname, join, resolve } from 'node:path';
+import { dirname, resolve } from 'node:path';
 import { RuleFactory } from '../src';
 import { FileSystemCollectionDesc, FileSystemSchematicDesc } from './description';
 import { ExportStringRef } from './export-ref';
@@ -46,20 +46,14 @@ export class NodeModulesEngineHost extends FileSystemEngineHostBase {
       }
     }
 
-    const relativeBase = requester ? dirname(requester) : process.cwd();
     let collectionPath: string | undefined = undefined;
-
-    if (name.startsWith('.')) {
-      name = resolve(relativeBase, name);
-    }
-
     const resolveOptions = {
       paths: requester ? [dirname(requester), ...(this.paths || [])] : this.paths,
     };
 
     // Try to resolve as a package
     try {
-      const packageJsonPath = require.resolve(join(name, 'package.json'), resolveOptions);
+      const packageJsonPath = require.resolve(`${name}/package.json`, resolveOptions);
       const { schematics } = require(packageJsonPath);
 
       if (!schematics || typeof schematics !== 'string') {
diff --git a/tests/legacy-cli/e2e/tests/generate/schematics-collections-relative.ts b/tests/legacy-cli/e2e/tests/generate/schematics-collections-relative.ts
new file mode 100644
index 000000000000..f6f583bf0e72
--- /dev/null
+++ b/tests/legacy-cli/e2e/tests/generate/schematics-collections-relative.ts
@@ -0,0 +1,53 @@
+import assert from 'node:assert';
+import { join } from 'node:path';
+import { ng } from '../../utils/process';
+import { writeMultipleFiles, createDir } from '../../utils/fs';
+import { updateJsonFile } from '../../utils/project';
+
+export default async function () {
+  // setup temp collection
+  await createDir('./fake-schematics');
+  await writeMultipleFiles({
+    './fake-schematics/package.json': JSON.stringify({
+      'schematics': './collection.json',
+    }),
+    './fake-schematics/collection.json': JSON.stringify({
+      'schematics': {
+        'fake': {
+          'description': 'Fake schematic',
+          'schema': './fake-schema.json',
+          'factory': './fake',
+        },
+      },
+    }),
+    './fake-schematics/fake-schema.json': JSON.stringify({
+      '$id': 'FakeSchema',
+      'title': 'Fake Schema',
+      'type': 'object',
+    }),
+    './fake-schematics/fake.js': `
+      exports.default = () => (host, context) => context.logger.info('fake schematic run.');
+    `,
+  });
+
+  await updateJsonFile('angular.json', (json) => {
+    json.cli ??= {};
+    json.cli.schematicCollections = ['./fake-schematics'];
+  });
+
+  const { stdout: stdout1 } = await ng('generate', '--help');
+  assert.match(stdout1, /Fake schematic/);
+
+  const { stdout: stdout2 } = await ng('generate', 'fake');
+  assert.match(stdout2, /fake schematic run/);
+
+  // change cwd to a nested directory to validate the relative schematic is resolved correctly
+  const originalCwd = process.cwd();
+  try {
+    process.chdir(join(originalCwd, 'src/app'));
+    const { stdout: stdout3 } = await ng('generate', 'fake');
+    assert.match(stdout3, /fake schematic run/);
+  } finally {
+    process.chdir(originalCwd);
+  }
+}