From fd31fa633da33415a542e03889f77e99eb23452f Mon Sep 17 00:00:00 2001 From: Jonathan Gelin Date: Mon, 23 Jun 2025 08:42:59 +0200 Subject: [PATCH] fix(js): resolve asset paths relative to workspace root instead of cwd (#31664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR fixes an issue where asset files copied during a build using the `@nx/js:tsc` executor are placed in the wrong directory depending on the current working directory from which the `nx` command is executed. This behavior becomes particularly problematic in scenarios like release workflows that rely on `preVersionCommand` to run E2E tests. For instance, when using tools like Jest from the root of an E2E project, scripts like `start-local-registry` may trigger a build and run the `preVersionCommand`. However, instead of placing assets in the expected `dist` folder of the project, they are incorrectly copied relative to the E2E folder’s location. ## Reproduction Steps 1. Create a new Nx workspace: ```bash npx --yes create-nx-workspace assets-issue --preset=ts --no-interactive cd assets-issue ``` 2. Add the Nx Plugin package: ```bash nx add @nx/plugin ``` 3. Generate a new plugin: ```bash nx g @nx/plugin:plugin packages/my-plugin --linter eslint --unitTestRunner jest ``` 4. Add a generator to the plugin: ```bash nx g @nx/plugin:generator packages/my-plugin/src/generators/my-generator ``` 5. Build the plugin from the workspace root: ```bash nx build my-plugin ``` ✅ Assets are copied correctly: ``` dist/packages/my-plugin/generators/files/src/index.ts.template dist/packages/my-plugin/generators/schema.json dist/packages/my-plugin/generators/schema.d.ts ``` 6. Now build the same project from a nested folder: ```bash mkdir e2e && cd e2e nx build my-plugin --skip-nx-cache ``` ❌ Assets are copied relative to the current folder: ``` e2e/packages/my-plugin/dist/generators/files/src/index.ts.template e2e/packages/my-plugin/dist/generators/schema.json e2e/packages/my-plugin/dist/generators/schema.d.ts ``` ## Expected Behavior The build output—especially copied assets—should always respect the project’s `outputPath` configuration regardless of where the `nx` command is invoked from. The behavior should be consistent and **not influenced by `process.cwd()`**. --- .../utils/assets/copy-assets-handler.spec.ts | 64 +++++++++++++++++++ .../src/utils/assets/copy-assets-handler.ts | 9 ++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/js/src/utils/assets/copy-assets-handler.spec.ts b/packages/js/src/utils/assets/copy-assets-handler.spec.ts index a65e5a6481..6fa736d797 100644 --- a/packages/js/src/utils/assets/copy-assets-handler.spec.ts +++ b/packages/js/src/utils/assets/copy-assets-handler.spec.ts @@ -66,8 +66,12 @@ describe('AssetInputOutputHandler', () => { let projectDir: string; let outputDir: string; let callback: jest.SpyInstance; + let originalCwd: string; beforeEach(() => { + // Store original cwd to restore later + originalCwd = process.cwd(); + // Resolve to real paths to avoid symlink discrepancies with watcher. const tmp = fs.realpathSync(path.join(os.tmpdir())); @@ -102,6 +106,11 @@ describe('AssetInputOutputHandler', () => { }); }); + afterEach(() => { + // Restore original cwd + process.chdir(originalCwd); + }); + test('watchAndProcessOnAssetChange', async () => { const dispose = await sut.watchAndProcessOnAssetChange(); @@ -219,6 +228,61 @@ describe('AssetInputOutputHandler', () => { }, ]); }); + + test('should copy assets to correct location when running from nested directory', async () => { + // Create a nested directory structure to simulate running from a subdirectory + const nestedDir = path.join(rootDir, 'e2e', 'integration-tests'); + fs.mkdirSync(nestedDir, { recursive: true }); + + // Change to nested directory to simulate running nx command from there + process.chdir(nestedDir); + + // Create test files + fs.writeFileSync(path.join(rootDir, 'LICENSE'), 'license'); + fs.writeFileSync(path.join(projectDir, 'README.md'), 'readme'); + fs.writeFileSync(path.join(projectDir, 'docs/test1.md'), 'test'); + + // Create CopyAssetsHandler with relative outputDir (this is where the bug manifests) + const nestedSut = new CopyAssetsHandler({ + rootDir, + projectDir, + outputDir: 'dist/mylib', // relative path - this triggers the bug + callback: callback as any, + assets: [ + 'mylib/*.md', + { + input: 'mylib/docs', + glob: '**/*.md', + output: 'docs', + }, + 'LICENSE', + ], + }); + + await nestedSut.processAllAssetsOnce(); + + expect(callback).toHaveBeenCalledWith([ + { + type: 'create', + src: path.join(rootDir, 'LICENSE'), + dest: path.join(rootDir, 'dist/mylib/LICENSE'), + }, + ]); + expect(callback).toHaveBeenCalledWith([ + { + type: 'create', + src: path.join(rootDir, 'mylib/README.md'), + dest: path.join(rootDir, 'dist/mylib/README.md'), + }, + ]); + expect(callback).toHaveBeenCalledWith([ + { + type: 'create', + src: path.join(rootDir, 'mylib/docs/test1.md'), + dest: path.join(rootDir, 'dist/mylib/docs/test1.md'), + }, + ]); + }); }); function wait(ms: number) { diff --git a/packages/js/src/utils/assets/copy-assets-handler.ts b/packages/js/src/utils/assets/copy-assets-handler.ts index 6f874f056a..c104f45b9f 100644 --- a/packages/js/src/utils/assets/copy-assets-handler.ts +++ b/packages/js/src/utils/assets/copy-assets-handler.ts @@ -89,16 +89,21 @@ export class CopyAssetsHandler { let input: string; let output: string; let ignore: string[] | null = null; + + const resolvedOutputDir = path.isAbsolute(opts.outputDir) + ? opts.outputDir + : path.resolve(opts.rootDir, opts.outputDir); + if (typeof f === 'string') { pattern = f; input = path.relative(opts.rootDir, opts.projectDir); - output = path.relative(opts.rootDir, opts.outputDir); + output = path.relative(opts.rootDir, resolvedOutputDir); } else { isGlob = true; pattern = pathPosix.join(f.input, f.glob); input = f.input; output = pathPosix.join( - path.relative(opts.rootDir, opts.outputDir), + path.relative(opts.rootDir, resolvedOutputDir), f.output ); if (f.ignore)