From 0f3fd9c6d5c570ecc822413901691e7185f0ba89 Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Thu, 20 Jul 2023 10:04:01 -0400 Subject: [PATCH] fix(core): migrate old invalid glob syntax in target outputs (#18191) (cherry picked from commit fef5b65b45db1e67009d31ed3f79e22988a1d818) --- .../shared/reference/project-configuration.md | 4 +- e2e/nx-run/src/cache.test.ts | 14 ++-- packages/nx/migrations.json | 6 ++ .../update-16-5-4/update-output-globs.spec.ts | 77 +++++++++++++++++++ .../update-16-5-4/update-output-globs.ts | 54 +++++++++++++ .../nx/src/native/cache/expand_outputs.rs | 16 ++++ packages/nx/src/tasks-runner/utils.ts | 5 +- 7 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 packages/nx/src/migrations/update-16-5-4/update-output-globs.spec.ts create mode 100644 packages/nx/src/migrations/update-16-5-4/update-output-globs.ts diff --git a/docs/shared/reference/project-configuration.md b/docs/shared/reference/project-configuration.md index a4a80a51931e3..ebe6833c3314e 100644 --- a/docs/shared/reference/project-configuration.md +++ b/docs/shared/reference/project-configuration.md @@ -313,13 +313,13 @@ Sometimes, multiple targets might write to the same directory. When possible it } ``` -But if the above is not possible, globs (parsed with the [minimatch](https://github.com/isaacs/minimatch) library) can be specified as outputs to only cache a set of files rather than the whole directory. +But if the above is not possible, globs (parsed by the [GlobSet](https://docs.rs/globset/0.4.5/globset/#syntax) Rust library) can be specified as outputs to only cache a set of files rather than the whole directory. ```json { "targets": { "build-js": { - "outputs": ["{workspaceRoot}/dist/libs/mylib/**/*.js"] + "outputs": ["{workspaceRoot}/dist/libs/mylib/**/*.{js,map}"] }, "build-css": { "outputs": ["{workspaceRoot}/dist/libs/mylib/**/*.css"] diff --git a/e2e/nx-run/src/cache.test.ts b/e2e/nx-run/src/cache.test.ts index d571676106d29..cbebdc873770d 100644 --- a/e2e/nx-run/src/cache.test.ts +++ b/e2e/nx-run/src/cache.test.ts @@ -157,7 +157,7 @@ describe('cache', () => { updateProjectConfig(mylib, (c) => { c.targets.build = { executor: 'nx:run-commands', - outputs: ['{workspaceRoot}/dist/*.txt'], + outputs: ['{workspaceRoot}/dist/*.{txt,md}'], options: { commands: [ 'rm -rf dist', @@ -167,7 +167,8 @@ describe('cache', () => { 'echo c > dist/c.txt', 'echo d > dist/d.txt', 'echo e > dist/e.txt', - 'echo f > dist/f.txt', + 'echo f > dist/f.md', + 'echo g > dist/g.html', ], parallel: false, }, @@ -188,7 +189,8 @@ describe('cache', () => { expect(outputsWithUntouchedOutputs).toContain('c.txt'); expect(outputsWithUntouchedOutputs).toContain('d.txt'); expect(outputsWithUntouchedOutputs).toContain('e.txt'); - expect(outputsWithUntouchedOutputs).toContain('f.txt'); + expect(outputsWithUntouchedOutputs).toContain('f.md'); + expect(outputsWithUntouchedOutputs).toContain('g.html'); // Create a file in the dist that does not match output glob updateFile('dist/c.ts', ''); @@ -202,7 +204,8 @@ describe('cache', () => { expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('c.txt'); expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('d.txt'); expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('e.txt'); - expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('f.txt'); + expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('f.md'); + expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('g.html'); expect(outputsAfterAddingUntouchedFileAndRerunning).toContain('c.ts'); // Clear Dist @@ -217,8 +220,9 @@ describe('cache', () => { expect(outputsWithoutOutputs).toContain('c.txt'); expect(outputsWithoutOutputs).toContain('d.txt'); expect(outputsWithoutOutputs).toContain('e.txt'); - expect(outputsWithoutOutputs).toContain('f.txt'); + expect(outputsWithoutOutputs).toContain('f.md'); expect(outputsWithoutOutputs).not.toContain('c.ts'); + expect(outputsWithoutOutputs).not.toContain('g.html'); }); it('should use consider filesets when hashing', async () => { diff --git a/packages/nx/migrations.json b/packages/nx/migrations.json index f287f739d45ba..6e74b4d901043 100644 --- a/packages/nx/migrations.json +++ b/packages/nx/migrations.json @@ -77,6 +77,12 @@ "version": "16.2.0-beta.0", "description": "Remove outputPath from run commands", "implementation": "./src/migrations/update-16-2-0/remove-run-commands-output-path" + }, + "16.5.4-update-output-globs": { + "cli": "nx", + "version": "16.5.4-beta.0", + "description": "Update outdated non-standard globs to unix standard", + "implementation": "./src/migrations/update-16-5-4/update-output-globs" } } } diff --git a/packages/nx/src/migrations/update-16-5-4/update-output-globs.spec.ts b/packages/nx/src/migrations/update-16-5-4/update-output-globs.spec.ts new file mode 100644 index 0000000000000..6798e3ad7bb8a --- /dev/null +++ b/packages/nx/src/migrations/update-16-5-4/update-output-globs.spec.ts @@ -0,0 +1,77 @@ +import { createTreeWithEmptyWorkspace } from '../../generators/testing-utils/create-tree-with-empty-workspace'; +import { TargetConfiguration } from '../../config/workspace-json-project-json'; +import { + addProjectConfiguration, + readProjectConfiguration, +} from '../../generators/utils/project-configuration'; +import updateOutputsGlobs from './update-output-globs'; +import { readJson, updateJson } from '../../generators/utils/json'; +import { NxJsonConfiguration } from '../../config/nx-json'; + +describe('update output globs', () => { + it('should update output globs', () => { + const tree = createTreeWithEmptyWorkspace(); + const targets: Record = { + build: { + outputs: ['{options.outputPath}', 'dist/apps/my-app/*.(js|map|ts)'], + }, + lint: {}, + test: { + outputs: ['dist/apps/my-app/main.(js|map|ts)'], + }, + run: { + outputs: ['dist/apps/my-app'], + }, + }; + addProjectConfiguration(tree, 'my-app', { + root: 'apps/my-app', + targets, + }); + + updateJson(tree, 'nx.json', (json) => { + json.targetDefaults = { + lint: { + outputs: ['dist/apps/my-app', '*.(js|map|ts)'], + }, + }; + return json; + }); + + updateOutputsGlobs(tree); + + const migratedTargets = readProjectConfiguration(tree, 'my-app').targets; + expect(migratedTargets).toMatchInlineSnapshot(` + { + "build": { + "outputs": [ + "{options.outputPath}", + "dist/apps/my-app/*.{js,map,ts}", + ], + }, + "lint": {}, + "run": { + "outputs": [ + "dist/apps/my-app", + ], + }, + "test": { + "outputs": [ + "dist/apps/my-app/main.{js,map,ts}", + ], + }, + } + `); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.targetDefaults).toMatchInlineSnapshot(` + { + "lint": { + "outputs": [ + "dist/apps/my-app", + "*.{js,map,ts}", + ], + }, + } + `); + }); +}); diff --git a/packages/nx/src/migrations/update-16-5-4/update-output-globs.ts b/packages/nx/src/migrations/update-16-5-4/update-output-globs.ts new file mode 100644 index 0000000000000..47803469a4c3f --- /dev/null +++ b/packages/nx/src/migrations/update-16-5-4/update-output-globs.ts @@ -0,0 +1,54 @@ +import { Tree } from '../../generators/tree'; +import { + getProjects, + updateProjectConfiguration, +} from '../../generators/utils/project-configuration'; +import { formatChangedFilesWithPrettierIfAvailable } from '../../generators/internal-utils/format-changed-files-with-prettier-if-available'; +import { TargetConfiguration } from '../../config/workspace-json-project-json'; +import { updateJson } from '../../generators/utils/json'; +import { NxJsonConfiguration } from '../../config/nx-json'; + +function replaceOutput(output: string) { + // replace {projectRoot}/folder/*.(js|map|ts) to {projectRoot}/folder/*.{js,map,ts} + const regex = /\(([^)]+)\)/g; + return output.replace(regex, (match, group1) => { + let replacements = group1.split('|').join(','); + return `{${replacements}}`; + }); +} + +export default async function updateOutputsGlobs(tree: Tree) { + for (const [projectName, projectConfiguration] of getProjects( + tree + ).entries()) { + for (const [targetName, targetConfiguration] of Object.entries( + projectConfiguration.targets ?? {} + )) { + if (!Array.isArray(targetConfiguration.outputs)) { + continue; + } + + targetConfiguration.outputs = + targetConfiguration.outputs.map(replaceOutput); + } + updateProjectConfiguration(tree, projectName, projectConfiguration); + } + + if (tree.exists('nx.json')) { + updateJson(tree, 'nx.json', (json) => { + for (const [, targetConfiguration] of Object.entries( + json.targetDefaults ?? {} + )) { + if (!Array.isArray(targetConfiguration.outputs)) { + continue; + } + + targetConfiguration.outputs = + targetConfiguration.outputs.map(replaceOutput); + } + return json; + }); + } + + await formatChangedFilesWithPrettierIfAvailable(tree); +} diff --git a/packages/nx/src/native/cache/expand_outputs.rs b/packages/nx/src/native/cache/expand_outputs.rs index f478d7a995a0e..b039b92ba12d2 100644 --- a/packages/nx/src/native/cache/expand_outputs.rs +++ b/packages/nx/src/native/cache/expand_outputs.rs @@ -59,6 +59,10 @@ mod test { .child("nx.darwin-arm64.node") .touch() .unwrap(); + temp.child("multi").child("file.js").touch().unwrap(); + temp.child("multi").child("src.ts").touch().unwrap(); + temp.child("multi").child("file.map").touch().unwrap(); + temp.child("multi").child("file.txt").touch().unwrap(); temp } #[test] @@ -80,4 +84,16 @@ mod test { ] ); } + + #[test] + fn should_handle_multiple_extensions() { + let temp = setup_fs(); + let entries = vec!["multi/*.{js,map,ts}".to_string()]; + let mut result = expand_outputs(temp.display().to_string(), entries).unwrap(); + result.sort(); + assert_eq!( + result, + vec!["multi/file.js", "multi/file.map", "multi/src.ts"] + ); + } } diff --git a/packages/nx/src/tasks-runner/utils.ts b/packages/nx/src/tasks-runner/utils.ts index c083ae9dc8153..7bcfd0263e6c5 100644 --- a/packages/nx/src/tasks-runner/utils.ts +++ b/packages/nx/src/tasks-runner/utils.ts @@ -181,7 +181,10 @@ export function getOutputsForTargetAndConfiguration( options, }); }) - .filter((output) => !!output && !output.match(/{.*}/)); + .filter( + (output) => + !!output && !output.match(/{(projectRoot|workspaceRoot|(options.*))}/) + ); } // Keep backwards compatibility in case `outputs` doesn't exist