Compare commits

...

49 Commits

Author SHA1 Message Date
FrozenPandaz 0c04ed41e7 chore(misc): publish 16.5.5 2023-07-22 00:17:55 -04:00
FrozenPandaz 4105745f95 fix(core): revert globs migration 2023-07-21 17:19:50 -04:00
FrozenPandaz 9b2b8f311f chore(misc): publish 16.5.4 2023-07-21 10:40:05 -04:00
Jonathan Cammisuli 1bb96b7f44 fix(core): check if socketMessenger is null before using (#18116)
(cherry picked from commit eecbde52a5)
2023-07-20 18:08:26 -04:00
MaxKless 93a0200dd8 fix(graph): increase dropdown indicator padding (#18146)
(cherry picked from commit 835972277c)
2023-07-20 18:08:22 -04:00
Denis Frenademetz d120c3cd61 fix(core): do not cache external dependency hashes if they are not fully resolved (#18020)
(cherry picked from commit c3d4bfe9f0)
2023-07-20 18:08:17 -04:00
Katerina Skroumpelou 2201ecace6 fix(core): improve error handling for getting metadata during file wa… (#18223)
(cherry picked from commit 4ef66f4afd)
2023-07-20 18:08:04 -04:00
Katerina Skroumpelou 9382ed4826 fix(storybook): make sure skipLibCheck is true (#18212)
(cherry picked from commit d942e7b5fb)
2023-07-20 18:08:01 -04:00
Jonathan Cammisuli 0f3fd9c6d5 fix(core): migrate old invalid glob syntax in target outputs (#18191)
(cherry picked from commit fef5b65b45)
2023-07-20 18:07:51 -04:00
Jonathan Cammisuli 6b6038c77d fix(core): correctly output created and update events for the watcher on macos (#18186)
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
(cherry picked from commit fb0f1f0ec8)
2023-07-20 18:07:39 -04:00
FrozenPandaz bd02995bec fix(core): fix watch event priority 2023-07-20 18:06:53 -04:00
Miroslav Jonaš 81a661cf7f fix(core): use package.json version in yarn parser when node_modules are unavailable (#18121)
(cherry picked from commit d9a95b64de)
2023-07-20 18:00:27 -04:00
Emily Xiong 445a527043 fix(react-native): fix tsc app (#18124)
(cherry picked from commit 48d4927068)
2023-07-20 18:00:21 -04:00
Miroslav Jonaš d4f6c0ba81 fix(core): run ensurePackage with node_modules for yarn berry (#18144)
(cherry picked from commit 2c1d4bac96)
2023-07-20 18:00:14 -04:00
Miroslav Jonas 95d99e42fc fix(nx-dev): fix broken redirects after nesting tutorials
(cherry picked from commit 7280cb0f72)
2023-07-20 18:00:01 -04:00
FrozenPandaz e886dc76e2 chore(misc): publish 16.5.3 2023-07-18 09:00:01 -04:00
Jason Jean e7375813ba fix(core): fix project configuration typings (#18073)
(cherry picked from commit bcb3251ef5)
2023-07-17 17:19:28 -04:00
Jason Jean 2fcf807bb6 fix(core): parse project configs only in js (#18009)
(cherry picked from commit c04053b4e9)
2023-07-17 17:19:24 -04:00
Victor Savkin 9612d9b394 fix(misc): stat collector filters out version 16 (#18149)
(cherry picked from commit 57b2a76007)
2023-07-17 16:40:55 -04:00
Jack Hsu 6a839434d0 fix(js): run build in watch mode when specified for faster recompile time (#18145)
(cherry picked from commit e2c0abc1a7)
2023-07-17 16:40:52 -04:00
Miroslav Jonaš b20b8550e2 fix(core): ensure proper yarn version detection even for nested projects (#18052)
(cherry picked from commit 10fc4dd4d8)
2023-07-17 16:40:48 -04:00
Jonathan Cammisuli 371ba7a21c fix(core): write file directly instead of using fs-extra.outputFileSync (#18129)
(cherry picked from commit 187842e14c)
2023-07-17 16:40:45 -04:00
Craigory Coppola bd9e749ffc fix(misc): update invalid main field in @nrwl/eslint-plugin-nx package.json (#17974)
(cherry picked from commit 2fa248d4f5)
2023-07-17 16:40:42 -04:00
FrozenPandaz 928273940d chore(misc): publish 16.5.2 2023-07-13 14:43:09 -04:00
Katerina Skroumpelou 12aa582135 fix(storybook): change storybook tsconfig path in eslint file too (#18101)
(cherry picked from commit e545db7da9)
2023-07-13 09:56:43 -04:00
Leosvel Pérez Espinosa 2093cccb61 fix(misc): prompt for workspace name when creating it using nx init (#18079)
(cherry picked from commit 6ce7fc57ed)
2023-07-13 09:56:36 -04:00
Jack Hsu d36f0552b1 fix(linter): skip verifying deps of deps by default in package.json (#18058)
(cherry picked from commit ae773d547e)
2023-07-13 09:56:29 -04:00
C. T. Lin eba235b7c5 fix(core): use concat in buildAllWorkspaceFiles to avoid Maximum call stack size exceeded (#18065)
(cherry picked from commit c364207f3e)
2023-07-13 09:56:17 -04:00
Jonathan Cammisuli f8a4eaaafc fix(core): remove throttle from watch (#18077)
(cherry picked from commit 460e469617)
2023-07-13 09:56:10 -04:00
FrozenPandaz 7b7f1e7f72 chore(misc): publish 16.5.1 2023-07-10 13:55:11 -04:00
Chi Ma f525ba37ee fix(core): add missing perf_hooks import in tasks-runner/cache (#18043)
(cherry picked from commit e25582780a)
2023-07-10 11:12:22 -04:00
Leosvel Pérez Espinosa 5e8f9f4b1e fix(core): handle undefined target defaults (#18044)
(cherry picked from commit 92926b9346)
2023-07-10 11:12:10 -04:00
Leosvel Pérez Espinosa 13aa3be1f0 fix(angular): explicitly depend on find-cache-dir to avoid npm resolution conflict (#18049)
(cherry picked from commit a08cb91891)
2023-07-10 11:12:06 -04:00
Leosvel Pérez Espinosa f9a615bb04 fix(js): post-process skipped tasks by the ts compiler in tsc batch implementation (#17938)
(cherry picked from commit d10aeb49ac)
2023-07-10 11:12:00 -04:00
Katerina Skroumpelou 1a0e65a3d3 fix(storybook): dont throw if no project-level tsconfig (#18027)
(cherry picked from commit 878955b50d)
2023-07-10 11:11:47 -04:00
Nicholas Cunningham 25263f72d8 fix(nextjs): Update @types/react version for yarn resolution (#18028)
(cherry picked from commit b1f19e3e91)
2023-07-10 11:11:34 -04:00
Emily Xiong 9e448fe341 fix(js): restore registry for only localhost (#17999)
(cherry picked from commit 8d5cfcc2b3)
2023-07-07 16:02:12 -04:00
Miroslav Jonaš 802243e410 fix(core): ensure yarn runs install with correct version (#17997)
(cherry picked from commit d0c37727c5)
2023-07-07 16:02:04 -04:00
Katerina Skroumpelou 0c480fd94d fix(storybook): properly identify file as story (#18016)
(cherry picked from commit 21007d8922)
2023-07-07 16:02:00 -04:00
Katerina Skroumpelou b1a8917b8d fix(storybook): optional chain .length in utils (#18001)
(cherry picked from commit 3edbe49fa2)
2023-07-07 16:01:54 -04:00
Jack Hsu 0e15beafb2 feat(vite): pin Vite version range to patch releases only to avoid conflicts with Angular (#18010)
(cherry picked from commit 3fdc66dd69)
2023-07-07 16:01:46 -04:00
Miroslav Jonaš 66090ca3c9 chore(repo): bump versions of ci images (#17969)
(cherry picked from commit 601c65241b)
2023-07-07 16:01:33 -04:00
Nathan Walker 97ce47aa69 fix(js): remove duplicate alias (#17978)
(cherry picked from commit ccd195d60a)
2023-07-07 16:01:24 -04:00
Jack Hsu b39cafaf86 fix(js): add watchIgnore and runBuildTargetDependencies options to speed up build (#17953)
(cherry picked from commit c6a0615fa5)
2023-07-07 16:01:12 -04:00
Colum Ferry ea28f16faf fix(bundling): update the browsers for es5 terser check (#17784)
(cherry picked from commit 8c9ad0da69)
2023-07-07 16:01:05 -04:00
Colum Ferry 0d04949ffd fix(angular): automatically skip remotes not in the current workspace #17473 (#17497)
(cherry picked from commit c64f26ef3c)
2023-07-07 16:01:00 -04:00
Miroslav Jonaš ed22afecef fix(js): ignore dependencies from non-index root files for create-package-json (#17930)
(cherry picked from commit 28a071f243)
2023-07-07 16:00:55 -04:00
Vergil Penkov a496ef9144 fix(core): add license information for native packages (#17964)
(cherry picked from commit d42d2f46fb)
2023-07-07 16:00:52 -04:00
Denis Frenademetz 699af690b7 fix(core): ensure external dependency hashes are resolved in a deterministic way (#17926)
(cherry picked from commit 65adb94bf6)
2023-07-07 16:00:50 -04:00
121 changed files with 2865 additions and 1010 deletions
+18 -10
View File
@@ -5,7 +5,6 @@ version: 2.1
# -------------------------
orbs:
nx: nrwl/nx@1.6.1
node: circleci/node@5.1.0
rust: circleci/rust@1.6.0
browser-tools: circleci/browser-tools@1.4.0
@@ -20,14 +19,14 @@ executors:
linux:
<<: *defaults
docker:
- image: cimg/rust:1.66.1-browsers
- image: cimg/rust:1.70.0-browsers
resource_class: medium+
macos:
<<: *defaults
resource_class: macos.x86.medium.gen2
macos:
xcode: &_XCODE_VERSION '13.0.0'
xcode: '14.2.0'
# -------------------------
# COMMANDS
@@ -42,10 +41,22 @@ commands:
name: Restore pnpm Package Cache
keys:
- node-deps-{{ arch }}-v3-{{ checksum "pnpm-lock.yaml" }}
- run:
name: Install pnpm package manager
command: |
npm install -g @pnpm/exe@8.3.1
- when:
condition:
equal: [<< parameters.os >>, linux]
steps:
- run:
name: Install pnpm package manager (linux)
command: |
npm install --prefix=$HOME/.local -g @pnpm/exe@8.3.1
- when:
condition:
equal: [<< parameters.os >>, macos]
steps:
- run:
name: Install pnpm package manager (macos)
command: |
npm install -g @pnpm/exe@8.3.1
- run:
name: Install Dependencies
command: |
@@ -95,9 +106,6 @@ commands:
sudo apt-get install -y ca-certificates lsof
- browser-tools/install-chrome
- browser-tools/install-chromedriver
- node/install:
# Use LTS version
node-version: ''
- run-pnpm-install:
os: << parameters.os >>
Generated
-43
View File
@@ -844,15 +844,6 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "jsonc-parser"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b56a20e76235284255a09fcd1f45cf55d3c524ea657ebd3854735925c57743d"
dependencies = [
"serde_json",
]
[[package]]
name = "kqueue"
version = "1.0.7"
@@ -1171,13 +1162,10 @@ dependencies = [
"ignore",
"ignore-files",
"itertools",
"jsonc-parser",
"napi",
"napi-build",
"napi-derive",
"rayon",
"serde",
"serde_json",
"thiserror",
"tokio",
"tracing",
@@ -1447,12 +1435,6 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "ryu"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "same-file"
version = "1.0.6"
@@ -1479,31 +1461,6 @@ name = "serde"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.107",
]
[[package]]
name = "serde_json"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha1_smol"
+2 -1
View File
@@ -1397,7 +1397,7 @@ execSync(`${getPackageManagerCommand().addDev} my-dev-package`);
### getPackageManagerVersion
**getPackageManagerVersion**(`packageManager?`): `string`
**getPackageManagerVersion**(`packageManager?`, `cwd?`): `string`
Returns the version of the package manager used in the workspace.
By default, the package manager is derived based on the lock file,
@@ -1408,6 +1408,7 @@ but it can also be passed in explicitly.
| Name | Type |
| :--------------- | :------------------------------------------------------------------ |
| `packageManager` | [`PackageManager`](../../devkit/documents/nx_devkit#packagemanager) |
| `cwd` | `string` |
#### Returns
@@ -107,7 +107,7 @@
"skipRemotes": {
"type": "array",
"items": { "type": "string" },
"description": "List of remote applications to not automatically serve, either statically or in development mode. This can be useful for multi-repository module federation setups where the host application uses a remote application from an external repository."
"description": "List of remote applications to not automatically serve, either statically or in development mode."
},
"pathToManifestFile": {
"type": "string",
@@ -72,7 +72,7 @@
"skipRemotes": {
"type": "array",
"items": { "type": "string" },
"description": "List of remote applications to not automatically serve, either statically or in development mode. This can be useful for multi-repository module federation setups where the host application uses a remote application from an external repository."
"description": "List of remote applications to not automatically serve, either statically or in development mode."
},
"verbose": {
"type": "boolean",
@@ -1397,7 +1397,7 @@ execSync(`${getPackageManagerCommand().addDev} my-dev-package`);
### getPackageManagerVersion
**getPackageManagerVersion**(`packageManager?`): `string`
**getPackageManagerVersion**(`packageManager?`, `cwd?`): `string`
Returns the version of the package manager used in the workspace.
By default, the package manager is derived based on the lock file,
@@ -1408,6 +1408,7 @@ but it can also be passed in explicitly.
| Name | Type |
| :--------------- | :------------------------------------------------------------------ |
| `packageManager` | [`PackageManager`](../../devkit/documents/nx_devkit#packagemanager) |
| `cwd` | `string` |
#### Returns
+20 -7
View File
@@ -28,12 +28,14 @@
"host": {
"type": "string",
"default": "localhost",
"description": "The host to inspect the process on."
"description": "The host to inspect the process on.",
"x-priority": "important"
},
"port": {
"type": "number",
"default": 9229,
"description": "The port to inspect the process on. Setting port to 0 will assign random free ports to all forked processes."
"description": "The port to inspect the process on. Setting port to 0 will assign random free ports to all forked processes.",
"x-priority": "important"
},
"inspect": {
"oneOf": [
@@ -41,33 +43,44 @@
{ "type": "boolean" }
],
"description": "Ensures the app is starting with debugging.",
"default": "inspect"
"default": "inspect",
"x-priority": "important"
},
"runtimeArgs": {
"type": "array",
"description": "Extra args passed to the node process.",
"default": [],
"items": { "type": "string" }
"items": { "type": "string" },
"x-priority": "important"
},
"args": {
"type": "array",
"description": "Extra args when starting the app.",
"default": [],
"items": { "type": "string" }
"items": { "type": "string" },
"x-priority": "important"
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": true
"default": true,
"x-priority": "important"
},
"debounce": {
"type": "number",
"description": "Delay in milliseconds to wait before restarting. Useful to batch multiple file changes events together. Set to zero (0) to disable.",
"default": 500
"default": 500,
"x-priority": "important"
},
"runBuildTargetDependencies": {
"type": "boolean",
"description": "Whether to run dependencies before running the build. Set this to true if the project does not build libraries from source (e.g. 'buildLibsFromSource: false').",
"default": false
}
},
"additionalProperties": false,
"required": ["buildTarget"],
"examplesFile": "---\ntitle: JS Node executor examples\ndescription: This page contains examples for the @nx/js:node executor.\n---\n\nThe `@nx/js:node` executor runs the output of a build target. For example, an application uses esbuild ([`@nx/esbuild:esbuild`](/packages/esbuild/executors/esbuild)) to output the bundle to `dist/my-app` folder, which can then be executed by `@nx/js:node`.\n\n`project.json`:\n\n```json\n\"my-app\": {\n \"targets\": {\n \"serve\": {\n \"executor\": \"@nx/js:node\",\n \"options\": {\n \"buildTarget\": \"my-app:build\"\n }\n },\n \"build\": {\n \"executor\": \"@nx/esbuild:esbuild\",\n \"options\": {\n \"main\": \"my-app/src/main.ts\",\n \"output\": [\"dist/my-app\"],\n //...\n }\n },\n }\n}\n```\n\n```bash\nnpx nx serve my-app\n```\n\n## Examples\n\n{% tabs %}\n{% tab label=\"Pass extra Node CLI arguments\" %}\n\nUsing `runtimeArgs`, you can pass arguments to the underlying `node` command. For example, if you want to set [`--no-warnings`](https://nodejs.org/api/cli.html#--no-warnings) to silence all Node warnings, then add the following to the `project.json` file.\n\n```json\n\"my-app\": {\n \"targets\": {\n \"serve\": {\n \"executor\": \"@nx/js:node\",\n \"options\": {\n \"runtimeArgs\": [\"--no-warnings\"],\n //...\n },\n },\n }\n}\n```\n\n{% /tab %}\n\n{% tab label=\"Run all task dependencies\" %}\n\nIf your application build depends on other tasks, and you want those tasks to also be executed, then set the `runBuildTargetDependencies` to `true`. For example, a library may have a task to generate GraphQL schemas, which is consume by the application. In this case, you want to run the generate task before building and running the application.\n\nThis option is also useful when the build consumes a library from its output, not its source. For example, if an executor that supports `buildLibsFromSource` option has it set to `false` (e.g. [`@nx/webpack:webpack`](/packages/webpack/executors/webpack)).\n\nNote that this option will increase the build time, so use it only when necessary.\n\n```json\n\"my-app\": {\n \"targets\": {\n \"serve\": {\n \"executor\": \"@nx/js:node\",\n \"options\": {\n \"runBuildTargetDependencies\": true,\n //...\n },\n },\n }\n}\n```\n\n{% /tab %}\n\n{% /tabs %}\n",
"presets": []
},
"description": "Execute a Node application.",
@@ -18,7 +18,7 @@
"skipRemotes": {
"type": "array",
"items": { "type": "string" },
"description": "List of remote applications to not automatically serve, either statically or in development mode. This can be useful for multi-repository module federation setups where the host application uses a remote application from an external repository.",
"description": "List of remote applications to not automatically serve, either statically or in development mode.",
"x-priority": "important"
},
"buildTarget": {
@@ -34,7 +34,7 @@
"skipRemotes": {
"type": "array",
"items": { "type": "string" },
"description": "List of remote applications to not automatically serve, either statically or in development mode. This can be useful for multi-repository module federation setups where the host application uses a remote application from an external repository.",
"description": "List of remote applications to not automatically serve, either statically or in development mode.",
"x-priority": "important"
},
"host": {
@@ -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"]
+17
View File
@@ -2,6 +2,7 @@ import {
checkFilesExist,
cleanupProject,
expectTestsPass,
getPackageManagerCommand,
killPorts,
newProject,
promisifiedTreeKill,
@@ -9,6 +10,7 @@ import {
readResolvedConfiguration,
runCLI,
runCLIAsync,
runCommand,
runCommandUntil,
uniq,
updateFile,
@@ -129,4 +131,19 @@ describe('expo', () => {
checkFilesExist(`dist/libs/${libName}/src/index.d.ts`);
}).not.toThrow();
});
it('should tsc app', async () => {
expect(() => {
const pmc = getPackageManagerCommand();
runCommand(
`${pmc.runUninstalledPackage} tsc -p apps/${appName}/tsconfig.app.json`
);
checkFilesExist(
`dist/out-tsc/apps/${appName}/src/app/App.js`,
`dist/out-tsc/apps/${appName}/src/app/App.d.ts`,
`dist/out-tsc/libs/${libName}/src/index.js`,
`dist/out-tsc/libs/${libName}/src/index.d.ts`
);
}).not.toThrow();
});
});
+9
View File
@@ -189,6 +189,15 @@ describe('js e2e', () => {
`dist/libs/${lib}/src/lib/${lib}.js`,
`dist/libs/${lib}/src/lib/${lib}.d.ts`
);
// run a second time skipping the nx cache and with the outputs present
const secondBatchBuildOutput = runCLI(
`build ${parentLib} --skip-nx-cache`,
{ env: { NX_BATCH_MODE: 'true' } }
);
expect(secondBatchBuildOutput).toContain(
`Successfully ran target build for project ${parentLib} and 1 task it depends on`
);
}, 240_000);
it('should not create a `.babelrc` file when creating libs with js executors (--compiler=tsc)', () => {
+7 -5
View File
@@ -452,6 +452,12 @@ describe('Linter', () => {
];
return json;
});
// Set this to false for now until the `@nx/js:lib` generator is updated to include ts/swc helpers by default.
// TODO(jack): Remove this once the above is addressed in another PR.
updateJson(`libs/${mylib}/tsconfig.lib.json`, (json) => {
json.compilerOptions.importHelpers = false;
return json;
});
updateJson(`libs/${mylib}/project.json`, (json) => {
json.targets.lint.options.lintFilePatterns = [
`libs/${mylib}/**/*.ts`,
@@ -465,8 +471,7 @@ describe('Linter', () => {
it('should report dependency check issues', () => {
const rootPackageJson = readJson('package.json');
const nxVersion = rootPackageJson.devDependencies.nx;
const swcCoreVersion = rootPackageJson.devDependencies['@swc/core'];
const swcHelpersVersion = rootPackageJson.dependencies['@swc/helpers'];
const tslibVersion = rootPackageJson.devDependencies['tslib'];
let out = runCLI(`lint ${mylib}`, { silenceError: true });
expect(out).toContain('All files pass linting');
@@ -495,9 +500,6 @@ describe('Linter', () => {
{
"dependencies": {
"@nx/devkit": "${nxVersion}",
"@swc/core": "${swcCoreVersion}",
"@swc/helpers": "${swcHelpersVersion}",
"nx": "${nxVersion}",
},
"name": "@proj/${mylib}",
"type": "commonjs",
+52
View File
@@ -5,9 +5,12 @@ import {
readFile,
runCLI,
runCLIAsync,
runCommandUntil,
waitUntil,
tmpProjPath,
uniq,
updateFile,
updateProjectConfig,
} from '@nx/e2e/utils';
import { execSync } from 'child_process';
@@ -47,5 +50,54 @@ describe('Node Applications + webpack', () => {
await runCLIAsync(`build ${app} --optimization`);
const optimizedContent = readFile(`dist/apps/${app}/main.js`);
expect(optimizedContent).toContain('console.log("foo "+"bar")');
// Test that serve can re-run dependency builds.
const lib = uniq('nodelib');
runCLI(`generate @nx/js:lib ${lib} --bundler=esbuild --no-interactive`);
updateProjectConfig(app, (config) => {
// Since we read from lib from dist, we should re-build it when lib changes.
config.targets.build.options.buildLibsFromSource = false;
config.targets.serve.options.runBuildTargetDependencies = true;
return config;
});
updateFile(
`apps/${app}/src/main.ts`,
`
import { ${lib} } from '@proj/${lib}';
console.log('Hello ' + ${lib}());
`
);
const serveProcess = await runCommandUntil(
`serve ${app} --watch --runBuildTargetDependencies`,
(output) => {
return output.includes(`Hello`);
}
);
// Update library source and check that it triggers rebuild.
const terminalOutputs: string[] = [];
serveProcess.stdout.on('data', (chunk) => {
const data = chunk.toString();
terminalOutputs.push(data);
});
updateFile(
`libs/${lib}/src/index.ts`,
`export function ${lib}() { return 'should rebuild lib'; }`
);
await waitUntil(
() => {
return terminalOutputs.some((output) =>
output.includes(`should rebuild lib`)
);
},
{ timeout: 30_000, ms: 200 }
);
serveProcess.kill();
}, 300_000);
});
+9 -5
View File
@@ -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 () => {
+17
View File
@@ -194,4 +194,21 @@ describe('react native', () => {
},
});
});
it('should tsc app', async () => {
expect(() => {
const pmc = getPackageManagerCommand();
runCommand(
`${pmc.runUninstalledPackage} tsc -p apps/${appName}/tsconfig.app.json`
);
checkFilesExist(
`dist/out-tsc/apps/${appName}/src/main.js`,
`dist/out-tsc/apps/${appName}/src/main.d.ts`,
`dist/out-tsc/apps/${appName}/src/app/App.js`,
`dist/out-tsc/apps/${appName}/src/app/App.d.ts`,
`dist/out-tsc/libs/${libName}/src/index.js`,
`dist/out-tsc/libs/${libName}/src/index.d.ts`
);
}).not.toThrow();
});
});
@@ -9,10 +9,12 @@ import {
getSelectedPackageManager,
packageManagerLockFile,
readJson,
runCommand,
runCreateWorkspace,
uniq,
} from '@nx/e2e/utils';
import { existsSync, mkdirSync } from 'fs-extra';
import { readFileSync } from 'fs';
import { existsSync, mkdirSync, rmSync } from 'fs-extra';
describe('create-nx-workspace', () => {
const packageManager = getSelectedPackageManager() || 'pnpm';
@@ -414,7 +416,7 @@ describe('create-nx-workspace', () => {
});
});
describe('create-nx-workspace custom parent folder', () => {
describe('create-nx-workspace parent folder', () => {
const tmpDir = `${e2eCwd}/${uniq('with space')}`;
const wsName = uniq('parent');
const packageManager = getSelectedPackageManager() || 'pnpm';
@@ -433,3 +435,59 @@ describe('create-nx-workspace custom parent folder', () => {
expect(existsSync(`${tmpDir}/${wsName}/package.json`)).toBeTruthy();
});
});
describe('create-nx-workspace yarn berry', () => {
const tmpDir = `${e2eCwd}/${uniq('yarn-berry')}`;
let wsName: string;
beforeAll(() => {
mkdirSync(tmpDir, { recursive: true });
runCommand('corepack prepare yarn@stable --activate', { cwd: tmpDir });
runCommand('yarn set version stable', { cwd: tmpDir });
// previous command creates a package.json file which we don't want
rmSync(`${tmpDir}/package.json`);
process.env.YARN_ENABLE_IMMUTABLE_INSTALLS = 'false';
});
afterEach(() => cleanupProject({ cwd: `${tmpDir}/${wsName}` }));
it('should create a workspace with yarn berry', () => {
wsName = uniq('apps');
runCreateWorkspace(wsName, {
preset: 'apps',
packageManager: 'yarn',
cwd: tmpDir,
});
expect(existsSync(`${tmpDir}/${wsName}/.yarnrc.yml`)).toBeTruthy();
expect(
readFileSync(`${tmpDir}/${wsName}/.yarnrc.yml`, { encoding: 'utf-8' })
).toMatchInlineSnapshot(`
"nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.6.1.cjs
"
`);
});
it('should create a js workspace with yarn berry', () => {
wsName = uniq('ts');
runCreateWorkspace(wsName, {
preset: 'ts',
packageManager: 'yarn',
cwd: tmpDir,
});
expect(existsSync(`${tmpDir}/${wsName}/.yarnrc.yml`)).toBeTruthy();
expect(
readFileSync(`${tmpDir}/${wsName}/.yarnrc.yml`, { encoding: 'utf-8' })
).toMatchInlineSnapshot(`
"nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.6.1.cjs
"
`);
});
});
+6 -1
View File
@@ -36,5 +36,10 @@ module.exports = {
translate: ['group-hover'],
},
},
plugins: [require('@tailwindcss/typography')],
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms')({
strategy: 'class',
}),
],
};
+1 -1
View File
@@ -9,7 +9,7 @@ export function Dropdown(props: DropdownProps) {
const { className, children, ...rest } = props;
return (
<select
className={`flex items-center rounded-md rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700 ${className}`}
className={`form-select flex items-center rounded-md rounded-md border border-slate-300 bg-white pl-4 pr-8 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700 ${className}`}
{...rest}
>
{children}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"packages": ["build/packages/*", "build/packages/nx/native-packages/*"],
"version": "16.5.0",
"version": "16.5.5",
"granularPathspec": false,
"command": {
"publish": {
+8
View File
@@ -190,6 +190,14 @@ module.exports = withNx({
permanent: true,
});
}
// Nesting 5min tutorials in Getting Started
for (let s of Object.keys(redirectRules.nested5minuteTutorialUrls)) {
rules.push({
source: s,
destination: redirectRules.nested5minuteTutorialUrls[s],
permanent: true,
});
}
// Docs
rules.push({
+2 -1
View File
@@ -410,7 +410,8 @@ const oldNodeTutorialPaths = [
];
const extraNodeRedirects = {
'/getting-started/node-tutorial': '/getting-started/node-server-tutorial',
'/getting-started/node-tutorial':
'/getting-started/tutorials/node-server-tutorial',
'/node-tutorial/1-code-generation': '/node-server-tutorial/1-code-generation',
'/node-tutorial/2-project-graph': '/node-server-tutorial/2-project-graph',
'/node-tutorial/3-task-running': '/node-server-tutorial/3-task-running',
+16
View File
@@ -100,5 +100,21 @@ describe('Redirect rules configuration', () => {
).toEqual(`${newNodePathPrefix}${des}`);
});
});
test('old tutorial links', () => {
const oldTutorialUrls = [
'/tutorials/package-based-repo-tutorial',
'/tutorials/integrated-repo-tutorial',
'/tutorials/react-standalone-tutorial',
'/tutorials/angular-standalone-tutorial',
'/tutorials/node-server-tutorial',
];
for (const url of oldTutorialUrls) {
expect(redirectRules.nested5minuteTutorialUrls[url]).toEqual(
'/getting-started' + url
);
}
});
});
});
+2 -1
View File
@@ -26,7 +26,8 @@
"webpack",
"http-server",
"magic-string",
"enquirer"
"enquirer",
"find-cache-dir"
],
"keepLifecycleScripts": true
}
@@ -15,8 +15,8 @@
"ESLint",
"CLI"
],
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"main": "./index.js",
"typings": "./index.d.ts",
"author": "Victor Savkin",
"license": "MIT",
"bugs": {
+2 -2
View File
@@ -16,8 +16,8 @@
"Tsc",
"CLI"
],
"main": "src/index.js",
"typings": "src/index.d.ts",
"main": "./index.js",
"typings": "./index.d.ts",
"license": "MIT",
"bugs": {
"url": "https://github.com/nrwl/nx/issues"
+2 -1
View File
@@ -25,7 +25,8 @@
"webpack",
"http-server",
"magic-string",
"enquirer"
"enquirer",
"find-cache-dir"
],
"keepLifecycleScripts": true
}
+1
View File
@@ -49,6 +49,7 @@
"@typescript-eslint/type-utils": "^5.36.1",
"chalk": "^4.1.0",
"chokidar": "^3.5.1",
"find-cache-dir": "^3.3.2",
"http-server": "^14.1.0",
"ignore": "^5.0.4",
"magic-string": "~0.26.2",
@@ -1,5 +1,10 @@
import type { Schema } from './schema';
import { readCachedProjectGraph, workspaceRoot, Workspaces } from '@nx/devkit';
import {
logger,
readCachedProjectGraph,
workspaceRoot,
Workspaces,
} from '@nx/devkit';
import { scheduleTarget } from 'nx/src/adapter/ngcli-adapter';
import { executeWebpackDevServerBuilder } from '../webpack-dev-server/webpack-dev-server.impl';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
@@ -51,6 +56,12 @@ export function executeModuleFederationDevServerBuilder(
const remotesToSkip = new Set(
findMatchingProjects(options.skipRemotes, projectGraph.nodes) ?? []
);
if (remotesToSkip.size > 0) {
logger.info(
`Remotes not served automatically: ${[...remotesToSkip].join(', ')}`
);
}
const staticRemotes = getStaticRemotes(
project,
context,
@@ -117,7 +117,7 @@
"items": {
"type": "string"
},
"description": "List of remote applications to not automatically serve, either statically or in development mode. This can be useful for multi-repository module federation setups where the host application uses a remote application from an external repository."
"description": "List of remote applications to not automatically serve, either statically or in development mode."
},
"pathToManifestFile": {
"type": "string",
@@ -73,7 +73,7 @@
"items": {
"type": "string"
},
"description": "List of remote applications to not automatically serve, either statically or in development mode. This can be useful for multi-repository module federation setups where the host application uses a remote application from an external repository."
"description": "List of remote applications to not automatically serve, either statically or in development mode."
},
"verbose": {
"type": "boolean",
@@ -1,7 +1,7 @@
import { ProjectConfiguration } from 'nx/src/config/workspace-json-project-json';
import { join } from 'path';
import { existsSync, readFileSync } from 'fs';
import { Remotes } from '@nx/devkit';
import { logger, Remotes } from '@nx/devkit';
export function getDynamicRemotes(
project: ProjectConfiguration,
@@ -45,19 +45,26 @@ export function getDynamicRemotes(
return [];
}
const dynamicRemotes = Object.entries(parsedManifest)
const allDynamicRemotes = Object.entries(parsedManifest)
.map(([remoteName]) => remoteName)
.filter((r) => !remotesToSkip.has(r));
const invalidDynamicRemotes = dynamicRemotes.filter(
(remote) => !workspaceProjects[remote]
);
if (invalidDynamicRemotes.length) {
throw new Error(
invalidDynamicRemotes.length === 1
? `Invalid dynamic remote configured in "${pathToManifestFile}": ${invalidDynamicRemotes[0]}.`
: `Invalid dynamic remotes configured in "${pathToManifestFile}": ${invalidDynamicRemotes.join(
', '
)}.`
const remotesNotInWorkspace: string[] = [];
const dynamicRemotes = allDynamicRemotes.filter((remote) => {
if (!workspaceProjects[remote]) {
remotesNotInWorkspace.push(remote);
return false;
}
return true;
});
if (remotesNotInWorkspace.length > 0) {
logger.warn(
`Skipping serving ${remotesNotInWorkspace.join(
', '
)} as they could not be found in the workspace. Ensure they are served correctly.`
);
}
@@ -89,22 +96,27 @@ export function getStaticRemotes(
Array.isArray(mfeConfig.remotes) && mfeConfig.remotes.length > 0
? mfeConfig.remotes
: [];
const staticRemotes = remotesConfig
const allStaticRemotes = remotesConfig
.map((remoteDefinition) =>
Array.isArray(remoteDefinition) ? remoteDefinition[0] : remoteDefinition
)
.filter((r) => !remotesToSkip.has(r));
const remotesNotInWorkspace: string[] = [];
const invalidStaticRemotes = staticRemotes.filter(
(remote) => !workspaceProjects[remote]
);
if (invalidStaticRemotes.length) {
throw new Error(
invalidStaticRemotes.length === 1
? `Invalid static remote configured in "${mfConfigPath}": ${invalidStaticRemotes[0]}.`
: `Invalid static remotes configured in "${mfConfigPath}": ${invalidStaticRemotes.join(
', '
)}.`
const staticRemotes = allStaticRemotes.filter((remote) => {
if (!workspaceProjects[remote]) {
remotesNotInWorkspace.push(remote);
return false;
}
return true;
});
if (remotesNotInWorkspace.length > 0) {
logger.warn(
`Skipping serving ${remotesNotInWorkspace.join(
', '
)} as they could not be found in the workspace. Ensure they are served correctly.`
);
}
@@ -53,7 +53,9 @@ export async function createEmptyWorkspace<T extends CreateWorkspaceOptions>(
/\s/.test(nxWorkspaceRoot) &&
packageManager === 'npm'
) {
const pmVersion = +getPackageManagerVersion(packageManager).split('.')[0];
const pmVersion = +getPackageManagerVersion(packageManager, tmpDir).split(
'.'
)[0];
if (pmVersion < 7) {
nxWorkspaceRoot = `\\"${nxWorkspaceRoot.slice(1, -1)}\\"`;
}
@@ -33,7 +33,10 @@ export async function createPreset<T extends CreateWorkspaceOptions>(
/\s/.test(nxWorkspaceRoot) &&
packageManager === 'npm'
) {
const pmVersion = +getPackageManagerVersion(packageManager).split('.')[0];
const pmVersion = +getPackageManagerVersion(
packageManager,
workingDir
).split('.')[0];
if (pmVersion < 7) {
nxWorkspaceRoot = `\\"${nxWorkspaceRoot.slice(1, -1)}\\"`;
}
@@ -23,7 +23,7 @@ export async function createSandbox(packageManager: PackageManager) {
`Installing dependencies with ${packageManager}`
).start();
const { install } = getPackageManagerCommand(packageManager);
const { install, preInstall } = getPackageManagerCommand(packageManager);
const tmpDir = dirSync().name;
try {
@@ -39,6 +39,10 @@ export async function createSandbox(packageManager: PackageManager) {
);
generatePackageManagerFiles(tmpDir, packageManager);
if (preInstall) {
await execAndWait(preInstall, tmpDir);
}
await execAndWait(install, tmpDir);
installSpinner.succeed();
@@ -60,7 +60,7 @@ export async function recordStat(opts: {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
console.log(`Record stat. Major: ${major}`);
}
if (major < 10 || major > 15) return; // test version, skip it
if (major < 10 || major > 16) return; // test version, skip it
const axios = require('axios');
await (axios['default'] ?? axios)
.create({
@@ -1,5 +1,5 @@
import { execSync } from 'child_process';
import { existsSync, writeFileSync } from 'fs';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
/*
@@ -36,6 +36,7 @@ export function getPackageManagerCommand(
): {
install: string;
exec: string;
preInstall?: string;
} {
const [pmMajor, pmMinor] =
getPackageManagerVersion(packageManager).split('.');
@@ -45,10 +46,14 @@ export function getPackageManagerCommand(
const useBerry = +pmMajor >= 2;
const installCommand = 'yarn install --silent';
return {
preInstall: useBerry
? 'yarn set version stable'
: 'yarn set version classic',
install: useBerry
? installCommand
: `${installCommand} --ignore-scripts`,
exec: 'yarn',
// using npx is necessary to avoid yarn classic manipulating the version detection when using berry
exec: useBerry ? 'npx' : 'yarn',
};
case 'pnpm':
@@ -81,15 +86,28 @@ export function generatePackageManagerFiles(
join(root, '.yarnrc.yml'),
'nodeLinker: node-modules\nenableScripts: false'
);
// avoids errors when using nested yarn projects
writeFileSync(join(root, 'yarn.lock'), '');
}
break;
}
}
const pmVersionCache = new Map<PackageManager, string>();
export function getPackageManagerVersion(
packageManager: PackageManager
packageManager: PackageManager,
cwd = process.cwd()
): string {
return execSync(`${packageManager} --version`).toString('utf-8').trim();
if (pmVersionCache.has(packageManager)) {
return pmVersionCache.get(packageManager) as string;
}
const version = execSync(`${packageManager} --version`, {
cwd,
encoding: 'utf-8',
}).trim();
pmVersionCache.set(packageManager, version);
return version;
}
/**
@@ -11,7 +11,9 @@ export function pointToTutorialAndCourse(preset: Preset) {
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/tutorials/package-based-repo-tutorial`],
bodyLines: [
`https://nx.dev/getting-started/tutorials/package-based-repo-tutorial`,
],
});
break;
@@ -19,14 +21,18 @@ export function pointToTutorialAndCourse(preset: Preset) {
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/tutorials/integrated-repo-tutorial`],
bodyLines: [
`https://nx.dev/getting-started/tutorials/integrated-repo-tutorial`,
],
});
break;
case Preset.ReactStandalone:
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/tutorials/react-standalone-tutorial`],
bodyLines: [
`https://nx.dev/getting-started/tutorials/react-standalone-tutorial`,
],
});
break;
case Preset.ReactMonorepo:
@@ -42,7 +48,9 @@ export function pointToTutorialAndCourse(preset: Preset) {
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/tutorials/angular-standalone-tutorial`],
bodyLines: [
`https://nx.dev/getting-started/tutorials/angular-standalone-tutorial`,
],
});
break;
case Preset.AngularMonorepo:
@@ -57,7 +65,9 @@ export function pointToTutorialAndCourse(preset: Preset) {
output.addVerticalSeparator();
output.note({
title,
bodyLines: [`https://nx.dev/tutorials/node-server-tutorial`],
bodyLines: [
`https://nx.dev/getting-started/tutorials/node-server-tutorial`,
],
});
break;
}
+1 -1
View File
@@ -5,5 +5,5 @@ export const cypressViteDevServerVersion = '^2.2.1';
export const cypressVersion = '^12.16.0';
export const cypressWebpackVersion = '^2.0.0';
export const webpackHttpPluginVersion = '^5.5.0';
export const viteVersion = '^4.3.4';
export const viteVersion = '~4.3.9';
export const htmlWebpackPluginVersion = '^5.5.0';
@@ -1,4 +1,4 @@
import { execSync } from 'child_process';
import { type ExecSyncOptions, execSync } from 'child_process';
import { join } from 'path';
import { requireNx } from '../../nx';
@@ -38,9 +38,13 @@ export function installPackagesTask(
if (storedPackageJsonValue != packageJsonValue || alwaysRun) {
global['__packageJsonInstallCache__'] = packageJsonValue;
const pmc = getPackageManagerCommand(packageManager);
execSync(pmc.install, {
const execSyncOptions: ExecSyncOptions = {
cwd: join(tree.root, cwd),
stdio: process.env.NX_GENERATE_QUIET === 'true' ? 'ignore' : 'inherit',
});
};
if (pmc.preInstall) {
execSync(pmc.preInstall, execSyncOptions);
}
execSync(pmc.install, execSyncOptions);
}
}
+34 -1
View File
@@ -10,6 +10,8 @@ import { installPackagesTask } from '../tasks/install-packages-task';
import { requireNx } from '../../nx';
import { dirSync } from 'tmp';
import { join } from 'path';
import type { PackageManager } from 'nx/src/utils/package-manager';
import { writeFileSync } from 'fs';
const {
readJson,
@@ -18,6 +20,7 @@ const {
workspaceRoot,
detectPackageManager,
createTempNpmDirectory,
getPackageManagerVersion,
} = requireNx();
const UNIDENTIFIED_VERSION = 'UNIDENTIFIED_VERSION';
@@ -464,12 +467,21 @@ export function ensurePackage<T extends any = any>(
console.log(`Fetching ${pkg}...`);
const packageManager = detectPackageManager();
const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
generatePackageManagerFiles(tempDir, packageManager);
const preInstallCommand = getPackageManagerCommand(packageManager).preInstall;
if (preInstallCommand) {
// ensure package.json and repo in tmp folder is set to a proper package manager state
execSync(preInstallCommand, {
cwd: tempDir,
stdio: isVerbose ? 'inherit' : 'ignore',
});
}
let addCommand = getPackageManagerCommand(packageManager).addDev;
if (packageManager === 'pnpm') {
addCommand = 'pnpm add -D'; // we need to ensure that we are not using workspace command
}
const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
execSync(`${addCommand} ${pkg}@${requiredVersion}`, {
cwd: tempDir,
stdio: isVerbose ? 'inherit' : 'ignore',
@@ -500,6 +512,27 @@ export function ensurePackage<T extends any = any>(
}
}
/**
* Generates necessary files needed for the package manager to work
* and for the node_modules to be accessible.
*/
function generatePackageManagerFiles(
root: string,
packageManager: PackageManager = detectPackageManager()
) {
const [pmMajor] = getPackageManagerVersion(packageManager).split('.');
switch (packageManager) {
case 'yarn':
if (+pmMajor >= 2) {
writeFileSync(
join(root, '.yarnrc.yml'),
'nodeLinker: node-modules\nenableScripts: false'
);
}
break;
}
}
function addToNodePath(dir: string) {
// NODE_PATH is a delimited list of paths.
// The delimiter is different for windows.
@@ -104,7 +104,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -134,6 +134,74 @@ describe('Dependency checks (eslint)', () => {
expect(failures.length).toEqual(0);
});
it('should exclude files not matching input of the build target', () => {
const packageJson = {
name: '@mycompany/liba',
dependencies: {},
};
const fileSys = {
'./libs/liba/package.json': JSON.stringify(packageJson, null, 2),
'./libs/liba/src/index.ts': '',
'./libs/liba/project.json': JSON.stringify(
{
name: 'liba',
targets: {
build: {
command: 'tsc -p tsconfig.lib.json',
},
},
},
null,
2
),
'./nx.json': JSON.stringify({
targetDefaults: {
build: {
inputs: [
'{projectRoot}/**/*',
'!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)',
],
},
},
}),
'./package.json': JSON.stringify(rootPackageJson, null, 2),
};
vol.fromJSON(fileSys, '/root');
const failures = runRule(
{},
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
liba: {
name: 'liba',
type: 'lib',
data: {
root: 'libs/liba',
targets: {
build: {},
},
},
},
},
externalNodes,
dependencies: {
liba: [{ source: 'liba', target: 'npm:external1', type: 'static' }],
},
},
{
liba: [
createFile(`libs/liba/src/main.ts`, []),
createFile(`libs/liba/src/main.spec.ts`, ['npm:external1']),
createFile(`libs/liba/package.json`, []),
],
}
);
expect(failures.length).toEqual(0);
});
it('should report missing dependencies section and fix it', () => {
const packageJson = {
name: '@mycompany/liba',
@@ -148,7 +216,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -191,7 +259,6 @@ describe('Dependency checks (eslint)', () => {
"{
"name": "@mycompany/liba",
"dependencies": {
"external1": "~16.1.2"
}
}"
`);
@@ -211,7 +278,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['external1'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -255,7 +322,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['external1'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -299,7 +366,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['external1'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -344,7 +411,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -416,7 +483,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -473,7 +540,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -534,7 +601,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ buildTargets: ['notbuild'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -590,7 +657,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ checkMissingDependencies: false },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -646,7 +713,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['external2'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -706,7 +773,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -778,7 +845,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -857,7 +924,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -936,7 +1003,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -1015,7 +1082,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ checkObsoleteDependencies: false },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -1068,7 +1135,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['unneeded'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -1119,7 +1186,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -1204,7 +1271,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ checkVersionMismatches: false },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -1261,7 +1328,7 @@ describe('Dependency checks (eslint)', () => {
const failures = runRule(
{ ignoredDependencies: ['external1'] },
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -1330,7 +1397,7 @@ describe('Dependency checks (eslint)', () => {
include: ['**/*.ts'],
};
const tsConfiogBaseJson = {
const tsConfigBaseJson = {
compilerOptions: {
target: 'es2015',
importHelpers: true,
@@ -1353,15 +1420,15 @@ describe('Dependency checks (eslint)', () => {
const fileSys = {
'./libs/liba/package.json': JSON.stringify(packageJson, null, 2),
'./libs/liba/src/index.ts': '',
'./libs/libb/tsconfig.json': JSON.stringify(tsConfigJson, null, 2),
'./libs/liba/tsconfig.json': JSON.stringify(tsConfigJson, null, 2),
'./package.json': JSON.stringify(rootPackageJson, null, 2),
'./tsconfig.base.json': JSON.stringify(tsConfiogBaseJson, null, 2),
'./tsconfig.base.json': JSON.stringify(tsConfigBaseJson, null, 2),
};
vol.fromJSON(fileSys, '/root');
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -1370,21 +1437,11 @@ describe('Dependency checks (eslint)', () => {
type: 'lib',
data: {
root: 'libs/liba',
targets: {
build: {},
},
},
},
libb: {
name: 'libb',
type: 'lib',
data: {
root: 'libs/libb',
targets: {
build: {
executor: '@nx/js:tsc',
options: {
tsConfig: 'libs/libb/tsconfig.json',
tsConfig: 'libs/liba/tsconfig.json',
},
},
},
@@ -1393,26 +1450,20 @@ describe('Dependency checks (eslint)', () => {
},
externalNodes,
dependencies: {
liba: [
{ source: 'liba', target: 'npm:external1', type: 'static' },
{ source: 'liba', target: 'libb', type: 'static' },
],
libb: [{ source: 'libb', target: 'npm:external2', type: 'static' }],
liba: [{ source: 'liba', target: 'npm:external1', type: 'static' }],
},
},
{
liba: [
createFile(`libs/liba/src/main.ts`, ['npm:external1']),
createFile(`libs/liba/package.json`, ['npm:external1']),
createFile(`libs/libb/src/main.ts`, ['npm:external2']),
],
}
);
expect(failures.length).toEqual(1);
expect(failures[0].message).toMatchInlineSnapshot(`
"The "liba" uses the following packages, but they are missing from the "dependencies":
- tslib
- external2"
- tslib"
`);
expect(failures[0].line).toEqual(3);
});
@@ -1435,14 +1486,14 @@ it('should require swc if @nx/js:swc executor', () => {
const fileSys = {
'./libs/liba/package.json': JSON.stringify(packageJson, null, 2),
'./libs/liba/src/index.ts': '',
'./libs/libb/.swcrc': JSON.stringify(swcrc, null, 2),
'./libs/liba/.swcrc': JSON.stringify(swcrc, null, 2),
'./package.json': JSON.stringify(rootPackageJson, null, 2),
};
vol.fromJSON(fileSys, '/root');
const failures = runRule(
{},
`${process.cwd()}/proj/libs/liba/package.json`,
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
@@ -1451,22 +1502,10 @@ it('should require swc if @nx/js:swc executor', () => {
type: 'lib',
data: {
root: 'libs/liba',
targets: {
build: {},
},
},
},
libb: {
name: 'libb',
type: 'lib',
data: {
root: 'libs/libb',
targets: {
build: {
executor: '@nx/js:swc',
options: {
tsConfig: 'libs/libb/tsconfig.json',
},
options: {},
},
},
},
@@ -1474,26 +1513,21 @@ it('should require swc if @nx/js:swc executor', () => {
},
externalNodes,
dependencies: {
liba: [
{ source: 'liba', target: 'npm:external1', type: 'static' },
{ source: 'liba', target: 'libb', type: 'static' },
],
libb: [],
liba: [{ source: 'liba', target: 'npm:external1', type: 'static' }],
},
},
{
liba: [
createFile(`libs/liba/src/main.ts`, ['npm:external1']),
createFile(`libs/liba/package.json`, ['npm:external1']),
createFile(`libs/libb/src/main.ts`),
],
}
);
expect(failures.length).toEqual(1);
expect(failures[0].message).toMatchInlineSnapshot(`
"The "liba" uses the following packages, but they are missing from the "dependencies":
- @swc/helpers"
`);
"The "liba" uses the following packages, but they are missing from the "dependencies":
- @swc/helpers"
`);
expect(failures[0].line).toEqual(3);
});
@@ -1518,7 +1552,6 @@ function runRule(
projectGraph: ProjectGraph,
projectFileMap: ProjectFileMap
): Linter.LintMessage[] {
globalThis.projectPath = `${process.cwd()}/proj`;
globalThis.projectGraph = projectGraph;
globalThis.projectFileMap = projectFileMap;
globalThis.projectRootMappings = createProjectRootMappings(
@@ -1,17 +1,22 @@
import { join } from 'path';
import { satisfies } from 'semver';
import { AST } from 'jsonc-eslint-parser';
import { normalizePath, workspaceRoot } from '@nx/devkit';
import { type JSONLiteral } from 'jsonc-eslint-parser/lib/parser/ast';
import {
normalizePath,
ProjectGraphProjectNode,
FileData,
workspaceRoot,
} from '@nx/devkit';
import { findNpmDependencies } from '@nx/js/src/utils/find-npm-dependencies';
import { createESLintRule } from '../utils/create-eslint-rule';
import { readProjectGraph } from '../utils/project-graph-utils';
import { findProject, getSourceFilePath } from '../utils/runtime-lint-utils';
import { join } from 'path';
import { findProjectsNpmDependencies } from '@nx/js/src/internal';
import { satisfies } from 'semver';
import { getHelperDependenciesFromProjectGraph } from '@nx/js';
import {
getAllDependencies,
removePackageJsonFromFileMap,
getPackageJson,
} from '../utils/package-json-utils';
import { JSONLiteral } from 'jsonc-eslint-parser/lib/parser/ast';
export type Options = [
{
@@ -21,6 +26,7 @@ export type Options = [
checkVersionMismatches?: boolean;
checkMissingPackageJson?: boolean;
ignoredDependencies?: string[];
includeTransitiveDependencies?: boolean;
}
];
@@ -50,6 +56,7 @@ export default createESLintRule<Options, MessageIds>({
checkMissingDependencies: { type: 'boolean' },
checkObsoleteDependencies: { type: 'boolean' },
checkVersionMismatches: { type: 'boolean' },
includeTransitiveDependencies: { type: 'boolean' },
},
additionalProperties: false,
},
@@ -68,6 +75,7 @@ export default createESLintRule<Options, MessageIds>({
checkObsoleteDependencies: true,
checkVersionMismatches: true,
ignoredDependencies: [],
includeTransitiveDependencies: false,
},
],
create(
@@ -79,6 +87,7 @@ export default createESLintRule<Options, MessageIds>({
checkMissingDependencies,
checkObsoleteDependencies,
checkVersionMismatches,
includeTransitiveDependencies,
},
]
) {
@@ -91,8 +100,7 @@ export default createESLintRule<Options, MessageIds>({
return {};
}
const projectPath = normalizePath(globalThis.projectPath || workspaceRoot);
const sourceFilePath = getSourceFilePath(fileName, projectPath);
const sourceFilePath = getSourceFilePath(fileName, workspaceRoot);
const { projectGraph, projectRootMappings, projectFileMap } =
readProjectGraph(RULE_NAME);
@@ -119,28 +127,19 @@ export default createESLintRule<Options, MessageIds>({
return {};
}
// gather helper dependencies for @nx/js executors
const helperDependencies = getHelperDependenciesFromProjectGraph(
workspaceRoot,
sourceProject.name,
projectGraph
);
const rootPackageJson = getPackageJson(join(workspaceRoot, 'package.json'));
// find all dependencies for the project
const npmDeps = findProjectsNpmDependencies(
const npmDependencies = findNpmDependencies(
workspaceRoot,
sourceProject,
projectGraph,
buildTarget,
projectFileMap,
buildTarget, // TODO: What if child library has a build target different from the parent?
{
helperDependencies: helperDependencies.map((dep) => dep.target),
},
removePackageJsonFromFileMap(projectFileMap)
includeTransitiveDependencies,
}
);
const projDependencies = {
...npmDeps.dependencies,
...npmDeps.peerDependencies,
};
const expectedDependencyNames = Object.keys(projDependencies);
const expectedDependencyNames = Object.keys(npmDependencies);
const projPackageJsonPath = join(
workspaceRoot,
@@ -148,12 +147,12 @@ export default createESLintRule<Options, MessageIds>({
'package.json'
);
globalThis.projPackageJsonDeps ??= getAllDependencies(projPackageJsonPath);
globalThis.projPackageJsonDeps ??= getAllDependencies(
getPackageJson(projPackageJsonPath)
);
const projPackageJsonDeps: Record<string, string> =
globalThis.projPackageJsonDeps;
const rootPackageJsonDeps = getAllDependencies(
join(workspaceRoot, 'package.json')
);
const rootPackageJsonDeps = getAllDependencies(rootPackageJson);
function validateMissingDependencies(node: AST.JSONProperty) {
if (!checkMissingDependencies) {
@@ -175,7 +174,7 @@ export default createESLintRule<Options, MessageIds>({
fix(fixer) {
missingDeps.forEach((d) => {
projPackageJsonDeps[d] =
rootPackageJsonDeps[d] || projDependencies[d];
rootPackageJsonDeps[d] || npmDependencies[d];
});
const deps = (node.value as AST.JSONObjectExpression).properties;
@@ -208,8 +207,9 @@ export default createESLintRule<Options, MessageIds>({
return;
}
if (
projDependencies[packageName] === '*' ||
satisfies(projDependencies[packageName], packageRange)
npmDependencies[packageName] === '*' ||
packageRange === '*' ||
satisfies(npmDependencies[packageName], packageRange)
) {
return;
}
@@ -219,13 +219,13 @@ export default createESLintRule<Options, MessageIds>({
messageId: 'versionMismatch',
data: {
packageName: packageName,
version: projDependencies[packageName],
version: npmDependencies[packageName],
},
fix: (fixer) =>
fixer.replaceText(
node as any,
`"${packageName}": "${
rootPackageJsonDeps[packageName] || projDependencies[packageName]
rootPackageJsonDeps[packageName] || npmDependencies[packageName]
}"`
),
});
@@ -303,15 +303,15 @@ export default createESLintRule<Options, MessageIds>({
.join(),
},
fix: (fixer) => {
expectedDependencyNames.sort().reduce((acc, d) => {
acc[d] = rootPackageJsonDeps[d] || projDependencies[d];
return acc;
}, projPackageJsonDeps);
const dependencies = Object.keys(projPackageJsonDeps)
.map((d) => `\n "${d}": "${projPackageJsonDeps[d]}"`)
.join(',');
expectedDependencyNames.sort().reduce((acc, d) => {
acc[d] = rootPackageJsonDeps[d] || dependencies[d];
return acc;
}, projPackageJsonDeps);
if (!node.properties.length) {
return fixer.replaceText(
node as any,
@@ -332,7 +332,7 @@ export default createESLintRule<Options, MessageIds>({
['JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=/^(dev|peer|optional)?dependencies$/i]'](
node: AST.JSONProperty
) {
return validateMissingDependencies(node);
validateMissingDependencies(node);
},
['JSONExpressionStatement > JSONObjectExpression > JSONProperty[key.value=/^(dev|peer|optional)?dependencies$/i] > JSONObjectExpression > JSONProperty'](
node: AST.JSONProperty
@@ -345,19 +345,15 @@ export default createESLintRule<Options, MessageIds>({
}
if (expectedDependencyNames.includes(packageName)) {
return validateVersionMatchesInstalled(
node,
packageName,
packageRange
);
validateVersionMatchesInstalled(node, packageName, packageRange);
} else {
return reportObsoleteDependency(node, packageName);
reportObsoleteDependency(node, packageName);
}
},
['JSONExpressionStatement > JSONObjectExpression'](
node: AST.JSONObjectExpression
) {
return validateDependenciesSectionExistance(node);
validateDependenciesSectionExistance(node);
},
};
},
@@ -1,26 +1,21 @@
import { ProjectFileMap, readJsonFile } from '@nx/devkit';
import { existsSync } from 'fs';
import { PackageJson } from 'nx/src/utils/package-json';
export function getAllDependencies(path: string): Record<string, string> {
export function getAllDependencies(
packageJson: PackageJson
): Record<string, string> {
return {
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.peerDependencies,
...packageJson.optionalDependencies,
};
}
export function getPackageJson(path: string): PackageJson {
if (existsSync(path)) {
const packageJson = readJsonFile(path);
return {
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.peerDependencies,
};
return readJsonFile(path);
}
return {};
}
export function removePackageJsonFromFileMap(
projectFileMap: ProjectFileMap
): ProjectFileMap {
const newFileMap = {};
Object.keys(projectFileMap).forEach((key) => {
newFileMap[key] = projectFileMap[key].filter(
(f) => !f.file.endsWith('/package.json')
);
});
return newFileMap;
return {} as PackageJson;
}
@@ -4,6 +4,7 @@
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
"types": ["node"]
},
"files": ["<%= offsetFromRoot %>node_modules/@nx/expo/typings/svg.d.ts"],
"exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.spec.tsx", "test-setup.ts"],
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
}
@@ -8,19 +8,13 @@
"skipLibCheck": true,
"resolveJsonModule": true,
"strict": true,
"composite": true,
"declaration": true
},
"files": [
"../../node_modules/@nx/expo/typings/svg.d.ts"
],
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
}
],
"exclude": [
"node_modules"
]
}
+82
View File
@@ -0,0 +1,82 @@
---
title: JS Node executor examples
description: This page contains examples for the @nx/js:node executor.
---
The `@nx/js:node` executor runs the output of a build target. For example, an application uses esbuild ([`@nx/esbuild:esbuild`](/packages/esbuild/executors/esbuild)) to output the bundle to `dist/my-app` folder, which can then be executed by `@nx/js:node`.
`project.json`:
```json
"my-app": {
"targets": {
"serve": {
"executor": "@nx/js:node",
"options": {
"buildTarget": "my-app:build"
}
},
"build": {
"executor": "@nx/esbuild:esbuild",
"options": {
"main": "my-app/src/main.ts",
"output": ["dist/my-app"],
//...
}
},
}
}
```
```bash
npx nx serve my-app
```
## Examples
{% tabs %}
{% tab label="Pass extra Node CLI arguments" %}
Using `runtimeArgs`, you can pass arguments to the underlying `node` command. For example, if you want to set [`--no-warnings`](https://nodejs.org/api/cli.html#--no-warnings) to silence all Node warnings, then add the following to the `project.json` file.
```json
"my-app": {
"targets": {
"serve": {
"executor": "@nx/js:node",
"options": {
"runtimeArgs": ["--no-warnings"],
//...
},
},
}
}
```
{% /tab %}
{% tab label="Run all task dependencies" %}
If your application build depends on other tasks, and you want those tasks to also be executed, then set the `runBuildTargetDependencies` to `true`. For example, a library may have a task to generate GraphQL schemas, which is consume by the application. In this case, you want to run the generate task before building and running the application.
This option is also useful when the build consumes a library from its output, not its source. For example, if an executor that supports `buildLibsFromSource` option has it set to `false` (e.g. [`@nx/webpack:webpack`](/packages/webpack/executors/webpack)).
Note that this option will increase the build time, so use it only when necessary.
```json
"my-app": {
"targets": {
"serve": {
"executor": "@nx/js:node",
"options": {
"runBuildTargetDependencies": true,
//...
},
},
}
}
```
{% /tab %}
{% /tabs %}
-1
View File
@@ -12,7 +12,6 @@
"init": {
"factory": "./src/generators/init/init#initSchematic",
"schema": "./src/generators/init/schema.json",
"aliases": ["lib"],
"x-type": "init",
"description": "Initialize a TS/JS workspace.",
"hidden": true
+121 -73
View File
@@ -8,6 +8,7 @@ import {
ProjectGraphProjectNode,
readTargetOptions,
runExecutor,
Target,
} from '@nx/devkit';
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
import { daemonClient } from 'nx/src/daemon/client/client';
@@ -60,6 +61,14 @@ export async function* nodeExecutor(
const buildTargetExecutor =
project.data.targets[buildTarget.target]?.executor;
if (
buildTargetExecutor === 'nx:run-commands' ||
buildTargetExecutor === '@nrwl/workspace:run-commands'
) {
// Run commands does not emit build event, so we have to switch to run entire build through Nx CLI.
options.runBuildTargetDependencies = true;
}
const buildOptions: Record<string, any> = {
...readTargetOptions(buildTarget, context),
...options.buildTargetOptions,
@@ -85,8 +94,9 @@ export async function* nodeExecutor(
buildTargetExecutor
);
const tasks: ActiveTask[] = [];
let additionalExitHandler: null | (() => void) = null;
let currentTask: ActiveTask = null;
const tasks: ActiveTask[] = [];
yield* createAsyncIterable<{
success: boolean;
@@ -107,55 +117,34 @@ export async function* nodeExecutor(
options.debounce ?? 1_000
);
const addToQueue = async () => {
const addToQueue = async (
childProcess: null | ChildProcess,
buildResult: Promise<{ success: boolean }>
) => {
const task: ActiveTask = {
id: randomUUID(),
killed: false,
childProcess: null,
childProcess,
promise: null,
start: async () => {
let buildFailed = false;
// Run the build
task.promise = new Promise<void>(async (resolve, reject) => {
task.childProcess = exec(
`npx nx run ${context.projectName}:${buildTarget.target}${
buildTarget.configuration ? `:${buildTarget.configuration}` : ''
}`,
{
cwd: context.root,
},
(error, stdout, stderr) => {
if (
// Build succeeded
!error ||
// If task was killed then another build process has started, ignore errors.
task.killed
) {
resolve();
return;
}
// Wait for build to finish.
const result = await buildResult;
logger.info(stdout);
buildFailed = true;
if (options.watch) {
logger.error(
`Build failed, waiting for changes to restart...`
);
resolve(); // Don't reject because it'll error out and kill the Nx process.
} else {
logger.error(`Build failed. See above for errors.`);
reject();
}
if (!result.success) {
// If in watch-mode, don't throw or else the process exits.
if (options.watch) {
if (!task.killed) {
// Only log build error if task was not killed by a new change.
logger.error(`Build failed, waiting for changes to restart...`);
}
);
});
return;
} else {
throw new Error(`Build failed. See above for errors.`);
}
}
// Wait for build to finish
await task.promise;
// Task may have been stopped due to another running task.
// OR build failed, so don't start the process.
if (task.killed || buildFailed) return;
// Before running the program, check if the task has been killed (by a new change during watch).
if (task.killed) return;
// Run the program
task.promise = new Promise<void>((resolve, reject) => {
@@ -173,16 +162,17 @@ export async function* nodeExecutor(
}
);
task.childProcess.stderr.on('data', (data) => {
const handleStdErr = (data) => {
// Don't log out error if task is killed and new one has started.
// This could happen if a new build is triggered while new process is starting, since the operation is not atomic.
// Log the error in normal mode
if (!options.watch || !task.killed) {
logger.error(data.toString());
}
});
};
task.childProcess.stderr.on('data', handleStdErr);
task.childProcess.once('exit', (code) => {
task.childProcess.off('data', handleStdErr);
if (options.watch && !task.killed) {
logger.info(
`NX Process exited with code ${code}, waiting for changes to restart...`
@@ -203,15 +193,46 @@ export async function* nodeExecutor(
if (task.childProcess) {
await killTree(task.childProcess.pid, signal);
}
await task.promise;
try {
await task.promise;
} catch {
// Doesn't matter if task fails, we just need to wait until it finishes.
}
},
};
tasks.push(task);
};
if (options.watch) {
const stopWatch = await daemonClient.registerFileWatcher(
if (options.runBuildTargetDependencies) {
// If a all dependencies need to be rebuild on changes, then register with watcher
// and run through CLI, otherwise only the current project will rebuild.
const runBuild = async () => {
let childProcess: ChildProcess = null;
const whenReady = new Promise<{ success: boolean }>(async (resolve) => {
childProcess = fork(
require.resolve('nx'),
[
'run',
`${context.projectName}:${buildTarget.target}${
buildTarget.configuration ? `:${buildTarget.configuration}` : ''
}`,
],
{
cwd: context.root,
stdio: 'inherit',
}
);
childProcess.once('exit', (code) => {
if (code === 0) resolve({ success: true });
// If process is killed due to current task being killed, then resolve with success.
else resolve({ success: !!currentTask?.killed });
});
});
await addToQueue(childProcess, whenReady);
await debouncedProcessQueue();
};
additionalExitHandler = await daemonClient.registerFileWatcher(
{
watchProjects: [context.projectName],
includeDependentProjects: true,
@@ -224,37 +245,52 @@ export async function* nodeExecutor(
logger.error(`Watch error: ${err?.message ?? 'Unknown'}`);
} else {
logger.info(`NX File change detected. Restarting...`);
await addToQueue();
await debouncedProcessQueue();
await runBuild();
}
}
);
const stopAllTasks = (signal: NodeJS.Signals = 'SIGTERM') => {
for (const task of tasks) {
task.stop(signal);
await runBuild(); // run first build
} else {
// Otherwise, run the build executor, which will not run task dependencies.
// This is mostly fine for bundlers like webpack that should already watch for dependency libs.
// For tsc/swc or custom build commands, consider using `runBuildTargetDependencies` instead.
const output = await runExecutor(
buildTarget,
{
...options.buildTargetOptions,
watch: options.watch,
},
context
);
while (true) {
const event = await output.next();
await addToQueue(null, Promise.resolve(event.value));
await debouncedProcessQueue();
if (event.done || !options.watch) {
break;
}
};
process.on('SIGTERM', async () => {
stopWatch();
stopAllTasks('SIGTERM');
process.exit(128 + 15);
});
process.on('SIGINT', async () => {
stopWatch();
stopAllTasks('SIGINT');
process.exit(128 + 2);
});
process.on('SIGHUP', async () => {
stopWatch();
stopAllTasks('SIGHUP');
process.exit(128 + 1);
});
}
}
await addToQueue();
await processQueue();
const stopAllTasks = (signal: NodeJS.Signals = 'SIGTERM') => {
additionalExitHandler?.();
for (const task of tasks) {
task.stop(signal);
}
};
process.on('SIGTERM', async () => {
stopAllTasks('SIGTERM');
process.exit(128 + 15);
});
process.on('SIGINT', async () => {
stopAllTasks('SIGINT');
process.exit(128 + 2);
});
process.on('SIGHUP', async () => {
stopAllTasks('SIGHUP');
process.exit(128 + 1);
});
});
}
@@ -321,6 +357,18 @@ function getFileToRun(
buildOptions: Record<string, any>,
buildTargetExecutor: string
): string {
// If using run-commands or another custom executor, then user should set
// outputFileName, but we can try the default value that we use.
if (!buildOptions?.outputPath && !buildOptions?.outputFileName) {
const fallbackFile = path.join('dist', project.data.root, 'main.js');
logger.warn(
`Build option ${chalk.bold('outputFileName')} not set for ${chalk.bold(
project.name
)}. Using fallback value of ${chalk.bold(fallbackFile)}.`
);
return join(context.root, fallbackFile);
}
let outputFileName = buildOptions.outputFileName;
if (!outputFileName) {
+1
View File
@@ -14,4 +14,5 @@ export interface NodeExecutorOptions {
port: number;
watch?: boolean;
debounce?: number;
runBuildTargetDependencies?: boolean;
}
+21 -8
View File
@@ -27,12 +27,14 @@
"host": {
"type": "string",
"default": "localhost",
"description": "The host to inspect the process on."
"description": "The host to inspect the process on.",
"x-priority": "important"
},
"port": {
"type": "number",
"default": 9229,
"description": "The port to inspect the process on. Setting port to 0 will assign random free ports to all forked processes."
"description": "The port to inspect the process on. Setting port to 0 will assign random free ports to all forked processes.",
"x-priority": "important"
},
"inspect": {
"oneOf": [
@@ -45,7 +47,8 @@
}
],
"description": "Ensures the app is starting with debugging.",
"default": "inspect"
"default": "inspect",
"x-priority": "important"
},
"runtimeArgs": {
"type": "array",
@@ -53,7 +56,8 @@
"default": [],
"items": {
"type": "string"
}
},
"x-priority": "important"
},
"args": {
"type": "array",
@@ -61,19 +65,28 @@
"default": [],
"items": {
"type": "string"
}
},
"x-priority": "important"
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": true
"default": true,
"x-priority": "important"
},
"debounce": {
"type": "number",
"description": "Delay in milliseconds to wait before restarting. Useful to batch multiple file changes events together. Set to zero (0) to disable.",
"default": 500
"default": 500,
"x-priority": "important"
},
"runBuildTargetDependencies": {
"type": "boolean",
"description": "Whether to run dependencies before running the build. Set this to true if the project does not build libraries from source (e.g. 'buildLibsFromSource: false').",
"default": false
}
},
"additionalProperties": false,
"required": ["buildTarget"]
"required": ["buildTarget"],
"examplesFile": "../../../docs/node-examples.md"
}
+73 -26
View File
@@ -76,6 +76,20 @@ export async function* tscBatchExecutor(
},
};
const processTaskPostCompilation = (tsConfig: string) => {
if (tsConfigTaskInfoMap[tsConfig]) {
const taskInfo = tsConfigTaskInfoMap[tsConfig];
taskInfo.assetsHandler.processAllAssetsOnceSync();
updatePackageJson(
taskInfo.options,
taskInfo.context,
taskInfo.projectGraphNode,
taskInfo.buildableProjectNodeDependencies
);
taskInfo.endTime = Date.now();
}
};
const typescriptCompilation = compileTypescriptSolution(
tsCompilationContext,
shouldWatch,
@@ -86,19 +100,7 @@ export async function* tscBatchExecutor(
tsConfigTaskInfoMap[tsConfig].startTime = Date.now();
}
},
afterProjectCompilationCallback: (tsConfig) => {
if (tsConfigTaskInfoMap[tsConfig]) {
const taskInfo = tsConfigTaskInfoMap[tsConfig];
taskInfo.assetsHandler.processAllAssetsOnceSync();
updatePackageJson(
taskInfo.options,
taskInfo.context,
taskInfo.projectGraphNode,
taskInfo.buildableProjectNodeDependencies
);
taskInfo.endTime = Date.now();
}
},
afterProjectCompilationCallback: processTaskPostCompilation,
}
);
@@ -136,23 +138,68 @@ export async function* tscBatchExecutor(
});
}
return yield* mapAsyncIterable(typescriptCompilation, async (iterator) => {
const { value, done } = await iterator.next();
const toBatchExecutorTaskResult = (
tsConfig: string,
success: boolean
): BatchExecutorTaskResult => ({
task: tsConfigTaskInfoMap[tsConfig].task,
result: {
success: success,
terminalOutput: tsConfigTaskInfoMap[tsConfig].terminalOutput,
startTime: tsConfigTaskInfoMap[tsConfig].startTime,
endTime: tsConfigTaskInfoMap[tsConfig].endTime,
},
});
let isCompilationDone = false;
const taskTsConfigsToReport = new Set(
Object.keys(taskGraph.tasks).map((t) => taskInMemoryTsConfigMap[t].path)
);
let tasksToReportIterator: IterableIterator<string>;
const processSkippedTasks = () => {
const { value: tsConfig, done } = tasksToReportIterator.next();
if (done) {
return { value, done: true };
return { value: undefined, done: true };
}
const taskResult: BatchExecutorTaskResult = {
task: tsConfigTaskInfoMap[value.tsConfig].task,
result: {
success: value.success,
terminalOutput: tsConfigTaskInfoMap[value.tsConfig].terminalOutput,
startTime: tsConfigTaskInfoMap[value.tsConfig].startTime,
endTime: tsConfigTaskInfoMap[value.tsConfig].endTime,
},
};
tsConfigTaskInfoMap[tsConfig].startTime = Date.now();
processTaskPostCompilation(tsConfig);
return { value: taskResult, done: false };
return { value: toBatchExecutorTaskResult(tsConfig, true), done: false };
};
return yield* mapAsyncIterable(typescriptCompilation, async (iterator) => {
if (isCompilationDone) {
return processSkippedTasks();
}
const { value, done } = await iterator.next();
if (done) {
if (taskTsConfigsToReport.size > 0) {
/**
* TS compilation is done but we still have tasks to report. This can
* happen if, for example, a project is identified as affected, but
* no file in the TS project is actually changed or if running a
* task with `--skip-nx-cache` and the outputs are already there. There
* can still be changes to assets or other files we need to process.
*
* Switch to handle the iterator for the tasks we still need to report.
*/
isCompilationDone = true;
tasksToReportIterator = taskTsConfigsToReport.values();
return processSkippedTasks();
}
return { value: undefined, done: true };
}
taskTsConfigsToReport.delete(value.tsConfig);
return {
value: toBatchExecutorTaskResult(value.tsConfig, value.success),
done: false,
};
});
}
@@ -131,7 +131,7 @@ function setupNpm(options: VerdaccioExecutorSchema) {
return () => {};
}
let npmRegistryPath;
let npmRegistryPath: string;
try {
npmRegistryPath = execSync(
`npm config get registry --location ${options.location}`
@@ -154,7 +154,13 @@ function setupNpm(options: VerdaccioExecutorSchema) {
return () => {
try {
if (npmRegistryPath) {
const currentNpmRegistryPath = execSync(
`npm config get registry --location ${options.location}`
)
?.toString()
?.trim()
?.replace('\u001b[2K\u001b[1G', ''); // strip out ansi codes
if (npmRegistryPath && currentNpmRegistryPath.includes('localhost')) {
execSync(
`npm config set registry ${npmRegistryPath} --location ${options.location}`
);
@@ -240,7 +246,13 @@ function setupYarn(options: VerdaccioExecutorSchema) {
return () => {
try {
if (yarnRegistryPath) {
const currentYarnRegistryPath = execSync(
`yarn config get ${registryConfigName}`
)
?.toString()
?.trim()
?.replace('\u001b[2K\u001b[1G', ''); // strip out ansi codes
if (yarnRegistryPath && currentYarnRegistryPath.includes('localhost')) {
execSync(
`yarn config set ${registryConfigName} ${yarnRegistryPath}` +
(options.location === 'user' ? ' --home' : '')
@@ -0,0 +1,400 @@
import 'nx/src/utils/testing/mock-fs';
import { vol } from 'memfs';
import { findNpmDependencies } from './find-npm-dependencies';
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
workspaceRoot: '/root',
}));
jest.mock('nx/src/utils/workspace-root', () => ({
workspaceRoot: '/root',
}));
describe('findNpmDependencies', () => {
const nxJson = {
targetDefaults: {
build: {
inputs: [
'{projectRoot}/**/*',
'!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)',
],
},
},
};
afterEach(() => {
vol.reset();
});
it('should pick up external npm dependencies and their versions', () => {
vol.fromJSON(
{
'./nx.json': JSON.stringify(nxJson),
},
'/root'
);
const libWithExternalDeps = {
name: 'my-lib',
type: 'lib' as const,
data: {
root: 'libs/my-lib',
targets: { build: {} },
},
};
const projectGraph = {
nodes: {
'my-lib': libWithExternalDeps,
},
externalNodes: {
'npm:foo': {
name: 'npm:foo' as const,
type: 'npm' as const,
data: {
packageName: 'foo',
version: '1.0.0',
},
},
},
dependencies: {},
};
const projectFileMap = {
'my-lib': [
{
file: 'libs/my-lib/index.ts',
hash: '123',
deps: ['npm:foo'],
},
],
};
const results = findNpmDependencies(
'/root',
libWithExternalDeps,
projectGraph,
projectFileMap,
'build'
);
expect(results).toEqual({
foo: '1.0.0',
});
});
it('should pick up helper npm dependencies if required', () => {
vol.fromJSON(
{
'./nx.json': JSON.stringify(nxJson),
'./libs/my-lib/tsconfig.json': JSON.stringify({
compilerOptions: {
importHelpers: true,
},
}),
'./libs/my-lib/.swcrc': JSON.stringify({
jsc: {
externalHelpers: true,
},
}),
},
'/root'
);
const libWithHelpers = {
name: 'my-lib',
type: 'lib' as const,
data: {
root: 'libs/my-lib',
targets: {
build1: {
executor: '@nx/js:tsc',
options: {
tsConfig: 'libs/my-lib/tsconfig.json',
},
},
build2: {
executor: '@nx/js:swc',
options: {},
},
},
},
};
const projectGraph = {
nodes: {
'my-lib': libWithHelpers,
},
externalNodes: {
'npm:tslib': {
name: 'npm:tslib' as const,
type: 'npm' as const,
data: {
packageName: 'tslib',
version: '2.6.0',
},
},
'npm:@swc/helpers': {
name: 'npm:@swc/helpers' as const,
type: 'npm' as const,
data: {
packageName: '@swc/helpers',
version: '0.5.0',
},
},
},
dependencies: {},
};
const projectFileMap = {
'my-lib': [],
};
expect(
findNpmDependencies(
'/root',
libWithHelpers,
projectGraph,
projectFileMap,
'build1'
)
).toEqual({
tslib: '2.6.0',
});
expect(
findNpmDependencies(
'/root',
libWithHelpers,
projectGraph,
projectFileMap,
'build2'
)
).toEqual({
'@swc/helpers': '0.5.0',
});
});
it('should not pick up helper npm dependencies if not required', () => {
vol.fromJSON(
{
'./libs/my-lib/tsconfig.json': JSON.stringify({
compilerOptions: {
importHelpers: false,
},
}),
'./libs/my-lib/.swcrc': JSON.stringify({
jsc: {
externalHelpers: false,
},
}),
},
'/root'
);
const libWithInlinedHelpers = {
name: 'my-lib',
type: 'lib' as const,
data: {
root: 'libs/my-lib',
targets: {
build1: {
executor: '@nx/js:tsc',
options: {
tsConfig: 'libs/my-lib/tsconfig.json',
},
},
build2: {
executor: '@nx/js:swc',
options: {},
},
},
},
};
const projectGraph = {
nodes: {
'my-lib': libWithInlinedHelpers,
},
externalNodes: {
'npm:tslib': {
name: 'npm:tslib' as const,
type: 'npm' as const,
data: {
packageName: 'tslib',
version: '2.6.0',
},
},
'npm:@swc/helpers': {
name: 'npm:@swc/helpers' as const,
type: 'npm' as const,
data: {
packageName: '@swc/helpers',
version: '0.5.0',
},
},
},
dependencies: {},
};
const projectFileMap = {
'my-lib': [],
};
const results = findNpmDependencies(
'/root',
libWithInlinedHelpers,
projectGraph,
projectFileMap,
'build'
);
expect(results).toEqual({});
});
it('should support recursive collection of dependencies', () => {
vol.fromJSON(
{
'./nx.json': JSON.stringify(nxJson),
},
'/root'
);
const parentLib = {
name: 'parent',
type: 'lib' as const,
data: {
root: 'libs/parent',
targets: { build: {} },
},
};
const projectGraph = {
nodes: {
parent: parentLib,
child1: {
name: 'child1',
type: 'lib' as const,
data: {
root: 'libs/child1',
targets: { build: {} },
},
},
child2: {
name: 'child2',
type: 'lib' as const,
data: {
root: 'libs/child2',
targets: { build: {} },
},
},
},
externalNodes: {
'npm:foo': {
name: 'npm:foo' as const,
type: 'npm' as const,
data: {
packageName: 'foo',
version: '1.0.0',
},
},
},
dependencies: {
parent: [
{
type: 'static',
source: 'parent',
target: 'child1',
},
],
child1: [
{
type: 'static',
source: 'child1',
target: 'child2',
},
],
child2: [
{
type: 'static',
source: 'child2',
target: 'npm:foo',
},
],
},
};
const projectFileMap = {
parent: [{ file: 'libs/parent/index.ts', hash: '123', deps: ['child1'] }],
child1: [{ file: 'libs/child1/index.ts', hash: '123', deps: ['child2'] }],
child2: [
{ file: 'libs/child2/index.ts', hash: '123', deps: ['npm:foo'] },
],
};
const results = findNpmDependencies(
'/root',
parentLib,
projectGraph,
projectFileMap,
'build',
{
includeTransitiveDependencies: true,
}
);
expect(results).toEqual({
foo: '1.0.0',
});
});
it('should find workspace dependencies', () => {
vol.fromJSON(
{
'./libs/lib3/package.json': JSON.stringify({
name: '@acme/lib3',
version: '0.0.1',
}),
'./nx.json': JSON.stringify(nxJson),
},
'/root'
);
const lib1 = {
name: 'lib1',
type: 'lib' as const,
data: {
root: 'libs/lib1',
targets: { build: {} },
},
};
const lib2 = {
name: 'lib2',
type: 'lib' as const,
data: {
root: 'libs/lib2',
targets: { build: {} },
},
};
const lib3 = {
name: 'lib3',
type: 'lib' as const,
data: {
root: 'libs/lib3',
targets: { build: {} },
},
};
const projectGraph = {
nodes: {
lib1: lib1,
lib2: lib2,
lib3: lib3,
},
externalNodes: {},
dependencies: {},
};
const projectFileMap = {
lib1: [{ file: 'libs/lib1/index.ts', hash: '123', deps: ['lib3'] }],
lib2: [{ file: 'libs/lib1/index.ts', hash: '123', deps: ['lib3'] }],
lib3: [],
};
expect(
findNpmDependencies('/root', lib1, projectGraph, projectFileMap, 'build')
).toEqual({
'@acme/lib3': '*',
});
expect(
findNpmDependencies('/root', lib2, projectGraph, projectFileMap, 'build')
).toEqual({
'@acme/lib3': '*',
});
});
});
@@ -0,0 +1,191 @@
import { join } from 'path';
import { readNxJson } from 'nx/src/project-graph/file-utils';
import {
getTargetInputs,
filterUsingGlobPatterns,
} from 'nx/src/hasher/task-hasher';
import {
type ProjectGraph,
type ProjectGraphProjectNode,
type ProjectFileMap,
readJsonFile,
FileData,
joinPathFragments,
} from '@nx/devkit';
import { fileExists } from 'nx/src/utils/fileutils';
import { fileDataDepTarget } from 'nx/src/config/project-graph';
import { readTsConfig } from './typescript/ts-config';
/**
* Finds all npm dependencies and their expected versions for a given project.
*/
export function findNpmDependencies(
workspaceRoot: string,
sourceProject: ProjectGraphProjectNode,
projectGraph: ProjectGraph,
projectFileMap: ProjectFileMap,
buildTarget: string,
options: {
includeTransitiveDependencies?: boolean;
} = {}
): Record<string, string> {
let seen: null | Set<string> = null;
if (options.includeTransitiveDependencies) {
seen = new Set<string>();
}
const results: Record<string, string> = {};
function collectAll(
currentProject: ProjectGraphProjectNode,
collectedDeps: Record<string, string>
): void {
if (seen?.has(currentProject.name)) return;
collectDependenciesFromFileMap(
workspaceRoot,
currentProject,
projectGraph,
projectFileMap,
buildTarget,
collectedDeps
);
collectHelperDependencies(
workspaceRoot,
currentProject,
projectGraph,
buildTarget,
collectedDeps
);
if (options.includeTransitiveDependencies) {
const projectDeps = projectGraph.dependencies[currentProject.name];
for (const dep of projectDeps) {
const projectDep = projectGraph.nodes[dep.target];
if (projectDep) collectAll(projectDep, collectedDeps);
}
}
}
collectAll(sourceProject, results);
return results;
}
// Keep track of workspace libs we already read package.json for so we don't read from disk again.
const seenWorkspaceDeps: Record<string, { name: string; version: string }> = {};
function collectDependenciesFromFileMap(
workspaceRoot: string,
sourceProject: ProjectGraphProjectNode,
projectGraph: ProjectGraph,
projectFileMap: ProjectFileMap,
buildTarget: string,
npmDeps: Record<string, string>
): void {
const rawFiles = projectFileMap[sourceProject.name];
if (!rawFiles) return;
// Cannot read inputs if the target does not exist on the project.
if (!sourceProject.data.targets[buildTarget]) return;
const inputs = getTargetInputs(
readNxJson(),
sourceProject,
buildTarget
).selfInputs;
const files = filterUsingGlobPatterns(
sourceProject.data.root,
projectFileMap[sourceProject.name] || [],
inputs
);
for (const fileData of files) {
if (
!fileData.deps ||
fileData.file ===
joinPathFragments(sourceProject.data.root, 'package.json')
) {
continue;
}
for (const dep of fileData.deps) {
const target = fileDataDepTarget(dep);
// If the node is external, then read package info from `data`.
const externalDep = projectGraph.externalNodes[target];
if (externalDep?.type === 'npm') {
npmDeps[externalDep.data.packageName] = externalDep.data.version;
continue;
}
// If node is internal, then try reading package info from `package.json` (which must exist for this to work).
const workspaceDep = projectGraph.nodes[target];
if (!workspaceDep) continue;
const cached = seenWorkspaceDeps[workspaceDep.name];
if (cached) {
npmDeps[cached.name] = cached.version;
} else {
const packageJson = readPackageJson(workspaceDep, workspaceRoot);
if (packageJson) {
// This is a workspace lib so we can't reliably read in a specific version since it depends on how the workspace is set up.
// ASSUMPTION: Most users will use '*' for workspace lib versions. Otherwise, they can manually update it.
npmDeps[packageJson.name] = '*';
seenWorkspaceDeps[workspaceDep.name] = {
name: packageJson.name,
version: '*',
};
}
}
}
}
}
function readPackageJson(
project: ProjectGraphProjectNode,
workspaceRoot: string
): null | {
name: string;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
} {
const packageJsonPath = join(
workspaceRoot,
project.data.root,
'package.json'
);
if (fileExists(packageJsonPath)) return readJsonFile(packageJsonPath);
return null;
}
function collectHelperDependencies(
workspaceRoot: string,
sourceProject: ProjectGraphProjectNode,
projectGraph: ProjectGraph,
buildTarget: string,
npmDeps: Record<string, string>
): void {
const target = sourceProject.data.targets[buildTarget];
if (!target) return;
if (target.executor === '@nx/js:tsc' && target.options?.tsConfig) {
const tsConfig = readTsConfig(join(workspaceRoot, target.options.tsConfig));
if (tsConfig?.options['importHelpers']) {
npmDeps['tslib'] = projectGraph.externalNodes['npm:tslib']?.data.version;
}
}
if (target.executor === '@nx/js:swc') {
const swcConfigPath = target.options.swcrc
? join(workspaceRoot, target.options.swcrc)
: join(workspaceRoot, sourceProject.data.root, '.swcrc');
const swcConfig = fileExists(swcConfigPath)
? readJsonFile(swcConfigPath)
: {};
if (swcConfig?.jsc?.externalHelpers) {
npmDeps['@swc/helpers'] =
projectGraph.externalNodes['npm:@swc/helpers']?.data.version;
}
}
}
@@ -290,7 +290,7 @@ describe('updatePackageJson', () => {
const fileMap = {
'@org/lib1': [
{
file: 'test.ts',
file: 'libs/lib1/src/test.ts',
hash: '',
deps: ['npm:external1', 'npm:external2'],
},
-3
View File
@@ -13,12 +13,9 @@ hashbrown = { version = "0.14.0", features = ["rayon"] }
ignore = '0.4'
ignore-files = "1.3.0"
itertools = "0.10.5"
jsonc-parser = { version = "0.21.1", features = ["serde"] }
napi = { version = '2.12.6', default-features = false, features = ['anyhow', 'napi4', 'tokio_rt'] }
napi-derive = '2.9.3'
rayon = "1.7.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0.40"
tokio = { version = "1.28.2", features = ["fs"] }
tracing = "0.1.37"
@@ -1,6 +1,7 @@
{
"name": "@nx/nx-darwin-arm64",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
@@ -1,6 +1,7 @@
{
"name": "@nx/nx-darwin-x64",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
@@ -1,6 +1,7 @@
{
"name": "@nx/nx-freebsd-x64",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
@@ -1,6 +1,7 @@
{
"name": "@nx/nx-linux-arm-gnueabihf",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
@@ -1,6 +1,7 @@
{
"name": "@nx/nx-linux-arm64-gnu",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
@@ -1,6 +1,7 @@
{
"name": "@nx/nx-linux-arm64-musl",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
@@ -1,6 +1,7 @@
{
"name": "@nx/nx-linux-x64-gnu",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
@@ -1,6 +1,7 @@
{
"name": "@nx/nx-linux-x64-musl",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
@@ -1,6 +1,7 @@
{
"name": "@nx/nx-win32-arm64-msvc",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
@@ -1,6 +1,7 @@
{
"name": "@nx/nx-win32-x64-msvc",
"version": "0.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
+2 -1
View File
@@ -25,7 +25,8 @@ export interface InitArgs {
}
export async function initHandler(options: InitArgs) {
const args = process.argv.slice(2).join(' ');
// strip the 'init' command itself so we don't forward it
const args = process.argv.slice(3).join(' ');
const flags = parser(args, {
boolean: ['useDotNxInstallation'],
alias: {
+15 -15
View File
@@ -95,7 +95,7 @@ export class Workspaces {
return this.cachedProjectsConfig;
}
const nxJson = this.readNxJson();
const projectsConfigurations = buildProjectsConfigurationsFromProjectPaths(
let projectsConfigurations = buildProjectsConfigurationsFromProjectPaths(
nxJson,
globForProjectFiles(
this.root,
@@ -116,15 +116,18 @@ export class Workspaces {
opts?._includeProjectsFromAngularJson
)
) {
projectsConfigurations.projects = mergeAngularJsonAndProjects(
projectsConfigurations.projects,
projectsConfigurations = mergeAngularJsonAndProjects(
projectsConfigurations,
this.root
);
}
this.cachedProjectsConfig = this.mergeTargetDefaultsIntoProjectDescriptions(
projectsConfigurations,
nxJson
);
this.cachedProjectsConfig = {
version: 2,
projects: this.mergeTargetDefaultsIntoProjectDescriptions(
projectsConfigurations,
nxJson
),
};
return this.cachedProjectsConfig;
}
@@ -140,10 +143,10 @@ export class Workspaces {
}
private mergeTargetDefaultsIntoProjectDescriptions(
config: ProjectsConfigurations,
projects: Record<string, ProjectConfiguration>,
nxJson: NxJsonConfiguration
) {
for (const proj of Object.values(config.projects)) {
for (const proj of Object.values(projects)) {
if (proj.targets) {
for (const targetName of Object.keys(proj.targets)) {
const projectTargetDefinition = proj.targets[targetName];
@@ -163,7 +166,7 @@ export class Workspaces {
}
}
}
return config;
return projects;
}
isNxExecutor(nodeModule: string, executor: string) {
@@ -808,7 +811,7 @@ export function buildProjectsConfigurationsFromProjectPaths(
projectFiles: string[], // making this parameter allows devkit to pick up newly created projects
readJson: <T extends Object>(string) => T = <T extends Object>(string) =>
readJsonFile<T>(string) // making this an arg allows us to reuse in devkit
): ProjectsConfigurations {
): Record<string, ProjectConfiguration> {
const projects: Record<string, ProjectConfiguration> = {};
for (const file of projectFiles) {
@@ -868,10 +871,7 @@ export function buildProjectsConfigurationsFromProjectPaths(
}
}
return {
version: 2,
projects: projects,
};
return projects;
}
export function mergeTargetConfigurations(
+40 -23
View File
@@ -37,6 +37,12 @@ export type ChangedFile = {
type: 'create' | 'update' | 'delete';
};
enum DaemonStatus {
CONNECTING,
DISCONNECTED,
CONNECTED,
}
export class DaemonClient {
constructor(private readonly nxJson: NxJsonConfiguration) {
this.reset();
@@ -50,7 +56,9 @@ export class DaemonClient {
private currentReject;
private _enabled: boolean | undefined;
private _connected: boolean;
private _daemonStatus: DaemonStatus = DaemonStatus.DISCONNECTED;
private _waitForDaemonReady: Promise<void> | null = null;
private _daemonReady: () => void | null = null;
private _out: FileHandle = null;
private _err: FileHandle = null;
@@ -104,7 +112,10 @@ export class DaemonClient {
this._out = null;
this._err = null;
this._connected = false;
this._daemonStatus = DaemonStatus.DISCONNECTED;
this._waitForDaemonReady = new Promise<void>(
(resolve) => (this._daemonReady = resolve)
);
}
async requestShutdown(): Promise<void> {
@@ -149,27 +160,28 @@ export class DaemonClient {
) => void
): Promise<UnregisterCallback> {
await this.getProjectGraph();
const messenger = new SocketMessenger(connect(FULL_OS_SOCKET_PATH)).listen(
(message) => {
try {
const parsedMessage = JSON.parse(message);
callback(null, parsedMessage);
} catch (e) {
callback(e, null);
}
},
() => {
callback('closed', null);
},
(err) => callback(err, null)
);
let messenger: SocketMessenger | undefined;
await this.queue.sendToQueue(() =>
messenger.sendMessage({ type: 'REGISTER_FILE_WATCHER', config })
);
await this.queue.sendToQueue(() => {
messenger = new SocketMessenger(connect(FULL_OS_SOCKET_PATH)).listen(
(message) => {
try {
const parsedMessage = JSON.parse(message);
callback(null, parsedMessage);
} catch (e) {
callback(e, null);
}
},
() => {
callback('closed', null);
},
(err) => callback(err, null)
);
return messenger.sendMessage({ type: 'REGISTER_FILE_WATCHER', config });
});
return () => {
messenger.close();
messenger?.close();
};
}
@@ -232,7 +244,7 @@ export class DaemonClient {
// it's ok for the daemon to terminate if the client doesn't wait on
// any messages from the daemon
if (this.queue.isEmpty()) {
this._connected = false;
this.reset();
} else {
output.error({
title: 'Daemon process terminated and closed the connection',
@@ -280,12 +292,17 @@ export class DaemonClient {
}
private async sendMessageToDaemon(message: Message): Promise<any> {
if (!this._connected) {
this._connected = true;
if (this._daemonStatus == DaemonStatus.DISCONNECTED) {
this._daemonStatus = DaemonStatus.CONNECTING;
if (!(await this.isServerAvailable())) {
await this.startInBackground();
}
this.setUpConnection();
this._daemonStatus = DaemonStatus.CONNECTED;
this._daemonReady();
} else if (this._daemonStatus == DaemonStatus.CONNECTING) {
await this._waitForDaemonReady;
}
return new Promise((resolve, reject) => {
@@ -22,6 +22,7 @@ import {
retrieveWorkspaceFiles,
retrieveProjectConfigurations,
} from '../../project-graph/utils/retrieve-workspace-files';
import { ProjectConfiguration } from '../../config/workspace-json-project-json';
let cachedSerializedProjectGraphPromise: Promise<{
error: Error | null;
@@ -115,8 +116,18 @@ export function addUpdatedAndDeletedFiles(
}
}
function computeWorkspaceConfigHash(projectsConfigurations: any) {
return hashArray([JSON.stringify(projectsConfigurations)]);
function computeWorkspaceConfigHash(
projectsConfigurations: Record<string, ProjectConfiguration>
) {
const projectConfigurationStrings = Object.entries(projectsConfigurations)
.sort(([projectNameA], [projectNameB]) =>
projectNameA.localeCompare(projectNameB)
)
.map(
([projectName, projectConfig]) =>
`${projectName}:${JSON.stringify(projectConfig)}`
);
return hashArray(projectConfigurationStrings);
}
/**
@@ -198,7 +198,7 @@ function readAndCombineAllProjectConfigurations(tree: Tree): {
nxJson,
projectFiles,
(file) => readJson(tree, file)
).projects;
);
}
/**
+146
View File
@@ -1161,6 +1161,152 @@ describe('TaskHasher', () => {
expect(hash.value).toContain('|5.0.0|');
});
it('should hash entire subtree in a deterministic way', async () => {
const createHasher = () =>
new InProcessTaskHasher(
{},
allWorkspaceFiles,
{
nodes: {
appA: {
name: 'appA',
type: 'app',
data: {
root: 'apps/appA',
targets: { build: { executor: '@nx/webpack:webpack' } },
},
},
appB: {
name: 'appB',
type: 'app',
data: {
root: 'apps/appB',
targets: { build: { executor: '@nx/webpack:webpack' } },
},
},
},
externalNodes: {
'npm:packageA': {
name: 'npm:packageA',
type: 'npm',
data: {
packageName: 'packageA',
version: '0.0.0',
hash: '$packageA0.0.0$',
},
},
'npm:packageB': {
name: 'npm:packageB',
type: 'npm',
data: {
packageName: 'packageB',
version: '0.0.0',
hash: '$packageB0.0.0$',
},
},
'npm:packageC': {
name: 'npm:packageC',
type: 'npm',
data: {
packageName: 'packageC',
version: '0.0.0',
hash: '$packageC0.0.0$',
},
},
},
dependencies: {
appA: [
{
source: 'appA',
target: 'npm:packageA',
type: DependencyType.static,
},
{
source: 'appA',
target: 'npm:packageB',
type: DependencyType.static,
},
{
source: 'appA',
target: 'npm:packageC',
type: DependencyType.static,
},
],
appB: [
{
source: 'appB',
target: 'npm:packageC',
type: DependencyType.static,
},
],
'npm:packageC': [
{
source: 'npm:packageC',
target: 'npm:packageA',
type: DependencyType.static,
},
{
source: 'npm:packageC',
target: 'npm:packageB',
type: DependencyType.static,
},
],
'npm:packageB': [
{
source: 'npm:packageB',
target: 'npm:packageA',
type: DependencyType.static,
},
],
'npm:packageA': [
{
source: 'npm:packageA',
target: 'npm:packageC',
type: DependencyType.static,
},
],
},
},
{
roots: ['app-build'],
tasks: {
'app-build': {
id: 'app-build',
target: { project: 'app', target: 'build' },
overrides: {},
},
},
dependencies: {},
},
{} as any,
{},
fileHasher
);
const computeTaskHash = async (hasher, appName) => {
const hashAppA = await hasher.hashTask({
target: { project: appName, target: 'build' },
id: `${appName}-build`,
overrides: { prop: 'prop-value' },
});
return hashAppA.value;
};
const hasher1 = createHasher();
const hasAppA1 = await computeTaskHash(hasher1, 'appA');
const hashAppB1 = await computeTaskHash(hasher1, 'appB');
const hasher2 = createHasher();
const hashAppB2 = await computeTaskHash(hasher2, 'appB');
const hasAppA2 = await computeTaskHash(hasher2, 'appA');
expect(hashAppB1).toEqual(hashAppB2);
expect(hasAppA1).toEqual(hasAppA2);
});
it('should not hash when nx:run-commands executor', async () => {
const hasher = new InProcessTaskHasher(
{},
+31 -12
View File
@@ -332,7 +332,7 @@ class TaskHasherImpl {
visited
);
} else {
const hash = this.hashExternalDependency(d.target);
const { hash } = this.hashExternalDependency(d.target);
return {
value: hash,
details: {
@@ -410,15 +410,19 @@ class TaskHasherImpl {
private hashExternalDependency(
projectName: string,
visited = new Set<string>()
): string {
parentProjects = new Set<string>()
): { fullyResolved: boolean; hash: string } {
// try to retrieve the hash from cache
if (this.externalDepsHashCache[projectName]) {
return this.externalDepsHashCache[projectName];
return {
fullyResolved: true,
hash: this.externalDepsHashCache[projectName],
};
}
visited.add(projectName);
parentProjects.add(projectName);
const node = this.projectGraph.externalNodes[projectName];
let partialHash: string;
let fullyResolved = true;
if (node) {
const partialHashes: string[] = [];
if (node.data.hash) {
@@ -431,11 +435,22 @@ class TaskHasherImpl {
// we want to calculate the hash of the entire dependency tree
if (this.projectGraph.dependencies[projectName]) {
this.projectGraph.dependencies[projectName].forEach((d) => {
if (!visited.has(d.target)) {
partialHashes.push(this.hashExternalDependency(d.target, visited));
if (!parentProjects.has(d.target)) {
const hashResult = this.hashExternalDependency(
d.target,
new Set(parentProjects)
);
partialHashes.push(hashResult.hash);
if (!hashResult.fullyResolved) {
fullyResolved = false;
}
} else {
// NOTE: do not store hash to cache since it is only a partial hash
fullyResolved = false;
}
});
}
partialHash = hashArray(partialHashes);
} else {
// unknown dependency
@@ -444,8 +459,12 @@ class TaskHasherImpl {
// in this case we have no information about the versioning of the given package
partialHash = `__${projectName}__`;
}
this.externalDepsHashCache[projectName] = partialHash;
return partialHash;
if (fullyResolved) {
this.externalDepsHashCache[projectName] = partialHash;
}
return { fullyResolved, hash: partialHash };
}
private hashTarget(
@@ -460,7 +479,7 @@ class TaskHasherImpl {
return;
}
let hash;
let hash: string;
// we can only vouch for @nx packages's executor dependencies
// if it's "run commands" or third-party we skip traversing since we have no info what this command depends on
if (
@@ -470,7 +489,7 @@ class TaskHasherImpl {
const executorPackage = target.executor.split(':')[0];
const executorNodeName =
this.findExternalDependencyNodeName(executorPackage);
hash = this.hashExternalDependency(executorNodeName);
hash = this.hashExternalDependency(executorNodeName).hash;
} else {
// use command external dependencies if available to construct the hash
const partialHashes: string[] = [];
@@ -482,7 +501,7 @@ class TaskHasherImpl {
const externalDependencies = input['externalDependencies'];
for (let dep of externalDependencies) {
dep = this.findExternalDependencyNodeName(dep);
partialHashes.push(this.hashExternalDependency(dep));
partialHashes.push(this.hashExternalDependency(dep).hash);
}
}
}
+16
View File
@@ -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"]
);
}
}
+3 -4
View File
@@ -37,14 +37,13 @@ export const enum WorkspaceErrors {
Generic = 'Generic'
}
/** Get workspace config files based on provided globs */
export function getConfigFiles(workspaceRoot: string, globs: Array<string>): Array<string>
export function getProjectConfigurations(workspaceRoot: string, globs: Array<string>, parseConfigurations: (arg0: Array<string>) => Record<string, object>): Record<string, object>
export interface NxWorkspaceFiles {
projectFileMap: Record<string, Array<FileData>>
globalFiles: Array<FileData>
configFiles: Array<string>
projectConfigurations: Record<string, object>
}
/** Throws exceptions */
export function getWorkspaceFilesNative(workspaceRoot: string, globs: Array<string>): NxWorkspaceFiles
export function getWorkspaceFilesNative(workspaceRoot: string, globs: Array<string>, parseConfigurations: (arg0: Array<string>) => Record<string, object>): NxWorkspaceFiles
export class Watcher {
origin: string
/**
+2 -2
View File
@@ -246,7 +246,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { expandOutputs, remove, copy, hashArray, hashFile, hashFiles, hashFilesMatchingGlobs, EventType, Watcher, WorkspaceErrors, getConfigFiles, getWorkspaceFilesNative } = nativeBinding
const { expandOutputs, remove, copy, hashArray, hashFile, hashFiles, hashFilesMatchingGlobs, EventType, Watcher, WorkspaceErrors, getProjectConfigurations, getWorkspaceFilesNative } = nativeBinding
module.exports.expandOutputs = expandOutputs
module.exports.remove = remove
@@ -258,5 +258,5 @@ module.exports.hashFilesMatchingGlobs = hashFilesMatchingGlobs
module.exports.EventType = EventType
module.exports.Watcher = Watcher
module.exports.WorkspaceErrors = WorkspaceErrors
module.exports.getConfigFiles = getConfigFiles
module.exports.getProjectConfigurations = getProjectConfigurations
module.exports.getWorkspaceFilesNative = getWorkspaceFilesNative
+20 -16
View File
@@ -27,7 +27,7 @@ describe('watcher', () => {
});
it('should trigger the callback for files that are not ignored', (done) => {
watcher = new Watcher(realpathSync(temp.tempDir));
watcher = new Watcher(temp.tempDir);
watcher.watch((error, paths) => {
expect(paths).toMatchInlineSnapshot(`
[
@@ -48,7 +48,7 @@ describe('watcher', () => {
});
it('should trigger the callback when files are updated', (done) => {
watcher = new Watcher(realpathSync(temp.tempDir));
watcher = new Watcher(temp.tempDir);
watcher.watch((err, paths) => {
expect(paths).toMatchInlineSnapshot(`
@@ -62,7 +62,7 @@ describe('watcher', () => {
done();
});
wait().then(() => {
wait(1000).then(() => {
// nxignored file should not trigger a callback
temp.appendFile('app2/main.js', 'update');
temp.appendFile('app1/main.js', 'update');
@@ -70,18 +70,22 @@ describe('watcher', () => {
});
it('should watch file renames', (done) => {
watcher = new Watcher(realpathSync(temp.tempDir));
watcher = new Watcher(temp.tempDir);
watcher.watch((err, paths) => {
expect(paths.length).toBe(2);
expect(paths.find((p) => p.type === 'update')).toMatchObject({
path: 'app1/rename.js',
type: 'update',
});
expect(paths.find((p) => p.type === 'delete')).toMatchObject({
path: 'app1/main.js',
type: 'delete',
});
expect(paths.find((p) => p.type === 'create')).toMatchInlineSnapshot(`
{
"path": "app1/rename.js",
"type": "create",
}
`);
expect(paths.find((p) => p.type === 'delete')).toMatchInlineSnapshot(`
{
"path": "app1/main.js",
"type": "delete",
}
`);
done();
});
@@ -91,7 +95,7 @@ describe('watcher', () => {
});
it('should trigger on deletes', (done) => {
watcher = new Watcher(realpathSync(temp.tempDir));
watcher = new Watcher(temp.tempDir);
watcher.watch((err, paths) => {
expect(paths).toMatchInlineSnapshot(`
@@ -111,7 +115,7 @@ describe('watcher', () => {
});
it('should ignore nested gitignores', (done) => {
watcher = new Watcher(realpathSync(temp.tempDir));
watcher = new Watcher(temp.tempDir);
watcher.watch((err, paths) => {
expect(paths).toMatchInlineSnapshot(`
@@ -133,10 +137,10 @@ describe('watcher', () => {
});
});
function wait() {
function wait(timeout = 500) {
return new Promise<void>((res) => {
setTimeout(() => {
res();
}, 500);
}, timeout);
});
}
@@ -1,12 +1,24 @@
import {
getConfigFiles,
getWorkspaceFilesNative,
WorkspaceErrors,
} from '../index';
import { getProjectConfigurations, getWorkspaceFilesNative } from '../index';
import { TempFs } from '../../utils/testing/temp-fs';
import { NxJsonConfiguration } from '../../config/nx-json';
import { dirname, join } from 'path';
import { readJsonFile } from '../../utils/fileutils';
describe('workspace files', () => {
function createParseConfigurationsFunction(tempDir: string) {
return (filenames: string[]) => {
const res = {};
for (const filename of filenames) {
const json = readJsonFile(join(tempDir, filename));
res[json.name] = {
...json,
root: dirname(filename),
};
}
return res;
};
}
it('should gather workspace file information', async () => {
const fs = new TempFs('workspace-files');
const nxJson: NxJsonConfiguration = {};
@@ -41,12 +53,16 @@ describe('workspace files', () => {
});
let globs = ['project.json', '**/project.json', 'libs/*/package.json'];
let { projectFileMap, configFiles, globalFiles } = getWorkspaceFilesNative(
fs.tempDir,
globs
);
let { projectFileMap, projectConfigurations, globalFiles } =
getWorkspaceFilesNative(
fs.tempDir,
globs,
createParseConfigurationsFunction(fs.tempDir)
);
let sortedConfigs = configFiles.sort();
let sortedConfigs = Object.values(projectConfigurations).sort((a, b) =>
a['name'].localeCompare(b['name'])
);
expect(projectFileMap).toMatchInlineSnapshot(`
{
@@ -104,11 +120,26 @@ describe('workspace files', () => {
`);
expect(sortedConfigs).toMatchInlineSnapshot(`
[
"libs/nested/project/project.json",
"libs/package-project/package.json",
"libs/project1/project.json",
"libs/project2/project.json",
"libs/project3/project.json",
{
"name": "nested-project",
"root": "libs/nested/project",
},
{
"name": "package-project",
"root": "libs/package-project",
},
{
"name": "project1",
"root": "libs/project1",
},
{
"name": "project2",
"root": "libs/project2",
},
{
"name": "project3",
"root": "libs/project3",
},
]
`);
expect(globalFiles).toMatchInlineSnapshot(`
@@ -148,7 +179,8 @@ describe('workspace files', () => {
const globs = ['project.json', '**/project.json', '**/package.json'];
const { globalFiles, projectFileMap } = getWorkspaceFilesNative(
fs.tempDir,
globs
globs,
createParseConfigurationsFunction(fs.tempDir)
);
expect(globalFiles).toEqual([]);
@@ -201,140 +233,117 @@ describe('workspace files', () => {
});
let globs = ['project.json', '**/project.json', '**/package.json'];
let { configFiles } = getWorkspaceFilesNative(fs.tempDir, globs);
configFiles = configFiles.sort();
expect(configFiles).toMatchInlineSnapshot(`
[
"libs/project1/project.json",
"project.json",
]
`);
let configFiles2 = getConfigFiles(fs.tempDir, globs).sort();
expect(configFiles2).toMatchInlineSnapshot(`
[
"libs/project1/project.json",
"project.json",
]
`);
});
describe('errors', () => {
it('it should infer names of configuration files without a name', async () => {
const fs = new TempFs('workspace-files');
const nxJson: NxJsonConfiguration = {};
await fs.createFiles({
'./nx.json': JSON.stringify(nxJson),
'./package.json': JSON.stringify({
name: 'repo-name',
version: '0.0.0',
dependencies: {},
}),
'./libs/project1/project.json': JSON.stringify({
name: 'project1',
}),
'./libs/project1/index.js': '',
'./libs/project2/project.json': JSON.stringify({}),
});
let globs = ['project.json', '**/project.json', 'libs/*/package.json'];
expect(getWorkspaceFilesNative(fs.tempDir, globs).projectFileMap)
.toMatchInlineSnapshot(`
{
"project1": [
{
"file": "libs/project1/index.js",
"hash": "3244421341483603138",
},
{
"file": "libs/project1/project.json",
"hash": "13466615737813422520",
},
],
"project2": [
{
"file": "libs/project2/project.json",
"hash": "1389868326933519382",
},
],
let projectConfigurations = getProjectConfigurations(
fs.tempDir,
globs,
(filenames) => {
const res = {};
for (const filename of filenames) {
const json = readJsonFile(join(fs.tempDir, filename));
res[json.name] = {
...json,
root: dirname(filename),
};
}
`);
});
it('handles comments', async () => {
const fs = new TempFs('workspace-files');
const nxJson: NxJsonConfiguration = {};
await fs.createFiles({
'./nx.json': JSON.stringify(nxJson),
'./package.json': JSON.stringify({
name: 'repo-name',
version: '0.0.0',
dependencies: {},
}),
'./libs/project1/project.json': `{
"name": "temp"
// this should not fail
}`,
'./libs/project1/index.js': '',
});
let globs = ['project.json', '**/project.json', 'libs/*/package.json'];
expect(() => getWorkspaceFilesNative(fs.tempDir, globs)).not.toThrow();
});
it('handles extra comma', async () => {
const fs = new TempFs('workspace-files');
const nxJson: NxJsonConfiguration = {};
await fs.createFiles({
'./nx.json': JSON.stringify(nxJson),
'./package.json': JSON.stringify({
name: 'repo-name',
version: '0.0.0',
dependencies: {},
}),
'./libs/project1/project.json': `{
"name": "temp",
}`,
'./libs/project1/index.js': '',
});
let globs = ['**/project.json'];
expect(() => getWorkspaceFilesNative(fs.tempDir, globs)).not.toThrow();
});
it('throws parsing errors: missing brackets', async () => {
const fs = new TempFs('workspace-files');
const nxJson: NxJsonConfiguration = {};
await fs.createFiles({
'./nx.json': JSON.stringify(nxJson),
'./package.json': JSON.stringify({
name: 'repo-name',
version: '0.0.0',
dependencies: {},
}),
'./libs/project1/project.json': `{
"name": "temp", "property": "child": 2 }
}`,
'./libs/project1/index.js': '',
});
let globs = ['**/project.json'];
const error = getError(() => getWorkspaceFilesNative(fs.tempDir, globs));
expect(error.message).toMatchInlineSnapshot(
`"libs/project1/project.json"`
);
expect(error).toHaveProperty('code', WorkspaceErrors.ParseError);
});
return res;
}
);
expect(projectConfigurations).toMatchInlineSnapshot(`
{
"project1": {
"name": "project1",
"root": "libs/project1",
},
"repo-name": {
"name": "repo-name",
"root": ".",
},
}
`);
});
});
const getError = (fn: () => unknown): Error => {
try {
fn();
} catch (error: unknown) {
return error as Error;
}
};
// describe('errors', () => {
// it('it should infer names of configuration files without a name', async () => {
// const fs = new TempFs('workspace-files');
// const nxJson: NxJsonConfiguration = {};
// await fs.createFiles({
// './nx.json': JSON.stringify(nxJson),
// './package.json': JSON.stringify({
// name: 'repo-name',
// version: '0.0.0',
// dependencies: {},
// }),
// './libs/project1/project.json': JSON.stringify({
// name: 'project1',
// }),
// './libs/project1/index.js': '',
// './libs/project2/project.json': JSON.stringify({}),
// });
//
// let globs = ['project.json', '**/project.json', 'libs/*/package.json'];
// expect(getWorkspaceFilesNative(fs.tempDir, globs).projectFileMap)
// .toMatchInlineSnapshot(`
// {
// "libs/project1": [
// {
// "file": "libs/project1/index.js",
// "hash": "3244421341483603138",
// },
// {
// "file": "libs/project1/project.json",
// "hash": "13466615737813422520",
// },
// ],
// "libs/project2": [
// {
// "file": "libs/project2/project.json",
// "hash": "1389868326933519382",
// },
// ],
// }
// `);
// });
//
// it('handles comments', async () => {
// const fs = new TempFs('workspace-files');
// const nxJson: NxJsonConfiguration = {};
// await fs.createFiles({
// './nx.json': JSON.stringify(nxJson),
// './package.json': JSON.stringify({
// name: 'repo-name',
// version: '0.0.0',
// dependencies: {},
// }),
// './libs/project1/project.json': `{
// "name": "temp"
// // this should not fail
// }`,
// './libs/project1/index.js': '',
// });
//
// let globs = ['project.json', '**/project.json', 'libs/*/package.json'];
// expect(() => getWorkspaceFilesNative(fs.tempDir, globs)).not.toThrow();
// });
//
// it('handles extra comma', async () => {
// const fs = new TempFs('workspace-files');
// const nxJson: NxJsonConfiguration = {};
// await fs.createFiles({
// './nx.json': JSON.stringify(nxJson),
// './package.json': JSON.stringify({
// name: 'repo-name',
// version: '0.0.0',
// dependencies: {},
// }),
// './libs/project1/project.json': `{
// "name": "temp",
// }`,
// './libs/project1/index.js': '',
// });
//
// let globs = ['**/project.json'];
// expect(() => getWorkspaceFilesNative(fs.tempDir, globs)).not.toThrow();
// });
// });
});
+4
View File
@@ -20,6 +20,10 @@ fn normalize_path<P>(path: P) -> String
where
P: AsRef<Path>,
{
if path.as_ref() == Path::new("") {
return ".".into();
}
// convert back-slashes in Windows paths, since the js expects only forward-slash path separators
if cfg!(windows) {
path.as_ref().display().to_string().replace('\\', "/")
+41 -9
View File
@@ -1,7 +1,9 @@
use napi::bindgen_prelude::*;
use std::path::PathBuf;
use tracing::trace;
use watchexec_events::filekind::FileEventKind;
use watchexec_events::filekind::ModifyKind::Name;
use watchexec_events::filekind::RenameMode;
use watchexec_events::{Event, Tag};
#[napi(string_enum)]
@@ -24,11 +26,11 @@ pub struct WatchEvent {
pub r#type: EventType,
}
impl From<WatchEventInternal> for WatchEvent {
fn from(value: WatchEventInternal) -> Self {
impl From<&WatchEventInternal> for WatchEvent {
fn from(value: &WatchEventInternal) -> Self {
let path = value
.path
.strip_prefix(&value.origin.expect("origin is available"))
.strip_prefix(value.origin.as_ref().expect("origin is available"))
.unwrap_or(&value.path)
.display()
.to_string();
@@ -67,11 +69,41 @@ impl From<&Event> for WatchEventInternal {
let event_type = if matches!(path.1, None) && !path_ref.exists() {
EventType::delete
} else {
match event_kind {
FileEventKind::Create(_) => EventType::create,
FileEventKind::Modify(_) => EventType::update,
FileEventKind::Remove(_) => EventType::delete,
_ => EventType::update,
#[cfg(target_os = "macos")]
{
use std::fs;
use std::os::macos::fs::MetadataExt;
let t = fs::metadata(path_ref);
match t {
Err(_) => EventType::delete,
Ok(t) => {
let modified_time = t.st_mtime();
let birth_time = t.st_birthtime();
// if a file is created and updated near the same time, we always get a create event
// so we need to check the timestamps to see if it was created or updated
// if the modified time is the same as birth_time then it was created
if modified_time == birth_time {
EventType::create
} else {
EventType::update
}
}
}
}
#[cfg(not(target_os = "macos"))]
{
use watchexec_events::filekind::FileEventKind;
match event_kind {
FileEventKind::Create(_) => EventType::create,
FileEventKind::Modify(Name(RenameMode::To)) => EventType::create,
FileEventKind::Modify(Name(RenameMode::From)) => EventType::delete,
FileEventKind::Modify(_) => EventType::update,
_ => EventType::update,
}
}
};
@@ -2,7 +2,6 @@ use crate::native::watch::utils::get_ignore_files;
use crate::native::watch::watch_filterer::WatchFilterer;
use ignore_files::IgnoreFilter;
use std::sync::Arc;
use std::time::Duration;
use tracing::trace;
use watchexec::config::RuntimeConfig;
use watchexec_filterer_ignore::IgnoreFilterer;
@@ -36,7 +35,6 @@ pub(super) async fn create_runtime(
runtime.filterer(Arc::new(WatchFilterer {
inner: IgnoreFilterer(filter),
}));
runtime.action_throttle(Duration::from_millis(500));
// let watch_directories = get_watch_directories(origin);
// trace!(directories = ?watch_directories, "watching");
+38 -23
View File
@@ -1,10 +1,10 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::convert::Infallible;
use std::path::MAIN_SEPARATOR;
use std::sync::Arc;
use crate::native::watch::types::{WatchEvent, WatchEventInternal};
use itertools::Itertools;
use crate::native::watch::types::{EventType, WatchEvent, WatchEventInternal};
use napi::bindgen_prelude::*;
use napi::threadsafe_function::{
ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,
@@ -68,27 +68,22 @@ impl Watcher {
.with_env_filter(EnvFilter::from_env("NX_NATIVE_LOGGING"))
.try_init();
let mut callback_tsfn: ThreadsafeFunction<HashMap<String, Vec<WatchEventInternal>>> =
callback.create_threadsafe_function(
0,
|ctx: ThreadSafeCallContext<HashMap<String, Vec<WatchEventInternal>>>| {
let mut watch_events: Vec<WatchEvent> = vec![];
trace!(?ctx.value, "Base collection that will be sent");
let mut callback_tsfn: ThreadsafeFunction<HashMap<String, WatchEventInternal>> = callback
.create_threadsafe_function(
0,
|ctx: ThreadSafeCallContext<HashMap<String, WatchEventInternal>>| {
let mut watch_events: Vec<WatchEvent> = vec![];
trace!(?ctx.value, "Base collection that will be sent");
for (_, value) in ctx.value {
let event = value
.first()
.expect("should always have at least 1 element")
.to_owned();
for event in ctx.value.values() {
watch_events.push(event.into());
}
watch_events.push(event.into());
}
trace!(?watch_events, "sending to node");
trace!(?watch_events, "sending to node");
Ok(vec![watch_events])
},
)?;
Ok(vec![watch_events])
},
)?;
callback_tsfn.unref(&env)?;
@@ -150,10 +145,30 @@ impl Watcher {
})
.collect::<Vec<WatchEventInternal>>();
let group_events = events
.into_iter()
.into_group_map_by(|g| g.path.display().to_string());
let mut group_events: HashMap<String, WatchEventInternal> = HashMap::new();
for g in events.into_iter() {
let path = g.path.display().to_string();
// Delete > Create > Modify
match group_events.entry(path) {
// Delete should override anything
Entry::Occupied(mut e) if matches!(g.r#type, EventType::delete) => {
e.insert(g);
}
// Create should override update
Entry::Occupied(mut e)
if matches!(g.r#type, EventType::create)
&& matches!(e.get().r#type, EventType::update) =>
{
e.insert(g);
}
Entry::Occupied(_) => {}
// If its empty, insert
Entry::Vacant(e) => {
e.insert(g);
}
}
}
callback_tsfn.call(Ok(group_events), ThreadsafeFunctionCallMode::NonBlocking);
action.outcome(Outcome::Start);
@@ -2,29 +2,42 @@ use crate::native::utils::glob::build_glob_set;
use crate::native::utils::path::Normalize;
use crate::native::walker::nx_walker;
use globset::GlobSet;
use napi::JsObject;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[napi]
/// Get workspace config files based on provided globs
pub fn get_config_files(workspace_root: String, globs: Vec<String>) -> anyhow::Result<Vec<String>> {
pub fn get_project_configurations<ConfigurationParser>(
workspace_root: String,
globs: Vec<String>,
parse_configurations: ConfigurationParser,
) -> napi::Result<HashMap<String, JsObject>>
where
ConfigurationParser: Fn(Vec<String>) -> napi::Result<HashMap<String, JsObject>>,
{
let globs = build_glob_set(globs)?;
Ok(nx_walker(workspace_root, move |rec| {
let mut config_paths: HashMap<PathBuf, (PathBuf, Vec<u8>)> = HashMap::new();
for (path, content) in rec {
insert_config_file_into_map((path, content), &mut config_paths, &globs);
let config_paths: Vec<String> = nx_walker(workspace_root, move |rec| {
let mut config_paths: HashMap<PathBuf, PathBuf> = HashMap::new();
for (path, _) in rec {
insert_config_file_into_map(path, &mut config_paths, &globs);
}
config_paths
.into_iter()
.map(|(_, (val, _))| val.to_normalized_string())
.into_values()
.map(|p| p.to_normalized_string())
.collect()
}))
});
parse_configurations(config_paths)
}
pub fn insert_config_file_into_map(
(path, content): (PathBuf, Vec<u8>),
config_paths: &mut HashMap<PathBuf, (PathBuf, Vec<u8>)>,
path: PathBuf,
config_paths: &mut HashMap<PathBuf, PathBuf>,
globs: &GlobSet,
) {
if globs.is_match(&path) {
@@ -34,25 +47,24 @@ pub fn insert_config_file_into_map(
.file_name()
.expect("Config paths always have file names");
if file_name == "project.json" {
config_paths.insert(parent, (path, content));
config_paths.insert(parent, path);
} else if file_name == "package.json" {
match config_paths.entry(parent) {
Entry::Occupied(mut o) => {
if o.get()
.0
.file_name()
.expect("Config paths always have file names")
!= "project.json"
{
o.insert((path, content));
o.insert(path);
}
}
Entry::Vacant(v) => {
v.insert((path, content));
v.insert(path);
}
}
} else {
config_paths.entry(parent).or_insert((path, content));
config_paths.entry(parent).or_insert(path);
}
}
}
@@ -65,34 +77,23 @@ mod test {
#[test]
fn should_insert_config_files_properly() {
let mut config_paths: HashMap<PathBuf, (PathBuf, Vec<u8>)> = HashMap::new();
let mut config_paths: HashMap<PathBuf, PathBuf> = HashMap::new();
let globs = build_glob_set(vec!["**/*".into()]).unwrap();
insert_config_file_into_map(PathBuf::from("project.json"), &mut config_paths, &globs);
insert_config_file_into_map(PathBuf::from("package.json"), &mut config_paths, &globs);
insert_config_file_into_map(
(PathBuf::from("project.json"), vec![]),
PathBuf::from("lib1/project.json"),
&mut config_paths,
&globs,
);
insert_config_file_into_map(
(PathBuf::from("package.json"), vec![]),
&mut config_paths,
&globs,
);
insert_config_file_into_map(
(PathBuf::from("lib1/project.json"), vec![]),
&mut config_paths,
&globs,
);
insert_config_file_into_map(
(PathBuf::from("lib2/package.json"), vec![]),
PathBuf::from("lib2/package.json"),
&mut config_paths,
&globs,
);
let config_files: Vec<PathBuf> = config_paths
.into_iter()
.map(|(_, (path, _))| path)
.collect();
let config_files: Vec<PathBuf> = config_paths.into_values().collect();
assert!(config_files.contains(&PathBuf::from("project.json")));
assert!(config_files.contains(&PathBuf::from("lib1/project.json")));
@@ -1,5 +1,7 @@
use jsonc_parser::ParseOptions;
use std::collections::HashMap;
use itertools::Itertools;
use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode};
use napi::{JsFunction, JsObject, JsUnknown, Status};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use rayon::prelude::*;
@@ -13,21 +15,24 @@ use crate::native::utils::path::Normalize;
use crate::native::walker::nx_walker;
use crate::native::workspace::errors::{InternalWorkspaceErrors, WorkspaceErrors};
use crate::native::workspace::get_config_files::insert_config_file_into_map;
use crate::native::workspace::types::{FileLocation, ProjectConfiguration};
use crate::native::workspace::types::FileLocation;
#[napi(object)]
pub struct NxWorkspaceFiles {
pub project_file_map: HashMap<String, Vec<FileData>>,
pub global_files: Vec<FileData>,
pub config_files: Vec<String>,
pub project_configurations: HashMap<String, JsObject>,
}
#[napi]
/// Throws exceptions
pub fn get_workspace_files_native(
pub fn get_workspace_files_native<ConfigurationParser>(
workspace_root: String,
globs: Vec<String>,
) -> napi::Result<NxWorkspaceFiles, WorkspaceErrors> {
parse_configurations: ConfigurationParser,
) -> napi::Result<NxWorkspaceFiles, WorkspaceErrors>
where
ConfigurationParser: Fn(Vec<String>) -> napi::Result<HashMap<String, JsObject>>,
{
enable_logger();
trace!("{workspace_root}, {globs:?}");
@@ -35,7 +40,12 @@ pub fn get_workspace_files_native(
let (projects, mut file_data) = get_file_data(&workspace_root, globs)
.map_err(|err| napi::Error::new(WorkspaceErrors::Generic, err.to_string()))?;
let root_map = create_root_map(&projects)?;
let projects_vec: Vec<String> = projects.iter().map(|p| p.to_normalized_string()).collect();
let project_configurations = parse_configurations(projects_vec)
.map_err(|e| napi::Error::new(WorkspaceErrors::ParseError, e.to_string()))?;
let root_map = create_root_map(&project_configurations);
trace!(?root_map);
@@ -46,14 +56,14 @@ pub fn get_workspace_files_native(
.into_par_iter()
.map(|file_data| {
let file_path = Path::new(&file_data.file);
let mut parent = file_path.parent().unwrap_or_else(|| Path::new(""));
let mut parent = file_path.parent().unwrap_or_else(|| Path::new("."));
while root_map.get(parent).is_none() && parent != Path::new("") {
parent = parent.parent().unwrap_or_else(|| Path::new(""));
while root_map.get(parent).is_none() && parent != Path::new(".") {
parent = parent.parent().unwrap_or_else(|| Path::new("."));
}
match root_map.get(parent) {
Some(project_name) => (FileLocation::Project(project_name.clone()), file_data),
Some(project_name) => (FileLocation::Project(project_name.into()), file_data),
None => (FileLocation::Global, file_data),
}
})
@@ -76,7 +86,7 @@ pub fn get_workspace_files_native(
FileLocation::Global => global_files.push(file_data),
FileLocation::Project(project_name) => match project_file_map.get_mut(&project_name) {
None => {
project_file_map.insert(project_name, vec![file_data]);
project_file_map.insert(project_name.clone(), vec![file_data]);
}
Some(project_files) => project_files.push(file_data),
},
@@ -86,98 +96,34 @@ pub fn get_workspace_files_native(
Ok(NxWorkspaceFiles {
project_file_map,
global_files,
config_files: projects
.keys()
.map(|path| path.to_normalized_string())
.collect(),
project_configurations,
})
}
fn create_root_map(
projects: &HashMap<PathBuf, Vec<u8>>,
) -> Result<hashbrown::HashMap<&Path, String>, InternalWorkspaceErrors> {
projects
.par_iter()
.map(|(path, content)| {
let file_name = path
.file_name()
.expect("path should always have a filename");
return if file_name == "project.json" || file_name == "package.json" {
// use serde_json to do the initial parse, if that fails fall back to jsonc_parser.
// If all those fail, expose the error from jsonc_parser
let project_configuration: ProjectConfiguration =
read_project_configuration(content, path)?;
let Some(parent_path) = path.parent() else {
return Err(InternalWorkspaceErrors::Generic {
msg: format!("{path:?} has no parent"),
})
};
let name: String = if let Some(name) = project_configuration.name {
Ok(name)
} else {
parent_path
.file_name()
.unwrap_or_default()
.to_os_string()
.into_string()
.map_err(|os_string| InternalWorkspaceErrors::Generic {
msg: format!("Cannot turn {os_string:?} into String"),
})
}?;
Ok((parent_path, name))
} else if let Some(parent_path) = path.parent() {
Ok((
parent_path,
parent_path
.file_name()
.unwrap_or_default()
.to_os_string()
.into_string()
.map_err(|os_string| InternalWorkspaceErrors::Generic {
msg: format!("Cannot turn {os_string:?} into String"),
})?,
))
} else {
Err(InternalWorkspaceErrors::Generic {
msg: format!("{path:?} has no parent"),
})
};
project_configurations: &HashMap<String, JsObject>,
) -> hashbrown::HashMap<PathBuf, String> {
project_configurations
.iter()
.map(|(project_name, project_configuration)| {
let root: String = project_configuration.get("root").unwrap().unwrap();
(PathBuf::from(root), project_name.clone())
})
.collect()
}
fn read_project_configuration(
content: &[u8],
path: &Path,
) -> Result<ProjectConfiguration, InternalWorkspaceErrors> {
serde_json::from_slice(content).or_else(|_| {
let content_str = std::str::from_utf8(content).expect("content should be valid utf8");
let parser_value =
jsonc_parser::parse_to_serde_value(content_str, &ParseOptions::default()).map_err(
|_| InternalWorkspaceErrors::ParseError {
file: PathBuf::from(path),
},
)?;
serde_json::from_value(parser_value.into()).map_err(|_| InternalWorkspaceErrors::Generic {
msg: format!("Failed to parse {path:?}"),
})
})
}
type WorkspaceData = (HashMap<PathBuf, Vec<u8>>, Vec<FileData>);
type WorkspaceData = (HashSet<PathBuf>, Vec<FileData>);
fn get_file_data(workspace_root: &str, globs: Vec<String>) -> anyhow::Result<WorkspaceData> {
let globs = build_glob_set(globs)?;
let (projects, file_data) = nx_walker(workspace_root, move |rec| {
let mut projects: HashMap<PathBuf, (PathBuf, Vec<u8>)> = HashMap::new();
let mut projects: HashMap<PathBuf, PathBuf> = HashMap::new();
let mut file_hashes: Vec<FileData> = vec![];
for (path, content) in rec {
file_hashes.push(FileData {
file: path.to_normalized_string(),
hash: xxh3::xxh3_64(&content).to_string(),
});
insert_config_file_into_map((path, content), &mut projects, &globs)
insert_config_file_into_map(path, &mut projects, &globs)
}
(projects, file_hashes)
});
@@ -1,10 +1,3 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub(crate) struct ProjectConfiguration {
pub name: Option<String>,
}
#[derive(Debug, Eq, PartialEq)]
pub enum FileLocation {
Global,
@@ -14,7 +14,7 @@ import { workspaceRoot } from '../../../utils/workspace-root';
import { ProjectGraph } from '../../../config/project-graph';
import { ProjectGraphBuilder } from '../../../project-graph/project-graph-builder';
import { PackageJson } from '../../../utils/package-json';
import { fileHasher, hashArray } from '../../../hasher/file-hasher';
import { hashArray } from '../../../hasher/file-hasher';
import { output } from '../../../utils/output';
import { parseNpmLockfile, stringifyNpmLockfile } from './npm-parser';
@@ -22,6 +22,7 @@ import { parsePnpmLockfile, stringifyPnpmLockfile } from './pnpm-parser';
import { parseYarnLockfile, stringifyYarnLockfile } from './yarn-parser';
import { pruneProjectGraph } from './project-graph-pruning';
import { normalizePackageJson } from './utils/package-json';
import { readJsonFile } from '../../../utils/fileutils';
const YARN_LOCK_FILE = 'yarn.lock';
const NPM_LOCK_FILE = 'package-lock.json';
@@ -86,7 +87,8 @@ export function parseLockFile(
try {
if (packageManager === 'yarn') {
const content = readFileSync(YARN_LOCK_PATH, 'utf8');
parseYarnLockfile(content, builder);
const packageJson = readJsonFile('package.json');
parseYarnLockfile(content, packageJson, builder);
return builder.getUpdatedProjectGraph();
}
if (packageManager === 'pnpm') {
@@ -145,19 +147,19 @@ export function createLockFile(
): string {
const normalizedPackageJson = normalizePackageJson(packageJson);
const content = readFileSync(getLockFileName(packageManager), 'utf8');
const rootPackageJson = readJsonFile('package.json');
const builder = new ProjectGraphBuilder();
try {
if (packageManager === 'yarn') {
parseYarnLockfile(content, builder);
parseYarnLockfile(content, rootPackageJson, builder);
const graph = builder.getUpdatedProjectGraph();
const prunedGraph = pruneProjectGraph(graph, packageJson);
return stringifyYarnLockfile(prunedGraph, content, normalizedPackageJson);
}
if (packageManager === 'pnpm') {
parsePnpmLockfile(content, builder);
const graph = builder.getUpdatedProjectGraph();
const prunedGraph = pruneProjectGraph(graph, packageJson);
return stringifyPnpmLockfile(prunedGraph, content, normalizedPackageJson);
@@ -157,6 +157,7 @@ describe('yarn LockFile utility', () => {
});
let lockFile;
let packageJson;
let graph: ProjectGraph;
@@ -166,7 +167,11 @@ describe('yarn LockFile utility', () => {
__dirname,
'__fixtures__/nextjs/yarn.lock'
)).default;
parseYarnLockfile(lockFile, builder);
packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/nextjs/package.json'
));
parseYarnLockfile(lockFile, packageJson, builder);
graph = builder.getUpdatedProjectGraph();
});
@@ -219,10 +224,141 @@ describe('yarn LockFile utility', () => {
describe('auxiliary packages', () => {
beforeEach(() => {
const fileSys = {
'node_modules/@eslint/eslintrc/package.json': '{"version": "1.3.3"}',
'node_modules/@humanwhocodes/config-array/package.json':
'{"version": "0.11.7"}',
'node_modules/@humanwhocodes/module-importer/package.json':
'{"version": "1.0.1"}',
'node_modules/@humanwhocodes/object-schema/package.json':
'{"version": "1.2.1"}',
'node_modules/@nodelib/fs.scandir/package.json': '{"version": "2.1.5"}',
'node_modules/@nodelib/fs.stat/package.json': '{"version": "2.0.5"}',
'node_modules/@nodelib/fs.walk/package.json': '{"version": "1.2.8"}',
'node_modules/@nrwl/devkit/package.json': '{"version": "15.0.13"}',
'node_modules/@phenomnomnominal/tsquery/package.json':
'{"version": "4.1.1"}',
'node_modules/acorn/package.json': '{"version": "8.8.1"}',
'node_modules/acorn-jsx/package.json': '{"version": "5.3.2"}',
'node_modules/ajv/package.json': '{"version": "6.12.6"}',
'node_modules/ansi-regex/package.json': '{"version": "5.0.1"}',
'node_modules/ansi-styles/package.json': '{"version": "4.3.0"}',
'node_modules/app-root-path/package.json': '{"version": "3.1.0"}',
'node_modules/argparse/package.json': '{"version": "2.0.1"}',
'node_modules/async/package.json': '{"version": "3.2.4"}',
'node_modules/balanced-match/package.json': '{"version": "1.0.2"}',
'node_modules/brace-expansion/package.json': '{"version": "1.1.11"}',
'node_modules/callsites/package.json': '{"version": "3.1.0"}',
'node_modules/chalk/package.json': '{"version": "4.1.2"}',
'node_modules/cliui/package.json': '{"version": "8.0.1"}',
'node_modules/color-convert/package.json': '{"version": "2.0.1"}',
'node_modules/color-name/package.json': '{"version": "1.1.4"}',
'node_modules/concat-map/package.json': '{"version": "0.0.1"}',
'node_modules/cross-spawn/package.json': '{"version": "7.0.3"}',
'node_modules/debug/package.json': '{"version": "4.3.4"}',
'node_modules/deep-is/package.json': '{"version": "0.1.4"}',
'node_modules/doctrine/package.json': '{"version": "3.0.0"}',
'node_modules/ejs/package.json': '{"version": "3.1.8"}',
'node_modules/emoji-regex/package.json': '{"version": "8.0.0"}',
'node_modules/escalade/package.json': '{"version": "3.1.1"}',
'node_modules/escape-string-regexp/package.json':
'{"version": "4.0.0"}',
'node_modules/eslint/package.json': '{"version": "8.29.0"}',
'node_modules/eslint-plugin-disable-autofix/package.json':
'{"version": "3.0.0"}',
'node_modules/eslint-rule-composer/package.json':
'{"version": "0.3.0"}',
'node_modules/eslint-scope/package.json': '{"version": "7.1.1"}',
'node_modules/eslint-utils/package.json': '{"version": "3.0.0"}',
'node_modules/eslint-visitor-keys/package.json': '{"version": "3.3.0"}',
'node_modules/ignore/package.json': '{"version": "5.2.4"}',
'node_modules/espree/package.json': '{"version": "9.4.1"}',
'node_modules/esquery/package.json': '{"version": "1.4.0"}',
'node_modules/esrecurse/package.json': '{"version": "4.3.0"}',
'node_modules/estraverse/package.json': '{"version": "5.3.0"}',
'node_modules/esutils/package.json': '{"version": "2.0.3"}',
'node_modules/fast-deep-equal/package.json': '{"version": "3.1.3"}',
'node_modules/fast-json-stable-stringify/package.json':
'{"version": "2.1.0"}',
'node_modules/fast-levenshtein/package.json': '{"version": "2.0.6"}',
'node_modules/fastq/package.json': '{"version": "1.14.0"}',
'node_modules/file-entry-cache/package.json': '{"version": "6.0.1"}',
'node_modules/filelist/package.json': '{"version": "1.0.4"}',
'node_modules/find-up/package.json': '{"version": "5.0.0"}',
'node_modules/flat-cache/package.json': '{"version": "3.0.4"}',
'node_modules/flatted/package.json': '{"version": "3.2.7"}',
'node_modules/fs.realpath/package.json': '{"version": "1.0.0"}',
'node_modules/get-caller-file/package.json': '{"version": "2.0.5"}',
'node_modules/glob/package.json': '{"version": "7.2.3"}',
'node_modules/glob-parent/package.json': '{"version": "6.0.2"}',
'node_modules/globals/package.json': '{"version": "13.18.0"}',
'node_modules/grapheme-splitter/package.json': '{"version": "1.0.4"}',
'node_modules/has-flag/package.json': '{"version": "4.0.0"}',
'node_modules/ignore/package.json': '{"version": "5.2.1"}',
'node_modules/import-fresh/package.json': '{"version": "3.3.0"}',
'node_modules/imurmurhash/package.json': '{"version": "0.1.4"}',
'node_modules/inflight/package.json': '{"version": "1.0.6"}',
'node_modules/inherits/package.json': '{"version": "2.0.4"}',
'node_modules/is-extglob/package.json': '{"version": "2.1.1"}',
'node_modules/is-fullwidth-code-point/package.json':
'{"version": "3.0.0"}',
'node_modules/is-glob/package.json': '{"version": "4.0.3"}',
'node_modules/is-path-inside/package.json': '{"version": "3.0.3"}',
'node_modules/isexe/package.json': '{"version": "2.0.0"}',
'node_modules/jake/package.json': '{"version": "10.8.5"}',
'node_modules/js-sdsl/package.json': '{"version": "4.2.0"}',
'node_modules/js-tokens/package.json': '{"version": "4.0.0"}',
'node_modules/js-yaml/package.json': '{"version": "4.1.0"}',
'node_modules/json-schema-traverse/package.json':
'{"version": "0.4.1"}',
'node_modules/json-stable-stringify-without-jsonify/package.json':
'{"version": "1.0.1"}',
'node_modules/levn/package.json': '{"version": "0.4.1"}',
'node_modules/locate-path/package.json': '{"version": "6.0.0"}',
'node_modules/lodash.merge/package.json': '{"version": "4.6.2"}',
'node_modules/loose-envify/package.json': '{"version": "1.4.0"}',
'node_modules/lru-cache/package.json': '{"version": "6.0.0"}',
'node_modules/minimatch/package.json': '{"version": "3.1.2"}',
'node_modules/ms/package.json': '{"version": "2.1.2"}',
'node_modules/natural-compare/package.json': '{"version": "1.4.0"}',
'node_modules/once/package.json': '{"version": "1.4.0"}',
'node_modules/optionator/package.json': '{"version": "0.9.1"}',
'node_modules/p-limit/package.json': '{"version": "3.1.0"}',
'node_modules/p-locate/package.json': '{"version": "5.0.0"}',
'node_modules/parent-module/package.json': '{"version": "1.0.1"}',
'node_modules/path-exists/package.json': '{"version": "4.0.0"}',
'node_modules/path-is-absolute/package.json': '{"version": "1.0.1"}',
'node_modules/path-key/package.json': '{"version": "3.1.1"}',
'node_modules/postgres/package.json': '{"version": "3.2.4"}',
'node_modules/prelude-ls/package.json': '{"version": "1.2.1"}',
'node_modules/punycode/package.json': '{"version": "2.1.1"}',
'node_modules/queue-microtask/package.json': '{"version": "1.2.3"}',
'node_modules/react/package.json': '{"version": "18.2.0"}',
'node_modules/regexpp/package.json': '{"version": "3.2.0"}',
'node_modules/require-directory/package.json': '{"version": "2.1.1"}',
'node_modules/resolve-from/package.json': '{"version": "4.0.0"}',
'node_modules/reusify/package.json': '{"version": "1.0.4"}',
'node_modules/rimraf/package.json': '{"version": "3.0.2"}',
'node_modules/run-parallel/package.json': '{"version": "1.2.0"}',
'node_modules/semver/package.json': '{"version": "7.3.4"}',
'node_modules/shebang-command/package.json': '{"version": "2.0.0"}',
'node_modules/shebang-regex/package.json': '{"version": "3.0.0"}',
'node_modules/string-width/package.json': '{"version": "4.2.3"}',
'node_modules/strip-ansi/package.json': '{"version": "6.0.1"}',
'node_modules/strip-json-comments/package.json': '{"version": "3.1.1"}',
'node_modules/supports-color/package.json': '{"version": "7.2.0"}',
'node_modules/text-table/package.json': '{"version": "0.2.0"}',
'node_modules/tslib/package.json': '{"version": "2.4.1"}',
'node_modules/type-check/package.json': '{"version": "0.4.0"}',
'node_modules/type-fest/package.json': '{"version": "0.20.2"}',
'node_modules/uri-js/package.json': '{"version": "4.4.1"}',
'node_modules/which/package.json': '{"version": "2.0.2"}',
'node_modules/word-wrap/package.json': '{"version": "1.2.3"}',
'node_modules/wrap-ansi/package.json': '{"version": "7.0.0"}',
'node_modules/wrappy/package.json': '{"version": "1.0.2"}',
'node_modules/y18n/package.json': '{"version": "5.0.8"}',
'node_modules/yallist/package.json': '{"version": "4.0.0"}',
'node_modules/yargs/package.json': '{"version": "17.6.2"}',
'node_modules/yargs-parser/package.json': '{"version": "21.1.1"}',
'node_modules/yocto-queue/package.json': '{"version": "0.1.0"}',
};
vol.fromJSON(fileSys, '/root');
});
@@ -232,8 +368,12 @@ describe('yarn LockFile utility', () => {
__dirname,
'__fixtures__/auxiliary-packages/yarn.lock'
)).default;
const packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/auxiliary-packages/package.json'
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(classicLockFile, builder);
parseYarnLockfile(classicLockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(Object.keys(graph.externalNodes).length).toEqual(127);
@@ -289,6 +429,10 @@ describe('yarn LockFile utility', () => {
__dirname,
'__fixtures__/auxiliary-packages/yarn.lock'
)).default;
const packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/auxiliary-packages/package.json'
));
const normalizedPackageJson = {
name: 'test',
version: '0.0.0',
@@ -311,7 +455,7 @@ describe('yarn LockFile utility', () => {
)).default;
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
const prunedGraph = pruneProjectGraph(graph, normalizedPackageJson);
const result = stringifyYarnLockfile(
@@ -349,7 +493,7 @@ describe('yarn LockFile utility', () => {
)).default;
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, normalizedPackageJson, builder);
const graph = builder.getUpdatedProjectGraph();
const prunedGraph = pruneProjectGraph(graph, normalizedPackageJson);
const result = stringifyYarnLockfile(
@@ -370,8 +514,12 @@ describe('yarn LockFile utility', () => {
__dirname,
'__fixtures__/auxiliary-packages/yarn-berry.lock'
)).default;
const packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/auxiliary-packages/package.json'
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(berryLockFile, builder);
parseYarnLockfile(berryLockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(Object.keys(graph.externalNodes).length).toEqual(129);
@@ -448,9 +596,13 @@ describe('yarn LockFile utility', () => {
__dirname,
'__fixtures__/auxiliary-packages/yarn-berry.lock.pruned'
)).default;
const packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/auxiliary-packages/package.json'
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
const prunedGraph = pruneProjectGraph(graph, normalizedPackageJson);
const result = stringifyYarnLockfile(
@@ -464,6 +616,81 @@ describe('yarn LockFile utility', () => {
});
});
describe('auxiliary packages PnP', () => {
it('should parse yarn berry pnp', () => {
const berryLockFile = require(joinPathFragments(
__dirname,
'__fixtures__/auxiliary-packages/yarn-berry.lock'
)).default;
const packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/auxiliary-packages/package.json'
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(berryLockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(Object.keys(graph.externalNodes).length).toEqual(129);
expect(graph.externalNodes['npm:react']).toMatchInlineSnapshot(`
{
"data": {
"hash": "88e38092da8839b830cda6feef2e8505dec8ace60579e46aa5490fc3dc9bba0bd50336507dc166f43e3afc1c42939c09fe33b25fae889d6f402721dcd78fca1b",
"packageName": "react",
"version": "18.2.0",
},
"name": "npm:react",
"type": "npm",
}
`);
expect(graph.externalNodes['npm:typescript']).toMatchInlineSnapshot(`
{
"data": {
"hash": "ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db",
"packageName": "typescript",
"version": "4.8.4",
},
"name": "npm:typescript",
"type": "npm",
}
`);
expect(graph.externalNodes['npm:@nrwl/devkit']).toMatchInlineSnapshot(`
{
"data": {
"hash": "7dcc3600998448c496228e062d7edd8ecf959fa1ddb9721e91bb1f60f1a2284fd0e12e09edc022170988e2fb54acf101c79dc09fe9c54a21c9941e682eb73b92",
"packageName": "@nrwl/devkit",
"version": "15.0.13",
},
"name": "npm:@nrwl/devkit",
"type": "npm",
}
`);
expect(graph.externalNodes['npm:postgres']).toMatchInlineSnapshot(`
{
"data": {
"hash": "521660853e0c9f1c604cf43d32c75e2b4675e2d912eaec7bb6749716539dd53f1dfaf575a422087f6a53362f5162f9a4b8a88cc1dadf9d7580423fc05137767a",
"packageName": "postgres",
"version": "https://github.com/charsleysa/postgres.git#commit=3b1a01b2da3e2fafb1a79006f838eff11a8de3cb",
},
"name": "npm:postgres",
"type": "npm",
}
`);
expect(graph.externalNodes['npm:eslint-plugin-disable-autofix'])
.toMatchInlineSnapshot(`
{
"data": {
"hash": "fb7272c37e5701df14a79d0f8a9d6a0cb521972011ba91d70290eefc33fca589307908a6fb63e2985257b1c7cc3839c076d1c8def0caabddf21a91f13d7c8fc1",
"packageName": "eslint-plugin-disable-autofix",
"version": "npm:@mattlewis92/eslint-plugin-disable-autofix@3.0.0",
},
"name": "npm:eslint-plugin-disable-autofix",
"type": "npm",
}
`);
});
});
describe('duplicate packages', () => {
beforeEach(() => {
const fileSys = {
@@ -507,8 +734,12 @@ describe('yarn LockFile utility', () => {
__dirname,
'__fixtures__/duplicate-package/yarn.lock'
)).default;
const packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/duplicate-package/package.json'
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(classicLockFile, builder);
parseYarnLockfile(classicLockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(Object.keys(graph.externalNodes).length).toEqual(371);
});
@@ -538,7 +769,7 @@ describe('yarn LockFile utility', () => {
'__fixtures__/optional/package.json'
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(Object.keys(graph.externalNodes).length).toEqual(103);
@@ -716,8 +947,12 @@ describe('yarn LockFile utility', () => {
__dirname,
'__fixtures__/pruning/typescript/package.json'
));
const packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/pruning/package.json'
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
const prunedGraph = pruneProjectGraph(graph, typescriptPackageJson);
const result = stringifyYarnLockfile(
@@ -743,8 +978,12 @@ describe('yarn LockFile utility', () => {
__dirname,
'__fixtures__/pruning/devkit-yargs/package.json'
));
const packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/pruning/package.json'
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
const prunedGraph = pruneProjectGraph(graph, multiPackageJson);
const result = stringifyYarnLockfile(
@@ -776,8 +1015,12 @@ describe('yarn LockFile utility', () => {
__dirname,
'__fixtures__/workspaces/yarn.lock'
)).default;
const packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/workspaces/package.json'
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(Object.keys(graph.externalNodes).length).toEqual(5);
});
@@ -787,8 +1030,12 @@ describe('yarn LockFile utility', () => {
__dirname,
'__fixtures__/workspaces/yarn.lock.berry'
)).default;
const packageJson = require(joinPathFragments(
__dirname,
'__fixtures__/workspaces/package.json'
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(Object.keys(graph.externalNodes).length).toEqual(5);
});
@@ -809,6 +1056,17 @@ describe('yarn LockFile utility', () => {
types: './src/index.d.ts',
};
beforeEach(() => {
const fileSys = {
'node_modules/@gitlab-examples/semantic-release-npm/package.json':
'{"version": "2.0.1"}',
'node_modules/tslib/package.json': '{"version": "2.5.0"}',
'node_modules/type-fest/package.json': '{"version": "0.20.2"}',
'node_modules/@gar/promisify/package.json': '{"version": "1.1.3"}',
};
vol.fromJSON(fileSys, '/root');
});
it('should parse and prune classic', () => {
const lockFile = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
@@ -830,8 +1088,21 @@ type-fest@^0.20.2:
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
`;
const packageJson: PackageJson = {
name: '@my-ns/example',
version: '0.0.1',
type: 'commonjs',
dependencies: {
'@gitlab-examples/semantic-release-npm': '^2.0.1',
'type-fest': '^0.20.2',
},
peerDependencies: {
tslib: '^2.4.0',
},
};
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(graph.externalNodes['npm:tslib']).toMatchInlineSnapshot(`
{
@@ -905,8 +1176,22 @@ __metadata:
languageName: node
linkType: hard
`;
const packageJson: PackageJson = {
name: '@my-ns/example',
version: '0.0.1',
type: 'commonjs',
dependencies: {
'@gitlab-examples/semantic-release-npm': '^2.0.1',
'@gar/promisify': '^1.1.3',
},
peerDependencies: {
tslib: '^2.4.0',
},
};
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(graph.externalNodes['npm:tslib']).toMatchInlineSnapshot(`
{
@@ -973,7 +1258,21 @@ __metadata:
describe('mixed keys', () => {
beforeEach(() => {
const fileSys = {
'node_modules/wrap-ansi/package.json': '{"version": "7.0.0"}',
'node_modules/@isaacs/cliui/package.json': '{"version": "8.0.2"}',
'node_modules/ansi-regex/package.json': '{"version": "5.0.1"}',
'node_modules/ansi-styles/package.json': '{"version": "4.3.0"}',
'node_modules/cliui/package.json': '{"version": "8.0.1"}',
'node_modules/color-convert/package.json': '{"version": "2.0.1"}',
'node_modules/color-name/package.json': '{"version": "1.1.4"}',
'node_modules/eastasianwidth/package.json': '{"version": "0.2.0"}',
'node_modules/emoji-regex/package.json': '{"version": "8.0.0"}',
'node_modules/is-fullwidth-code-point/package.json':
'{"version": "3.0.0"}',
'node_modules/string-width/package.json': '{"version": "5.1.2"}',
'node_modules/string-width-cjs/package.json': '{"version": "4.2.3"}',
'node_modules/strip-ansi/package.json': '{"version": "7.0.1"}',
'node_modules/strip-ansi-cjs/package.json': '{"version": "6.0.1"}',
'node_modules/wrap-ansi/package.json': '{"version": "8.1.0"}',
'node_modules/wrap-ansi-cjs/package.json': '{"version": "7.0.0"}',
};
vol.fromJSON(fileSys, '/root');
@@ -990,7 +1289,7 @@ __metadata:
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(graph.externalNodes).toMatchInlineSnapshot(`
{
@@ -1003,13 +1302,13 @@ __metadata:
"name": "npm:@isaacs/cliui",
"type": "npm",
},
"npm:ansi-regex@5.0.1": {
"npm:ansi-regex": {
"data": {
"hash": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"packageName": "ansi-regex",
"version": "5.0.1",
},
"name": "npm:ansi-regex@5.0.1",
"name": "npm:ansi-regex",
"type": "npm",
},
"npm:ansi-regex@6.0.1": {
@@ -1021,13 +1320,13 @@ __metadata:
"name": "npm:ansi-regex@6.0.1",
"type": "npm",
},
"npm:ansi-styles@4.3.0": {
"npm:ansi-styles": {
"data": {
"hash": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"packageName": "ansi-styles",
"version": "4.3.0",
},
"name": "npm:ansi-styles@4.3.0",
"name": "npm:ansi-styles",
"type": "npm",
},
"npm:ansi-styles@6.2.1": {
@@ -1075,13 +1374,13 @@ __metadata:
"name": "npm:eastasianwidth",
"type": "npm",
},
"npm:emoji-regex@8.0.0": {
"npm:emoji-regex": {
"data": {
"hash": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"packageName": "emoji-regex",
"version": "8.0.0",
},
"name": "npm:emoji-regex@8.0.0",
"name": "npm:emoji-regex",
"type": "npm",
},
"npm:emoji-regex@9.2.2": {
@@ -1102,6 +1401,15 @@ __metadata:
"name": "npm:is-fullwidth-code-point",
"type": "npm",
},
"npm:string-width": {
"data": {
"hash": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"packageName": "string-width",
"version": "5.1.2",
},
"name": "npm:string-width",
"type": "npm",
},
"npm:string-width-cjs": {
"data": {
"hash": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
@@ -1120,13 +1428,13 @@ __metadata:
"name": "npm:string-width@4.2.3",
"type": "npm",
},
"npm:string-width@5.1.2": {
"npm:strip-ansi": {
"data": {
"hash": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"packageName": "string-width",
"version": "5.1.2",
"hash": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
"packageName": "strip-ansi",
"version": "7.0.1",
},
"name": "npm:string-width@5.1.2",
"name": "npm:strip-ansi",
"type": "npm",
},
"npm:strip-ansi-cjs": {
@@ -1147,20 +1455,11 @@ __metadata:
"name": "npm:strip-ansi@6.0.1",
"type": "npm",
},
"npm:strip-ansi@7.0.1": {
"data": {
"hash": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
"packageName": "strip-ansi",
"version": "7.0.1",
},
"name": "npm:strip-ansi@7.0.1",
"type": "npm",
},
"npm:wrap-ansi": {
"data": {
"hash": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"hash": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"packageName": "wrap-ansi",
"version": "7.0.0",
"version": "8.1.0",
},
"name": "npm:wrap-ansi",
"type": "npm",
@@ -1174,13 +1473,13 @@ __metadata:
"name": "npm:wrap-ansi-cjs",
"type": "npm",
},
"npm:wrap-ansi@8.1.0": {
"npm:wrap-ansi@7.0.0": {
"data": {
"hash": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"hash": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"packageName": "wrap-ansi",
"version": "8.1.0",
"version": "7.0.0",
},
"name": "npm:wrap-ansi@8.1.0",
"name": "npm:wrap-ansi@7.0.0",
"type": "npm",
},
}
@@ -1202,7 +1501,7 @@ __metadata:
));
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(graph.externalNodes).toMatchInlineSnapshot(`
{
@@ -1215,13 +1514,13 @@ __metadata:
"name": "npm:@isaacs/cliui",
"type": "npm",
},
"npm:ansi-regex@5.0.1": {
"npm:ansi-regex": {
"data": {
"hash": "2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b",
"packageName": "ansi-regex",
"version": "5.0.1",
},
"name": "npm:ansi-regex@5.0.1",
"name": "npm:ansi-regex",
"type": "npm",
},
"npm:ansi-regex@6.0.1": {
@@ -1233,13 +1532,13 @@ __metadata:
"name": "npm:ansi-regex@6.0.1",
"type": "npm",
},
"npm:ansi-styles@4.3.0": {
"npm:ansi-styles": {
"data": {
"hash": "513b44c3b2105dd14cc42a19271e80f386466c4be574bccf60b627432f9198571ebf4ab1e4c3ba17347658f4ee1711c163d574248c0c1cdc2d5917a0ad582ec4",
"packageName": "ansi-styles",
"version": "4.3.0",
},
"name": "npm:ansi-styles@4.3.0",
"name": "npm:ansi-styles",
"type": "npm",
},
"npm:ansi-styles@6.2.1": {
@@ -1287,13 +1586,13 @@ __metadata:
"name": "npm:eastasianwidth",
"type": "npm",
},
"npm:emoji-regex@8.0.0": {
"npm:emoji-regex": {
"data": {
"hash": "d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192",
"packageName": "emoji-regex",
"version": "8.0.0",
},
"name": "npm:emoji-regex@8.0.0",
"name": "npm:emoji-regex",
"type": "npm",
},
"npm:emoji-regex@9.2.2": {
@@ -1314,6 +1613,15 @@ __metadata:
"name": "npm:is-fullwidth-code-point",
"type": "npm",
},
"npm:string-width": {
"data": {
"hash": "7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193",
"packageName": "string-width",
"version": "5.1.2",
},
"name": "npm:string-width",
"type": "npm",
},
"npm:string-width-cjs": {
"data": {
"hash": "e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb",
@@ -1332,15 +1640,6 @@ __metadata:
"name": "npm:string-width@4.2.3",
"type": "npm",
},
"npm:string-width@5.1.2": {
"data": {
"hash": "7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193",
"packageName": "string-width",
"version": "5.1.2",
},
"name": "npm:string-width@5.1.2",
"type": "npm",
},
"npm:strip-ansi-cjs": {
"data": {
"hash": "f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c",
@@ -1370,9 +1669,9 @@ __metadata:
},
"npm:wrap-ansi": {
"data": {
"hash": "a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b",
"hash": "371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238",
"packageName": "wrap-ansi",
"version": "7.0.0",
"version": "8.1.0",
},
"name": "npm:wrap-ansi",
"type": "npm",
@@ -1386,13 +1685,13 @@ __metadata:
"name": "npm:wrap-ansi-cjs",
"type": "npm",
},
"npm:wrap-ansi@8.1.0": {
"npm:wrap-ansi@7.0.0": {
"data": {
"hash": "371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238",
"hash": "a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b",
"packageName": "wrap-ansi",
"version": "8.1.0",
"version": "7.0.0",
},
"name": "npm:wrap-ansi@8.1.0",
"name": "npm:wrap-ansi@7.0.0",
"type": "npm",
},
}
@@ -1405,6 +1704,18 @@ __metadata:
});
describe('invalid resolved', () => {
beforeEach(() => {
const fileSys = {
'node_modules/@octokit/request-error/package.json':
'{"version": "3.0.3"}',
'node_modules/@octokit/types/package.json': '{"version": "9.2.0"}',
'node_modules/@octokit/webhooks-types/package.json':
'{"version": "5.8.0"}',
'node_modules/@octokit/webhooks/package.json': '{"version": "9.26.0"}',
};
vol.fromJSON(fileSys, '/root');
});
it('should parse yarn.lock with invalid resolved field', () => {
const lockFile = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
@@ -1440,8 +1751,21 @@ __metadata:
"@octokit/webhooks-types" "5.8.0"
aggregate-error "^3.1.0"
`;
const packageJson: PackageJson = {
name: '@my-ns/example',
version: '0.0.1',
type: 'commonjs',
dependencies: {
'@octokit/request-error': '^3',
'@octokit/types': '^9',
'@octokit/webhooks-types': '5.8.0',
'@octokit/webhooks': '^9.8.4',
},
};
const builder = new ProjectGraphBuilder();
parseYarnLockfile(lockFile, builder);
parseYarnLockfile(lockFile, packageJson, builder);
const graph = builder.getUpdatedProjectGraph();
expect(graph.externalNodes).toMatchInlineSnapshot(`
{
@@ -1,6 +1,6 @@
import { getHoistedPackageVersion } from './utils/package-json';
import { ProjectGraphBuilder } from '../../../project-graph/project-graph-builder';
import { satisfies, Range } from 'semver';
import { satisfies, Range, gt } from 'semver';
import { NormalizedPackageJson } from './utils/package-json';
import {
ProjectGraph,
@@ -37,6 +37,7 @@ type YarnDependency = {
export function parseYarnLockfile(
lockFileContent: string,
packageJson: NormalizedPackageJson,
builder: ProjectGraphBuilder
) {
const { parseSyml } = require('@yarnpkg/parsers');
@@ -44,7 +45,7 @@ export function parseYarnLockfile(
// we use key => node map to avoid duplicate work when parsing keys
const keyMap = new Map<string, ProjectGraphExternalNode>();
addNodes(data, builder, keyMap);
addNodes(data, packageJson, builder, keyMap);
addDependencies(data, builder, keyMap);
}
@@ -59,11 +60,18 @@ function getPackageNames(keys: string): string[] {
function addNodes(
{ __metadata, ...dependencies }: YarnLockFile,
packageJson: NormalizedPackageJson,
builder: ProjectGraphBuilder,
keyMap: Map<string, ProjectGraphExternalNode>
) {
const isBerry = !!__metadata;
const nodes: Map<string, Map<string, ProjectGraphExternalNode>> = new Map();
const combinedDeps = {
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.peerDependencies,
...packageJson.optionalDependencies,
};
Object.entries(dependencies).forEach(([keys, snapshot]) => {
// ignore workspace projects & patches
@@ -103,23 +111,22 @@ function addNodes(
};
keyMap.set(key, node);
// use actual version so we can detect it later based on npm package's version
const mapKey =
snapshot.version && version !== snapshot.version
? snapshot.version
: version;
if (!nodes.has(packageName)) {
nodes.set(packageName, new Map([[version, node]]));
nodes.set(packageName, new Map([[mapKey, node]]));
} else {
nodes.get(packageName).set(version, node);
nodes.get(packageName).set(mapKey, node);
}
});
});
});
for (const [packageName, versionMap] of nodes.entries()) {
let hoistedNode: ProjectGraphExternalNode;
if (versionMap.size === 1) {
hoistedNode = versionMap.values().next().value;
} else {
const hoistedVersion = getHoistedVersion(packageName);
hoistedNode = versionMap.get(hoistedVersion);
}
const hoistedNode = findHoistedNode(packageName, versionMap, combinedDeps);
if (hoistedNode) {
hoistedNode.name = `npm:${packageName}`;
}
@@ -130,6 +137,44 @@ function addNodes(
}
}
function findHoistedNode(
packageName: string,
versionMap: Map<string, ProjectGraphExternalNode>,
combinedDeps: Record<string, string>
): ProjectGraphExternalNode {
const hoistedVersion = getHoistedVersion(packageName);
if (hoistedVersion) {
return versionMap.get(hoistedVersion);
}
const rootVersionSpecifier = combinedDeps[packageName];
if (!rootVersionSpecifier) {
return;
}
const versions = Array.from(versionMap.keys()).sort((a, b) =>
gt(a, b) ? -1 : 1
);
// take the highest version found
if (rootVersionSpecifier === '*') {
return versionMap.get(versions[0]);
}
// take version that satisfies the root version specifier
let version = versions.find((v) => satisfies(v, rootVersionSpecifier));
if (!version) {
// try to find alias version
version = versions.find(
(v) =>
versionMap.get(v).name === `npm:${packageName}@${rootVersionSpecifier}`
);
}
if (!version) {
// try to find tarball package
version = versions.find((v) => versionMap.get(v).data.version !== v);
}
if (version) {
return versionMap.get(version);
}
}
function findVersion(
packageName: string,
key: string,
@@ -43,11 +43,19 @@ export function createPackageJson(
const projectNode = graph.nodes[projectName];
const isLibrary = projectNode.type === 'lib';
const rootPackageJson = readJsonFile(
`${options.root || workspaceRoot}/package.json`
);
const npmDeps = findProjectsNpmDependencies(
projectNode,
graph,
options.target,
{ helperDependencies: options.helperDependencies },
rootPackageJson,
{
helperDependencies: options.helperDependencies,
isProduction: options.isProduction,
},
fileMap
);
@@ -92,9 +100,6 @@ export function createPackageJson(
);
};
const rootPackageJson = readJsonFile(
`${options.root || workspaceRoot}/package.json`
);
Object.entries(npmDeps.dependencies).forEach(([packageName, version]) => {
if (
rootPackageJson.devDependencies?.[packageName] &&
@@ -179,9 +184,11 @@ export function findProjectsNpmDependencies(
projectNode: ProjectGraphProjectNode,
graph: ProjectGraph,
target: string,
rootPackageJson: PackageJson,
options: {
helperDependencies?: string[];
ignoredDependencies?: string[];
isProduction?: boolean;
},
fileMap?: ProjectFileMap
): NpmDeps {
@@ -208,13 +215,22 @@ export function findProjectsNpmDependencies(
recursivelyCollectPeerDependencies(dep, graph, npmDeps, seen);
});
// if it's production, we want to ignore all found devDependencies
const ignoredDependencies =
options.isProduction && rootPackageJson.devDependencies
? [
...(options.ignoredDependencies || []),
...Object.keys(rootPackageJson.devDependencies),
]
: options.ignoredDependencies || [];
findAllNpmDeps(
fileMap,
projectNode,
graph,
npmDeps,
seen,
options.ignoredDependencies || [],
ignoredDependencies,
dependencyInputs,
selfInputs
);
@@ -1,24 +1,26 @@
import { ProjectType } from '../config/workspace-json-project-json';
import { createProjectFileMap, updateProjectFileMap } from './file-map-utils';
describe('fileMapUtils', () => {
describe('createFileMap', () => {
it('should map files to projects', () => {
const projectsConfigurations = {
version: 2,
projects: {
demo: {
root: 'apps/demo',
sourceRoot: 'apps/demo/src',
projectType: 'application',
projectType: 'application' as ProjectType,
},
'demo-e2e': {
root: 'apps/demo-e2e',
sourceRoot: 'apps/demo-e2e/src',
projectType: 'application',
projectType: 'application' as ProjectType,
},
ui: {
root: 'libs/ui',
sourceRoot: 'libs/ui/src',
projectType: 'library',
projectType: 'library' as ProjectType,
},
},
};
@@ -29,7 +31,7 @@ describe('fileMapUtils', () => {
{ file: 'tools/myfile.txt', hash: 'some-hash' },
];
const result = createProjectFileMap(projectsConfigurations as any, files);
const result = createProjectFileMap(projectsConfigurations, files);
expect(result).toEqual({
projectFileMap: {
@@ -52,22 +54,20 @@ describe('fileMapUtils', () => {
describe('updateFileMap', () => {
it('should map files to projects', () => {
const projectsConfigurations = {
projects: {
demo: {
root: 'apps/demo',
sourceRoot: 'apps/demo/src',
projectType: 'application',
},
'demo-e2e': {
root: 'apps/demo-e2e',
sourceRoot: 'apps/demo-e2e/src',
projectType: 'application',
},
ui: {
root: 'libs/ui',
sourceRoot: 'libs/ui/src',
projectType: 'library',
},
demo: {
root: 'apps/demo',
sourceRoot: 'apps/demo/src',
projectType: 'application' as ProjectType,
},
'demo-e2e': {
root: 'apps/demo-e2e',
sourceRoot: 'apps/demo-e2e/src',
projectType: 'application' as ProjectType,
},
ui: {
root: 'libs/ui',
sourceRoot: 'libs/ui/src',
projectType: 'library' as ProjectType,
},
};
const files = [
@@ -87,7 +87,7 @@ describe('fileMapUtils', () => {
],
};
const result = updateProjectFileMap(
projectsConfigurations as any,
projectsConfigurations,
projectFileMap,
files,
new Map([
@@ -7,7 +7,10 @@ import {
createProjectRootMappingsFromProjectConfigurations,
findProjectForPath,
} from './utils/find-project-for-path';
import { ProjectsConfigurations } from '../config/workspace-json-project-json';
import {
ProjectConfiguration,
ProjectsConfigurations,
} from '../config/workspace-json-project-json';
import { daemonClient } from '../daemon/client/client';
import { readProjectsConfigurationFromProjectGraph } from './project-graph';
import { fileHasher } from '../hasher/file-hasher';
@@ -52,16 +55,14 @@ export function createProjectFileMap(
}
export function updateProjectFileMap(
projectsConfigurations: ProjectsConfigurations,
projectsConfigurations: Record<string, ProjectConfiguration>,
projectFileMap: ProjectFileMap,
allWorkspaceFiles: FileData[],
updatedFiles: Map<string, string>,
deletedFiles: string[]
): { projectFileMap: ProjectFileMap; allWorkspaceFiles: FileData[] } {
const projectRootMappings =
createProjectRootMappingsFromProjectConfigurations(
projectsConfigurations.projects
);
createProjectRootMappingsFromProjectConfigurations(projectsConfigurations);
for (const f of updatedFiles.keys()) {
const matchingProjectFiles =
@@ -9,14 +9,17 @@ import {
import { getNxRequirePaths } from '../../utils/installation-directory';
import { readJsonFile } from '../../utils/fileutils';
import { join } from 'path';
import { ProjectsConfigurations } from '../../config/workspace-json-project-json';
import {
ProjectConfiguration,
ProjectsConfigurations,
} from '../../config/workspace-json-project-json';
import {
mergeAngularJsonAndProjects,
shouldMergeAngularProjects,
} from '../../adapter/angular-json';
import { NxJsonConfiguration } from '../../config/nx-json';
import { FileData, ProjectFileMap } from '../../config/project-graph';
import { NxWorkspaceFiles, WorkspaceErrors } from '../../native';
import type { NxWorkspaceFiles } from '../../native';
/**
* Walks the workspace directory to create the `projectFileMap`, `ProjectConfigurations` and `allWorkspaceFiles`
@@ -40,19 +43,21 @@ export async function retrieveWorkspaceFiles(
);
performance.mark('get-workspace-files:start');
let workspaceFiles: NxWorkspaceFiles;
try {
workspaceFiles = getWorkspaceFilesNative(workspaceRoot, globs);
} catch (e) {
// If the error is a parse error from Rust, then use the JS readJsonFile function to write a pretty error message
if (e.code === WorkspaceErrors.ParseError) {
readJsonFile(join(workspaceRoot, e.message));
// readJsonFile should always fail, but if it doesn't, then throw the original error
throw e;
} else {
throw e;
}
}
const { projectConfigurations, projectFileMap, globalFiles } =
getWorkspaceFilesNative(
workspaceRoot,
globs,
(configs: string[]): Record<string, ProjectConfiguration> => {
const projectConfigurations = createProjectConfigurations(
workspaceRoot,
nxJson,
configs
);
return projectConfigurations.projects;
}
) as NxWorkspaceFiles;
performance.mark('get-workspace-files:end');
performance.measure(
'get-workspace-files',
@@ -61,16 +66,12 @@ export async function retrieveWorkspaceFiles(
);
return {
allWorkspaceFiles: buildAllWorkspaceFiles(
workspaceFiles.projectFileMap,
workspaceFiles.globalFiles
),
projectFileMap: workspaceFiles.projectFileMap,
projectConfigurations: createProjectConfigurations(
workspaceRoot,
nxJson,
workspaceFiles.configFiles
),
allWorkspaceFiles: buildAllWorkspaceFiles(projectFileMap, globalFiles),
projectFileMap,
projectConfigurations: {
version: 2,
projects: projectConfigurations,
} as ProjectsConfigurations,
};
}
@@ -83,11 +84,22 @@ export async function retrieveWorkspaceFiles(
export async function retrieveProjectConfigurations(
workspaceRoot: string,
nxJson: NxJsonConfiguration
) {
const { getConfigFiles } = require('../../native');
): Promise<Record<string, ProjectConfiguration>> {
const { getProjectConfigurations } = require('../../native');
const globs = await configurationGlobs(workspaceRoot, nxJson);
const configPaths = getConfigFiles(workspaceRoot, globs);
return createProjectConfigurations(workspaceRoot, nxJson, configPaths);
return getProjectConfigurations(
workspaceRoot,
globs,
(configs: string[]): Record<string, ProjectConfiguration> => {
const projectConfigurations = createProjectConfigurations(
workspaceRoot,
nxJson,
configs
);
return projectConfigurations.projects;
}
);
}
function buildAllWorkspaceFiles(
@@ -97,7 +109,7 @@ function buildAllWorkspaceFiles(
performance.mark('get-all-workspace-files:start');
let fileData = Object.values(projectFileMap).flat();
fileData.push(...globalFiles);
fileData = fileData.concat(globalFiles);
performance.mark('get-all-workspace-files:end');
performance.measure(
'get-all-workspace-files',
@@ -123,8 +135,8 @@ function createProjectConfigurations(
);
if (shouldMergeAngularProjects(workspaceRoot, false)) {
projectConfigurations.projects = mergeAngularJsonAndProjects(
projectConfigurations.projects,
projectConfigurations = mergeAngularJsonAndProjects(
projectConfigurations,
workspaceRoot
);
}
@@ -135,14 +147,17 @@ function createProjectConfigurations(
'build-project-configs:end'
);
return projectConfigurations;
return {
version: 2,
projects: projectConfigurations,
};
}
function mergeTargetDefaultsIntoProjectDescriptions(
config: ProjectsConfigurations,
projects: Record<string, ProjectConfiguration>,
nxJson: NxJsonConfiguration
) {
for (const proj of Object.values(config.projects)) {
for (const proj of Object.values(projects)) {
if (proj.targets) {
for (const targetName of Object.keys(proj.targets)) {
const projectTargetDefinition = proj.targets[targetName];
@@ -162,7 +177,7 @@ function mergeTargetDefaultsIntoProjectDescriptions(
}
}
}
return config;
return projects;
}
async function configurationGlobs(
+1
View File
@@ -1,6 +1,7 @@
import { workspaceRoot } from '../utils/workspace-root';
import { mkdir, mkdirSync, pathExists, readFile, writeFile } from 'fs-extra';
import { join } from 'path';
import { performance } from 'perf_hooks';
import { DefaultTasksRunnerOptions } from './default-tasks-runner';
import { spawn } from 'child_process';
import { cacheDir } from '../utils/cache-directory';
@@ -388,10 +388,10 @@ export function createTaskGraph(
}
export function mapTargetDefaultsToDependencies(
defaults: TargetDefaults
defaults: TargetDefaults | undefined
): TargetDependencies {
const res = {};
Object.keys(defaults).forEach((k) => {
Object.keys(defaults ?? {}).forEach((k) => {
res[k] = defaults[k].dependsOn;
});
+4 -1
View File
@@ -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
+1 -1
View File
@@ -54,7 +54,7 @@ export async function recordStat(opts: {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
console.log(`Record stat. Major: ${major}`);
}
if (major < 10 || major > 15) return; // test version, skip it
if (major < 10 || major > 16) return; // test version, skip it
const axios = require('axios');
await (axios['default'] ?? axios)
.create({
@@ -2,8 +2,8 @@ import {
findMatchingProjects,
getMatchingStringsWithCache,
} from './find-matching-projects';
import minimatch = require('minimatch');
import type { ProjectGraphProjectNode } from '../config/project-graph';
import minimatch = require('minimatch');
describe('findMatchingProjects', () => {
let projectGraph: Record<string, ProjectGraphProjectNode> = {
+12 -4
View File
@@ -15,6 +15,7 @@ const execAsync = promisify(exec);
export type PackageManager = 'yarn' | 'pnpm' | 'npm';
export interface PackageManagerCommands {
preInstall?: string;
install: string;
ciInstall: string;
add: string;
@@ -60,10 +61,13 @@ export function getPackageManagerCommand(
): PackageManagerCommands {
const commands: { [pm in PackageManager]: () => PackageManagerCommands } = {
yarn: () => {
const yarnVersion = getPackageManagerVersion('yarn');
const yarnVersion = getPackageManagerVersion('yarn', root);
const useBerry = gte(yarnVersion, '2.0.0');
return {
preInstall: useBerry
? 'yarn set version stable'
: 'yarn set version classic',
install: 'yarn',
ciInstall: useBerry
? 'yarn install --immutable'
@@ -77,7 +81,7 @@ export function getPackageManagerCommand(
};
},
pnpm: () => {
const pnpmVersion = getPackageManagerVersion('pnpm');
const pnpmVersion = getPackageManagerVersion('pnpm', root);
const useExec = gte(pnpmVersion, '6.13.0');
const includeDoubleDashBeforeArgs = lt(pnpmVersion, '7.0.0');
const isPnpmWorkspace = existsSync(join(root, 'pnpm-workspace.yaml'));
@@ -122,9 +126,13 @@ export function getPackageManagerCommand(
* but it can also be passed in explicitly.
*/
export function getPackageManagerVersion(
packageManager: PackageManager = detectPackageManager()
packageManager: PackageManager = detectPackageManager(),
cwd = process.cwd()
): string {
return execSync(`${packageManager} --version`).toString('utf-8').trim();
return execSync(`${packageManager} --version`, {
cwd,
encoding: 'utf-8',
}).trim();
}
/**
+14 -9
View File
@@ -1,16 +1,17 @@
import { join } from 'path';
import { dirname, join } from 'path';
import { tmpdir } from 'os';
import {
mkdtempSync,
readFile,
outputFile,
rmSync,
emptyDirSync,
outputFileSync,
mkdirpSync,
mkdtempSync,
outputFile,
readFile,
realpathSync,
rmSync,
unlinkSync,
} from 'fs-extra';
import { joinPathFragments } from '../path';
import { appendFileSync, writeFileSync, renameSync } from 'fs';
import { appendFileSync, existsSync, renameSync, writeFileSync } from 'fs';
type NestedFiles = {
[fileName: string]: string;
@@ -19,7 +20,7 @@ type NestedFiles = {
export class TempFs {
readonly tempDir: string;
constructor(private dirname: string, overrideWorkspaceRoot = true) {
this.tempDir = mkdtempSync(join(tmpdir(), this.dirname));
this.tempDir = realpathSync(mkdtempSync(join(tmpdir(), this.dirname)));
if (overrideWorkspaceRoot) {
process.env.NX_WORKSPACE_ROOT_PATH = this.tempDir;
}
@@ -44,7 +45,11 @@ export class TempFs {
}
createFileSync(filePath: string, content: string) {
outputFileSync(joinPathFragments(this.tempDir, filePath), content);
let dir = joinPathFragments(this.tempDir, dirname(filePath));
if (!existsSync(dir)) {
mkdirpSync(dir);
}
writeFileSync(joinPathFragments(this.tempDir, filePath), content);
}
async readFile(filePath: string): Promise<string> {

Some files were not shown because too many files have changed in this diff Show More