diff --git a/CODEOWNERS b/CODEOWNERS index b983bc355a0df..7419f6615225c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -96,7 +96,6 @@ rust-toolchain @nrwl/nx-native-reviewers /packages/webpack/** @nrwl/nx-js-reviewers /e2e/webpack/** @nrwl/nx-js-reviewers /packages/rspack/** @nrwl/nx-js-reviewers -/packages/rspack/src/utils/module-federation @nrwl/nx-js-reviewers /e2e/rspack/** @nrwl/nx-js-reviewers /packages/esbuild/** @nrwl/nx-js-reviewers /e2e/esbuild/** @nrwl/nx-js-reviewers diff --git a/docs/blog/2024-12-03-define-the-relationship-with-monorepos.md b/docs/blog/2024-12-03-define-the-relationship-with-monorepos.md new file mode 100644 index 0000000000000..9217c3f170a9e --- /dev/null +++ b/docs/blog/2024-12-03-define-the-relationship-with-monorepos.md @@ -0,0 +1,86 @@ +--- +title: Define the relationship with monorepos +slug: define-the-relationship-with-monorepos +authors: ['Philip Fulcher'] +tags: [nx] +cover_image: /blog/images/2024-12-03/header.avif +--- + +A monorepo might sound like a big, intimidating thing, but we're going to break it down to just the bare essentials. **A monorepo helps you better manage the relationships that already exist between your projects.** Once we understand those relationships, we can use tools to very quickly define them and pull them together into a more cohesive whole. + +## So, what is a monorepo? + +Let's start with the big question: what even is a monorepo? + +> That's like when Google only has one repo for the entire company, right? + +Not quite. That scale of monorepo doesn't work for most organizations, and we can still use a monorepo approach for smaller systems. + +> So it's when you throw all your code in one repo and figure out how to manage it later? + +This is also a pretty bad idea. We need tools and processes to manage this volume of code in a single repo. + +The best definition I've seen comes from [monorepo.tools](https://monorepo.tools): + +> A monorepo is a single repository containing multiple distinct projects with well-defined relationships. + +Let's dig into that last part. What do we mean by **well-defined relationships?** It's useful to stat thinking about relationships between code in terms of distance. + +## The shortest distance: importing from another file + +We'll start with the smallest possible distance between two pieces of code: importing something from another file. + +![Diagram showing a form component depending on a button component](/blog/images/2024-12-03/another-file.avif) + +Say you have a button component. You create a form and import that button to use. This is a relationship we take for granted because we do it all the time, but there are some distinct benefits to this. + +First, we see the impact of our change immediately. We make a change in the button, and we either see the result rendered in the browser, or we get a failed compilation. Or we have a test suite running that will fail or pass a test. Or lint rule warnings appear in our IDE. We immediately see the result of the change we've made, and the impact on the relationship. + +This makes iteration fast: we see the impact of the change and can either refine that change or move on to the next one. + +## One step away in distance: importing from a package + +Let's take a step further away and think about the relationship when you have imported something from a package. + +![Diagram showing a form component depending on a design system package that bundles a button component](/blog/images/2024-12-03/package.avif) + +Say you have a design system published for your organization. You import the button from that package to use in your form. This looks very similar to what we did before, but we've introduced a big change in this relationship: seeing change is no longer immediate. + +If we make a change in the button, there will be some sort of compilation, bundling, and publishing that will need to happen. And we'll need to consume the latest version of the package to actually see the change. + +This is a slow process when you're working alone, but it can be managed with some tools. However, what happens if this change crosses team boundaries? Your design system team makes a change to the button and has to go through a PR review and merge process. Eventually that change is released as a new version and published. At some point later, you upgrade your version of the dependency just to find out the button has changed. But there's a bug! You report back to the design system team, and now they're going through this entire process again to get the fix in and published. This iteration cycle is very slow because understanding the impact of the change in the design system is no longer immediately apparent to consumers. + +## Even further away: APIs + +Let's step one step further: using APIs. Your frontend (in most cases) requires a backend, and it will be broken without it. + +![Diagram showing a frontend sending requests to a backend, and the backends responds to the frontend](/blog/images/2024-12-03/api.avif) + +There is an **implicit dependency** between the frontend and backend. You don't import code directly, but you do depend on the backend to function. + +Let's say that there's a new API endpoint needed, and you agreed with the team that it would be called `api/v1/contact/create` and would accept a payload with `contactName`. + +But, the backend team had a conversation about naming standards during their sprint and made the decision that it could really be `contact/init` with a payload of `fullName`. + +Understanding the impact of this change is now far-removed from making the change. Not only is there the PR merge, packaging, and releasing, but also deployment. It may not be until the end of the sprint before the impact of this change is actually understood. This iteration is practically glacial. + +## How do monorepos help? + +How do monorepos help with these relationships? **They shorten the distance of relationships between code.** Code is **colocated** so that the impact of your change can be understood immediately. + +In the example with the design system, the button will be imported directly instead of going through a build and package process. The design system team can immediately see the impact of the change they're making and either adjust their approach or fix the issue directly in the consuming app. + +For the API team, we can define that implicit relationship between backend and frontend so that we trigger e2e tests to confirm a change works. Or we can generate models from the backend to be consumed by the frontend. When the models are changed, the frontend code that imported those models will immediately reveal the impact of the change. + +You might think that moving projects into a single repository changes the relationship between the projects. But the relationships we've talked about here already exist; the monorepo makes those relationships explicit and well-defined. With good monorepo tooling, we can understand the impact of change along any of these relationships faster in a monorepo. + +## Learn more + +- [Nx Docs](/getting-started/intro) +- [X/Twitter](https://twitter.com/nxdevtools) +- [LinkedIn](https://www.linkedin.com/company/nrwl/) +- [Bluesky](https://bsky.app/profile/nx.dev) +- [Nx GitHub](https://github.com/nrwl/nx) +- [Nx Official Discord Server](https://go.nx.dev/community) +- [Nx Youtube Channel](https://www.youtube.com/@nxdevtools) +- [Speed up your CI](/nx-cloud) diff --git a/docs/blog/images/2024-12-03/another-file.avif b/docs/blog/images/2024-12-03/another-file.avif new file mode 100644 index 0000000000000..4763865a522e9 Binary files /dev/null and b/docs/blog/images/2024-12-03/another-file.avif differ diff --git a/docs/blog/images/2024-12-03/api.avif b/docs/blog/images/2024-12-03/api.avif new file mode 100644 index 0000000000000..92ac189fad137 Binary files /dev/null and b/docs/blog/images/2024-12-03/api.avif differ diff --git a/docs/blog/images/2024-12-03/header.avif b/docs/blog/images/2024-12-03/header.avif new file mode 100644 index 0000000000000..e54995da39d04 Binary files /dev/null and b/docs/blog/images/2024-12-03/header.avif differ diff --git a/docs/blog/images/2024-12-03/header.png b/docs/blog/images/2024-12-03/header.png new file mode 100644 index 0000000000000..f75a3eebe5460 Binary files /dev/null and b/docs/blog/images/2024-12-03/header.png differ diff --git a/docs/blog/images/2024-12-03/package.avif b/docs/blog/images/2024-12-03/package.avif new file mode 100644 index 0000000000000..ba7e384025062 Binary files /dev/null and b/docs/blog/images/2024-12-03/package.avif differ diff --git a/docs/generated/packages/playwright/documents/overview.md b/docs/generated/packages/playwright/documents/overview.md index 961c8e1220dfb..b53cb846f03c3 100644 --- a/docs/generated/packages/playwright/documents/overview.md +++ b/docs/generated/packages/playwright/documents/overview.md @@ -67,25 +67,9 @@ The `targetName` and `ciTargetName` options control the name of the inferred Pla ### Splitting E2E Tests -The `@nx/playwright/plugin` will automatically split your e2e tasks by file if you provide a `ciTargetName`. You can read more about the Atomizer feature [here](/ci/features/split-e2e-tasks). This will create a target with that name which can be used in CI to run the tests for each file in a distributed fashion. +`@nx/playwright/plugin` leverages Nx Atomizer to split your e2e tests into smaller tasks in a fully automated way. This allows for a much more efficient distribution of tasks in CI. You can read more about the Atomizer feature [here](/ci/features/split-e2e-tasks). -```json {% fileName="nx.json" %} -{ - "plugins": [ - { - "plugin": "@nx/playwright/plugin", - "options": { - "targetName": "e2e", - "ciTargetName": "e2e-ci" - } - } - ] -} -``` - -### Splitting E2E tasks by file - -The `@nx/playwright/plugin` will automatically split your e2e tasks by file. You can read more about this feature [here](/ci/features/split-e2e-tasks). +If you would like to disable Atomizer for Playwright tasks, set `ciTargetName` to `false`. {% /tab %} {% tab label="Nx < 18" %} diff --git a/docs/nx-cloud/reference/launch-templates.md b/docs/nx-cloud/reference/launch-templates.md index c24ecefe64427..04bccf43ceb56 100644 --- a/docs/nx-cloud/reference/launch-templates.md +++ b/docs/nx-cloud/reference/launch-templates.md @@ -360,12 +360,6 @@ If you need to send environment variables to agents, you can use the [--with-env nx-cloud start-ci-run --distribute-on="8 linux-medium-js" --with-env-vars="VAR1,VAR2" ``` -Or pass all the environment variables except OS-specific ones with this `--with-env-vars="auto"`: - -``` -nx-cloud start-ci-run --distribute-on="8 linux-medium-js" --with-env-vars="auto" -``` - ## Pass Values Between Steps If you need to pass a value from one step to another step, such as assigning the value to an existing or new environment variable. You can write to the `NX_CLOUD_ENV` environment file. diff --git a/docs/nx-cloud/reference/nx-cloud-cli.md b/docs/nx-cloud/reference/nx-cloud-cli.md index ce7077b6fa3e5..fee8056dfb12e 100644 --- a/docs/nx-cloud/reference/nx-cloud-cli.md +++ b/docs/nx-cloud/reference/nx-cloud-cli.md @@ -206,13 +206,6 @@ main CI jobs and the Nx Agent machines. If you want to pass other environment variables from the main job to Nx Agents, you can do it as follows: `--with-env-vars="VAR1,VAR2"`. This will set `VAR1` and `VAR2` on Nx Agents to the same values set on the main job before any steps run. -You can also pass `--with-env-vars="auto"` which will filter out all OS-specific environment variables and pass the rest to Nx Agents. - -{% callout type="warning" title="Use Caution With 'auto'" %} -Using `--with-env-vars="auto"` will override any existing environment variables on the Nx Agent, some of which might be critical to the -functionality of that machine. In case of unexpected issues on Nx Agents, try fallback to the explicit variable definition using `--with-env-vars="VAR1,VAR2,..."`. -{% /callout %} - Note: none of the values passed to Nx Agents are stored by Nx Cloud. ### Enabling/Disabling Distribution diff --git a/docs/shared/packages/playwright/playwright-plugin.md b/docs/shared/packages/playwright/playwright-plugin.md index 961c8e1220dfb..b53cb846f03c3 100644 --- a/docs/shared/packages/playwright/playwright-plugin.md +++ b/docs/shared/packages/playwright/playwright-plugin.md @@ -67,25 +67,9 @@ The `targetName` and `ciTargetName` options control the name of the inferred Pla ### Splitting E2E Tests -The `@nx/playwright/plugin` will automatically split your e2e tasks by file if you provide a `ciTargetName`. You can read more about the Atomizer feature [here](/ci/features/split-e2e-tasks). This will create a target with that name which can be used in CI to run the tests for each file in a distributed fashion. +`@nx/playwright/plugin` leverages Nx Atomizer to split your e2e tests into smaller tasks in a fully automated way. This allows for a much more efficient distribution of tasks in CI. You can read more about the Atomizer feature [here](/ci/features/split-e2e-tasks). -```json {% fileName="nx.json" %} -{ - "plugins": [ - { - "plugin": "@nx/playwright/plugin", - "options": { - "targetName": "e2e", - "ciTargetName": "e2e-ci" - } - } - ] -} -``` - -### Splitting E2E tasks by file - -The `@nx/playwright/plugin` will automatically split your e2e tasks by file. You can read more about this feature [here](/ci/features/split-e2e-tasks). +If you would like to disable Atomizer for Playwright tasks, set `ciTargetName` to `false`. {% /tab %} {% tab label="Nx < 18" %} diff --git a/e2e/react/src/react.test.ts b/e2e/react/src/react.test.ts index 531703008168a..8e5db024e982e 100644 --- a/e2e/react/src/react.test.ts +++ b/e2e/react/src/react.test.ts @@ -63,6 +63,27 @@ describe('React Applications', () => { } }, 250_000); + it('None buildable libs using (useTsSolution = true) should be excluded from js/ts plugin', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); + + runCLI( + `generate @nx/react:app apps/${appName} --name=${appName} --useTsSolution true --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` + ); + runCLI( + `generate @nx/react:lib ${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint` + ); + + const nxJson = JSON.parse(readFile('nx.json')); + + const jsTypescriptPlugin = nxJson.plugins.find( + (plugin) => plugin.plugin === '@nx/js/typescript' + ); + expect(jsTypescriptPlugin).toBeDefined(); + + expect(jsTypescriptPlugin.exclude.includes(`${libName}/*`)).toBeTruthy(); + }, 250_000); + it('should be able to use Rspack to build and test apps', async () => { const appName = uniq('app'); const libName = uniq('lib'); diff --git a/nx-dev/ui-company/src/lib/the-team.tsx b/nx-dev/ui-company/src/lib/the-team.tsx index 46424d1239e4f..2b9791158b233 100644 --- a/nx-dev/ui-company/src/lib/the-team.tsx +++ b/nx-dev/ui-company/src/lib/the-team.tsx @@ -96,11 +96,6 @@ const teamMembers = [ title: 'Director of Professional Services', imageUrl: 'joe-johnson.avif', }, - { - name: 'Johanna Pearce', - title: 'Architect', - imageUrl: 'johanna-pearce.avif', - }, { name: 'Jonathan Cammisuli', title: 'Architect', diff --git a/packages/angular/migrations.json b/packages/angular/migrations.json index 97a800742ad04..a73e327498dfe 100644 --- a/packages/angular/migrations.json +++ b/packages/angular/migrations.json @@ -292,7 +292,7 @@ "cli": "nx", "version": "20.2.0-beta.5", "requires": { - "@angular/core": ">=19.0.0-rc.1" + "@angular/core": ">=19.0.0" }, "description": "Add the '@angular/localize/init' polyfill to the 'polyfills' option of targets using esbuild-based executors.", "factory": "./src/migrations/update-20-2-0/add-localize-polyfill-to-targets" @@ -301,10 +301,19 @@ "cli": "nx", "version": "20.2.0-beta.5", "requires": { - "@angular/core": ">=19.0.0-rc.1" + "@angular/core": ">=19.0.0" }, "description": "Update '@angular/ssr' import paths to use the new '/node' entry point when 'CommonEngine' is detected.", "factory": "./src/migrations/update-20-2-0/update-angular-ssr-imports-to-use-node-entry-point" + }, + "disable-angular-eslint-prefer-standalone": { + "cli": "nx", + "version": "20.2.0-beta.6", + "requires": { + "@angular/core": ">=19.0.0" + }, + "description": "Disable the Angular ESLint prefer-standalone rule if not set.", + "factory": "./src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone" } }, "packageJsonUpdates": { @@ -1284,19 +1293,6 @@ } } }, - "20.2.0-analog": { - "version": "20.2.0-beta.5", - "packages": { - "@analogjs/vitest-angular": { - "version": "~1.10.0-beta.6", - "alwaysAddToPackageJson": false - }, - "@analogjs/vite-plugin-angular": { - "version": "~1.10.0-beta.6", - "alwaysAddToPackageJson": false - } - } - }, "20.2.0-jest": { "version": "20.2.0-beta.5", "requires": { @@ -1341,6 +1337,19 @@ "alwaysAddToPackageJson": false } } + }, + "20.2.0-analog": { + "version": "20.2.0-beta.7", + "packages": { + "@analogjs/vitest-angular": { + "version": "~1.10.0", + "alwaysAddToPackageJson": false + }, + "@analogjs/vite-plugin-angular": { + "version": "~1.10.0", + "alwaysAddToPackageJson": false + } + } } } } diff --git a/packages/angular/package.json b/packages/angular/package.json index 54b76661b4752..1ebeda5888af8 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -66,10 +66,10 @@ "piscina": "^4.4.0" }, "peerDependencies": { - "@angular-devkit/build-angular": ">= 16.0.0 < 19.0.0", - "@angular-devkit/core": ">= 16.0.0 < 19.0.0", - "@angular-devkit/schematics": ">= 16.0.0 < 19.0.0", - "@schematics/angular": ">= 16.0.0 < 19.0.0", + "@angular-devkit/build-angular": ">= 17.0.0 < 20.0.0", + "@angular-devkit/core": ">= 17.0.0 < 20.0.0", + "@angular-devkit/schematics": ">= 17.0.0 < 20.0.0", + "@schematics/angular": ">= 17.0.0 < 20.0.0", "rxjs": "^6.5.3 || ^7.5.0" }, "publishConfig": { diff --git a/packages/angular/src/executors/module-federation-dev-server/lib/build-static-remotes.ts b/packages/angular/src/executors/module-federation-dev-server/lib/build-static-remotes.ts deleted file mode 100644 index 54acba1ea8455..0000000000000 --- a/packages/angular/src/executors/module-federation-dev-server/lib/build-static-remotes.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { type Schema } from '../schema'; -import { type ExecutorContext, logger } from '@nx/devkit'; -import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; -import { fork } from 'node:child_process'; -import { join } from 'node:path'; -import { createWriteStream } from 'node:fs'; -import type { StaticRemotesConfig } from '@nx/module-federation/src/utils'; - -export async function buildStaticRemotes( - staticRemotesConfig: StaticRemotesConfig, - nxBin, - context: ExecutorContext, - options: Schema -) { - if (!staticRemotesConfig.remotes.length) { - return; - } - const mappedLocationOfRemotes: Record = {}; - for (const app of staticRemotesConfig.remotes) { - mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${ - options.host - }:${options.staticRemotesPort}/${ - staticRemotesConfig.config[app].urlSegment - }`; - } - - await new Promise((res, rej) => { - logger.info( - `NX Building ${staticRemotesConfig.remotes.length} static remotes...` - ); - const staticProcess = fork( - nxBin, - [ - 'run-many', - `--target=build`, - `--projects=${staticRemotesConfig.remotes.join(',')}`, - ...(context.configurationName - ? [`--configuration=${context.configurationName}`] - : []), - ...(options.parallel ? [`--parallel=${options.parallel}`] : []), - ], - { - cwd: context.root, - stdio: ['ignore', 'pipe', 'pipe', 'ipc'], - } - ); - // File to debug build failures e.g. 2024-01-01T00_00_0_0Z-build.log' - const remoteBuildLogFile = join( - workspaceDataDirectory, - `${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log` - ); - const stdoutStream = createWriteStream(remoteBuildLogFile); - staticProcess.stdout.on('data', (data) => { - const ANSII_CODE_REGEX = - /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; - const stdoutString = data.toString().replace(ANSII_CODE_REGEX, ''); - stdoutStream.write(stdoutString); - - // in addition to writing into the stdout stream, also show error directly in console - // so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output. - if (stdoutString.includes('ERROR in')) { - logger.log(stdoutString); - } - - if (stdoutString.includes('Successfully ran target build')) { - staticProcess.stdout.removeAllListeners('data'); - logger.info( - `NX Built ${staticRemotesConfig.remotes.length} static remotes` - ); - res(); - } - }); - staticProcess.stderr.on('data', (data) => logger.info(data.toString())); - staticProcess.once('exit', (code) => { - stdoutStream.end(); - staticProcess.stdout.removeAllListeners('data'); - staticProcess.stderr.removeAllListeners('data'); - if (code !== 0) { - rej( - `Remote failed to start. A complete log can be found in: ${remoteBuildLogFile}` - ); - } else { - res(); - } - }); - process.on('SIGTERM', () => staticProcess.kill('SIGTERM')); - process.on('exit', () => staticProcess.kill('SIGTERM')); - }); - - return mappedLocationOfRemotes; -} diff --git a/packages/angular/src/executors/module-federation-dev-server/lib/index.ts b/packages/angular/src/executors/module-federation-dev-server/lib/index.ts index 331719948e2b9..2f41930a016b8 100644 --- a/packages/angular/src/executors/module-federation-dev-server/lib/index.ts +++ b/packages/angular/src/executors/module-federation-dev-server/lib/index.ts @@ -1,4 +1,2 @@ -export * from './build-static-remotes'; export * from './normalize-options'; export * from './start-dev-remotes'; -export * from './start-static-remotes-file-server'; diff --git a/packages/angular/src/executors/module-federation-dev-server/lib/normalize-options.ts b/packages/angular/src/executors/module-federation-dev-server/lib/normalize-options.ts index 19f46c1ab02a4..41d63136dbfb0 100644 --- a/packages/angular/src/executors/module-federation-dev-server/lib/normalize-options.ts +++ b/packages/angular/src/executors/module-federation-dev-server/lib/normalize-options.ts @@ -26,6 +26,7 @@ export function normalizeOptions(schema: Schema): NormalizedSchema { liveReload: schema.liveReload ?? true, open: schema.open ?? false, ssl: schema.ssl ?? false, + verbose: schema.verbose ?? false, sslCert: schema.sslCert ? join(workspaceRoot, schema.sslCert) : undefined, sslKey: schema.sslKey ? join(workspaceRoot, schema.sslKey) : undefined, }; diff --git a/packages/angular/src/executors/module-federation-dev-server/lib/start-dev-remotes.ts b/packages/angular/src/executors/module-federation-dev-server/lib/start-dev-remotes.ts index 321aa6ed7a27a..58836fab8ee16 100644 --- a/packages/angular/src/executors/module-federation-dev-server/lib/start-dev-remotes.ts +++ b/packages/angular/src/executors/module-federation-dev-server/lib/start-dev-remotes.ts @@ -8,7 +8,7 @@ import { export async function startRemotes( remotes: string[], workspaceProjects: Record, - options: Schema, + options: Pick, context: ExecutorContext, target: 'serve' | 'serve-static' = 'serve' ) { diff --git a/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts b/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts index f3d5b7d4a5b34..7575516818cb6 100644 --- a/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts +++ b/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts @@ -4,24 +4,14 @@ import { readProjectsConfigurationFromProjectGraph, } from '@nx/devkit'; import { type Schema } from './schema'; -import { - buildStaticRemotes, - normalizeOptions, - startRemotes, - startStaticRemotesFileServer, -} from './lib'; +import { normalizeOptions, startRemotes } from './lib'; import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await'; import { combineAsyncIterables, createAsyncIterable, mapAsyncIterable, } from '@nx/devkit/src/utils/async-iterable'; -import { - getModuleFederationConfig, - getRemotes, - startRemoteProxies, - parseStaticRemotesConfig, -} from '@nx/module-federation/src/utils'; +import { startRemoteIterators } from '@nx/module-federation/src/executors/utils'; import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open'; import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl'; import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter'; @@ -37,8 +27,6 @@ export async function* moduleFederationDevServerExecutor( schema: Schema, context: ExecutorContext ) { - // Force Node to resolve to look for the nx binary that is inside node_modules - const nxBin = require.resolve('nx/bin/nx'); const options = normalizeOptions(schema); const { projects: workspaceProjects } = @@ -101,76 +89,14 @@ export async function* moduleFederationDevServerExecutor( validateDevRemotes(options, workspaceProjects); - const moduleFederationConfig = getModuleFederationConfig( - project.targets.build.options.tsConfig, - context.root, - project.root, - 'angular' - ); - - const remoteNames = options.devRemotes.map((r) => - typeof r === 'string' ? r : r.remoteName - ); - - const remotes = getRemotes( - remoteNames, - options.skipRemotes, - moduleFederationConfig, - { - projectName: project.name, - projectGraph: context.projectGraph, - root: context.root, - }, - pathToManifestFile - ); - - options.staticRemotesPort ??= remotes.staticRemotePort; - - // Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin - process.env.NX_MF_DEV_REMOTES = JSON.stringify([ - ...( - remotes.devRemotes.map((r) => - typeof r === 'string' ? r : r.remoteName - ) ?? [] - ).map((r) => r.replace(/-/g, '_')), - project.name.replace(/-/g, '_'), - ]); - - const staticRemotesConfig = parseStaticRemotesConfig( - [...remotes.staticRemotes, ...remotes.dynamicRemotes], - context - ); - const mappedLocationsOfStaticRemotes = await buildStaticRemotes( - staticRemotesConfig, - nxBin, - context, - options - ); - - const devRemoteIters = await startRemotes( - remotes.devRemotes, - workspaceProjects, - options, - context, - 'serve' - ); - - const staticRemotesIter = startStaticRemotesFileServer( - staticRemotesConfig, - context, - options - ); - - startRemoteProxies( - staticRemotesConfig, - mappedLocationsOfStaticRemotes, - options.ssl - ? { - pathToCert: options.sslCert, - pathToKey: options.sslKey, - } - : undefined - ); + const { remotes, staticRemotesIter, devRemoteIters } = + await startRemoteIterators( + options, + context, + startRemotes, + pathToManifestFile, + 'angular' + ); const removeBaseUrlEmission = (iter: AsyncIterable) => mapAsyncIterable(iter, (v) => ({ diff --git a/packages/angular/src/executors/module-federation-dev-server/schema.d.ts b/packages/angular/src/executors/module-federation-dev-server/schema.d.ts index c2e5f77430021..f8bcc10f31c24 100644 --- a/packages/angular/src/executors/module-federation-dev-server/schema.d.ts +++ b/packages/angular/src/executors/module-federation-dev-server/schema.d.ts @@ -43,4 +43,5 @@ export type NormalizedSchema = SchemaWithBuildTarget & { liveReload: boolean; open: boolean; ssl: boolean; + verbose: boolean; }; diff --git a/packages/angular/src/executors/module-federation-ssr-dev-server/lib/build-static-remotes.ts b/packages/angular/src/executors/module-federation-ssr-dev-server/lib/build-static-remotes.ts deleted file mode 100644 index fd026d73b4417..0000000000000 --- a/packages/angular/src/executors/module-federation-ssr-dev-server/lib/build-static-remotes.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { Schema } from '../schema'; -import { type ExecutorContext, logger } from '@nx/devkit'; -import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; -import { fork } from 'node:child_process'; -import { join } from 'node:path'; -import { createWriteStream } from 'node:fs'; -import type { StaticRemotesConfig } from '@nx/module-federation/src/utils'; - -export async function buildStaticRemotes( - staticRemotesConfig: StaticRemotesConfig, - nxBin, - context: ExecutorContext, - options: Schema -) { - if (!staticRemotesConfig.remotes.length) { - return; - } - const mappedLocationOfRemotes: Record = {}; - for (const app of staticRemotesConfig.remotes) { - mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${ - options.host - }:${options.staticRemotesPort}/${ - staticRemotesConfig.config[app].urlSegment - }`; - } - - await new Promise((resolve, reject) => { - logger.info( - `NX Building ${staticRemotesConfig.remotes.length} static remotes...` - ); - const staticProcess = fork( - nxBin, - [ - 'run-many', - `--target=server`, - `--projects=${staticRemotesConfig.remotes.join(',')}`, - ...(context.configurationName - ? [`--configuration=${context.configurationName}`] - : []), - ...(options.parallel ? [`--parallel=${options.parallel}`] : []), - ], - { - cwd: context.root, - stdio: ['ignore', 'pipe', 'pipe', 'ipc'], - } - ); - // File to debug build failures e.g. 2024-01-01T00_00_0_0Z-build.log' - const remoteBuildLogFile = join( - workspaceDataDirectory, - `${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log` - ); - const stdoutStream = createWriteStream(remoteBuildLogFile); - staticProcess.stdout.on('data', (data) => { - const ANSII_CODE_REGEX = - /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; - const stdoutString = data.toString().replace(ANSII_CODE_REGEX, ''); - stdoutStream.write(stdoutString); - - // in addition to writing into the stdout stream, also show error directly in console - // so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output. - if (stdoutString.includes('ERROR in')) { - logger.log(stdoutString); - } - - if (stdoutString.includes('Successfully ran target server')) { - staticProcess.stdout.removeAllListeners('data'); - logger.info( - `NX Built ${staticRemotesConfig.remotes.length} static remotes` - ); - resolve(); - } - }); - staticProcess.stderr.on('data', (data) => logger.info(data.toString())); - staticProcess.once('exit', (code) => { - stdoutStream.end(); - staticProcess.stdout.removeAllListeners('data'); - staticProcess.stderr.removeAllListeners('data'); - if (code !== 0) { - reject( - `Remote failed to start. A complete log can be found in: ${remoteBuildLogFile}` - ); - } else { - resolve(); - } - }); - process.on('SIGTERM', () => staticProcess.kill('SIGTERM')); - process.on('exit', () => staticProcess.kill('SIGTERM')); - }); - - return mappedLocationOfRemotes; -} diff --git a/packages/angular/src/executors/module-federation-ssr-dev-server/lib/normalize-options.ts b/packages/angular/src/executors/module-federation-ssr-dev-server/lib/normalize-options.ts index 70e22c1de1df4..89d4a67204485 100644 --- a/packages/angular/src/executors/module-federation-ssr-dev-server/lib/normalize-options.ts +++ b/packages/angular/src/executors/module-federation-ssr-dev-server/lib/normalize-options.ts @@ -1,8 +1,8 @@ import { workspaceRoot } from '@nx/devkit'; -import type { Schema } from '../schema'; +import type { NormalizedSchema, Schema } from '../schema'; import { join } from 'path'; -export function normalizeOptions(options: Schema): Schema { +export function normalizeOptions(options: Schema): NormalizedSchema { const devServeRemotes = !options.devRemotes ? [] : Array.isArray(options.devRemotes) @@ -12,6 +12,7 @@ export function normalizeOptions(options: Schema): Schema { return { ...options, devRemotes: devServeRemotes, + verbose: options.verbose ?? false, ssl: options.ssl ?? false, sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined, sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined, diff --git a/packages/angular/src/executors/module-federation-ssr-dev-server/lib/start-dev-remotes.ts b/packages/angular/src/executors/module-federation-ssr-dev-server/lib/start-dev-remotes.ts index a935b10473eb4..a0650f5eb357e 100644 --- a/packages/angular/src/executors/module-federation-ssr-dev-server/lib/start-dev-remotes.ts +++ b/packages/angular/src/executors/module-federation-ssr-dev-server/lib/start-dev-remotes.ts @@ -8,7 +8,7 @@ import { export async function startRemotes( remotes: string[], workspaceProjects: Record, - options: Schema, + options: Pick, context: ExecutorContext ) { const target = 'serve-ssr'; diff --git a/packages/angular/src/executors/module-federation-ssr-dev-server/lib/start-static-remotes.ts b/packages/angular/src/executors/module-federation-ssr-dev-server/lib/start-static-remotes.ts deleted file mode 100644 index 0e2d8972d740a..0000000000000 --- a/packages/angular/src/executors/module-federation-ssr-dev-server/lib/start-static-remotes.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { type ExecutorContext, workspaceRoot } from '@nx/devkit'; -import { type Schema } from '../schema'; -import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl'; -import { join } from 'path'; -import { cpSync, rmSync } from 'fs'; -import type { StaticRemotesConfig } from '@nx/module-federation/src/utils'; -import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable'; - -export function startStaticRemotes( - ssrStaticRemotesConfig: StaticRemotesConfig, - context: ExecutorContext, - options: Schema -) { - if (ssrStaticRemotesConfig.remotes.length === 0) { - return createAsyncIterable(({ next, done }) => { - next({ success: true }); - done(); - }); - } - - // The directories are usually generated with /browser and /server suffixes so we need to copy them to a common directory - const commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes'); - for (const app of ssrStaticRemotesConfig.remotes) { - const remoteConfig = ssrStaticRemotesConfig.config[app]; - - cpSync( - remoteConfig.outputPath, - join(commonOutputDirectory, remoteConfig.urlSegment), - { - force: true, - recursive: true, - } - ); - } - - const staticRemotesIter = fileServerExecutor( - { - cors: true, - watch: false, - staticFilePath: commonOutputDirectory, - parallel: false, - spa: false, - withDeps: false, - host: options.host, - port: options.staticRemotesPort, - ssl: options.ssl, - sslCert: options.sslCert, - sslKey: options.sslKey, - cacheSeconds: -1, - }, - context - ); - return staticRemotesIter; -} diff --git a/packages/angular/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts b/packages/angular/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts index 9810eba64d438..c53565e683605 100644 --- a/packages/angular/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts +++ b/packages/angular/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts @@ -1,37 +1,29 @@ import { executeSSRDevServerBuilder } from '@angular-devkit/build-angular'; import { type ExecutorContext, logger } from '@nx/devkit'; -import { - combineAsyncIterables, - createAsyncIterable, - mapAsyncIterable, -} from '@nx/devkit/src/utils/async-iterable'; -import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await'; -import { - getModuleFederationConfig, - getRemotes, - parseStaticSsrRemotesConfig, - startSsrRemoteProxies, -} from '@nx/module-federation/src/utils'; -import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open'; import { existsSync } from 'fs'; -import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter'; import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph'; import { extname, join } from 'path'; import { getDynamicMfManifestFile, validateDevRemotes, } from '../../builders/utilities/module-federation'; -import { buildStaticRemotes } from './lib/build-static-remotes'; -import { normalizeOptions } from './lib/normalize-options'; -import { startRemotes } from './lib/start-dev-remotes'; -import { startStaticRemotes } from './lib/start-static-remotes'; import type { Schema } from './schema'; +import { startRemoteIterators } from '@nx/module-federation/src/executors/utils'; +import { startRemotes } from './lib/start-dev-remotes'; +import { + combineAsyncIterables, + createAsyncIterable, + mapAsyncIterable, +} from '@nx/devkit/src/utils/async-iterable'; +import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await'; +import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter'; +import { normalizeOptions } from './lib/normalize-options'; +import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open'; export async function* moduleFederationSsrDevServerExecutor( schema: Schema, context: ExecutorContext ) { - const nxBin = require.resolve('nx/bin/nx'); const options = normalizeOptions(schema); const currIter = eachValueFrom( @@ -79,73 +71,15 @@ export async function* moduleFederationSsrDevServerExecutor( validateDevRemotes({ devRemotes: options.devRemotes }, workspaceProjects); - const moduleFederationConfig = getModuleFederationConfig( - project.targets.build.options.tsConfig, - context.root, - project.root, - 'angular' - ); - - const remoteNames = options.devRemotes.map((r) => - typeof r === 'string' ? r : r.remoteName - ); - - const remotes = getRemotes( - remoteNames, - options.skipRemotes, - moduleFederationConfig, - { - projectName: project.name, - projectGraph: context.projectGraph, - root: context.root, - }, - pathToManifestFile - ); - - options.staticRemotesPort ??= remotes.staticRemotePort; - - const staticRemotesConfig = parseStaticSsrRemotesConfig( - [...remotes.staticRemotes, ...remotes.dynamicRemotes], - context - ); - - const mappedLocationsOfStaticRemotes = await buildStaticRemotes( - staticRemotesConfig, - nxBin, - context, - options - ); - - // Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin - process.env.NX_MF_DEV_REMOTES = JSON.stringify([ - ...( - options.devRemotes.map((r) => - typeof r === 'string' ? r : r.remoteName - ) ?? [] - ).map((r) => r.replace(/-/g, '_')), - project.name.replace(/-/g, '_'), - ]); - - const devRemotes = await startRemotes( - remotes.devRemotes, - workspaceProjects, - options, - context - ); - - const staticRemotes = startStaticRemotes( - staticRemotesConfig, - context, - options - ); - - startSsrRemoteProxies( - staticRemotesConfig, - mappedLocationsOfStaticRemotes, - options.ssl - ? { pathToCert: options.sslCert, pathToKey: options.sslKey } - : undefined - ); + const { remotes, staticRemotesIter, devRemoteIters } = + await startRemoteIterators( + options, + context, + startRemotes, + pathToManifestFile, + 'angular', + true + ); const removeBaseUrlEmission = (iter: AsyncIterable) => mapAsyncIterable(iter, (v) => ({ @@ -154,8 +88,8 @@ export async function* moduleFederationSsrDevServerExecutor( })); const combined = combineAsyncIterables( - removeBaseUrlEmission(staticRemotes), - ...(devRemotes ? devRemotes.map(removeBaseUrlEmission) : []), + removeBaseUrlEmission(staticRemotesIter), + ...(devRemoteIters ? devRemoteIters.map(removeBaseUrlEmission) : []), createAsyncIterable<{ success: true; baseUrl: string }>( async ({ next, done }) => { if (!options.isInitialHost) { @@ -172,7 +106,7 @@ export async function* moduleFederationSsrDevServerExecutor( return; } try { - const portsToWaitFor = staticRemotes + const portsToWaitFor = staticRemotesIter ? [options.staticRemotesPort, ...remotes.remotePorts] : [...remotes.remotePorts]; await Promise.all( @@ -198,7 +132,7 @@ export async function* moduleFederationSsrDevServerExecutor( } ) ); - let refs = 2 + (devRemotes?.length ?? 0); + let refs = 2 + (devRemoteIters?.length ?? 0); for await (const result of combined) { if (result.success === false) throw new Error('Remotes failed to start'); if (result.success) refs--; diff --git a/packages/angular/src/executors/module-federation-ssr-dev-server/schema.d.ts b/packages/angular/src/executors/module-federation-ssr-dev-server/schema.d.ts index 4c43a78789aae..821a5834dd974 100644 --- a/packages/angular/src/executors/module-federation-ssr-dev-server/schema.d.ts +++ b/packages/angular/src/executors/module-federation-ssr-dev-server/schema.d.ts @@ -9,3 +9,9 @@ export interface Schema extends SSRDevServerBuilderOptions { staticRemotesPort?: number; isInitialHost?: boolean; } + +export interface NormalizedSchema extends Schema { + devRemotes: DevRemoteDefinition[]; + ssl: boolean; + verbose: boolean; +} diff --git a/packages/angular/src/generators/utils/add-jest.spec.ts b/packages/angular/src/generators/utils/add-jest.spec.ts new file mode 100644 index 0000000000000..6066909d6f2ac --- /dev/null +++ b/packages/angular/src/generators/utils/add-jest.spec.ts @@ -0,0 +1,54 @@ +import type { Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { UnitTestRunner } from '../../utils/test-runners'; +import { addJest } from './add-jest'; +import { generateTestApplication } from './testing'; + +describe('addJest', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + await generateTestApplication(tree, { + name: 'app1', + directory: 'app1', + unitTestRunner: UnitTestRunner.None, + skipFormat: true, + }); + }); + + it('generate the test setup file', async () => { + await addJest(tree, { + name: 'app1', + projectRoot: 'app1', + skipPackageJson: false, + strict: false, + }); + + expect(tree.read('app1/src/test-setup.ts', 'utf-8')).toMatchInlineSnapshot(` + "import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + + setupZoneTestEnv(); + " + `); + }); + + it('generate the test setup file with strict', async () => { + await addJest(tree, { + name: 'app1', + projectRoot: 'app1', + skipPackageJson: false, + strict: true, + }); + + expect(tree.read('app1/src/test-setup.ts', 'utf-8')).toMatchInlineSnapshot(` + "import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + + setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true + }); + " + `); + }); +}); diff --git a/packages/angular/src/generators/utils/add-jest.ts b/packages/angular/src/generators/utils/add-jest.ts index b9fe5e713083c..1365ced1d5265 100644 --- a/packages/angular/src/generators/utils/add-jest.ts +++ b/packages/angular/src/generators/utils/add-jest.ts @@ -53,14 +53,13 @@ export async function addJest( const contents = tree.read(setupFile, 'utf-8'); tree.write( setupFile, - `// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment -globalThis.ngJest = { - testEnvironmentOptions: { - errorOnUnknownElements: true, - errorOnUnknownProperties: true, - }, -}; -${contents}` + contents.replace( + 'setupZoneTestEnv();', + `setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true +});` + ) ); } } diff --git a/packages/angular/src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone.spec.ts b/packages/angular/src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone.spec.ts new file mode 100644 index 0000000000000..0fb1b4bedc586 --- /dev/null +++ b/packages/angular/src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone.spec.ts @@ -0,0 +1,428 @@ +import { + addProjectConfiguration, + writeJson, + type ProjectConfiguration, + type ProjectGraph, + type Tree, +} from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import migration from './disable-angular-eslint-prefer-standalone'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: () => Promise.resolve(projectGraph), +})); + +describe('disable-angular-eslint-prefer-standalone', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + + const projectConfig: ProjectConfiguration = { + name: 'app1', + root: 'apps/app1', + }; + projectGraph = { + dependencies: { + app1: [ + { + source: 'app1', + target: 'npm:@angular/core', + type: 'static', + }, + ], + }, + nodes: { + app1: { + data: projectConfig, + name: 'app1', + type: 'app', + }, + }, + }; + addProjectConfiguration(tree, projectConfig.name, projectConfig); + }); + + describe('.eslintrc.json', () => { + it('should not disable @angular-eslint/prefer-standalone when it is set', async () => { + writeJson(tree, 'apps/app1/.eslintrc.json', { + overrides: [ + { + files: ['*.ts'], + rules: { '@angular-eslint/prefer-standalone': ['error'] }, + }, + ], + }); + + await migration(tree); + + expect(tree.read('apps/app1/.eslintrc.json', 'utf8')) + .toMatchInlineSnapshot(` + "{ + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/prefer-standalone": ["error"] + } + } + ] + } + " + `); + }); + + it('should not disable @angular-eslint/prefer-standalone when there are multiple overrides for angular eslint and the rule is set in one of them', async () => { + writeJson(tree, 'apps/app1/.eslintrc.json', { + overrides: [ + { + files: ['*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { type: 'attribute', prefix: 'app', style: 'camelCase' }, + ], + }, + }, + { + files: ['*.ts'], + rules: { '@angular-eslint/prefer-standalone': ['error'] }, + }, + ], + }); + + await migration(tree); + + expect(tree.read('apps/app1/.eslintrc.json', 'utf8')) + .toMatchInlineSnapshot(` + "{ + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "app", + "style": "camelCase" + } + ] + } + }, + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/prefer-standalone": ["error"] + } + } + ] + } + " + `); + }); + + it('should disable @angular-eslint/prefer-standalone in an existing override for angular eslint', async () => { + writeJson(tree, 'apps/app1/.eslintrc.json', { + overrides: [ + { + files: ['*.ts'], + rules: { 'no-unused-vars': 'error' }, + }, + { + files: ['*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { type: 'attribute', prefix: 'app', style: 'camelCase' }, + ], + }, + }, + ], + }); + + await migration(tree); + + expect(tree.read('apps/app1/.eslintrc.json', 'utf8')) + .toMatchInlineSnapshot(` + "{ + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "no-unused-vars": "error" + } + }, + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "app", + "style": "camelCase" + } + ], + "@angular-eslint/prefer-standalone": "off" + } + } + ] + } + " + `); + }); + + it('should disable @angular-eslint/prefer-standalone in an existing override for ts files', async () => { + writeJson(tree, 'apps/app1/.eslintrc.json', { + overrides: [ + { + files: ['*.ts'], + rules: { 'no-unused-vars': 'error' }, + }, + ], + }); + + await migration(tree); + + expect(tree.read('apps/app1/.eslintrc.json', 'utf8')) + .toMatchInlineSnapshot(` + "{ + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "no-unused-vars": "error", + "@angular-eslint/prefer-standalone": "off" + } + } + ] + } + " + `); + }); + + it('should disable @angular-eslint/prefer-standalone in a new override', async () => { + writeJson(tree, 'apps/app1/.eslintrc.json', { + overrides: [ + { + files: ['*.html'], + rules: { 'some-rule-for-html': 'error' }, + }, + ], + }); + + await migration(tree); + + expect(tree.read('apps/app1/.eslintrc.json', 'utf8')) + .toMatchInlineSnapshot(` + "{ + "overrides": [ + { + "files": ["*.html"], + "rules": { + "some-rule-for-html": "error" + } + }, + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/prefer-standalone": "off" + } + } + ] + } + " + `); + }); + }); + + describe('flat config', () => { + it('should not disable @angular-eslint/prefer-standalone when it is set', async () => { + tree.write('eslint.config.js', 'module.exports = [];'); + tree.write( + 'apps/app1/eslint.config.js', + `module.exports = [ + { + files: ['*.ts'], + rules: { '@angular-eslint/prefer-standalone': ['error'] }, + }, + ]; + ` + ); + + await migration(tree); + + expect(tree.read('apps/app1/eslint.config.js', 'utf8')) + .toMatchInlineSnapshot(` + "module.exports = [ + { + files: ['*.ts'], + rules: { '@angular-eslint/prefer-standalone': ['error'] }, + }, + ]; + " + `); + }); + + it('should not disable @angular-eslint/prefer-standalone when there are multiple overrides for angular eslint and the rule is set in one of them', async () => { + tree.write('eslint.config.js', 'module.exports = [];'); + tree.write( + 'apps/app1/eslint.config.js', + `module.exports = [ + { + files: ['*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { type: 'attribute', prefix: 'app', style: 'camelCase' }, + ], + }, + }, + { + files: ['*.ts'], + rules: { '@angular-eslint/prefer-standalone': ['error'] }, + }, + ]; + ` + ); + + await migration(tree); + + expect(tree.read('apps/app1/eslint.config.js', 'utf8')) + .toMatchInlineSnapshot(` + "module.exports = [ + { + files: ['*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { type: 'attribute', prefix: 'app', style: 'camelCase' }, + ], + }, + }, + { + files: ['*.ts'], + rules: { '@angular-eslint/prefer-standalone': ['error'] }, + }, + ]; + " + `); + }); + + it('should disable @angular-eslint/prefer-standalone in an existing override for angular eslint', async () => { + tree.write('eslint.config.js', 'module.exports = [];'); + tree.write( + 'apps/app1/eslint.config.js', + `module.exports = [ + { + files: ['*.ts'], + rules: { 'no-unused-vars': 'error' }, + }, + { + files: ['*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { type: 'attribute', prefix: 'app', style: 'camelCase' }, + ], + }, + }, + ]; + ` + ); + + await migration(tree); + + expect(tree.read('apps/app1/eslint.config.js', 'utf8')) + .toMatchInlineSnapshot(` + "module.exports = [ + { + files: ['*.ts'], + rules: { 'no-unused-vars': 'error' }, + }, + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'app', + style: 'camelCase', + }, + ], + '@angular-eslint/prefer-standalone': 'off', + }, + }, + ]; + " + `); + }); + + it('should disable @angular-eslint/prefer-standalone in an existing override for ts files', async () => { + tree.write('eslint.config.js', 'module.exports = [];'); + tree.write( + 'apps/app1/eslint.config.js', + `module.exports = [ + { + files: ['*.ts'], + rules: { 'no-unused-vars': 'error' }, + }, + ]; + ` + ); + + await migration(tree); + + expect(tree.read('apps/app1/eslint.config.js', 'utf8')) + .toMatchInlineSnapshot(` + "module.exports = [ + { + files: ['**/*.ts'], + rules: { + 'no-unused-vars': 'error', + '@angular-eslint/prefer-standalone': 'off', + }, + }, + ]; + " + `); + }); + + it('should disable @angular-eslint/prefer-standalone in a new override', async () => { + tree.write('eslint.config.js', 'module.exports = [];'); + tree.write( + 'apps/app1/eslint.config.js', + `module.exports = [ + { + files: ['*.html'], + rules: { 'some-rule-for-html': 'error' }, + }, + ]; + ` + ); + + await migration(tree); + + expect(tree.read('apps/app1/eslint.config.js', 'utf8')) + .toMatchInlineSnapshot(` + "module.exports = [ + { + files: ['*.html'], + rules: { 'some-rule-for-html': 'error' }, + }, + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/prefer-standalone': 'off', + }, + }, + ]; + " + `); + }); + }); +}); diff --git a/packages/angular/src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone.ts b/packages/angular/src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone.ts new file mode 100644 index 0000000000000..deab7f750f48a --- /dev/null +++ b/packages/angular/src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone.ts @@ -0,0 +1,78 @@ +import { formatFiles, type Tree } from '@nx/devkit'; +import { + addOverrideToLintConfig, + isEslintConfigSupported, + lintConfigHasOverride, + updateOverrideInLintConfig, +} from '@nx/eslint/src/generators/utils/eslint-file'; +import { getProjectsFilteredByDependencies } from '../utils/projects'; + +const preferStandaloneRule = '@angular-eslint/prefer-standalone'; + +export default async function (tree: Tree) { + const projects = await getProjectsFilteredByDependencies(tree, [ + 'npm:@angular/core', + ]); + + for (const { + project: { root }, + } of projects) { + if (!isEslintConfigSupported(tree, root)) { + // ESLint config is not supported, skip + continue; + } + + if ( + lintConfigHasOverride( + tree, + root, + (o) => !!o.rules?.[preferStandaloneRule], + true + ) + ) { + // the @angular-eslint/prefer-standalone rule is set in an override, skip + continue; + } + + const ngEslintOverrideLookup: Parameters< + typeof lintConfigHasOverride + >[2] = (o) => + o.files?.includes('*.ts') && + Object.keys(o.rules ?? {}).some((r) => r.startsWith('@angular-eslint/')); + const tsFilesOverrideLookup: Parameters[2] = ( + o + ) => o.files?.length === 1 && o.files[0] === '*.ts'; + + if (lintConfigHasOverride(tree, root, ngEslintOverrideLookup, false)) { + // there is an override containing an Angular ESLint rule + updateOverrideInLintConfig(tree, root, ngEslintOverrideLookup, (o) => { + o.rules = { + ...o.rules, + [preferStandaloneRule]: 'off', + }; + return o; + }); + } else if ( + lintConfigHasOverride(tree, root, tsFilesOverrideLookup, false) + ) { + // there is an override for just *.ts files + updateOverrideInLintConfig(tree, root, tsFilesOverrideLookup, (o) => { + o.rules = { + ...o.rules, + [preferStandaloneRule]: 'off', + }; + return o; + }); + } else { + // there are no overrides for any Angular ESLint rule or just *.ts files, add a new override + addOverrideToLintConfig(tree, root, { + files: ['*.ts'], + rules: { + [preferStandaloneRule]: 'off', + }, + }); + } + } + + await formatFiles(tree); +} diff --git a/packages/angular/src/utils/versions.ts b/packages/angular/src/utils/versions.ts index 81e17cb51619f..0cdd6fef3adf0 100644 --- a/packages/angular/src/utils/versions.ts +++ b/packages/angular/src/utils/versions.ts @@ -28,6 +28,6 @@ export const tsNodeVersion = '10.9.1'; export const jestPresetAngularVersion = '~14.4.0'; export const typesNodeVersion = '18.16.9'; export const jasmineMarblesVersion = '^0.9.2'; -export const analogVitestAngular = '~1.10.0-beta.6'; +export const analogVitestAngular = '~1.10.0'; export const jsoncEslintParserVersion = '^2.1.0'; diff --git a/packages/jest/src/generators/configuration/configuration.spec.ts b/packages/jest/src/generators/configuration/configuration.spec.ts index 333a4d3c7a587..449cbc7d42dd7 100644 --- a/packages/jest/src/generators/configuration/configuration.spec.ts +++ b/packages/jest/src/generators/configuration/configuration.spec.ts @@ -55,7 +55,9 @@ describe('jestProject', () => { } as JestProjectSchema); expect(tree.read('libs/lib1/src/test-setup.ts', 'utf-8')) .toMatchInlineSnapshot(` - "import 'jest-preset-angular/setup-jest'; + "import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + + setupZoneTestEnv(); " `); expect(tree.exists('libs/lib1/jest.config.ts')).toBeTruthy(); diff --git a/packages/jest/src/generators/configuration/files-angular/src/test-setup.ts__tmpl__ b/packages/jest/src/generators/configuration/files-angular/src/test-setup.ts__tmpl__ index 1100b3e8a6ed0..58c511e08265c 100644 --- a/packages/jest/src/generators/configuration/files-angular/src/test-setup.ts__tmpl__ +++ b/packages/jest/src/generators/configuration/files-angular/src/test-setup.ts__tmpl__ @@ -1 +1,3 @@ -import 'jest-preset-angular/setup-jest'; +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv(); diff --git a/packages/js/src/generators/typescript-sync/typescript-sync.spec.ts b/packages/js/src/generators/typescript-sync/typescript-sync.spec.ts index 69bb2b140a53f..0971681902e53 100644 --- a/packages/js/src/generators/typescript-sync/typescript-sync.spec.ts +++ b/packages/js/src/generators/typescript-sync/typescript-sync.spec.ts @@ -282,7 +282,14 @@ describe('syncGenerator()', () => { // This is a nested comment "target": "es5" }, - "references": [{ "path": "./packages/a" }, { "path": "./packages/b" }] + "references": [ + { + "path": "./packages/a" + }, + { + "path": "./packages/b" + } + ] } " `); @@ -302,7 +309,11 @@ describe('syncGenerator()', () => { "compilerOptions": { "composite": true }, - "references": [{ "path": "./packages/b" }] + "references": [ + { + "path": "./packages/b" + } + ] } " `); @@ -569,7 +580,11 @@ describe('syncGenerator()', () => { // This is a nested comment "target": "es5" }, - "references": [{ "path": "../bar/tsconfig.build.json" }] + "references": [ + { + "path": "../bar/tsconfig.build.json" + } + ] } " `); @@ -593,9 +608,15 @@ describe('syncGenerator()', () => { "composite": true }, "references": [ - { "path": "./packages/a" }, - { "path": "./packages/b" }, - { "path": "./packages/foo" } + { + "path": "./packages/a" + }, + { + "path": "./packages/b" + }, + { + "path": "./packages/foo" + } ] } " @@ -639,21 +660,21 @@ describe('syncGenerator()', () => { expect(readJson(tree, 'packages/b/tsconfig.json').references) .toMatchInlineSnapshot(` - [ - { - "path": "../a", - }, - ] - `); + [ + { + "path": "../a", + }, + ] + `); expect( readJson(tree, `packages/b/${runtimeTsConfigFileName}`).references ).toMatchInlineSnapshot(` - [ - { - "path": "../a/${runtimeTsConfigFileName}", - }, - ] - `); + [ + { + "path": "../a/${runtimeTsConfigFileName}", + }, + ] + `); } ); @@ -908,96 +929,96 @@ describe('syncGenerator()', () => { ).toBeUndefined(); expect(readJson(tree, 'packages/b/tsconfig.json').references) .toMatchInlineSnapshot(` - [ - { - "path": "../a", - }, - ] - `); + [ + { + "path": "../a", + }, + ] + `); expect( readJson(tree, `packages/b/${runtimeTsConfigFileName}`).references ).toMatchInlineSnapshot(` - [ - { - "path": "../a/${runtimeTsConfigFileName}", - }, - ] - `); + [ + { + "path": "../a/${runtimeTsConfigFileName}", + }, + ] + `); expect(readJson(tree, 'packages/c/tsconfig.json').references) .toMatchInlineSnapshot(` - [ - { - "path": "../a", - }, - { - "path": "../b", - }, - ] - `); + [ + { + "path": "../a", + }, + { + "path": "../b", + }, + ] + `); expect( readJson(tree, `packages/c/${runtimeTsConfigFileName}`).references ).toMatchInlineSnapshot(` - [ - { - "path": "../a/${runtimeTsConfigFileName}", - }, - { - "path": "../b/${runtimeTsConfigFileName}", - }, - ] - `); + [ + { + "path": "../a/${runtimeTsConfigFileName}", + }, + { + "path": "../b/${runtimeTsConfigFileName}", + }, + ] + `); expect(readJson(tree, 'packages/d/tsconfig.json').references) .toMatchInlineSnapshot(` - [ - { - "path": "../a", - }, - { - "path": "../b", - }, - ] - `); + [ + { + "path": "../a", + }, + { + "path": "../b", + }, + ] + `); expect( readJson(tree, `packages/d/${runtimeTsConfigFileName}`).references ).toMatchInlineSnapshot(` - [ - { - "path": "../a/${runtimeTsConfigFileName}", - }, - { - "path": "../b/${runtimeTsConfigFileName}", - }, - ] - `); + [ + { + "path": "../a/${runtimeTsConfigFileName}", + }, + { + "path": "../b/${runtimeTsConfigFileName}", + }, + ] + `); expect(readJson(tree, 'packages/e/tsconfig.json').references) .toMatchInlineSnapshot(` - [ - { - "path": "../a", - }, - { - "path": "../b", - }, - { - "path": "../d", - }, - ] - `); + [ + { + "path": "../a", + }, + { + "path": "../b", + }, + { + "path": "../d", + }, + ] + `); expect( readJson(tree, `packages/e/${runtimeTsConfigFileName}`).references ).toMatchInlineSnapshot(` - [ - { - "path": "../a/${runtimeTsConfigFileName}", - }, - { - "path": "../b/${runtimeTsConfigFileName}", - }, - { - "path": "../d/${runtimeTsConfigFileName}", - }, - ] - `); + [ + { + "path": "../a/${runtimeTsConfigFileName}", + }, + { + "path": "../b/${runtimeTsConfigFileName}", + }, + { + "path": "../d/${runtimeTsConfigFileName}", + }, + ] + `); } ); @@ -1096,20 +1117,20 @@ describe('syncGenerator()', () => { expect(readJson(tree, 'packages/b/tsconfig.json').references) .toMatchInlineSnapshot(` - [ - { - "path": "../a", - }, - ] - `); + [ + { + "path": "../a", + }, + ] + `); expect(readJson(tree, 'packages/b/tsconfig.custom.json').references) .toMatchInlineSnapshot(` - [ - { - "path": "../a/tsconfig.custom.json", - }, - ] - `); + [ + { + "path": "../a/tsconfig.custom.json", + }, + ] + `); }); it('should sync project references to multiple configured runtime tsconfig files', async () => { diff --git a/packages/js/src/generators/typescript-sync/typescript-sync.ts b/packages/js/src/generators/typescript-sync/typescript-sync.ts index 1b0936ac258d8..95b4811965515 100644 --- a/packages/js/src/generators/typescript-sync/typescript-sync.ts +++ b/packages/js/src/generators/typescript-sync/typescript-sync.ts @@ -261,7 +261,7 @@ export async function syncGenerator(tree: Tree): Promise { return { outOfSyncMessage: - 'Based on the workspace project graph, some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.', + 'Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.', }; } } @@ -604,7 +604,7 @@ function patchTsconfigJsonReferences( stringifiedJsonContents, ['references'], updatedReferences, - {} + { formattingOptions: { keepLines: false } } ); const updatedJsonContents = applyEdits(stringifiedJsonContents, edits); // The final contents will be formatted by formatFiles() later diff --git a/packages/js/src/utils/buildable-libs-utils.spec.ts b/packages/js/src/utils/buildable-libs-utils.spec.ts index 2c3443bea78d8..f9cb121e31be4 100644 --- a/packages/js/src/utils/buildable-libs-utils.spec.ts +++ b/packages/js/src/utils/buildable-libs-utils.spec.ts @@ -1,10 +1,14 @@ import { DependencyType, ProjectGraph, TaskGraph } from '@nx/devkit'; +import { TempFs } from '@nx/devkit/internal-testing-utils'; +import { readFileSync } from 'fs'; import { - calculateProjectDependencies, calculateDependenciesFromTaskGraph, + calculateProjectDependencies, + createTmpTsConfig, DependentBuildableProjectNode, updatePaths, } from './buildable-libs-utils'; +import { join } from 'path'; describe('updatePaths', () => { const deps: DependentBuildableProjectNode[] = [ @@ -768,3 +772,45 @@ describe('missingDependencies', () => { ).toThrow(); }); }); + +describe('createTmpTsConfig', () => { + it('should create a temporary tsconfig file extending the provided tsconfig', () => { + const fs = new TempFs('buildable-libs-utils#createTmpTsConfig'); + const tsconfigPath = 'packages/foo/tsconfig.json'; + fs.createFileSync(tsconfigPath, '{}'); + + const tmpTsConfigPath = createTmpTsConfig( + tsconfigPath, + fs.tempDir, + 'packages/foo', + [] + ); + + const tmpTsConfig = readFileSync(tmpTsConfigPath, 'utf8'); + // would be generated at /tmp/packages/foo/build/tsconfig.generated.json + // while the extended tsconfig path is /packages/foo/tsconfig.json + expect(JSON.parse(tmpTsConfig).extends).toBe( + '../../../../packages/foo/tsconfig.json' + ); + }); + + it('should also work when the provided tsconfig is an absolute path', () => { + const fs = new TempFs('buildable-libs-utils#createTmpTsConfig'); + const tsconfigPath = join(fs.tempDir, 'packages/foo/tsconfig.json'); + fs.createFileSync(tsconfigPath, '{}'); + + const tmpTsConfigPath = createTmpTsConfig( + tsconfigPath, + fs.tempDir, + 'packages/foo', + [] + ); + + const tmpTsConfig = readFileSync(tmpTsConfigPath, 'utf8'); + // would be generated at /tmp/packages/foo/build/tsconfig.generated.json + // while the extended tsconfig path is /packages/foo/tsconfig.json + expect(JSON.parse(tmpTsConfig).extends).toBe( + '../../../../packages/foo/tsconfig.json' + ); + }); +}); diff --git a/packages/js/src/utils/buildable-libs-utils.ts b/packages/js/src/utils/buildable-libs-utils.ts index a2754b3dc4555..a272a44cd91e8 100644 --- a/packages/js/src/utils/buildable-libs-utils.ts +++ b/packages/js/src/utils/buildable-libs-utils.ts @@ -16,7 +16,7 @@ import { unlinkSync } from 'fs'; import { isNpmProject } from 'nx/src/project-graph/operators'; import { directoryExists, fileExists } from 'nx/src/utils/fileutils'; import { output } from 'nx/src/utils/output'; -import { dirname, join, relative, isAbsolute, extname } from 'path'; +import { dirname, join, relative, extname, resolve } from 'path'; import type * as ts from 'typescript'; import { readTsConfigPaths } from './typescript/ts-config'; @@ -194,18 +194,23 @@ function collectDependencies( } function readTsConfigWithRemappedPaths( - tsConfig: string, - generatedTsConfigPath: string, - dependencies: DependentBuildableProjectNode[] + originalTsconfigPath: string, + generatedTsconfigPath: string, + dependencies: DependentBuildableProjectNode[], + workspaceRoot: string ) { const generatedTsConfig: any = { compilerOptions: {} }; - const dirnameTsConfig = dirname(generatedTsConfigPath); - const relativeTsconfig = isAbsolute(dirnameTsConfig) - ? relative(workspaceRoot, dirnameTsConfig) - : dirnameTsConfig; - generatedTsConfig.extends = relative(relativeTsconfig, tsConfig); + const normalizedTsConfig = resolve(workspaceRoot, originalTsconfigPath); + const normalizedGeneratedTsConfigDir = resolve( + workspaceRoot, + dirname(generatedTsconfigPath) + ); + generatedTsConfig.extends = relative( + normalizedGeneratedTsConfigDir, + normalizedTsConfig + ); generatedTsConfig.compilerOptions.paths = computeCompilerOptionsPaths( - tsConfig, + originalTsconfigPath, dependencies ); @@ -443,7 +448,8 @@ export function createTmpTsConfig( const parsedTSConfig = readTsConfigWithRemappedPaths( tsconfigPath, tmpTsConfigPath, - dependencies + dependencies, + workspaceRoot ); process.on('exit', () => cleanupTmpTsConfigFile(tmpTsConfigPath)); diff --git a/packages/module-federation/README.md b/packages/module-federation/README.md index 7ff5a6dc3deb9..3c7df98c4e469 100644 --- a/packages/module-federation/README.md +++ b/packages/module-federation/README.md @@ -14,5 +14,7 @@ Nx is a build system, optimized for monorepos, with plugins for popular frameworks and tools and advanced CI capabilities including caching and distribution. This package is a [Module Federation plugin for Nx](https://nx.dev/nx-api/module-federation). +Module Federation allows developers to share code between deployed applications easier and makes team collaboration more efficient. +To learn more about Module Federation, check out the [official documentation](https://module-federation.io/). {{content}} diff --git a/packages/module-federation/package.json b/packages/module-federation/package.json index a40eb3618a568..21da73e961858 100644 --- a/packages/module-federation/package.json +++ b/packages/module-federation/package.json @@ -27,6 +27,7 @@ "tslib": "^2.3.0", "@nx/devkit": "file:../devkit", "@nx/js": "file:../js", + "@nx/web": "file:../web", "picocolors": "^1.1.0", "webpack": "5.88.0", "@rspack/core": "1.1.3", diff --git a/packages/react/src/utils/build-static.remotes.ts b/packages/module-federation/src/executors/utils/build-static-remotes.ts similarity index 87% rename from packages/react/src/utils/build-static.remotes.ts rename to packages/module-federation/src/executors/utils/build-static-remotes.ts index 946d65331d8d1..b99d8fd6610c6 100644 --- a/packages/react/src/utils/build-static.remotes.ts +++ b/packages/module-federation/src/executors/utils/build-static-remotes.ts @@ -1,7 +1,6 @@ -import { StaticRemotesConfig } from '@nx/module-federation/src/utils'; -import { ExecutorContext } from '@nx/devkit'; -import { ModuleFederationDevServerOptions } from '../executors/module-federation-dev-server/schema'; -import { logger } from 'nx/src/utils/logger'; +import { ExecutorContext, logger } from '@nx/devkit'; +import { type StaticRemotesConfig } from '../../utils'; +import { type BuildStaticRemotesOptions } from './models'; import { fork } from 'node:child_process'; import { join } from 'path'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; @@ -11,7 +10,8 @@ export async function buildStaticRemotes( staticRemotesConfig: StaticRemotesConfig, nxBin, context: ExecutorContext, - options: ModuleFederationDevServerOptions + options: BuildStaticRemotesOptions, + buildTarget: 'build' | 'server' = 'build' ) { if (!staticRemotesConfig.remotes.length) { return; @@ -34,7 +34,7 @@ export async function buildStaticRemotes( nxBin, [ 'run-many', - `--target=build`, + `--target=${buildTarget}`, `--projects=${staticRemotesConfig.remotes.join(',')}`, ...(context.configurationName ? [`--configuration=${context.configurationName}`] @@ -66,7 +66,7 @@ export async function buildStaticRemotes( logger.log(stdoutString); } - if (stdoutString.includes('Successfully ran target build')) { + if (stdoutString.includes(`Successfully ran target ${buildTarget}`)) { staticProcess.stdout.removeAllListeners('data'); logger.info( `NX Built ${staticRemotesConfig.remotes.length} static remotes` diff --git a/packages/module-federation/src/executors/utils/index.ts b/packages/module-federation/src/executors/utils/index.ts new file mode 100644 index 0000000000000..916cecc631e0a --- /dev/null +++ b/packages/module-federation/src/executors/utils/index.ts @@ -0,0 +1,4 @@ +export * from './start-static-remotes-file-server'; +export * from './build-static-remotes'; +export * from './start-remote-iterators'; +export { DevRemoteDefinition } from './models'; diff --git a/packages/module-federation/src/executors/utils/models.ts b/packages/module-federation/src/executors/utils/models.ts new file mode 100644 index 0000000000000..362705319e66f --- /dev/null +++ b/packages/module-federation/src/executors/utils/models.ts @@ -0,0 +1,38 @@ +import type { ProjectConfiguration, ExecutorContext } from '@nx/devkit'; + +export type DevRemoteDefinition = + | string + | { remoteName: string; configuration: string }; + +export type StartRemoteFn = ( + remotes: string[], + workspaceProjects: Record, + options: { + devRemotes: DevRemoteDefinition[]; + verbose: boolean; + }, + context: ExecutorContext, + target: 'serve' | 'serve-static' +) => Promise[]>; + +export interface StaticRemotesOptions { + staticRemotesPort?: number; + host?: string; + ssl?: boolean; + sslCert?: string; + sslKey?: string; +} + +export interface BuildStaticRemotesOptions extends StaticRemotesOptions { + parallel?: number; +} + +export interface StartRemoteIteratorsOptions extends BuildStaticRemotesOptions { + devRemotes: DevRemoteDefinition[]; + skipRemotes?: string[]; + buildTarget?: string; + liveReload?: boolean; + open?: boolean; + ssl?: boolean; + verbose: boolean; +} diff --git a/packages/module-federation/src/executors/utils/start-remote-iterators.ts b/packages/module-federation/src/executors/utils/start-remote-iterators.ts new file mode 100644 index 0000000000000..d21245a4b1eda --- /dev/null +++ b/packages/module-federation/src/executors/utils/start-remote-iterators.ts @@ -0,0 +1,123 @@ +import { StartRemoteFn, type StartRemoteIteratorsOptions } from './models'; +import { + getModuleFederationConfig, + getRemotes, + parseStaticRemotesConfig, + parseStaticSsrRemotesConfig, + startRemoteProxies, + startSsrRemoteProxies, +} from '../../utils'; +import { buildStaticRemotes } from './build-static-remotes'; +import { + startSsrStaticRemotesFileServer, + startStaticRemotesFileServer, +} from './start-static-remotes-file-server'; +import { + type ExecutorContext, + readProjectsConfigurationFromProjectGraph, +} from '@nx/devkit'; + +export async function startRemoteIterators( + options: StartRemoteIteratorsOptions, + context: ExecutorContext, + startRemoteFn: StartRemoteFn, + pathToManifestFile: string | undefined, + pluginName: 'react' | 'angular' = 'react', + isServer = false +) { + const nxBin = require.resolve('nx/bin/nx'); + const { projects: workspaceProjects } = + readProjectsConfigurationFromProjectGraph(context.projectGraph); + const project = workspaceProjects[context.projectName]; + const moduleFederationConfig = getModuleFederationConfig( + project.targets.build.options.tsConfig, + context.root, + project.root, + pluginName + ); + + const remoteNames = options.devRemotes.map((r) => + typeof r === 'string' ? r : r.remoteName + ); + + const remotes = getRemotes( + remoteNames, + options.skipRemotes, + moduleFederationConfig, + { + projectName: project.name, + projectGraph: context.projectGraph, + root: context.root, + }, + pathToManifestFile + ); + + options.staticRemotesPort ??= remotes.staticRemotePort; + + // Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin + process.env.NX_MF_DEV_REMOTES = JSON.stringify([ + ...( + remotes.devRemotes.map((r) => + typeof r === 'string' ? r : r.remoteName + ) ?? [] + ).map((r) => r.replace(/-/g, '_')), + project.name.replace(/-/g, '_'), + ]); + + const staticRemotesConfig = isServer + ? parseStaticSsrRemotesConfig( + [...remotes.staticRemotes, ...remotes.dynamicRemotes], + context + ) + : parseStaticRemotesConfig( + [...remotes.staticRemotes, ...remotes.dynamicRemotes], + context + ); + const mappedLocationsOfStaticRemotes = await buildStaticRemotes( + staticRemotesConfig, + nxBin, + context, + options, + isServer ? 'server' : 'build' + ); + + const devRemoteIters = await startRemoteFn( + remotes.devRemotes, + workspaceProjects, + options, + context, + 'serve' + ); + + const staticRemotesIter = isServer + ? startSsrStaticRemotesFileServer(staticRemotesConfig, context, options) + : startStaticRemotesFileServer(staticRemotesConfig, context, options); + + isServer + ? startSsrRemoteProxies( + staticRemotesConfig, + mappedLocationsOfStaticRemotes, + options.ssl + ? { + pathToCert: options.sslCert, + pathToKey: options.sslKey, + } + : undefined + ) + : startRemoteProxies( + staticRemotesConfig, + mappedLocationsOfStaticRemotes, + options.ssl + ? { + pathToCert: options.sslCert, + pathToKey: options.sslKey, + } + : undefined + ); + + return { + remotes, + devRemoteIters, + staticRemotesIter, + }; +} diff --git a/packages/angular/src/executors/module-federation-dev-server/lib/start-static-remotes-file-server.ts b/packages/module-federation/src/executors/utils/start-static-remotes-file-server.ts similarity index 56% rename from packages/angular/src/executors/module-federation-dev-server/lib/start-static-remotes-file-server.ts rename to packages/module-federation/src/executors/utils/start-static-remotes-file-server.ts index c44fadee06a74..982c1db4acc7f 100644 --- a/packages/angular/src/executors/module-federation-dev-server/lib/start-static-remotes-file-server.ts +++ b/packages/module-federation/src/executors/utils/start-static-remotes-file-server.ts @@ -1,14 +1,15 @@ import { type ExecutorContext, workspaceRoot } from '@nx/devkit'; -import { type Schema } from '../schema'; +import { type StaticRemotesOptions } from './models'; import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl'; import { join } from 'path'; import { cpSync } from 'fs'; -import type { StaticRemotesConfig } from '@nx/module-federation/src/utils'; +import type { StaticRemotesConfig } from '../../utils'; export function startStaticRemotesFileServer( staticRemotesConfig: StaticRemotesConfig, context: ExecutorContext, - options: Schema + options: StaticRemotesOptions, + forceMoveToCommonLocation = false ) { if ( !staticRemotesConfig.remotes || @@ -16,15 +17,17 @@ export function startStaticRemotesFileServer( ) { return; } - let shouldMoveToCommonLocation = false; + let shouldMoveToCommonLocation = forceMoveToCommonLocation || false; let commonOutputDirectory: string; - for (const app of staticRemotesConfig.remotes) { - const remoteBasePath = staticRemotesConfig.config[app].basePath; - if (!commonOutputDirectory) { - commonOutputDirectory = remoteBasePath; - } else if (commonOutputDirectory !== remoteBasePath) { - shouldMoveToCommonLocation = true; - break; + if (!forceMoveToCommonLocation) { + for (const app of staticRemotesConfig.remotes) { + const remoteBasePath = staticRemotesConfig.config[app].basePath; + if (!commonOutputDirectory) { + commonOutputDirectory = remoteBasePath; + } else if (commonOutputDirectory !== remoteBasePath) { + shouldMoveToCommonLocation = true; + break; + } } } @@ -62,3 +65,21 @@ export function startStaticRemotesFileServer( ); return staticRemotesIter; } + +export async function* startSsrStaticRemotesFileServer( + staticRemotesConfig: StaticRemotesConfig, + context: ExecutorContext, + options: StaticRemotesOptions +) { + const staticRemotesIter = startStaticRemotesFileServer( + staticRemotesConfig, + context, + options, + true + ); + if (!staticRemotesIter) { + yield { success: true }; + return; + } + yield* staticRemotesIter; +} diff --git a/packages/nx/bin/post-install.ts b/packages/nx/bin/post-install.ts index 714386af0aa0a..9822b4575e861 100644 --- a/packages/nx/bin/post-install.ts +++ b/packages/nx/bin/post-install.ts @@ -17,9 +17,11 @@ import { setupWorkspaceContext } from '../src/utils/workspace-context'; if (isMainNxPackage() && fileExists(join(workspaceRoot, 'nx.json'))) { assertSupportedPlatform(); setupWorkspaceContext(workspaceRoot); - try { - await daemonClient.stop(); - } catch (e) {} + if (daemonClient.enabled()) { + try { + await daemonClient.stop(); + } catch (e) {} + } const tasks: Array> = [ buildProjectGraphAndSourceMapsWithoutDaemon(), ]; diff --git a/packages/nx/plugins/package-json.ts b/packages/nx/plugins/package-json.ts index 502e0da250204..3ea2d5a85e627 100644 --- a/packages/nx/plugins/package-json.ts +++ b/packages/nx/plugins/package-json.ts @@ -1,13 +1,50 @@ -import type { NxPluginV2 } from '../src/project-graph/plugins'; +import { createNodesFromFiles, NxPluginV2 } from '../src/project-graph/plugins'; import { workspaceRoot } from '../src/utils/workspace-root'; import { createNodeFromPackageJson } from '../src/plugins/package-json'; +import { workspaceDataDirectory } from '../src/utils/cache-directory'; +import { join } from 'path'; +import { ProjectConfiguration } from '../src/config/workspace-json-project-json'; +import { readJsonFile, writeJsonFile } from '../src/utils/fileutils'; + +export type PackageJsonConfigurationCache = { + [hash: string]: ProjectConfiguration; +}; + +const cachePath = join(workspaceDataDirectory, 'package-json.hash'); + +export function readPackageJsonConfigurationCache() { + try { + return readJsonFile(cachePath); + } catch (e) { + return {}; + } +} + +function writeCache(cache: PackageJsonConfigurationCache) { + writeJsonFile(cachePath, cache); +} const plugin: NxPluginV2 = { name: 'nx-all-package-jsons-plugin', - createNodes: [ + createNodesV2: [ '*/**/package.json', - (f) => createNodeFromPackageJson(f, workspaceRoot), + (configFiles, options, context) => { + const cache = readPackageJsonConfigurationCache(); + + const result = createNodesFromFiles( + (f) => createNodeFromPackageJson(f, workspaceRoot, cache), + configFiles, + options, + context + ); + + writeCache(cache); + + return result; + }, ], }; module.exports = plugin; +module.exports.readPackageJsonConfigurationCache = + readPackageJsonConfigurationCache; diff --git a/packages/nx/src/command-line/affected/affected.ts b/packages/nx/src/command-line/affected/affected.ts index 2bda67751f240..b915fa2516410 100644 --- a/packages/nx/src/command-line/affected/affected.ts +++ b/packages/nx/src/command-line/affected/affected.ts @@ -53,7 +53,9 @@ export async function affected( await connectToNxCloudIfExplicitlyAsked(nxArgs); - const projectGraph = await createProjectGraphAsync({ exitOnError: true }); + const projectGraph = await createProjectGraphAsync({ + exitOnError: true, + }); const projects = await getAffectedGraphNodes(nxArgs, projectGraph); try { diff --git a/packages/nx/src/command-line/format/format.ts b/packages/nx/src/command-line/format/format.ts index c5a9c028a7ac4..655de5930729a 100644 --- a/packages/nx/src/command-line/format/format.ts +++ b/packages/nx/src/command-line/format/format.ts @@ -1,10 +1,10 @@ import { exec, execSync } from 'node:child_process'; import * as path from 'node:path'; import * as yargs from 'yargs'; -import { FileData, calculateFileChanges } from '../../project-graph/file-utils'; +import { calculateFileChanges, FileData } from '../../project-graph/file-utils'; import { - NxArgs, getProjectRoots, + NxArgs, parseFiles, splitArgsIntoNxArgsAndOverrides, } from '../../utils/command-line-utils'; @@ -52,7 +52,7 @@ export async function format( const patterns = (await getPatterns({ ...args, ...nxArgs } as any)).map( // prettier removes one of the \ // prettier-ignore - (p) => `"${p.replace(/\$/g, "\\\$")}"` + (p) => `"${p.replace(/\$/g, '\\\$')}"` ); // Chunkify the patterns array to prevent crashing the windows terminal @@ -156,7 +156,9 @@ async function getPatternsFromApps( allWorkspaceFiles: FileData[], projectGraph: ProjectGraph ): Promise { - const graph = await createProjectGraphAsync({ exitOnError: true }); + const graph = await createProjectGraphAsync({ + exitOnError: true, + }); const affectedGraph = await filterAffected( graph, calculateFileChanges(affectedFiles, allWorkspaceFiles) @@ -268,6 +270,7 @@ function sortTsConfig() { } let prettierPath: string; + function getPrettierPath() { if (prettierPath) { return prettierPath; diff --git a/packages/nx/src/command-line/run-many/run-many.spec.ts b/packages/nx/src/command-line/run-many/run-many.spec.ts index c2aa94cd1821e..5116e9d47c142 100644 --- a/packages/nx/src/command-line/run-many/run-many.spec.ts +++ b/packages/nx/src/command-line/run-many/run-many.spec.ts @@ -189,8 +189,8 @@ describe('run-many', () => { } }); - it('should be able to select and exclude via patterns', async () => { - performance.mark('start'); + it('should be able to select and exclude via patterns', () => { + const start = performance.now(); projectsToRun( { targets: ['test'], @@ -199,9 +199,8 @@ describe('run-many', () => { }, projectGraph ); - performance.mark('end'); - const measure = performance.measure('projects', 'start', 'end'); - expect(measure.duration).toBeLessThan(10000); + const end = performance.now(); + expect(end - start).toBeLessThan(10000); }); }); }); diff --git a/packages/nx/src/command-line/show/project.ts b/packages/nx/src/command-line/show/project.ts index 5cb1b7bcf211d..ab093399fe273 100644 --- a/packages/nx/src/command-line/show/project.ts +++ b/packages/nx/src/command-line/show/project.ts @@ -6,6 +6,8 @@ import { generateGraph } from '../graph/graph'; export async function showProjectHandler( args: ShowProjectOptions ): Promise { + performance.mark('code-loading:end'); + performance.measure('code-loading', 'init-local', 'code-loading:end'); const graph = await createProjectGraphAsync(); const node = graph.nodes[args.projectName]; if (!node) { @@ -70,5 +72,8 @@ export async function showProjectHandler( } } } + + // TODO: Find a better fix for this + await new Promise((res) => setImmediate(res)); await output.drain(); } diff --git a/packages/nx/src/command-line/show/projects.spec.ts b/packages/nx/src/command-line/show/projects.spec.ts index 137e47a8ab6eb..fbc69a62dc694 100644 --- a/packages/nx/src/command-line/show/projects.spec.ts +++ b/packages/nx/src/command-line/show/projects.spec.ts @@ -20,9 +20,13 @@ jest.mock('../../project-graph/project-graph', () => ({ .mockImplementation(() => Promise.resolve(graph)), })); +performance.mark = jest.fn(); +performance.measure = jest.fn(); + describe('show projects', () => { beforeEach(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); + performance.mark('init-local'); }); afterEach(() => { jest.clearAllMocks(); diff --git a/packages/nx/src/command-line/show/projects.ts b/packages/nx/src/command-line/show/projects.ts index 1da055b751ee3..5661b24d9fa23 100644 --- a/packages/nx/src/command-line/show/projects.ts +++ b/packages/nx/src/command-line/show/projects.ts @@ -23,6 +23,8 @@ import { ShowProjectsOptions } from './command-object'; export async function showProjectsHandler( args: ShowProjectsOptions ): Promise { + performance.mark('code-loading:end'); + performance.measure('code-loading', 'init-local', 'code-loading:end'); let graph = await createProjectGraphAsync(); const nxJson = readNxJson(); const { nxArgs } = splitArgsIntoNxArgsAndOverrides( @@ -82,6 +84,8 @@ export async function showProjectsHandler( } } + // TODO: Find a better fix for this + await new Promise((res) => setImmediate(res)); await output.drain(); } diff --git a/packages/nx/src/command-line/sync/sync.ts b/packages/nx/src/command-line/sync/sync.ts index 663635f57d789..0e187bcba6a61 100644 --- a/packages/nx/src/command-line/sync/sync.ts +++ b/packages/nx/src/command-line/sync/sync.ts @@ -47,10 +47,7 @@ export function syncHandler(options: SyncOptions): Promise { ? 'The workspace is up to date' : 'The workspace is already up to date', bodyLines: syncGenerators.map( - (generator) => - `The ${chalk.bold( - generator - )} sync generator didn't identify any files in the workspace that are out of sync.` + (generator) => `[${chalk.bold(generator)}]: All files are up to date.` ), }); return 0; @@ -133,9 +130,9 @@ export function syncHandler(options: SyncOptions): Promise { 'Syncing the workspace failed with the following error:', '', e.message, - ...(options.verbose && !!e.stack ? [`\n${e.stack}`] : []), + ...(!!e.stack ? [`\n${e.stack}`] : []), '', - 'Please rerun with `--verbose` and report the error at: https://github.com/nrwl/nx/issues/new/choose', + 'Please report the error at: https://github.com/nrwl/nx/issues/new/choose', ], }); diff --git a/packages/nx/src/daemon/server/handle-request-project-graph.ts b/packages/nx/src/daemon/server/handle-request-project-graph.ts index 1dc4507383c82..449d57a693805 100644 --- a/packages/nx/src/daemon/server/handle-request-project-graph.ts +++ b/packages/nx/src/daemon/server/handle-request-project-graph.ts @@ -3,8 +3,6 @@ import { serializeResult } from '../socket-utils'; import { serverLogger } from './logger'; import { getCachedSerializedProjectGraphPromise } from './project-graph-incremental-recomputation'; import { HandlerResult } from './server'; -import { getPlugins } from './plugins'; -import { readNxJson } from '../../config/nx-json'; export async function handleRequestProjectGraph(): Promise { try { diff --git a/packages/nx/src/daemon/server/plugins.ts b/packages/nx/src/daemon/server/plugins.ts deleted file mode 100644 index 3e6da219c98a8..0000000000000 --- a/packages/nx/src/daemon/server/plugins.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { hashObject } from '../../hasher/file-hasher'; -import { readNxJson } from '../../config/nx-json'; -import { - LoadedNxPlugin, - loadNxPlugins, -} from '../../project-graph/plugins/internal-api'; -import { workspaceRoot } from '../../utils/workspace-root'; - -let currentPluginsConfigurationHash: string; -let loadedPlugins: LoadedNxPlugin[]; -let cleanup: () => void; - -export async function getPlugins() { - const pluginsConfiguration = readNxJson().plugins ?? []; - const pluginsConfigurationHash = hashObject(pluginsConfiguration); - - // If the plugins configuration has not changed, reuse the current plugins - if ( - loadedPlugins && - pluginsConfigurationHash === currentPluginsConfigurationHash - ) { - return loadedPlugins; - } - - // Cleanup current plugins before loading new ones - if (cleanup) { - cleanup(); - } - - currentPluginsConfigurationHash = pluginsConfigurationHash; - const [result, cleanupFn] = await loadNxPlugins( - pluginsConfiguration, - workspaceRoot - ); - cleanup = cleanupFn; - loadedPlugins = result; - return result; -} - -export function cleanupPlugins() { - cleanup(); -} diff --git a/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts b/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts index fc3abef69a438..81f4830135ff7 100644 --- a/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts +++ b/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts @@ -31,12 +31,12 @@ import { serverLogger } from './logger'; import { NxWorkspaceFilesExternals } from '../../native'; import { ConfigurationResult } from '../../project-graph/utils/project-configuration-utils'; import { LoadedNxPlugin } from '../../project-graph/plugins/internal-api'; -import { getPlugins } from './plugins'; import { DaemonProjectGraphError, ProjectConfigurationsError, isAggregateProjectGraphError, } from '../../project-graph/error-types'; +import { getPlugins } from '../../project-graph/plugins/get-plugins'; interface SerializedProjectGraph { error: Error | null; diff --git a/packages/nx/src/daemon/server/shutdown-utils.ts b/packages/nx/src/daemon/server/shutdown-utils.ts index fde1e65d936f6..d49e74be69875 100644 --- a/packages/nx/src/daemon/server/shutdown-utils.ts +++ b/packages/nx/src/daemon/server/shutdown-utils.ts @@ -4,12 +4,12 @@ import { serverLogger } from './logger'; import { serializeResult } from '../socket-utils'; import { deleteDaemonJsonProcessCache } from '../cache'; import type { Watcher } from '../../native'; -import { cleanupPlugins } from './plugins'; import { DaemonProjectGraphError, ProjectGraphError, } from '../../project-graph/error-types'; import { removeDbConnections } from '../../utils/db-connection'; +import { cleanupPlugins } from '../../project-graph/plugins/get-plugins'; export const SERVER_INACTIVITY_TIMEOUT_MS = 10800000 as const; // 10800000 ms = 3 hours diff --git a/packages/nx/src/executors/utils/convert-nx-executor.ts b/packages/nx/src/executors/utils/convert-nx-executor.ts index 5a43944bfdc0d..961dad287762a 100644 --- a/packages/nx/src/executors/utils/convert-nx-executor.ts +++ b/packages/nx/src/executors/utils/convert-nx-executor.ts @@ -8,7 +8,7 @@ import { Executor, ExecutorContext } from '../../config/misc-interfaces'; import { retrieveProjectConfigurations } from '../../project-graph/utils/retrieve-workspace-files'; import { readProjectConfigurationsFromRootMap } from '../../project-graph/utils/project-configuration-utils'; import { ProjectsConfigurations } from '../../config/workspace-json-project-json'; -import { loadNxPlugins } from '../../project-graph/plugins/internal-api'; +import { getPlugins } from '../../project-graph/plugins/get-plugins'; /** * Convert an Nx Executor into an Angular Devkit Builder @@ -20,10 +20,7 @@ export function convertNxExecutor(executor: Executor) { const promise = async () => { const nxJsonConfiguration = readNxJson(builderContext.workspaceRoot); - const [plugins, cleanup] = await loadNxPlugins( - nxJsonConfiguration.plugins, - builderContext.workspaceRoot - ); + const plugins = await getPlugins(); const projectsConfigurations: ProjectsConfigurations = { version: 2, projects: readProjectConfigurationsFromRootMap( @@ -36,7 +33,6 @@ export function convertNxExecutor(executor: Executor) { ).projects ), }; - cleanup(); const context: ExecutorContext = { root: builderContext.workspaceRoot, projectName: builderContext.target.project, diff --git a/packages/nx/src/plugins/package-json/create-nodes.spec.ts b/packages/nx/src/plugins/package-json/create-nodes.spec.ts index fa6bdda6d70db..a83cf0a183088 100644 --- a/packages/nx/src/plugins/package-json/create-nodes.spec.ts +++ b/packages/nx/src/plugins/package-json/create-nodes.spec.ts @@ -48,7 +48,7 @@ describe('nx package.json workspaces plugin', () => { '/root' ); - expect(createNodeFromPackageJson('package.json', '/root')) + expect(createNodeFromPackageJson('package.json', '/root', {})) .toMatchInlineSnapshot(` { "projects": { @@ -90,8 +90,9 @@ describe('nx package.json workspaces plugin', () => { }, } `); - expect(createNodeFromPackageJson('packages/lib-a/package.json', '/root')) - .toMatchInlineSnapshot(` + expect( + createNodeFromPackageJson('packages/lib-a/package.json', '/root', {}) + ).toMatchInlineSnapshot(` { "projects": { "packages/lib-a": { @@ -132,8 +133,9 @@ describe('nx package.json workspaces plugin', () => { }, } `); - expect(createNodeFromPackageJson('packages/lib-b/package.json', '/root')) - .toMatchInlineSnapshot(` + expect( + createNodeFromPackageJson('packages/lib-b/package.json', '/root', {}) + ).toMatchInlineSnapshot(` { "projects": { "packages/lib-b": { @@ -731,13 +733,12 @@ describe('nx package.json workspaces plugin', () => { ); expect( - createNodeFromPackageJson('apps/myapp/package.json', '/root').projects[ - 'apps/myapp' - ].projectType + createNodeFromPackageJson('apps/myapp/package.json', '/root', {}) + .projects['apps/myapp'].projectType ).toEqual('application'); expect( - createNodeFromPackageJson('packages/mylib/package.json', '/root') + createNodeFromPackageJson('packages/mylib/package.json', '/root', {}) .projects['packages/mylib'].projectType ).toEqual('library'); }); @@ -760,7 +761,7 @@ describe('nx package.json workspaces plugin', () => { ); expect( - createNodeFromPackageJson('package.json', '/root').projects['.'] + createNodeFromPackageJson('package.json', '/root', {}).projects['.'] .projectType ).toEqual('library'); }); @@ -786,11 +787,11 @@ describe('nx package.json workspaces plugin', () => { ); expect( - createNodeFromPackageJson('packages/mylib/package.json', '/root') + createNodeFromPackageJson('packages/mylib/package.json', '/root', {}) .projects['packages/mylib'].projectType ).toEqual('library'); expect( - createNodeFromPackageJson('example/package.json', '/root').projects[ + createNodeFromPackageJson('example/package.json', '/root', {}).projects[ 'example' ].projectType ).toBeUndefined(); diff --git a/packages/nx/src/plugins/package-json/create-nodes.ts b/packages/nx/src/plugins/package-json/create-nodes.ts index 0670dffcd7b13..1a4bdea2e3c59 100644 --- a/packages/nx/src/plugins/package-json/create-nodes.ts +++ b/packages/nx/src/plugins/package-json/create-nodes.ts @@ -21,6 +21,11 @@ import { CreateNodesV2, } from '../../project-graph/plugins'; import { basename } from 'path'; +import { hashObject } from '../../hasher/file-hasher'; +import { + PackageJsonConfigurationCache, + readPackageJsonConfigurationCache, +} from '../../../plugins/package-json'; export const createNodesV2: CreateNodesV2 = [ combineGlobPatterns( @@ -41,6 +46,8 @@ export const createNodesV2: CreateNodesV2 = [ return projectJsonRoots.has(dirname(packageJsonPath)); }; + const cache = readPackageJsonConfigurationCache(); + return createNodesFromFiles( (packageJsonPath, options, context) => { if ( @@ -53,7 +60,8 @@ export const createNodesV2: CreateNodesV2 = [ return createNodeFromPackageJson( packageJsonPath, - context.workspaceRoot + context.workspaceRoot, + cache ); }, packageJsons, @@ -120,15 +128,35 @@ export function buildPackageJsonWorkspacesMatcher( export function createNodeFromPackageJson( pkgJsonPath: string, - workspaceRoot: string + workspaceRoot: string, + cache: PackageJsonConfigurationCache ) { const json: PackageJson = readJsonFile(join(workspaceRoot, pkgJsonPath)); + + const projectRoot = dirname(pkgJsonPath); + + const hash = hashObject({ + ...json, + root: projectRoot, + }); + + const cached = cache[hash]; + if (cached) { + return { + projects: { + [cached.root]: cached, + }, + }; + } + const project = buildProjectConfigurationFromPackageJson( json, workspaceRoot, pkgJsonPath, readNxJson(workspaceRoot) ); + + cache[hash] = project; return { projects: { [project.root]: project, diff --git a/packages/nx/src/project-graph/affected/affected-project-graph.ts b/packages/nx/src/project-graph/affected/affected-project-graph.ts index 52cc5c8f0f218..26cd04dd1a344 100644 --- a/packages/nx/src/project-graph/affected/affected-project-graph.ts +++ b/packages/nx/src/project-graph/affected/affected-project-graph.ts @@ -30,6 +30,7 @@ export async function filterAffected( const touchedProjects = []; for (const locator of touchedProjectLocators) { + performance.mark(locator.name + ':start'); const projects = await locator( touchedFiles, graph.nodes, @@ -37,6 +38,12 @@ export async function filterAffected( packageJson, graph ); + performance.mark(locator.name + ':end'); + performance.measure( + locator.name, + locator.name + ':start', + locator.name + ':end' + ); touchedProjects.push(...projects); } diff --git a/packages/nx/src/project-graph/affected/locators/project-glob-changes.spec.ts b/packages/nx/src/project-graph/affected/locators/project-glob-changes.spec.ts index 31578fd73cb10..0185d869b59a1 100644 --- a/packages/nx/src/project-graph/affected/locators/project-glob-changes.spec.ts +++ b/packages/nx/src/project-graph/affected/locators/project-glob-changes.spec.ts @@ -1,55 +1,58 @@ import { ProjectGraphProjectNode } from '../../../config/project-graph'; -import { ProjectConfiguration } from '../../../config/workspace-json-project-json'; - -import * as nxPlugin from '../../../project-graph/plugins'; import { DeletedFileChange } from '../../file-utils'; import { getTouchedProjectsFromProjectGlobChanges } from './project-glob-changes'; +jest.mock('../../../project-graph/plugins/get-plugins', () => ({ + ...jest.requireActual('../../../project-graph/plugins/get-plugins'), + getPlugins: async () => { + return [ + { + name: 'test', + createNodes: [ + '**/project.json', + async () => { + return []; + }, + ], + }, + ]; + }, +})); describe('getTouchedProjectsFromProjectGlobChanges', () => { - it('empty', () => {}); + it('should affect all projects if a project is removed', async () => { + const nodes = { + proj1: makeProjectGraphNode('proj1'), + proj2: makeProjectGraphNode('proj2'), + proj3: makeProjectGraphNode('proj3'), + }; + const result = await getTouchedProjectsFromProjectGlobChanges( + [ + { + file: 'libs/proj1/project.json', + hash: 'some-hash', + getChanges: () => [new DeletedFileChange()], + }, + ], + nodes, + { + plugins: [], + }, + {}, + { + nodes: nodes, + dependencies: {}, + } + ); + expect(result).toEqual(['proj1', 'proj2', 'proj3']); + }); }); -// describe('getTouchedProjectsFromProjectGlobChanges', () => { -// beforeEach(() => { -// jest.spyOn(nxPlugin, 'loadNxPlugins').mockResolvedValue([]); -// }); -// -// it('should affect all projects if a project is removed', async () => { -// const result = await getTouchedProjectsFromProjectGlobChanges( -// [ -// { -// file: 'libs/proj1/project.json', -// hash: 'some-hash', -// getChanges: () => [new DeletedFileChange()], -// }, -// ], -// { -// proj2: makeProjectGraphNode('proj2'), -// proj3: makeProjectGraphNode('proj3'), -// }, -// { -// plugins: [], -// } -// ); -// expect(result).toEqual(['proj2', 'proj3']); -// }); -// }); - -// function makeProjectGraphNode( -// name, -// configurationFile = 'project.json' -// ): ProjectGraphProjectNode { -// return { -// data: { -// files: [ -// { -// file: `libs/${name}/${configurationFile}`, -// hash: 'hash' + Math.floor(Math.random() * 10000), -// }, -// ], -// root: `libs/${name}`, -// }, -// name, -// type: 'lib', -// }; -// } +function makeProjectGraphNode(name): ProjectGraphProjectNode { + return { + data: { + root: `libs/${name}`, + }, + name, + type: 'lib', + }; +} diff --git a/packages/nx/src/project-graph/affected/locators/project-glob-changes.ts b/packages/nx/src/project-graph/affected/locators/project-glob-changes.ts index 6c54137366a25..26293b8bc2855 100644 --- a/packages/nx/src/project-graph/affected/locators/project-glob-changes.ts +++ b/packages/nx/src/project-graph/affected/locators/project-glob-changes.ts @@ -4,13 +4,25 @@ import { workspaceRoot } from '../../../utils/workspace-root'; import { join } from 'path'; import { existsSync } from 'fs'; import { configurationGlobs } from '../../utils/retrieve-workspace-files'; -import { loadNxPlugins } from '../../plugins/internal-api'; import { combineGlobPatterns } from '../../../utils/globs'; +import { getPlugins } from '../../plugins/get-plugins'; export const getTouchedProjectsFromProjectGlobChanges: TouchedProjectLocator = - async (touchedFiles, projectGraphNodes, nxJson): Promise => { - const [plugins] = await loadNxPlugins(nxJson?.plugins ?? [], workspaceRoot); - const globPattern = combineGlobPatterns(configurationGlobs(plugins)); + async (touchedFiles, projectGraphNodes): Promise => { + const globPattern = await (async () => { + // TODO: We need a quicker way to get patterns that should not + // require starting up plugin workers + if (process.env.NX_FORCE_REUSE_CACHED_GRAPH === 'true') { + return combineGlobPatterns([ + '**/package.json', + '**/project.json', + 'project.json', + 'package.json', + ]); + } + const plugins = await getPlugins(); + return combineGlobPatterns(configurationGlobs(plugins)); + })(); const touchedProjects = new Set(); for (const touchedFile of touchedFiles) { diff --git a/packages/nx/src/project-graph/build-project-graph.ts b/packages/nx/src/project-graph/build-project-graph.ts index 0ef18c56050a7..be61b9e227c78 100644 --- a/packages/nx/src/project-graph/build-project-graph.ts +++ b/packages/nx/src/project-graph/build-project-graph.ts @@ -72,6 +72,16 @@ export function getFileMap(): { } } +export function hydrateFileMap( + fileMap: FileMap, + allWorkspaceFiles: FileData[], + rustReferences: NxWorkspaceFilesExternals +) { + storedFileMap = fileMap; + storedAllWorkspaceFiles = allWorkspaceFiles; + storedRustReferences = rustReferences; +} + export async function buildProjectGraphUsingProjectFileMap( projectRootMap: Record, externalNodes: Record, @@ -210,8 +220,6 @@ async function buildProjectGraphUsingContext( plugins: LoadedNxPlugin[], sourceMap: ConfigurationSourceMaps ) { - performance.mark('build project graph:start'); - const builder = new ProjectGraphBuilder(null, ctx.fileMap.projectFileMap); builder.setVersion(projectGraphVersion); for (const node in knownExternalNodes) { @@ -262,13 +270,6 @@ async function buildProjectGraphUsingContext( const finalGraph = updatedBuilder.getUpdatedProjectGraph(); - performance.mark('build project graph:end'); - performance.measure( - 'build project graph', - 'build project graph:start', - 'build project graph:end' - ); - if (!error) { return finalGraph; } else { @@ -311,6 +312,7 @@ async function updateProjectGraphWithPlugins( const createDependencyPlugins = plugins.filter( (plugin) => plugin.createDependencies ); + performance.mark('createDependencies:start'); await Promise.all( createDependencyPlugins.map(async (plugin) => { performance.mark(`${plugin.name}:createDependencies - start`); @@ -344,6 +346,12 @@ async function updateProjectGraphWithPlugins( ); }) ); + performance.mark('createDependencies:end'); + performance.measure( + `createDependencies`, + `createDependencies:start`, + `createDependencies:end` + ); const graphWithDeps = builder.getUpdatedProjectGraph(); @@ -387,6 +395,7 @@ export async function applyProjectMetadata( const results: { metadata: ProjectsMetadata; pluginName: string }[] = []; const errors: CreateMetadataError[] = []; + performance.mark('createMetadata:start'); const promises = plugins.map(async (plugin) => { if (plugin.createMetadata) { performance.mark(`${plugin.name}:createMetadata - start`); @@ -424,5 +433,12 @@ export async function applyProjectMetadata( } } + performance.mark('createMetadata:end'); + performance.measure( + `createMetadata`, + `createMetadata:start`, + `createMetadata:end` + ); + return { errors, graph }; } diff --git a/packages/nx/src/project-graph/error-types.ts b/packages/nx/src/project-graph/error-types.ts index dda621c383828..1f61deb951d4b 100644 --- a/packages/nx/src/project-graph/error-types.ts +++ b/packages/nx/src/project-graph/error-types.ts @@ -4,7 +4,7 @@ import { } from './utils/project-configuration-utils'; import { ProjectConfiguration } from '../config/workspace-json-project-json'; import { ProjectGraph } from '../config/project-graph'; -import { CreateNodesFunctionV2 } from './plugins'; +import { CreateNodesFunctionV2 } from './plugins/public-api'; export class ProjectGraphError extends Error { readonly #errors: Array< diff --git a/packages/nx/src/project-graph/plugins/get-plugins.ts b/packages/nx/src/project-graph/plugins/get-plugins.ts new file mode 100644 index 0000000000000..1a245d318d598 --- /dev/null +++ b/packages/nx/src/project-graph/plugins/get-plugins.ts @@ -0,0 +1,69 @@ +import { hashObject } from '../../hasher/file-hasher'; +import { readNxJson } from '../../config/nx-json'; +import { LoadedNxPlugin, loadNxPlugins } from './internal-api'; +import { workspaceRoot } from '../../utils/workspace-root'; + +let currentPluginsConfigurationHash: string; +let loadedPlugins: LoadedNxPlugin[]; +let pendingPluginsPromise: + | Promise void]> + | undefined; +let cleanup: () => void; + +export async function getPlugins() { + const pluginsConfiguration = readNxJson().plugins ?? []; + const pluginsConfigurationHash = hashObject(pluginsConfiguration); + + // If the plugins configuration has not changed, reuse the current plugins + if ( + loadedPlugins && + pluginsConfigurationHash === currentPluginsConfigurationHash + ) { + return loadedPlugins; + } + + // Cleanup current plugins before loading new ones + if (cleanup) { + cleanup(); + } + + pendingPluginsPromise ??= loadNxPlugins(pluginsConfiguration, workspaceRoot); + + currentPluginsConfigurationHash = pluginsConfigurationHash; + const [result, cleanupFn] = await pendingPluginsPromise; + cleanup = cleanupFn; + loadedPlugins = result; + return result; +} + +let loadedDefaultPlugins: LoadedNxPlugin[]; +let cleanupDefaultPlugins: () => void; +let pendingDefaultPluginPromise: + | Promise void]> + | undefined; + +export async function getOnlyDefaultPlugins() { + // If the plugins configuration has not changed, reuse the current plugins + if (loadedDefaultPlugins) { + return loadedPlugins; + } + + // Cleanup current plugins before loading new ones + if (cleanupDefaultPlugins) { + cleanupDefaultPlugins(); + } + + pendingDefaultPluginPromise ??= loadNxPlugins([], workspaceRoot); + + const [result, cleanupFn] = await pendingDefaultPluginPromise; + cleanupDefaultPlugins = cleanupFn; + loadedPlugins = result; + return result; +} + +export function cleanupPlugins() { + pendingPluginsPromise = undefined; + pendingDefaultPluginPromise = undefined; + cleanup(); + cleanupDefaultPlugins(); +} diff --git a/packages/nx/src/project-graph/plugins/index.ts b/packages/nx/src/project-graph/plugins/index.ts index 3739a2e887039..3bd38dd1dc22b 100644 --- a/packages/nx/src/project-graph/plugins/index.ts +++ b/packages/nx/src/project-graph/plugins/index.ts @@ -1,4 +1,6 @@ export * from './public-api'; +// export * from './get-plugins'; + export { readPluginPackageJson, registerPluginTSTranspiler } from './loader'; export { createNodesFromFiles } from './utils'; diff --git a/packages/nx/src/project-graph/plugins/internal-api.ts b/packages/nx/src/project-graph/plugins/internal-api.ts index 0692d6874848a..49a6c85d6c565 100644 --- a/packages/nx/src/project-graph/plugins/internal-api.ts +++ b/packages/nx/src/project-graph/plugins/internal-api.ts @@ -143,31 +143,45 @@ function isIsolationEnabled() { return true; } +/** + * Use `getPlugins` instead. + * @deprecated Do not use this. Use `getPlugins` instead. + */ export async function loadNxPlugins( plugins: PluginConfiguration[], root = workspaceRoot ): Promise void]> { performance.mark('loadNxPlugins:start'); - const loadingMethod = isIsolationEnabled() ? loadNxPluginInIsolation : loadNxPlugin; plugins = await normalizePlugins(plugins, root); - const result: Promise[] = new Array(plugins?.length); - const cleanupFunctions: Array<() => void> = []; - await Promise.all( - plugins.map(async (plugin, idx) => { - const [loadedPluginPromise, cleanup] = await loadingMethod(plugin, root); - result[idx] = loadedPluginPromise; - cleanupFunctions.push(cleanup); - }) - ); - const ret = [ - await Promise.all(result), + await Promise.all( + plugins.map(async (plugin) => { + const pluginPath = typeof plugin === 'string' ? plugin : plugin.plugin; + performance.mark(`Load Nx Plugin: ${pluginPath} - start`); + + const [loadedPluginPromise, cleanup] = await loadingMethod( + plugin, + root + ); + + cleanupFunctions.push(cleanup); + const res = await loadedPluginPromise; + performance.mark(`Load Nx Plugin: ${pluginPath} - end`); + performance.measure( + `Load Nx Plugin: ${pluginPath}`, + `Load Nx Plugin: ${pluginPath} - start`, + `Load Nx Plugin: ${pluginPath} - end` + ); + + return res; + }) + ), () => { for (const fn of cleanupFunctions) { fn(); diff --git a/packages/nx/src/project-graph/plugins/isolation/index.ts b/packages/nx/src/project-graph/plugins/isolation/index.ts index 4bab409265189..1f77f6be5e7ee 100644 --- a/packages/nx/src/project-graph/plugins/isolation/index.ts +++ b/packages/nx/src/project-graph/plugins/isolation/index.ts @@ -7,11 +7,5 @@ export async function loadNxPluginInIsolation( plugin: PluginConfiguration, root = workspaceRoot ): Promise, () => void]> { - const [loadingPlugin, cleanup] = await loadRemoteNxPlugin(plugin, root); - return [ - loadingPlugin, - () => { - cleanup(); - }, - ] as const; + return loadRemoteNxPlugin(plugin, root); } diff --git a/packages/nx/src/project-graph/plugins/isolation/messaging.ts b/packages/nx/src/project-graph/plugins/isolation/messaging.ts index 950957aab51ae..f8e6e69a4378f 100644 --- a/packages/nx/src/project-graph/plugins/isolation/messaging.ts +++ b/packages/nx/src/project-graph/plugins/isolation/messaging.ts @@ -14,6 +14,9 @@ export interface PluginWorkerLoadMessage { payload: { plugin: PluginConfiguration; root: string; + name: string; + pluginPath: string; + shouldRegisterTSTranspiler: boolean; }; } diff --git a/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts b/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts index 740f8562ac68c..bb912362dd45e 100644 --- a/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts +++ b/packages/nx/src/project-graph/plugins/isolation/plugin-pool.ts @@ -16,6 +16,8 @@ import { isPluginWorkerResult, sendMessageOverSocket, } from './messaging'; +import { getNxRequirePaths } from '../../../utils/installation-directory'; +import { resolveNxPlugin } from '../loader'; const cleanupFunctions = new Set<() => void>(); @@ -59,6 +61,10 @@ export async function loadRemoteNxPlugin( if (nxPluginWorkerCache.has(cacheKey)) { return [nxPluginWorkerCache.get(cacheKey), () => {}]; } + const moduleName = typeof plugin === 'string' ? plugin : plugin.plugin; + + const { name, pluginPath, shouldRegisterTSTranspiler } = + await resolveNxPlugin(moduleName, root, getNxRequirePaths(root)); const { worker, socket } = await startPluginWorker(); @@ -77,7 +83,7 @@ export async function loadRemoteNxPlugin( const pluginPromise = new Promise((res, rej) => { sendMessageOverSocket(socket, { type: 'load', - payload: { plugin, root }, + payload: { plugin, root, name, pluginPath, shouldRegisterTSTranspiler }, }); // logger.verbose(`[plugin-worker] started worker: ${worker.pid}`); diff --git a/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts b/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts index a1d2451ae61ab..349ab00347916 100644 --- a/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts +++ b/packages/nx/src/project-graph/plugins/isolation/plugin-worker.ts @@ -1,11 +1,10 @@ import { consumeMessage, isPluginWorkerMessage } from './messaging'; -import { LoadedNxPlugin } from '../internal-api'; -import { loadNxPlugin } from '../loader'; import { createSerializableError } from '../../../utils/serializable-error'; import { consumeMessagesFromSocket } from '../../../utils/consume-messages-from-socket'; import { createServer } from 'net'; import { unlinkSync } from 'fs'; +import { registerPluginTSTranspiler } from '../loader'; if (process.env.NX_PERF_LOGGING === 'true') { require('../../../utils/perf-logging'); @@ -14,7 +13,7 @@ if (process.env.NX_PERF_LOGGING === 'true') { global.NX_GRAPH_CREATION = true; global.NX_PLUGIN_WORKER = true; let connected = false; -let plugin: LoadedNxPlugin; +let plugin; const socketPath = process.argv[2]; @@ -37,12 +36,30 @@ const server = createServer((socket) => { return; } return consumeMessage(socket, message, { - load: async ({ plugin: pluginConfiguration, root }) => { + load: async ({ + plugin: pluginConfiguration, + root, + name, + pluginPath, + shouldRegisterTSTranspiler, + }) => { if (loadTimeout) clearTimeout(loadTimeout); process.chdir(root); try { - const [promise] = loadNxPlugin(pluginConfiguration, root); - plugin = await promise; + const { loadResolvedNxPluginAsync } = await import( + '../load-resolved-plugin' + ); + + // Register the ts-transpiler if we are pointing to a + // plain ts file that's not part of a plugin project + if (shouldRegisterTSTranspiler) { + registerPluginTSTranspiler(); + } + plugin = await loadResolvedNxPluginAsync( + pluginConfiguration, + pluginPath, + name + ); return { type: 'load-result', payload: { diff --git a/packages/nx/src/project-graph/plugins/load-resolved-plugin.ts b/packages/nx/src/project-graph/plugins/load-resolved-plugin.ts new file mode 100644 index 0000000000000..a77337581a26f --- /dev/null +++ b/packages/nx/src/project-graph/plugins/load-resolved-plugin.ts @@ -0,0 +1,26 @@ +import type { PluginConfiguration } from '../../config/nx-json'; +import { LoadedNxPlugin } from './internal-api'; +import { NxPlugin } from './public-api'; + +export async function loadResolvedNxPluginAsync( + pluginConfiguration: PluginConfiguration, + pluginPath: string, + name: string +) { + const plugin = await importPluginModule(pluginPath); + plugin.name ??= name; + return new LoadedNxPlugin(plugin, pluginConfiguration); +} + +async function importPluginModule(pluginPath: string): Promise { + const m = await import(pluginPath); + if ( + m.default && + ('createNodes' in m.default || + 'createNodesV2' in m.default || + 'createDependencies' in m.default) + ) { + return m.default; + } + return m; +} diff --git a/packages/nx/src/project-graph/plugins/loader.ts b/packages/nx/src/project-graph/plugins/loader.ts index bfa539e4e4107..0e90af76855de 100644 --- a/packages/nx/src/project-graph/plugins/loader.ts +++ b/packages/nx/src/project-graph/plugins/loader.ts @@ -24,13 +24,13 @@ import { logger } from '../../utils/logger'; import type * as ts from 'typescript'; import { extname } from 'node:path'; -import { NxPlugin } from './public-api'; -import { PluginConfiguration } from '../../config/nx-json'; +import type { PluginConfiguration } from '../../config/nx-json'; import { retrieveProjectConfigurationsWithoutPluginInference } from '../utils/retrieve-workspace-files'; import { LoadedNxPlugin } from './internal-api'; import { LoadPluginError } from '../error-types'; import path = require('node:path/posix'); import { readTsConfig } from '../../plugins/js/utils/typescript'; +import { loadResolvedNxPluginAsync } from './load-resolved-plugin'; export function readPluginPackageJson( pluginName: string, @@ -200,18 +200,18 @@ export function getPluginPathAndName( root: string ) { let pluginPath: string; - let registerTSTranspiler = false; + let shouldRegisterTSTranspiler = false; try { pluginPath = require.resolve(moduleName, { paths, }); const extension = path.extname(pluginPath); - registerTSTranspiler = extension === '.ts'; + shouldRegisterTSTranspiler = extension === '.ts'; } catch (e) { if (e.code === 'MODULE_NOT_FOUND') { const plugin = resolveLocalNxPlugin(moduleName, projects, root); if (plugin) { - registerTSTranspiler = true; + shouldRegisterTSTranspiler = true; const main = readPluginMainFromProjectConfiguration( plugin.projectConfig ); @@ -226,18 +226,12 @@ export function getPluginPathAndName( } const packageJsonPath = path.join(pluginPath, 'package.json'); - // Register the ts-transpiler if we are pointing to a - // plain ts file that's not part of a plugin project - if (registerTSTranspiler) { - registerPluginTSTranspiler(); - } - const { name } = !['.ts', '.js'].some((x) => extname(moduleName) === x) && // Not trying to point to a ts or js file existsSync(packageJsonPath) // plugin has a package.json ? readJsonFile(packageJsonPath) // read name from package.json : { name: moduleName }; - return { pluginPath, name }; + return { pluginPath, name, shouldRegisterTSTranspiler }; } let projectsWithoutInference: Record; @@ -249,6 +243,27 @@ export function loadNxPlugin(plugin: PluginConfiguration, root: string) { ] as const; } +export async function resolveNxPlugin( + moduleName: string, + root: string, + paths: string[] +) { + try { + require.resolve(moduleName); + } catch { + // If a plugin cannot be resolved, we will need projects to resolve it + projectsWithoutInference ??= + await retrieveProjectConfigurationsWithoutPluginInference(root); + } + const { pluginPath, name, shouldRegisterTSTranspiler } = getPluginPathAndName( + moduleName, + paths, + projectsWithoutInference, + root + ); + return { pluginPath, name, shouldRegisterTSTranspiler }; +} + export async function loadNxPluginAsync( pluginConfiguration: PluginConfiguration, paths: string[], @@ -259,44 +274,14 @@ export async function loadNxPluginAsync( ? pluginConfiguration : pluginConfiguration.plugin; try { - try { - require.resolve(moduleName); - } catch { - // If a plugin cannot be resolved, we will need projects to resolve it - projectsWithoutInference ??= - await retrieveProjectConfigurationsWithoutPluginInference(root); - } + const { pluginPath, name, shouldRegisterTSTranspiler } = + await resolveNxPlugin(moduleName, root, paths); - performance.mark(`Load Nx Plugin: ${moduleName} - start`); - const { pluginPath, name } = getPluginPathAndName( - moduleName, - paths, - projectsWithoutInference, - root - ); - const plugin = await importPluginModule(pluginPath); - plugin.name ??= name; - performance.mark(`Load Nx Plugin: ${moduleName} - end`); - performance.measure( - `Load Nx Plugin: ${moduleName}`, - `Load Nx Plugin: ${moduleName} - start`, - `Load Nx Plugin: ${moduleName} - end` - ); - return new LoadedNxPlugin(plugin, pluginConfiguration); + if (shouldRegisterTSTranspiler) { + registerPluginTSTranspiler(); + } + return loadResolvedNxPluginAsync(pluginConfiguration, pluginPath, name); } catch (e) { throw new LoadPluginError(moduleName, e); } } - -async function importPluginModule(pluginPath: string): Promise { - const m = await import(pluginPath); - if ( - m.default && - ('createNodes' in m.default || - 'createNodesV2' in m.default || - 'createDependencies' in m.default) - ) { - return m.default; - } - return m; -} diff --git a/packages/nx/src/project-graph/project-graph.ts b/packages/nx/src/project-graph/project-graph.ts index eb1c101faee7b..2188c65a773c5 100644 --- a/packages/nx/src/project-graph/project-graph.ts +++ b/packages/nx/src/project-graph/project-graph.ts @@ -12,7 +12,10 @@ import { fileExists } from '../utils/fileutils'; import { output } from '../utils/output'; import { stripIndents } from '../utils/strip-indents'; import { workspaceRoot } from '../utils/workspace-root'; -import { buildProjectGraphUsingProjectFileMap } from './build-project-graph'; +import { + buildProjectGraphUsingProjectFileMap, + hydrateFileMap, +} from './build-project-graph'; import { AggregateProjectGraphError, isAggregateProjectGraphError, @@ -24,12 +27,13 @@ import { readProjectGraphCache, writeCache, } from './nx-deps-cache'; -import { loadNxPlugins } from './plugins/internal-api'; import { ConfigurationResult } from './utils/project-configuration-utils'; import { retrieveProjectConfigurations, retrieveWorkspaceFiles, } from './utils/retrieve-workspace-files'; +import { getPlugins } from './plugins/get-plugins'; +import { logger } from '../utils/logger'; /** * Synchronously reads the latest cached copy of the workspace's ProjectGraph. @@ -96,7 +100,7 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() { performance.mark('retrieve-project-configurations:start'); let configurationResult: ConfigurationResult; let projectConfigurationsError: ProjectConfigurationsError; - const [plugins, cleanup] = await loadNxPlugins(nxJson.plugins); + const plugins = await getPlugins(); try { configurationResult = await retrieveProjectConfigurations( plugins, @@ -147,14 +151,6 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() { } else { throw e; } - } finally { - // When plugins are isolated we don't clean them up during - // a single run of the CLI. They are cleaned up when the CLI - // process exits. Cleaning them here could cause issues if pending - // promises are not resolved. - if (process.env.NX_ISOLATE_PLUGINS !== 'true') { - cleanup(); - } } const { projectGraph, projectFileMapCache } = projectGraphResult; @@ -237,6 +233,25 @@ export async function createProjectGraphAsync( resetDaemonClient: false, } ): Promise { + if (process.env.NX_FORCE_REUSE_CACHED_GRAPH === 'true') { + try { + const graph = readCachedProjectGraph(); + const projectRootMap = Object.fromEntries( + Object.entries(graph.nodes).map(([project, { data }]) => [ + data.root, + project, + ]) + ); + const { allWorkspaceFiles, fileMap, rustReferences } = + await retrieveWorkspaceFiles(workspaceRoot, projectRootMap); + hydrateFileMap(fileMap, allWorkspaceFiles, rustReferences); + return graph; + // If no cached graph is found, we will fall through to the normal flow + } catch (e) { + logger.verbose('Unable to use cached project graph', e); + } + } + const projectGraphAndSourceMaps = await createProjectGraphAndSourceMapsAsync( opts ); diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.ts index 56cbb493eea4f..911199881bfe2 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.ts @@ -28,7 +28,7 @@ import { isAggregateCreateNodesError, AggregateCreateNodesError, } from '../error-types'; -import { CreateNodesResult } from '../plugins'; +import { CreateNodesResult } from '../plugins/public-api'; import { isGlobPattern } from '../../utils/globs'; export type SourceInformation = [file: string | null, plugin: string]; diff --git a/packages/nx/src/project-graph/utils/retrieve-workspace-files.spec.ts b/packages/nx/src/project-graph/utils/retrieve-workspace-files.spec.ts index 4499637562e47..b75b214284730 100644 --- a/packages/nx/src/project-graph/utils/retrieve-workspace-files.spec.ts +++ b/packages/nx/src/project-graph/utils/retrieve-workspace-files.spec.ts @@ -27,12 +27,11 @@ describe('retrieveProjectConfigurationPaths', () => { const configPaths = await retrieveProjectConfigurationPaths(fs.tempDir, [ { + name: 'test', createNodes: [ '{project.json,**/project.json}', - () => { - return { - projects: {}, - }; + async () => { + return []; }, ], }, diff --git a/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts b/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts index d87792f1093fc..31f76b59ac7c9 100644 --- a/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts +++ b/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts @@ -9,13 +9,14 @@ import { ConfigurationResult, createProjectConfigurations, } from './project-configuration-utils'; -import { LoadedNxPlugin, loadNxPlugins } from '../plugins/internal-api'; +import { LoadedNxPlugin } from '../plugins/internal-api'; import { getNxWorkspaceFilesFromContext, globWithWorkspaceContext, } from '../../utils/workspace-context'; import { buildAllWorkspaceFiles } from './build-all-workspace-files'; import { join } from 'path'; +import { getOnlyDefaultPlugins, getPlugins } from '../plugins/get-plugins'; /** * Walks the workspace directory to create the `projectFileMap`, `ProjectConfigurations` and `allWorkspaceFiles` @@ -96,23 +97,19 @@ export async function retrieveProjectConfigurationsWithAngularProjects( pluginsToLoad.push(join(__dirname, '../../adapter/angular-json')); } - const [plugins, cleanup] = await loadNxPlugins( - nxJson?.plugins ?? [], - workspaceRoot - ); + const plugins = await getPlugins(); const res = await retrieveProjectConfigurations( plugins, workspaceRoot, nxJson ); - cleanup(); return res; } export function retrieveProjectConfigurationPaths( root: string, - plugins: Array<{ createNodes?: readonly [string, ...unknown[]] } & unknown> + plugins: Array ): Promise { const projectGlobPatterns = configurationGlobs(plugins); return globWithWorkspaceContext(root, projectGlobPatterns); @@ -128,7 +125,7 @@ export async function retrieveProjectConfigurationsWithoutPluginInference( root: string ): Promise> { const nxJson = readNxJson(root); - const [plugins, cleanup] = await loadNxPlugins([]); // only load default plugins + const plugins = await getOnlyDefaultPlugins(); // only load default plugins const projectGlobPatterns = await retrieveProjectConfigurationPaths( root, plugins @@ -150,14 +147,10 @@ export async function retrieveProjectConfigurationsWithoutPluginInference( projectsWithoutPluginCache.set(cacheKey, projects); - cleanup(); - return projects; } -export function configurationGlobs( - plugins: Array<{ createNodes?: readonly [string, ...unknown[]] }> -): string[] { +export function configurationGlobs(plugins: Array): string[] { const globPatterns = []; for (const plugin of plugins) { if ('createNodes' in plugin && plugin.createNodes) { diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index 9730060cce9e0..cae85b256e199 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -304,9 +304,8 @@ async function ensureWorkspaceIsInSyncAndGetGraphs( const outOfSyncTitle = 'The workspace is out of sync'; const resultBodyLines = getSyncGeneratorSuccessResultsMessageLines(results); const fixMessage = - 'You can manually run `nx sync` to update your workspace with the identified changes or you can set `sync.applyChanges` to `true` in your `nx.json` to apply the changes automatically when running tasks in interactive environments.'; - const willErrorOnCiMessage = - 'Please note that having the workspace out of sync will result in an error in CI.'; + 'Make sure to run `nx sync` to apply the identified changes or set `sync.applyChanges` to `true` in your `nx.json` to apply them automatically when running tasks in interactive environments.'; + const willErrorOnCiMessage = 'This will result in an error in CI.'; if (isCI() || !process.stdout.isTTY) { // If the user is running in CI or is running in a non-TTY environment we diff --git a/packages/nx/src/utils/sync-generators.ts b/packages/nx/src/utils/sync-generators.ts index 68fac7d42e173..318cea9221dae 100644 --- a/packages/nx/src/utils/sync-generators.ts +++ b/packages/nx/src/utils/sync-generators.ts @@ -268,17 +268,10 @@ export function getSyncGeneratorSuccessResultsMessageLines( } messageLines.push( - `The ${chalk.bold( - result.generatorName - )} sync generator identified ${chalk.bold(result.changes.length)} file${ - result.changes.length === 1 ? '' : 's' - } in the workspace that ${ - result.changes.length === 1 ? 'is' : 'are' - } out of sync${result.outOfSyncMessage ? ':' : '.'}` + `[${chalk.bold(result.generatorName)}]: ${ + result.outOfSyncMessage ?? `Some files are out of sync.` + }` ); - if (result.outOfSyncMessage) { - messageLines.push(result.outOfSyncMessage); - } } return messageLines; @@ -295,16 +288,15 @@ export function getFailedSyncGeneratorsFixMessageLines( let isFirst = true; for (const result of results) { if ('error' in result) { - if (!isFirst) { + if (!isFirst && verbose) { messageLines.push(''); } isFirst = false; messageLines.push( - `The ${chalk.bold( - result.generatorName - )} sync generator reported the following error:`, - '', - errorToString(result.error, verbose) + `[${chalk.bold(result.generatorName)}]: ${errorToString( + result.error, + verbose + )}` ); if (globalGeneratorSet.has(result.generatorName)) { @@ -336,16 +328,15 @@ export function getFlushFailureMessageLines( const taskGenerators: string[] = []; let isFirst = true; for (const failure of result.generatorFailures) { - if (!isFirst) { + if (!isFirst && verbose) { messageLines.push(''); } isFirst = false; messageLines.push( - `The ${chalk.bold( - failure.generator - )} sync generator failed to apply its changes with the following error:`, - '', - errorToString(failure.error, verbose) + `[${chalk.bold(failure.generator)}]: ${errorToString( + failure.error, + verbose + )}` ); if (globalGeneratorSet.has(failure.generator)) { @@ -375,13 +366,11 @@ export function getFlushFailureMessageLines( ...[ '', result.generalFailure.message, - ...(verbose && !!result.generalFailure.stack + ...(!!result.generalFailure.stack ? [`\n${result.generalFailure.stack}`] : []), '', - verbose - ? 'Please report the error at: https://github.com/nrwl/nx/issues/new/choose' - : 'Please run with `--verbose` and report the error at: https://github.com/nrwl/nx/issues/new/choose', + 'Please report the error at: https://github.com/nrwl/nx/issues/new/choose', ] ); } @@ -545,7 +534,7 @@ function getFailedSyncGeneratorsMessageLines( function errorToString(error: SerializableSimpleError, verbose: boolean) { if (error.title) { - let message = ` ${chalk.red(error.title)}`; + let message = `${chalk.red(error.title)}`; if (error.bodyLines?.length) { message += ` @@ -557,7 +546,7 @@ function errorToString(error: SerializableSimpleError, verbose: boolean) { } } - return ` ${chalk.red(error.message)}${ + return `${chalk.red(error.message)}${ verbose && error.stack ? '\n ' + error.stack : '' }`; } diff --git a/packages/react/src/executors/module-federation-dev-server/lib/index.ts b/packages/react/src/executors/module-federation-dev-server/lib/index.ts new file mode 100644 index 0000000000000..7923ac83c9890 --- /dev/null +++ b/packages/react/src/executors/module-federation-dev-server/lib/index.ts @@ -0,0 +1,2 @@ +export * from './normalize-options'; +export * from './start-remotes'; diff --git a/packages/react/src/executors/module-federation-dev-server/lib/normalize-options.ts b/packages/react/src/executors/module-federation-dev-server/lib/normalize-options.ts new file mode 100644 index 0000000000000..1cebe6567eb20 --- /dev/null +++ b/packages/react/src/executors/module-federation-dev-server/lib/normalize-options.ts @@ -0,0 +1,29 @@ +import { + ExecutorContext, + parseTargetString, + readTargetOptions, +} from '@nx/devkit'; +import { + ModuleFederationDevServerOptions, + NormalizedModuleFederationDevServerOptions, +} from '../schema'; + +export function getBuildOptions(buildTarget: string, context: ExecutorContext) { + const target = parseTargetString(buildTarget, context); + + const buildOptions = readTargetOptions(target, context); + + return { + ...buildOptions, + }; +} + +export function normalizeOptions( + options: ModuleFederationDevServerOptions +): NormalizedModuleFederationDevServerOptions { + return { + ...options, + devRemotes: options.devRemotes ?? [], + verbose: options.verbose ?? false, + }; +} diff --git a/packages/react/src/executors/module-federation-dev-server/lib/start-remotes.ts b/packages/react/src/executors/module-federation-dev-server/lib/start-remotes.ts new file mode 100644 index 0000000000000..d680317e152dc --- /dev/null +++ b/packages/react/src/executors/module-federation-dev-server/lib/start-remotes.ts @@ -0,0 +1,62 @@ +import { ExecutorContext, ProjectConfiguration, runExecutor } from '@nx/devkit'; +import { NormalizedModuleFederationDevServerOptions } from '../schema'; + +export async function startRemotes( + remotes: string[], + workspaceProjects: Record, + options: Pick< + NormalizedModuleFederationDevServerOptions, + 'devRemotes' | 'host' | 'ssl' | 'sslCert' | 'sslKey' | 'verbose' + >, + context: ExecutorContext, + target: 'serve' | 'serve-static' = 'serve' +) { + const remoteIters: AsyncIterable<{ success: boolean }>[] = []; + + for (const app of remotes) { + const remoteProjectServeTarget = workspaceProjects[app].targets[target]; + const isUsingModuleFederationDevServerExecutor = + remoteProjectServeTarget.executor.includes( + 'module-federation-dev-server' + ); + + const configurationOverride = options.devRemotes?.find( + ( + r + ): r is { + remoteName: string; + configuration: string; + } => typeof r !== 'string' && r.remoteName === app + )?.configuration; + + const defaultOverrides = { + ...(options.host ? { host: options.host } : {}), + ...(options.ssl ? { ssl: options.ssl } : {}), + ...(options.sslCert ? { sslCert: options.sslCert } : {}), + ...(options.sslKey ? { sslKey: options.sslKey } : {}), + }; + const overrides = + target === 'serve' + ? { + watch: true, + ...(isUsingModuleFederationDevServerExecutor + ? { isInitialHost: false } + : {}), + ...defaultOverrides, + } + : { ...defaultOverrides }; + + remoteIters.push( + await runExecutor( + { + project: app, + target, + configuration: configurationOverride ?? context.configurationName, + }, + overrides, + context + ) + ); + } + return remoteIters; +} diff --git a/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts b/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts index 086cbd5a782da..59f01abdded67 100644 --- a/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts +++ b/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts @@ -1,162 +1,22 @@ -import { - ExecutorContext, - logger, - parseTargetString, - readTargetOptions, - runExecutor, - workspaceRoot, -} from '@nx/devkit'; +import { ExecutorContext, logger } from '@nx/devkit'; import devServerExecutor from '@nx/webpack/src/executors/dev-server/dev-server.impl'; import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl'; import { ModuleFederationDevServerOptions } from './schema'; -import { - getModuleFederationConfig, - getRemotes, - startRemoteProxies, - parseStaticRemotesConfig, - type StaticRemotesConfig, -} from '@nx/module-federation/src/utils'; +import { startRemoteIterators } from '@nx/module-federation/src/executors/utils'; import { combineAsyncIterables, createAsyncIterable, } from '@nx/devkit/src/utils/async-iterable'; import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open'; -import { cpSync, existsSync } from 'fs'; +import { existsSync } from 'fs'; import { extname, join } from 'path'; -import { buildStaticRemotes } from '../../utils/build-static.remotes'; - -function getBuildOptions(buildTarget: string, context: ExecutorContext) { - const target = parseTargetString(buildTarget, context); - - const buildOptions = readTargetOptions(target, context); - - return { - ...buildOptions, - }; -} - -function startStaticRemotesFileServer( - staticRemotesConfig: StaticRemotesConfig, - context: ExecutorContext, - options: ModuleFederationDevServerOptions -) { - if ( - !staticRemotesConfig.remotes || - staticRemotesConfig.remotes.length === 0 - ) { - return; - } - let shouldMoveToCommonLocation = false; - let commonOutputDirectory: string; - for (const app of staticRemotesConfig.remotes) { - const remoteBasePath = staticRemotesConfig.config[app].basePath; - if (!commonOutputDirectory) { - commonOutputDirectory = remoteBasePath; - } else if (commonOutputDirectory !== remoteBasePath) { - shouldMoveToCommonLocation = true; - break; - } - } - - if (shouldMoveToCommonLocation) { - commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes'); - for (const app of staticRemotesConfig.remotes) { - const remoteConfig = staticRemotesConfig.config[app]; - cpSync( - remoteConfig.outputPath, - join(commonOutputDirectory, remoteConfig.urlSegment), - { - force: true, - recursive: true, - } - ); - } - } - - const staticRemotesIter = fileServerExecutor( - { - cors: true, - watch: false, - staticFilePath: commonOutputDirectory, - parallel: false, - spa: false, - withDeps: false, - host: options.host, - port: options.staticRemotesPort, - ssl: options.ssl, - sslCert: options.sslCert, - sslKey: options.sslKey, - cacheSeconds: -1, - }, - context - ); - - return staticRemotesIter; -} - -async function startRemotes( - remotes: string[], - context: ExecutorContext, - options: ModuleFederationDevServerOptions, - target: 'serve' | 'serve-static' = 'serve' -) { - const remoteIters: AsyncIterable<{ success: boolean }>[] = []; - - for (const app of remotes) { - const remoteProjectServeTarget = - context.projectGraph.nodes[app].data.targets[target]; - const isUsingModuleFederationDevServerExecutor = - remoteProjectServeTarget.executor.includes( - 'module-federation-dev-server' - ); - - const configurationOverride = options.devRemotes?.find( - ( - r - ): r is { - remoteName: string; - configuration: string; - } => typeof r !== 'string' && r.remoteName === app - )?.configuration; - - const defaultOverrides = { - ...(options.host ? { host: options.host } : {}), - ...(options.ssl ? { ssl: options.ssl } : {}), - ...(options.sslCert ? { sslCert: options.sslCert } : {}), - ...(options.sslKey ? { sslKey: options.sslKey } : {}), - }; - const overrides = - target === 'serve' - ? { - watch: true, - ...(isUsingModuleFederationDevServerExecutor - ? { isInitialHost: false } - : {}), - ...defaultOverrides, - } - : { ...defaultOverrides }; - - remoteIters.push( - await runExecutor( - { - project: app, - target, - configuration: configurationOverride ?? context.configurationName, - }, - overrides, - context - ) - ); - } - return remoteIters; -} +import { getBuildOptions, normalizeOptions, startRemotes } from './lib'; export default async function* moduleFederationDevServer( - options: ModuleFederationDevServerOptions, + schema: ModuleFederationDevServerOptions, context: ExecutorContext ): AsyncIterableIterator<{ success: boolean; baseUrl?: string }> { - // Force Node to resolve to look for the nx binary that is inside node_modules - const nxBin = require.resolve('nx/bin/nx'); + const options = normalizeOptions(schema); const currIter = options.static ? fileServerExecutor( { @@ -201,74 +61,14 @@ export default async function* moduleFederationDevServer( return yield* currIter; } - const moduleFederationConfig = getModuleFederationConfig( - buildOptions.tsConfig, - context.root, - p.root, - 'react' - ); - - const remoteNames = options.devRemotes?.map((r) => - typeof r === 'string' ? r : r.remoteName - ); - - const remotes = getRemotes( - remoteNames, - options.skipRemotes, - moduleFederationConfig, - { - projectName: context.projectName, - projectGraph: context.projectGraph, - root: context.root, - }, - pathToManifestFile - ); - options.staticRemotesPort ??= remotes.staticRemotePort; - - // Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin - process.env.NX_MF_DEV_REMOTES = JSON.stringify([ - ...( - remotes.devRemotes.map((r) => - typeof r === 'string' ? r : r.remoteName - ) ?? [] - ).map((r) => r.replace(/-/g, '_')), - p.name.replace(/-/g, '_'), - ]); - - const staticRemotesConfig = parseStaticRemotesConfig( - [...remotes.staticRemotes, ...remotes.dynamicRemotes], - context - ); - const mappedLocationsOfStaticRemotes = await buildStaticRemotes( - staticRemotesConfig, - nxBin, - context, - options - ); - - const devRemoteIters = await startRemotes( - remotes.devRemotes, - context, - options, - 'serve' - ); - - const staticRemotesIter = startStaticRemotesFileServer( - staticRemotesConfig, - context, - options - ); - - startRemoteProxies( - staticRemotesConfig, - mappedLocationsOfStaticRemotes, - options.ssl - ? { - pathToCert: join(workspaceRoot, options.sslCert), - pathToKey: join(workspaceRoot, options.sslKey), - } - : undefined - ); + const { staticRemotesIter, devRemoteIters, remotes } = + await startRemoteIterators( + options, + context, + startRemotes, + pathToManifestFile, + 'react' + ); return yield* combineAsyncIterables( currIter, diff --git a/packages/react/src/executors/module-federation-dev-server/schema.d.ts b/packages/react/src/executors/module-federation-dev-server/schema.d.ts index 38a7266e98299..61bbca581ea9d 100644 --- a/packages/react/src/executors/module-federation-dev-server/schema.d.ts +++ b/packages/react/src/executors/module-federation-dev-server/schema.d.ts @@ -1,17 +1,19 @@ import { WebDevServerOptions } from '@nx/webpack'; +import { DevRemoteDefinition } from '@nx/module-federation/src/executors/utils'; export type ModuleFederationDevServerOptions = WebDevServerOptions & { - devRemotes?: ( - | string - | { - remoteName: string; - configuration: string; - } - )[]; + devRemotes?: DevRemoteDefinition[]; skipRemotes?: string[]; static?: boolean; isInitialHost?: boolean; parallel?: number; staticRemotesPort?: number; pathToManifestFile?: string; + verbose?: boolean; }; + +export type NormalizedModuleFederationDevServerOptions = + ModuleFederationDevServerOptions & { + devRemotes: DevRemoteDefinition[]; + verbose: boolean; + }; diff --git a/packages/react/src/executors/module-federation-ssr-dev-server/lib/index.ts b/packages/react/src/executors/module-federation-ssr-dev-server/lib/index.ts new file mode 100644 index 0000000000000..7923ac83c9890 --- /dev/null +++ b/packages/react/src/executors/module-federation-ssr-dev-server/lib/index.ts @@ -0,0 +1,2 @@ +export * from './normalize-options'; +export * from './start-remotes'; diff --git a/packages/react/src/executors/module-federation-ssr-dev-server/lib/normalize-options.ts b/packages/react/src/executors/module-federation-ssr-dev-server/lib/normalize-options.ts new file mode 100644 index 0000000000000..d3b030d22adcb --- /dev/null +++ b/packages/react/src/executors/module-federation-ssr-dev-server/lib/normalize-options.ts @@ -0,0 +1,34 @@ +import { + ModuleFederationSsrDevServerOptions, + NormalizedModuleFederationSsrDevServerOptions, +} from '../schema'; +import { join } from 'path'; +import { + workspaceRoot, + ExecutorContext, + parseTargetString, + readTargetOptions, +} from '@nx/devkit'; + +export function normalizeOptions( + options: ModuleFederationSsrDevServerOptions +): NormalizedModuleFederationSsrDevServerOptions { + return { + ...options, + devRemotes: options.devRemotes ?? [], + verbose: options.verbose ?? false, + ssl: options.ssl ?? false, + sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined, + sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined, + }; +} + +export function getBuildOptions(buildTarget: string, context: ExecutorContext) { + const target = parseTargetString(buildTarget, context); + + const buildOptions = readTargetOptions(target, context); + + return { + ...buildOptions, + }; +} diff --git a/packages/react/src/executors/module-federation-ssr-dev-server/lib/start-remotes.ts b/packages/react/src/executors/module-federation-ssr-dev-server/lib/start-remotes.ts new file mode 100644 index 0000000000000..6072069beb368 --- /dev/null +++ b/packages/react/src/executors/module-federation-ssr-dev-server/lib/start-remotes.ts @@ -0,0 +1,58 @@ +import { ModuleFederationSsrDevServerOptions } from '../schema'; +import { runExecutor, ExecutorContext, ProjectConfiguration } from '@nx/devkit'; + +export async function startRemotes( + remotes: string[], + workspaceProjects: Record, + options: Partial< + Pick< + ModuleFederationSsrDevServerOptions, + 'devRemotes' | 'host' | 'ssl' | 'sslCert' | 'sslKey' | 'verbose' + > + >, + context: ExecutorContext +) { + const remoteIters: AsyncIterable<{ success: boolean }>[] = []; + const target = 'serve'; + for (const app of remotes) { + const remoteProjectServeTarget = workspaceProjects[app].targets[target]; + const isUsingModuleFederationSsrDevServerExecutor = + remoteProjectServeTarget.executor.includes( + 'module-federation-ssr-dev-server' + ); + + const configurationOverride = options.devRemotes?.find( + (remote): remote is { remoteName: string; configuration: string } => + typeof remote !== 'string' && remote.remoteName === app + )?.configuration; + { + const defaultOverrides = { + ...(options.host ? { host: options.host } : {}), + ...(options.ssl ? { ssl: options.ssl } : {}), + ...(options.sslCert ? { sslCert: options.sslCert } : {}), + ...(options.sslKey ? { sslKey: options.sslKey } : {}), + }; + + const overrides = { + watch: true, + ...defaultOverrides, + ...(isUsingModuleFederationSsrDevServerExecutor + ? { isInitialHost: false } + : {}), + }; + + remoteIters.push( + await runExecutor( + { + project: app, + target, + configuration: configurationOverride ?? context.configurationName, + }, + overrides, + context + ) + ); + } + } + return remoteIters; +} diff --git a/packages/react/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts b/packages/react/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts index 2d8369c26fe1e..2580abb00d217 100644 --- a/packages/react/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts +++ b/packages/react/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts @@ -1,257 +1,21 @@ -import { - ExecutorContext, - logger, - parseTargetString, - readTargetOptions, - runExecutor, - workspaceRoot, -} from '@nx/devkit'; +import { ExecutorContext, logger } from '@nx/devkit'; import ssrDevServerExecutor from '@nx/webpack/src/executors/ssr-dev-server/ssr-dev-server.impl'; -import { WebSsrDevServerOptions } from '@nx/webpack/src/executors/ssr-dev-server/schema'; import { extname, join } from 'path'; -import { - getModuleFederationConfig, - getRemotes, - parseStaticSsrRemotesConfig, - type StaticRemotesConfig, - startSsrRemoteProxies, -} from '@nx/module-federation/src/utils'; - +import { startRemoteIterators } from '@nx/module-federation/src/executors/utils'; import { combineAsyncIterables, createAsyncIterable, } from '@nx/devkit/src/utils/async-iterable'; -import { fork } from 'child_process'; -import { cpSync, createWriteStream, existsSync } from 'fs'; - -import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl'; -import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; +import { existsSync } from 'fs'; import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open'; - -type ModuleFederationSsrDevServerOptions = WebSsrDevServerOptions & { - devRemotes?: ( - | string - | { - remoteName: string; - configuration: string; - } - )[]; - - skipRemotes?: string[]; - host: string; - pathToManifestFile?: string; - staticRemotesPort?: number; - parallel?: number; - ssl?: boolean; - sslKey?: string; - sslCert?: string; - isInitialHost?: boolean; -}; - -function normalizeOptions( - options: ModuleFederationSsrDevServerOptions -): ModuleFederationSsrDevServerOptions { - return { - ...options, - ssl: options.ssl ?? false, - sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined, - sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined, - }; -} - -function getBuildOptions(buildTarget: string, context: ExecutorContext) { - const target = parseTargetString(buildTarget, context); - - const buildOptions = readTargetOptions(target, context); - - return { - ...buildOptions, - }; -} - -async function* startSsrStaticRemotesFileServer( - ssrStaticRemotesConfig: StaticRemotesConfig, - context: ExecutorContext, - options: ModuleFederationSsrDevServerOptions -): - | AsyncGenerator<{ success: boolean; baseUrl?: string }> - | AsyncIterable<{ success: boolean; baseUrl?: string }> { - if (ssrStaticRemotesConfig.remotes.length === 0) { - yield { success: true }; - return; - } - - // The directories are usually generated with /browser and /server suffixes so we need to copy them to a common directory - const commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes'); - for (const app of ssrStaticRemotesConfig.remotes) { - const remoteConfig = ssrStaticRemotesConfig.config[app]; - - cpSync( - remoteConfig.outputPath, - join(commonOutputDirectory, remoteConfig.urlSegment), - { - force: true, - recursive: true, - } - ); - } - - const staticRemotesIter = fileServerExecutor( - { - cors: true, - watch: false, - staticFilePath: commonOutputDirectory, - parallel: false, - spa: false, - withDeps: false, - host: options.host, - port: options.staticRemotesPort, - ssl: options.ssl, - sslCert: options.sslCert, - sslKey: options.sslKey, - cacheSeconds: -1, - }, - context - ); - - yield* staticRemotesIter; -} - -async function startRemotes( - remotes: string[], - context: ExecutorContext, - options: ModuleFederationSsrDevServerOptions -) { - const remoteIters: AsyncIterable<{ success: boolean }>[] = []; - const target = 'serve'; - for (const app of remotes) { - const remoteProjectServeTarget = - context.projectGraph.nodes[app].data.targets[target]; - const isUsingModuleFederationSsrDevServerExecutor = - remoteProjectServeTarget.executor.includes( - 'module-federation-ssr-dev-server' - ); - - const configurationOverride = options.devRemotes?.find( - (remote): remote is { remoteName: string; configuration: string } => - typeof remote !== 'string' && remote.remoteName === app - )?.configuration; - { - const defaultOverrides = { - ...(options.host ? { host: options.host } : {}), - ...(options.ssl ? { ssl: options.ssl } : {}), - ...(options.sslCert ? { sslCert: options.sslCert } : {}), - ...(options.sslKey ? { sslKey: options.sslKey } : {}), - }; - - const overrides = { - watch: true, - ...defaultOverrides, - ...(isUsingModuleFederationSsrDevServerExecutor - ? { isInitialHost: false } - : {}), - }; - - remoteIters.push( - await runExecutor( - { - project: app, - target, - configuration: configurationOverride ?? context.configurationName, - }, - overrides, - context - ) - ); - } - } - return remoteIters; -} - -async function buildSsrStaticRemotes( - staticRemotesConfig: StaticRemotesConfig, - nxBin, - context: ExecutorContext, - options: ModuleFederationSsrDevServerOptions -) { - if (!staticRemotesConfig.remotes.length) { - return; - } - - logger.info( - `Nx is building ${staticRemotesConfig.remotes.length} static remotes...` - ); - const mapLocationOfRemotes: Record = {}; - - for (const remoteApp of staticRemotesConfig.remotes) { - mapLocationOfRemotes[remoteApp] = `http${options.ssl ? 's' : ''}://${ - options.host - }:${options.staticRemotesPort}/${ - staticRemotesConfig.config[remoteApp].urlSegment - }`; - } - - await new Promise((resolve) => { - const childProcess = fork( - nxBin, - [ - 'run-many', - '--target=server', - '--projects', - staticRemotesConfig.remotes.join(','), - ...(context.configurationName - ? [`--configuration=${context.configurationName}`] - : []), - ...(options.parallel ? [`--parallel=${options.parallel}`] : []), - ], - { - cwd: context.root, - stdio: ['ignore', 'pipe', 'pipe', 'ipc'], - } - ); - - // Add a listener to the child process to capture the build log - const remoteBuildLogFile = join( - workspaceDataDirectory, - `${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log` - ); - - const remoteBuildLogStream = createWriteStream(remoteBuildLogFile); - - childProcess.stdout.on('data', (data) => { - const ANSII_CODE_REGEX = - /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; - const stdoutString = data.toString().replace(ANSII_CODE_REGEX, ''); - remoteBuildLogStream.write(stdoutString); - - // in addition to writing into the stdout stream, also show error directly in console - // so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output. - if (stdoutString.includes('ERROR in')) { - logger.log(stdoutString); - } - - if (stdoutString.includes('Successfully ran target server')) { - childProcess.stdout.removeAllListeners('data'); - logger.info( - `Nx Built ${staticRemotesConfig.remotes.length} static remotes.` - ); - resolve(); - } - }); - - process.on('SIGTERM', () => childProcess.kill('SIGTERM')); - process.on('exit', () => childProcess.kill('SIGTERM')); - }); - return mapLocationOfRemotes; -} +import { ModuleFederationSsrDevServerOptions } from './schema'; +import { getBuildOptions, normalizeOptions, startRemotes } from './lib'; export default async function* moduleFederationSsrDevServer( ssrDevServerOptions: ModuleFederationSsrDevServerOptions, context: ExecutorContext ) { const options = normalizeOptions(ssrDevServerOptions); - // Force Node to resolve to look for the nx binary that is inside node_modules - const nxBin = require.resolve('nx/bin/nx'); let iter: any = ssrDevServerExecutor(options, context); const projectConfig = context.projectsConfigurations.projects[context.projectName]; @@ -285,74 +49,15 @@ export default async function* moduleFederationSsrDevServer( return yield* iter; } - const moduleFederationConfig = getModuleFederationConfig( - buildOptions.tsConfig, - context.root, - projectConfig.root, - 'react' - ); - - const remoteNames = options.devRemotes?.map((remote) => - typeof remote === 'string' ? remote : remote.remoteName - ); - - const remotes = getRemotes( - remoteNames, - options.skipRemotes, - moduleFederationConfig, - { - projectName: context.projectName, - projectGraph: context.projectGraph, - root: context.root, - }, - pathToManifestFile - ); - - options.staticRemotesPort ??= remotes.staticRemotePort; - - process.env.NX_MF_DEV_REMOTES = JSON.stringify([ - ...( - remotes.devRemotes.map((r) => - typeof r === 'string' ? r : r.remoteName - ) ?? [] - ).map((r) => r.replace(/-/g, '_')), - projectConfig.name.replace(/-/g, '_'), - ]); - - const staticRemotesConfig = parseStaticSsrRemotesConfig( - [...remotes.staticRemotes, ...remotes.dynamicRemotes], - context - ); - - const mappedLocationsOfStaticRemotes = await buildSsrStaticRemotes( - staticRemotesConfig, - nxBin, - context, - options - ); - - const devRemoteIters = await startRemotes( - remotes.devRemotes, - context, - options - ); - - const staticRemotesIter = startSsrStaticRemotesFileServer( - staticRemotesConfig, - context, - options - ); - - startSsrRemoteProxies( - staticRemotesConfig, - mappedLocationsOfStaticRemotes, - options.ssl - ? { - pathToCert: options.sslCert, - pathToKey: options.sslKey, - } - : undefined - ); + const { staticRemotesIter, devRemoteIters, remotes } = + await startRemoteIterators( + options, + context, + startRemotes, + pathToManifestFile, + 'react', + true + ); const combined = combineAsyncIterables(staticRemotesIter, ...devRemoteIters); diff --git a/packages/react/src/executors/module-federation-ssr-dev-server/schema.d.ts b/packages/react/src/executors/module-federation-ssr-dev-server/schema.d.ts new file mode 100644 index 0000000000000..d37070f0209cb --- /dev/null +++ b/packages/react/src/executors/module-federation-ssr-dev-server/schema.d.ts @@ -0,0 +1,29 @@ +import { WebSsrDevServerOptions } from '@nx/webpack/src/executors/ssr-dev-server/schema'; +import { DevRemoteDefinition } from '@nx/module-federation/src/executors/utils'; + +export type ModuleFederationSsrDevServerOptions = WebSsrDevServerOptions & { + devRemotes?: ( + | string + | { + remoteName: string; + configuration: string; + } + )[]; + + skipRemotes?: string[]; + host: string; + pathToManifestFile?: string; + staticRemotesPort?: number; + parallel?: number; + ssl?: boolean; + sslKey?: string; + sslCert?: string; + isInitialHost?: boolean; + verbose?: boolean; +}; + +export type NormalizedModuleFederationSsrDevServerOptions = + ModuleFederationSsrDevServerOptions & { + devRemotes: DevRemoteDefinition[]; + verbose: boolean; + }; diff --git a/packages/react/src/executors/module-federation-static-server/module-federation-static-server.impl.ts b/packages/react/src/executors/module-federation-static-server/module-federation-static-server.impl.ts index db32670306f57..c48826c594af9 100644 --- a/packages/react/src/executors/module-federation-static-server/module-federation-static-server.impl.ts +++ b/packages/react/src/executors/module-federation-static-server/module-federation-static-server.impl.ts @@ -16,7 +16,7 @@ import { parseStaticRemotesConfig, StaticRemotesConfig, } from '@nx/module-federation/src/utils'; -import { buildStaticRemotes } from '../../utils/build-static.remotes'; +import { buildStaticRemotes } from '@nx/module-federation/src/executors/utils'; import { fork } from 'child_process'; import type { WebpackExecutorOptions } from '@nx/webpack'; import * as process from 'node:process'; diff --git a/packages/react/src/generators/library/library.ts b/packages/react/src/generators/library/library.ts index 7a33143c2cf7c..cfa3e4832c3a5 100644 --- a/packages/react/src/generators/library/library.ts +++ b/packages/react/src/generators/library/library.ts @@ -6,9 +6,11 @@ import { GeneratorCallback, installPackagesTask, joinPathFragments, + readNxJson, runTasksInSerial, Tree, updateJson, + updateNxJson, writeJson, } from '@nx/devkit'; import { getRelativeCwd } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; @@ -30,6 +32,7 @@ import { extractTsConfigBase } from '../../utils/create-ts-config'; import { installCommonDependencies } from './lib/install-common-dependencies'; import { setDefaults } from './lib/set-defaults'; import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { ensureProjectIsExcludedFromPluginRegistrations } from '@nx/js/src/utils/typescript/plugin'; export async function libraryGenerator(host: Tree, schema: Schema) { return await libraryGeneratorInternal(host, { @@ -137,6 +140,10 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) { } else if (options.buildable && options.bundler === 'rollup') { const rollupTask = await addRollupBuildTarget(host, options); tasks.push(rollupTask); + } else if (options.bundler === 'none' && options.addPlugin) { + const nxJson = readNxJson(host); + ensureProjectIsExcludedFromPluginRegistrations(nxJson, options.projectRoot); + updateNxJson(host, nxJson); } // Set up test target diff --git a/packages/rspack/src/executors/module-federation-dev-server/lib/index.ts b/packages/rspack/src/executors/module-federation-dev-server/lib/index.ts new file mode 100644 index 0000000000000..7923ac83c9890 --- /dev/null +++ b/packages/rspack/src/executors/module-federation-dev-server/lib/index.ts @@ -0,0 +1,2 @@ +export * from './normalize-options'; +export * from './start-remotes'; diff --git a/packages/rspack/src/executors/module-federation-dev-server/lib/normalize-options.ts b/packages/rspack/src/executors/module-federation-dev-server/lib/normalize-options.ts new file mode 100644 index 0000000000000..1cebe6567eb20 --- /dev/null +++ b/packages/rspack/src/executors/module-federation-dev-server/lib/normalize-options.ts @@ -0,0 +1,29 @@ +import { + ExecutorContext, + parseTargetString, + readTargetOptions, +} from '@nx/devkit'; +import { + ModuleFederationDevServerOptions, + NormalizedModuleFederationDevServerOptions, +} from '../schema'; + +export function getBuildOptions(buildTarget: string, context: ExecutorContext) { + const target = parseTargetString(buildTarget, context); + + const buildOptions = readTargetOptions(target, context); + + return { + ...buildOptions, + }; +} + +export function normalizeOptions( + options: ModuleFederationDevServerOptions +): NormalizedModuleFederationDevServerOptions { + return { + ...options, + devRemotes: options.devRemotes ?? [], + verbose: options.verbose ?? false, + }; +} diff --git a/packages/rspack/src/executors/module-federation-dev-server/lib/start-remotes.ts b/packages/rspack/src/executors/module-federation-dev-server/lib/start-remotes.ts new file mode 100644 index 0000000000000..a64079d44a37b --- /dev/null +++ b/packages/rspack/src/executors/module-federation-dev-server/lib/start-remotes.ts @@ -0,0 +1,64 @@ +import { ModuleFederationDevServerOptions } from '../schema'; +import { ProjectConfiguration, ExecutorContext, runExecutor } from '@nx/devkit'; + +export async function startRemotes( + remotes: string[], + workspaceProjects: Record, + options: Partial< + Pick< + ModuleFederationDevServerOptions, + 'devRemotes' | 'host' | 'ssl' | 'sslCert' | 'sslKey' | 'verbose' + > + >, + context: ExecutorContext, + target: 'serve' | 'serve-static' = 'serve' +) { + const remoteIters: AsyncIterable<{ success: boolean }>[] = []; + + for (const app of remotes) { + const remoteProjectServeTarget = workspaceProjects[app].targets[target]; + const isUsingModuleFederationDevServerExecutor = + remoteProjectServeTarget.executor.includes( + 'module-federation-dev-server' + ); + + const configurationOverride = options.devRemotes?.find( + ( + r + ): r is { + remoteName: string; + configuration: string; + } => typeof r !== 'string' && r.remoteName === app + )?.configuration; + + const defaultOverrides = { + ...(options.host ? { host: options.host } : {}), + ...(options.ssl ? { ssl: options.ssl } : {}), + ...(options.sslCert ? { sslCert: options.sslCert } : {}), + ...(options.sslKey ? { sslKey: options.sslKey } : {}), + }; + const overrides = + target === 'serve' + ? { + watch: true, + ...(isUsingModuleFederationDevServerExecutor + ? { isInitialHost: false } + : {}), + ...defaultOverrides, + } + : { ...defaultOverrides }; + + remoteIters.push( + await runExecutor( + { + project: app, + target, + configuration: configurationOverride ?? context.configurationName, + }, + overrides, + context + ) + ); + } + return remoteIters; +} diff --git a/packages/rspack/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts b/packages/rspack/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts index 62db9b91f4a7f..2d00b01a3eae6 100644 --- a/packages/rspack/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts +++ b/packages/rspack/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts @@ -1,162 +1,22 @@ -import { - ExecutorContext, - logger, - parseTargetString, - readTargetOptions, - runExecutor, - workspaceRoot, -} from '@nx/devkit'; +import { ExecutorContext, logger } from '@nx/devkit'; import { combineAsyncIterables, createAsyncIterable, } from '@nx/devkit/src/utils/async-iterable'; import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl'; import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open'; -import { cpSync, existsSync } from 'fs'; +import { existsSync } from 'fs'; import { extname, join } from 'path'; -import { - getModuleFederationConfig, - getRemotes, - parseStaticRemotesConfig, - type StaticRemotesConfig, - startRemoteProxies, -} from '@nx/module-federation/src/utils'; -import { buildStaticRemotes } from '../../utils/module-federation/build-static.remotes'; +import { startRemoteIterators } from '@nx/module-federation/src/executors/utils'; import devServerExecutor from '../dev-server/dev-server.impl'; import { ModuleFederationDevServerOptions } from './schema'; - -function getBuildOptions(buildTarget: string, context: ExecutorContext) { - const target = parseTargetString(buildTarget, context); - - const buildOptions = readTargetOptions(target, context); - - return { - ...buildOptions, - }; -} - -function startStaticRemotesFileServer( - staticRemotesConfig: StaticRemotesConfig, - context: ExecutorContext, - options: ModuleFederationDevServerOptions -) { - if ( - !staticRemotesConfig.remotes || - staticRemotesConfig.remotes.length === 0 - ) { - return; - } - let shouldMoveToCommonLocation = false; - let commonOutputDirectory: string; - for (const app of staticRemotesConfig.remotes) { - const remoteBasePath = staticRemotesConfig.config[app].basePath; - if (!commonOutputDirectory) { - commonOutputDirectory = remoteBasePath; - } else if (commonOutputDirectory !== remoteBasePath) { - shouldMoveToCommonLocation = true; - break; - } - } - - if (shouldMoveToCommonLocation) { - commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes'); - for (const app of staticRemotesConfig.remotes) { - const remoteConfig = staticRemotesConfig.config[app]; - cpSync( - remoteConfig.outputPath, - join(commonOutputDirectory, remoteConfig.urlSegment), - { - force: true, - recursive: true, - } - ); - } - } - - const staticRemotesIter = fileServerExecutor( - { - cors: true, - watch: false, - staticFilePath: commonOutputDirectory, - parallel: false, - spa: false, - withDeps: false, - host: options.host, - port: options.staticRemotesPort, - ssl: options.ssl, - sslCert: options.sslCert, - sslKey: options.sslKey, - cacheSeconds: -1, - }, - context - ); - - return staticRemotesIter; -} - -async function startRemotes( - remotes: string[], - context: ExecutorContext, - options: ModuleFederationDevServerOptions, - target: 'serve' | 'serve-static' = 'serve' -) { - const remoteIters: AsyncIterable<{ success: boolean }>[] = []; - - for (const app of remotes) { - const remoteProjectServeTarget = - context.projectGraph.nodes[app].data.targets[target]; - const isUsingModuleFederationDevServerExecutor = - remoteProjectServeTarget.executor.includes( - 'module-federation-dev-server' - ); - - const configurationOverride = options.devRemotes?.find( - ( - r - ): r is { - remoteName: string; - configuration: string; - } => typeof r !== 'string' && r.remoteName === app - )?.configuration; - - const defaultOverrides = { - ...(options.host ? { host: options.host } : {}), - ...(options.ssl ? { ssl: options.ssl } : {}), - ...(options.sslCert ? { sslCert: options.sslCert } : {}), - ...(options.sslKey ? { sslKey: options.sslKey } : {}), - }; - const overrides = - target === 'serve' - ? { - watch: true, - ...(isUsingModuleFederationDevServerExecutor - ? { isInitialHost: false } - : {}), - ...defaultOverrides, - } - : { ...defaultOverrides }; - - remoteIters.push( - await runExecutor( - { - project: app, - target, - configuration: configurationOverride ?? context.configurationName, - }, - overrides, - context - ) - ); - } - return remoteIters; -} +import { getBuildOptions, normalizeOptions, startRemotes } from './lib'; export default async function* moduleFederationDevServer( - options: ModuleFederationDevServerOptions, + schema: ModuleFederationDevServerOptions, context: ExecutorContext ): AsyncIterableIterator<{ success: boolean; baseUrl?: string }> { - // Force Node to resolve to look for the nx binary that is inside node_modules - const nxBin = require.resolve('nx/bin/nx'); + const options = normalizeOptions(schema); const currIter = options.static ? fileServerExecutor( { @@ -201,74 +61,14 @@ export default async function* moduleFederationDevServer( return yield* currIter; } - const moduleFederationConfig = getModuleFederationConfig( - buildOptions.tsConfig, - context.root, - p.root, - 'react' - ); - - const remoteNames = options.devRemotes?.map((r) => - typeof r === 'string' ? r : r.remoteName - ); - - const remotes = getRemotes( - remoteNames, - options.skipRemotes, - moduleFederationConfig, - { - projectName: context.projectName, - projectGraph: context.projectGraph, - root: context.root, - }, - pathToManifestFile - ); - options.staticRemotesPort ??= remotes.staticRemotePort; - - // Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin - process.env.NX_MF_DEV_REMOTES = JSON.stringify([ - ...( - remotes.devRemotes.map((r) => - typeof r === 'string' ? r : r.remoteName - ) ?? [] - ).map((r) => r.replace(/-/g, '_')), - p.name.replace(/-/g, '_'), - ]); - - const staticRemotesConfig = parseStaticRemotesConfig( - [...remotes.staticRemotes, ...remotes.dynamicRemotes], - context - ); - const mappedLocationsOfStaticRemotes = await buildStaticRemotes( - staticRemotesConfig, - nxBin, - context, - options - ); - - const devRemoteIters = await startRemotes( - remotes.devRemotes, - context, - options, - 'serve' - ); - - const staticRemotesIter = startStaticRemotesFileServer( - staticRemotesConfig, - context, - options - ); - - startRemoteProxies( - staticRemotesConfig, - mappedLocationsOfStaticRemotes, - options.ssl - ? { - pathToCert: join(workspaceRoot, options.sslCert), - pathToKey: join(workspaceRoot, options.sslKey), - } - : undefined - ); + const { staticRemotesIter, devRemoteIters, remotes } = + await startRemoteIterators( + options, + context, + startRemotes, + pathToManifestFile, + 'react' + ); return yield* combineAsyncIterables( currIter, diff --git a/packages/rspack/src/executors/module-federation-dev-server/schema.d.ts b/packages/rspack/src/executors/module-federation-dev-server/schema.d.ts index 61c4c0657eb06..6df7a070173cd 100644 --- a/packages/rspack/src/executors/module-federation-dev-server/schema.d.ts +++ b/packages/rspack/src/executors/module-federation-dev-server/schema.d.ts @@ -15,4 +15,11 @@ export type ModuleFederationDevServerOptions = DevServerExecutorSchema & { parallel?: number; staticRemotesPort?: number; pathToManifestFile?: string; + verbose?: boolean; }; + +export type NormalizedModuleFederationDevServerOptions = + ModuleFederationDevServerOptions & { + devRemotes: DevServerExecutorSchema['devRemotes']; + verbose: boolean; + }; diff --git a/packages/rspack/src/executors/module-federation-ssr-dev-server/lib/index.ts b/packages/rspack/src/executors/module-federation-ssr-dev-server/lib/index.ts new file mode 100644 index 0000000000000..7923ac83c9890 --- /dev/null +++ b/packages/rspack/src/executors/module-federation-ssr-dev-server/lib/index.ts @@ -0,0 +1,2 @@ +export * from './normalize-options'; +export * from './start-remotes'; diff --git a/packages/rspack/src/executors/module-federation-ssr-dev-server/lib/normalize-options.ts b/packages/rspack/src/executors/module-federation-ssr-dev-server/lib/normalize-options.ts new file mode 100644 index 0000000000000..d3b030d22adcb --- /dev/null +++ b/packages/rspack/src/executors/module-federation-ssr-dev-server/lib/normalize-options.ts @@ -0,0 +1,34 @@ +import { + ModuleFederationSsrDevServerOptions, + NormalizedModuleFederationSsrDevServerOptions, +} from '../schema'; +import { join } from 'path'; +import { + workspaceRoot, + ExecutorContext, + parseTargetString, + readTargetOptions, +} from '@nx/devkit'; + +export function normalizeOptions( + options: ModuleFederationSsrDevServerOptions +): NormalizedModuleFederationSsrDevServerOptions { + return { + ...options, + devRemotes: options.devRemotes ?? [], + verbose: options.verbose ?? false, + ssl: options.ssl ?? false, + sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined, + sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined, + }; +} + +export function getBuildOptions(buildTarget: string, context: ExecutorContext) { + const target = parseTargetString(buildTarget, context); + + const buildOptions = readTargetOptions(target, context); + + return { + ...buildOptions, + }; +} diff --git a/packages/rspack/src/executors/module-federation-ssr-dev-server/lib/start-remotes.ts b/packages/rspack/src/executors/module-federation-ssr-dev-server/lib/start-remotes.ts new file mode 100644 index 0000000000000..6072069beb368 --- /dev/null +++ b/packages/rspack/src/executors/module-federation-ssr-dev-server/lib/start-remotes.ts @@ -0,0 +1,58 @@ +import { ModuleFederationSsrDevServerOptions } from '../schema'; +import { runExecutor, ExecutorContext, ProjectConfiguration } from '@nx/devkit'; + +export async function startRemotes( + remotes: string[], + workspaceProjects: Record, + options: Partial< + Pick< + ModuleFederationSsrDevServerOptions, + 'devRemotes' | 'host' | 'ssl' | 'sslCert' | 'sslKey' | 'verbose' + > + >, + context: ExecutorContext +) { + const remoteIters: AsyncIterable<{ success: boolean }>[] = []; + const target = 'serve'; + for (const app of remotes) { + const remoteProjectServeTarget = workspaceProjects[app].targets[target]; + const isUsingModuleFederationSsrDevServerExecutor = + remoteProjectServeTarget.executor.includes( + 'module-federation-ssr-dev-server' + ); + + const configurationOverride = options.devRemotes?.find( + (remote): remote is { remoteName: string; configuration: string } => + typeof remote !== 'string' && remote.remoteName === app + )?.configuration; + { + const defaultOverrides = { + ...(options.host ? { host: options.host } : {}), + ...(options.ssl ? { ssl: options.ssl } : {}), + ...(options.sslCert ? { sslCert: options.sslCert } : {}), + ...(options.sslKey ? { sslKey: options.sslKey } : {}), + }; + + const overrides = { + watch: true, + ...defaultOverrides, + ...(isUsingModuleFederationSsrDevServerExecutor + ? { isInitialHost: false } + : {}), + }; + + remoteIters.push( + await runExecutor( + { + project: app, + target, + configuration: configurationOverride ?? context.configurationName, + }, + overrides, + context + ) + ); + } + } + return remoteIters; +} diff --git a/packages/rspack/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts b/packages/rspack/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts index edecccbeffb73..5e8b673ac5332 100644 --- a/packages/rspack/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts +++ b/packages/rspack/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts @@ -1,256 +1,22 @@ -import { - ExecutorContext, - logger, - parseTargetString, - readTargetOptions, - runExecutor, - workspaceRoot, -} from '@nx/devkit'; +import { ExecutorContext, logger } from '@nx/devkit'; import { extname, join } from 'path'; -import { - getModuleFederationConfig, - getRemotes, - parseStaticSsrRemotesConfig, - type StaticRemotesConfig, - startSsrRemoteProxies, -} from '@nx/module-federation/src/utils'; -import { RspackSsrDevServerOptions } from '../ssr-dev-server/schema'; +import { startRemoteIterators } from '@nx/module-federation/src/executors/utils'; import ssrDevServerExecutor from '../ssr-dev-server/ssr-dev-server.impl'; - import { combineAsyncIterables, createAsyncIterable, } from '@nx/devkit/src/utils/async-iterable'; -import { fork } from 'child_process'; -import { cpSync, createWriteStream, existsSync } from 'fs'; - -import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl'; +import { existsSync } from 'fs'; import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open'; -import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; - -type ModuleFederationSsrDevServerOptions = RspackSsrDevServerOptions & { - devRemotes?: ( - | string - | { - remoteName: string; - configuration: string; - } - )[]; - - skipRemotes?: string[]; - host: string; - pathToManifestFile?: string; - staticRemotesPort?: number; - parallel?: number; - ssl?: boolean; - sslKey?: string; - sslCert?: string; - isInitialHost?: boolean; -}; - -function normalizeOptions( - options: ModuleFederationSsrDevServerOptions -): ModuleFederationSsrDevServerOptions { - return { - ...options, - ssl: options.ssl ?? false, - sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined, - sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined, - }; -} - -function getBuildOptions(buildTarget: string, context: ExecutorContext) { - const target = parseTargetString(buildTarget, context); - - const buildOptions = readTargetOptions(target, context); - - return { - ...buildOptions, - }; -} - -function startSsrStaticRemotesFileServer( - ssrStaticRemotesConfig: StaticRemotesConfig, - context: ExecutorContext, - options: ModuleFederationSsrDevServerOptions -) { - if (ssrStaticRemotesConfig.remotes.length === 0) { - return; - } - - // The directories are usually generated with /browser and /server suffixes so we need to copy them to a common directory - const commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes'); - for (const app of ssrStaticRemotesConfig.remotes) { - const remoteConfig = ssrStaticRemotesConfig.config[app]; - - cpSync( - remoteConfig.outputPath, - join(commonOutputDirectory, remoteConfig.urlSegment), - { - force: true, - recursive: true, - } - ); - } - - const staticRemotesIter = fileServerExecutor( - { - cors: true, - watch: false, - staticFilePath: commonOutputDirectory, - parallel: false, - spa: false, - withDeps: false, - host: options.host, - port: options.staticRemotesPort, - ssl: options.ssl, - sslCert: options.sslCert, - sslKey: options.sslKey, - cacheSeconds: -1, - }, - context - ); - - return staticRemotesIter; -} - -async function startRemotes( - remotes: string[], - context: ExecutorContext, - options: ModuleFederationSsrDevServerOptions -) { - const remoteIters: AsyncIterable<{ success: boolean }>[] = []; - const target = 'serve'; - for (const app of remotes) { - const remoteProjectServeTarget = - context.projectGraph.nodes[app].data.targets[target]; - const isUsingModuleFederationSsrDevServerExecutor = - remoteProjectServeTarget.executor.includes( - 'module-federation-ssr-dev-server' - ); - - const configurationOverride = options.devRemotes?.find( - (remote): remote is { remoteName: string; configuration: string } => - typeof remote !== 'string' && remote.remoteName === app - )?.configuration; - { - const defaultOverrides = { - ...(options.host ? { host: options.host } : {}), - ...(options.ssl ? { ssl: options.ssl } : {}), - ...(options.sslCert ? { sslCert: options.sslCert } : {}), - ...(options.sslKey ? { sslKey: options.sslKey } : {}), - }; - - const overrides = { - watch: true, - ...defaultOverrides, - ...(isUsingModuleFederationSsrDevServerExecutor - ? { isInitialHost: false } - : {}), - }; - - remoteIters.push( - await runExecutor( - { - project: app, - target, - configuration: configurationOverride ?? context.configurationName, - }, - overrides, - context - ) - ); - } - } - return remoteIters; -} - -async function buildSsrStaticRemotes( - staticRemotesConfig: StaticRemotesConfig, - nxBin, - context: ExecutorContext, - options: ModuleFederationSsrDevServerOptions -) { - if (!staticRemotesConfig.remotes.length) { - return; - } - - logger.info( - `Nx is building ${staticRemotesConfig.remotes.length} static remotes...` - ); - const mapLocationOfRemotes: Record = {}; - - for (const remoteApp of staticRemotesConfig.remotes) { - mapLocationOfRemotes[remoteApp] = `http${options.ssl ? 's' : ''}://${ - options.host - }:${options.staticRemotesPort}/${ - staticRemotesConfig.config[remoteApp].urlSegment - }`; - } - - await new Promise((resolve) => { - const childProcess = fork( - nxBin, - [ - 'run-many', - '--target=server', - '--projects', - staticRemotesConfig.remotes.join(','), - ...(context.configurationName - ? [`--configuration=${context.configurationName}`] - : []), - ...(options.parallel ? [`--parallel=${options.parallel}`] : []), - ], - { - cwd: context.root, - stdio: ['ignore', 'pipe', 'pipe', 'ipc'], - } - ); - - // Add a listener to the child process to capture the build log - const remoteBuildLogFile = join( - workspaceDataDirectory, - // eslint-disable-next-line - `${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log` - ); - - const remoteBuildLogStream = createWriteStream(remoteBuildLogFile); - - childProcess.stdout.on('data', (data) => { - const ANSII_CODE_REGEX = - // eslint-disable-next-line no-control-regex - /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; - const stdoutString = data.toString().replace(ANSII_CODE_REGEX, ''); - remoteBuildLogStream.write(stdoutString); - - // in addition to writing into the stdout stream, also show error directly in console - // so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output. - if (stdoutString.includes('ERROR in')) { - logger.log(stdoutString); - } - - if (stdoutString.includes('Successfully ran target server')) { - childProcess.stdout.removeAllListeners('data'); - logger.info( - `Nx Built ${staticRemotesConfig.remotes.length} static remotes.` - ); - resolve(); - } - }); - - process.on('SIGTERM', () => childProcess.kill('SIGTERM')); - process.on('exit', () => childProcess.kill('SIGTERM')); - }); - return mapLocationOfRemotes; -} +import { ModuleFederationSsrDevServerOptions } from './schema'; +import { getBuildOptions, normalizeOptions, startRemotes } from './lib'; export default async function* moduleFederationSsrDevServer( ssrDevServerOptions: ModuleFederationSsrDevServerOptions, context: ExecutorContext ) { const options = normalizeOptions(ssrDevServerOptions); - // Force Node to resolve to look for the nx binary that is inside node_modules - const nxBin = require.resolve('nx/bin/nx'); + const iter = ssrDevServerExecutor(options, context); const projectConfig = context.projectsConfigurations.projects[context.projectName]; @@ -284,74 +50,15 @@ export default async function* moduleFederationSsrDevServer( return yield* iter; } - const moduleFederationConfig = getModuleFederationConfig( - buildOptions.tsConfig, - context.root, - projectConfig.root, - 'react' - ); - - const remoteNames = options.devRemotes?.map((remote) => - typeof remote === 'string' ? remote : remote.remoteName - ); - - const remotes = getRemotes( - remoteNames, - options.skipRemotes, - moduleFederationConfig, - { - projectName: context.projectName, - projectGraph: context.projectGraph, - root: context.root, - }, - pathToManifestFile - ); - - options.staticRemotesPort ??= remotes.staticRemotePort; - - process.env.NX_MF_DEV_REMOTES = JSON.stringify([ - ...( - remotes.devRemotes.map((r) => - typeof r === 'string' ? r : r.remoteName - ) ?? [] - ).map((r) => r.replace(/-/g, '_')), - projectConfig.name.replace(/-/g, '_'), - ]); - - const staticRemotesConfig = parseStaticSsrRemotesConfig( - [...remotes.staticRemotes, ...remotes.dynamicRemotes], - context - ); - - const mappedLocationsOfStaticRemotes = await buildSsrStaticRemotes( - staticRemotesConfig, - nxBin, - context, - options - ); - - const devRemoteIters = await startRemotes( - remotes.devRemotes, - context, - options - ); - - const staticRemotesIter = startSsrStaticRemotesFileServer( - staticRemotesConfig, - context, - options - ); - - startSsrRemoteProxies( - staticRemotesConfig, - mappedLocationsOfStaticRemotes, - options.ssl - ? { - pathToCert: options.sslCert, - pathToKey: options.sslKey, - } - : undefined - ); + const { staticRemotesIter, devRemoteIters, remotes } = + await startRemoteIterators( + options, + context, + startRemotes, + pathToManifestFile, + 'react', + true + ); return yield* combineAsyncIterables( iter, diff --git a/packages/rspack/src/executors/module-federation-ssr-dev-server/schema.d.ts b/packages/rspack/src/executors/module-federation-ssr-dev-server/schema.d.ts new file mode 100644 index 0000000000000..8df7a459c72e1 --- /dev/null +++ b/packages/rspack/src/executors/module-federation-ssr-dev-server/schema.d.ts @@ -0,0 +1,29 @@ +import { DevRemoteDefinition } from '@nx/module-federation/src/executors/utils'; +import { RspackSsrDevServerOptions } from '../ssr-dev-server/schema'; + +export type ModuleFederationSsrDevServerOptions = RspackSsrDevServerOptions & { + devRemotes?: ( + | string + | { + remoteName: string; + configuration: string; + } + )[]; + + skipRemotes?: string[]; + host: string; + pathToManifestFile?: string; + staticRemotesPort?: number; + parallel?: number; + ssl?: boolean; + sslKey?: string; + sslCert?: string; + isInitialHost?: boolean; + verbose?: boolean; +}; + +export type NormalizedModuleFederationSsrDevServerOptions = + ModuleFederationSsrDevServerOptions & { + devRemotes: DevRemoteDefinition[]; + verbose: boolean; + }; diff --git a/packages/rspack/src/executors/module-federation-static-server/module-federation-static-server.impl.ts b/packages/rspack/src/executors/module-federation-static-server/module-federation-static-server.impl.ts index 3032cd49da325..5c93f1dc1c9c3 100644 --- a/packages/rspack/src/executors/module-federation-static-server/module-federation-static-server.impl.ts +++ b/packages/rspack/src/executors/module-federation-static-server/module-federation-static-server.impl.ts @@ -22,7 +22,7 @@ import { parseStaticRemotesConfig, StaticRemotesConfig, } from '@nx/module-federation/src/utils'; -import { buildStaticRemotes } from '../../utils/module-federation/build-static.remotes'; +import { buildStaticRemotes } from '@nx/module-federation/src/executors/utils'; import { ModuleFederationDevServerOptions } from '../module-federation-dev-server/schema'; import type { RspackExecutorSchema } from '../rspack/schema'; import { ModuleFederationStaticServerSchema } from './schema'; diff --git a/packages/rspack/src/utils/module-federation/build-static.remotes.ts b/packages/rspack/src/utils/module-federation/build-static.remotes.ts deleted file mode 100644 index 2db02836bed87..0000000000000 --- a/packages/rspack/src/utils/module-federation/build-static.remotes.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ExecutorContext } from '@nx/devkit'; -import { createWriteStream } from 'fs'; -import { fork } from 'node:child_process'; -import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; -import { logger } from 'nx/src/utils/logger'; -import { join } from 'path'; -import { ModuleFederationDevServerOptions } from '../../executors/module-federation-dev-server/schema'; -import type { StaticRemotesConfig } from '@nx/module-federation/src/utils'; - -export async function buildStaticRemotes( - staticRemotesConfig: StaticRemotesConfig, - nxBin, - context: ExecutorContext, - options: ModuleFederationDevServerOptions -) { - if (!staticRemotesConfig.remotes.length) { - return; - } - logger.info( - `NX Building ${staticRemotesConfig.remotes.length} static remotes...` - ); - const mappedLocationOfRemotes: Record = {}; - - for (const app of staticRemotesConfig.remotes) { - mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${ - options.host - }:${options.staticRemotesPort}/${ - staticRemotesConfig.config[app].urlSegment - }`; - } - - await new Promise((res, rej) => { - const staticProcess = fork( - nxBin, - [ - 'run-many', - `--target=build`, - `--projects=${staticRemotesConfig.remotes.join(',')}`, - ...(context.configurationName - ? [`--configuration=${context.configurationName}`] - : []), - ...(options.parallel ? [`--parallel=${options.parallel}`] : []), - ], - { - cwd: context.root, - stdio: ['ignore', 'pipe', 'pipe', 'ipc'], - } - ); - - // File to debug build failures e.g. 2024-01-01T00_00_0_0Z-build.log' - const remoteBuildLogFile = join( - workspaceDataDirectory, - // eslint-disable-next-line - `${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log` - ); - const stdoutStream = createWriteStream(remoteBuildLogFile); - - staticProcess.stdout.on('data', (data) => { - const ANSII_CODE_REGEX = - // eslint-disable-next-line no-control-regex - /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; - const stdoutString = data.toString().replace(ANSII_CODE_REGEX, ''); - stdoutStream.write(stdoutString); - - // in addition to writing into the stdout stream, also show error directly in console - // so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output. - if (stdoutString.includes('ERROR in')) { - logger.log(stdoutString); - } - - if (stdoutString.includes('Successfully ran target build')) { - staticProcess.stdout.removeAllListeners('data'); - logger.info( - `NX Built ${staticRemotesConfig.remotes.length} static remotes` - ); - res(); - } - }); - staticProcess.stderr.on('data', (data) => logger.info(data.toString())); - staticProcess.once('exit', (code) => { - stdoutStream.end(); - staticProcess.stdout.removeAllListeners('data'); - staticProcess.stderr.removeAllListeners('data'); - if (code !== 0) { - rej( - `Remote failed to start. A complete log can be found in: ${remoteBuildLogFile}` - ); - } else { - res(); - } - }); - process.on('SIGTERM', () => staticProcess.kill('SIGTERM')); - process.on('exit', () => staticProcess.kill('SIGTERM')); - }); - - return mappedLocationOfRemotes; -} diff --git a/packages/workspace/src/generators/new/generate-preset.ts b/packages/workspace/src/generators/new/generate-preset.ts index 1f9c353747ba3..da45a886222a4 100644 --- a/packages/workspace/src/generators/new/generate-preset.ts +++ b/packages/workspace/src/generators/new/generate-preset.ts @@ -13,7 +13,8 @@ import { getNpmPackageVersion } from '../utils/get-npm-package-version'; import { NormalizedSchema } from './new'; import { join } from 'path'; import * as yargsParser from 'yargs-parser'; -import { spawn, SpawnOptions } from 'child_process'; +import { fork, ForkOptions } from 'child_process'; +import { getNxRequirePaths } from 'nx/src/utils/installation-directory'; export function addPresetDependencies(host: Tree, options: NormalizedSchema) { const { dependencies, dev } = getPresetDependencies(options); @@ -32,25 +33,34 @@ export function generatePreset(host: Tree, opts: NormalizedSchema) { interactive: true, }, }); - const spawnOptions: SpawnOptions = { + + const newWorkspaceRoot = join(host.root, opts.directory); + const forkOptions: ForkOptions = { stdio: 'inherit', - shell: true, - cwd: join(host.root, opts.directory), - windowsHide: false, + cwd: newWorkspaceRoot, }; const pmc = getPackageManagerCommand(); - const executable = `${pmc.exec} nx`; + const nxInstallationPaths = getNxRequirePaths(newWorkspaceRoot); + const nxBinForNewWorkspaceRoot = require.resolve('nx/bin/nx', { + paths: nxInstallationPaths, + }); const args = getPresetArgs(opts); return new Promise((resolve, reject) => { - spawn(executable, args, spawnOptions).on('close', (code: number) => { - if (code === 0) { - resolve(); - } else { - const message = 'Workspace creation failed, see above.'; - reject(new Error(message)); + // This needs to be `fork` instead of `spawn` because `spawn` is failing on Windows with pnpm + yarn + // The root cause is unclear. Spawn causes the `@nx/workspace:preset` generator to be called twice + // and the second time it fails with `Project {projectName} already exists.` + fork(nxBinForNewWorkspaceRoot, args, forkOptions).on( + 'close', + (code: number) => { + if (code === 0) { + resolve(); + } else { + const message = 'Workspace creation failed, see above.'; + reject(new Error(message)); + } } - }); + ); }); function getPresetArgs(options: NormalizedSchema) { diff --git a/packages/workspace/src/generators/preset/__snapshots__/preset.spec.ts.snap b/packages/workspace/src/generators/preset/__snapshots__/preset.spec.ts.snap deleted file mode 100644 index 080376656a4ea..0000000000000 --- a/packages/workspace/src/generators/preset/__snapshots__/preset.spec.ts.snap +++ /dev/null @@ -1,252 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`preset should create files (preset = angular-monorepo) 1`] = ` -[ - "project.json", - "src", - "tsconfig.app.json", - "tsconfig.editor.json", - "tsconfig.json", - "public", - ".eslintrc.json", - "jest.config.ts", - "tsconfig.spec.json", -] -`; - -exports[`preset should create files (preset = angular-monorepo) 2`] = ` -[ - "index.html", - "styles.css", - "app", - "main.ts", - "test-setup.ts", -] -`; - -exports[`preset should create files (preset = angular-monorepo) 3`] = ` -[ - "app.component.css", - "app.component.html", - "app.component.spec.ts", - "app.component.ts", - "app.module.ts", - "nx-welcome.component.ts", -] -`; - -exports[`preset should create files (preset = react-monorepo) 1`] = ` -"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); -const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin'); -const { join } = require('path'); - -module.exports = { - output: { - path: join(__dirname, '../../dist/apps/proj'), - }, - devServer: { - port: 4200, - historyApiFallback: { - index: '/index.html', - disableDotRule: true, - htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], - }, - }, - plugins: [ - new NxAppWebpackPlugin({ - tsConfig: './tsconfig.app.json', - compiler: 'babel', - main: './src/main.tsx', - index: './src/index.html', - baseHref: '/', - assets: ['./src/favicon.ico', './src/assets'], - styles: ['./src/styles.css'], - outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none', - optimization: process.env['NODE_ENV'] === 'production', - }), - new NxReactWebpackPlugin({ - // Uncomment this line if you don't want to use SVGR - // See: https://react-svgr.com/ - // svgr: false - }), - ], -}; -" -`; - -exports[`preset should create files (preset = react-standalone bundler = vite) 1`] = ` -"/// -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig({ - root: __dirname, - cacheDir: './node_modules/.vite/proj', - server: { - port: 4200, - host: 'localhost', - }, - preview: { - port: 4300, - host: 'localhost', - }, - plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - build: { - outDir: './dist/proj', - emptyOutDir: true, - reportCompressedSize: true, - commonjsOptions: { - transformMixedEsModules: true, - }, - }, - test: { - watch: false, - globals: true, - environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['default'], - coverage: { - reportsDirectory: './coverage/proj', - provider: 'v8', - }, - }, -}); -" -`; - -exports[`preset should create files (preset = react-standalone bundler = webpack) 1`] = ` -"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); -const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin'); -const { join } = require('path'); - -module.exports = { - output: { - path: join(__dirname, './dist/proj'), - }, - devServer: { - port: 4200, - historyApiFallback: { - index: '/index.html', - disableDotRule: true, - htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], - }, - }, - plugins: [ - new NxAppWebpackPlugin({ - tsConfig: './tsconfig.app.json', - compiler: 'babel', - main: './src/main.tsx', - index: './src/index.html', - baseHref: '/', - assets: ['./src/favicon.ico', './src/assets'], - styles: ['./src/styles.css'], - outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none', - optimization: process.env['NODE_ENV'] === 'production', - }), - new NxReactWebpackPlugin({ - // Uncomment this line if you don't want to use SVGR - // See: https://react-svgr.com/ - // svgr: false - }), - ], -}; -" -`; - -exports[`preset should create files (preset = vue-monorepo) 1`] = ` -"/// -import { defineConfig } from 'vite'; -import vue from '@vitejs/plugin-vue'; -import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig({ - root: __dirname, - cacheDir: '../../node_modules/.vite/apps/proj', - server: { - port: 4200, - host: 'localhost', - }, - preview: { - port: 4300, - host: 'localhost', - }, - plugins: [vue(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - build: { - outDir: '../../dist/apps/proj', - emptyOutDir: true, - reportCompressedSize: true, - commonjsOptions: { - transformMixedEsModules: true, - }, - }, - test: { - watch: false, - globals: true, - environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['default'], - coverage: { - reportsDirectory: '../../coverage/apps/proj', - provider: 'v8', - }, - }, -}); -" -`; - -exports[`preset should create files (preset = vue-standalone) 1`] = ` -"/// -import { defineConfig } from 'vite'; -import vue from '@vitejs/plugin-vue'; -import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig({ - root: __dirname, - cacheDir: './node_modules/.vite/proj', - server: { - port: 4200, - host: 'localhost', - }, - preview: { - port: 4300, - host: 'localhost', - }, - plugins: [vue(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - build: { - outDir: './dist/proj', - emptyOutDir: true, - reportCompressedSize: true, - commonjsOptions: { - transformMixedEsModules: true, - }, - }, - test: { - watch: false, - globals: true, - environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['default'], - coverage: { - reportsDirectory: './coverage/proj', - provider: 'v8', - }, - }, -}); -" -`; diff --git a/packages/workspace/src/generators/preset/preset.spec.ts b/packages/workspace/src/generators/preset/preset.spec.ts index 78eb588458e32..06fbeceb99a13 100644 --- a/packages/workspace/src/generators/preset/preset.spec.ts +++ b/packages/workspace/src/generators/preset/preset.spec.ts @@ -12,133 +12,382 @@ describe('preset', () => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); }); - it(`should create files (preset = ${Preset.AngularMonorepo})`, async () => { + it(`should create files (preset = angular-monorepo)`, async () => { + const name = `angular-preset-monorepo`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.AngularMonorepo, style: 'css', linter: 'eslint', }); - expect(tree.children('apps/proj')).toMatchSnapshot(); - expect(tree.children('apps/proj/src/')).toMatchSnapshot(); - expect(tree.children('apps/proj/src/app')).toMatchSnapshot(); - }, 20000); + expect(tree.children(`apps/${name}`)).toMatchInlineSnapshot(` + [ + "project.json", + "src", + "tsconfig.app.json", + "tsconfig.editor.json", + "tsconfig.json", + "public", + ".eslintrc.json", + "jest.config.ts", + "tsconfig.spec.json", + ] + `); + expect(tree.children(`apps/${name}/src/`)).toMatchInlineSnapshot(` + [ + "index.html", + "styles.css", + "app", + "main.ts", + "test-setup.ts", + ] + `); + expect(tree.children(`apps/${name}/src/app`)).toMatchInlineSnapshot(` + [ + "app.component.css", + "app.component.html", + "app.component.spec.ts", + "app.component.ts", + "app.module.ts", + "nx-welcome.component.ts", + ] + `); + }, 60_000); - it(`should create files (preset = ${Preset.WebComponents})`, async () => { + it(`should create files (preset = web-components)`, async () => { + const name = `webcomponents-preset`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.WebComponents, }); - expect(tree.exists('/apps/proj/src/main.ts')).toBe(true); + expect(tree.exists(`/apps/${name}/src/main.ts`)).toBe(true); }); - it(`should create files (preset = ${Preset.ReactMonorepo})`, async () => { + it(`should create files (preset = react-monorepo)`, async () => { + const name = `react-preset-monorepo`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.ReactMonorepo, style: 'css', linter: 'eslint', }); - expect(tree.exists('/apps/proj/src/main.tsx')).toBe(true); - expect(tree.read('apps/proj/webpack.config.js', 'utf-8')).toMatchSnapshot(); + expect(tree.exists(`/apps/${name}/src/main.tsx`)).toBe(true); + expect(tree.read(`apps/${name}/webpack.config.js`, 'utf-8')) + .toMatchInlineSnapshot(` + "const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); + const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin'); + const { join } = require('path'); + + module.exports = { + output: { + path: join(__dirname, '../../dist/apps/react-preset-monorepo'), + }, + devServer: { + port: 4200, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + plugins: [ + new NxAppWebpackPlugin({ + tsConfig: './tsconfig.app.json', + compiler: 'babel', + main: './src/main.tsx', + index: './src/index.html', + baseHref: '/', + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.css'], + outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none', + optimization: process.env['NODE_ENV'] === 'production', + }), + new NxReactWebpackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + ], + }; + " + `); }); - it(`should create files (preset = ${Preset.VueMonorepo})`, async () => { + it(`should create files (preset = vue-monorepo)`, async () => { + const name = `vue-preset-monorepo`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.VueMonorepo, style: 'css', linter: 'eslint', }); - expect(tree.exists('apps/proj/src/main.ts')).toBe(true); - expect(tree.read('apps/proj/vite.config.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.exists(`apps/${name}/src/main.ts`)).toBe(true); + expect(tree.read(`apps/${name}/vite.config.ts`, 'utf-8')) + .toMatchInlineSnapshot(` + "/// + import { defineConfig } from 'vite'; + import vue from '@vitejs/plugin-vue'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + + export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/vue-preset-monorepo', + server: { + port: 4200, + host: 'localhost', + }, + preview: { + port: 4300, + host: 'localhost', + }, + plugins: [vue(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + build: { + outDir: '../../dist/apps/vue-preset-monorepo', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/vue-preset-monorepo', + provider: 'v8', + }, + }, + }); + " + `); }); - it(`should create files (preset = ${Preset.Nuxt})`, async () => { + it(`should create files (preset = nuxt)`, async () => { + const name = `nuxt-preset`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.Nuxt, style: 'css', linter: 'eslint', }); - expect(tree.exists('apps/proj/src/app.vue')).toBe(true); - expect(readProjectConfiguration(tree, 'proj')).toBeDefined(); + expect(tree.exists(`apps/${name}/src/app.vue`)).toBe(true); + expect(readProjectConfiguration(tree, name)).toBeDefined(); }); - it(`should create files (preset = ${Preset.NextJs})`, async () => { + it(`should create files (preset = next)`, async () => { + const name = `next-preset`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.NextJs, style: 'css', linter: 'eslint', }); - expect(tree.exists('/apps/proj/src/app/page.tsx')).toBe(true); + expect(tree.exists(`/apps/${name}/src/app/page.tsx`)).toBe(true); }); - it(`should create files (preset = ${Preset.Express})`, async () => { + it(`should create files (preset = express)`, async () => { + const name = `express-preset`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.Express, linter: 'eslint', }); - expect(tree.exists('apps/proj/src/main.ts')).toBe(true); - expect(tree.exists('apps/proj/.eslintrc.json')).toBe(true); + expect(tree.exists(`apps/${name}/src/main.ts`)).toBe(true); + expect(tree.exists(`apps/${name}/.eslintrc.json`)).toBe(true); }); it('should create files (preset = react-native)', async () => { + const name = `react-native-preset`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.ReactNative, linter: 'eslint', }); - expect(tree.exists('/apps/proj/src/app/App.tsx')).toBe(true); + expect(tree.exists(`/apps/${name}/src/app/App.tsx`)).toBe(true); }); - it(`should create files (preset = ${Preset.ReactStandalone} bundler = webpack)`, async () => { + it(`should create files (preset = react-standalone & bundler = webpack)`, async () => { + const name = `react-standalone-preset-webpack`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.ReactStandalone, style: 'css', linter: 'eslint', bundler: 'webpack', }); expect(tree.exists('webpack.config.js')).toBe(true); - expect(tree.read('webpack.config.js', 'utf-8')).toMatchSnapshot(); + expect(tree.read('webpack.config.js', 'utf-8')).toMatchInlineSnapshot(` + "const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); + const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin'); + const { join } = require('path'); + + module.exports = { + output: { + path: join(__dirname, './dist/react-standalone-preset-webpack'), + }, + devServer: { + port: 4200, + historyApiFallback: { + index: '/index.html', + disableDotRule: true, + htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + }, + }, + plugins: [ + new NxAppWebpackPlugin({ + tsConfig: './tsconfig.app.json', + compiler: 'babel', + main: './src/main.tsx', + index: './src/index.html', + baseHref: '/', + assets: ['./src/favicon.ico', './src/assets'], + styles: ['./src/styles.css'], + outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none', + optimization: process.env['NODE_ENV'] === 'production', + }), + new NxReactWebpackPlugin({ + // Uncomment this line if you don't want to use SVGR + // See: https://react-svgr.com/ + // svgr: false + }), + ], + }; + " + `); }); - it(`should create files (preset = ${Preset.ReactStandalone} bundler = vite)`, async () => { + it(`should create files (preset = react-standalone & bundler = vite)`, async () => { + const name = `react-standalone-preset-vite`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.ReactStandalone, style: 'css', linter: 'eslint', bundler: 'vite', }); expect(tree.exists('vite.config.ts')).toBe(true); - expect(tree.read('vite.config.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.read('vite.config.ts', 'utf-8')).toMatchInlineSnapshot(` + "/// + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + + export default defineConfig({ + root: __dirname, + cacheDir: './node_modules/.vite/react-standalone-preset-vite', + server: { + port: 4200, + host: 'localhost', + }, + preview: { + port: 4300, + host: 'localhost', + }, + plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + build: { + outDir: './dist/react-standalone-preset-vite', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './coverage/react-standalone-preset-vite', + provider: 'v8', + }, + }, + }); + " + `); }); - it(`should create files (preset = ${Preset.VueStandalone})`, async () => { + it(`should create files (preset = vue-standalone)`, async () => { + const name = `vue-standalone-preset`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.VueStandalone, style: 'css', e2eTestRunner: 'cypress', }); expect(tree.exists('vite.config.ts')).toBe(true); - expect(tree.read('vite.config.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.read('vite.config.ts', 'utf-8')).toMatchInlineSnapshot(` + "/// + import { defineConfig } from 'vite'; + import vue from '@vitejs/plugin-vue'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + + export default defineConfig({ + root: __dirname, + cacheDir: './node_modules/.vite/vue-standalone-preset', + server: { + port: 4200, + host: 'localhost', + }, + preview: { + port: 4300, + host: 'localhost', + }, + plugins: [vue(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + build: { + outDir: './dist/vue-standalone-preset', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './coverage/vue-standalone-preset', + provider: 'v8', + }, + }, + }); + " + `); }); - it(`should create files (preset = ${Preset.NuxtStandalone})`, async () => { + it(`should create files (preset = nuxt-standalone)`, async () => { + const name = `nuxt-standalone-preset`; await presetGenerator(tree, { - name: 'proj', + name, preset: Preset.NuxtStandalone, style: 'css', e2eTestRunner: 'cypress', }); expect(tree.exists('nuxt.config.ts')).toBe(true); - expect(readProjectConfiguration(tree, 'proj')).toBeDefined(); + expect(readProjectConfiguration(tree, name)).toBeDefined(); }); });