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)