From 9e87815c94e748d316a390ed19528b3652b8288a Mon Sep 17 00:00:00 2001 From: Travis Tarr Date: Mon, 30 Oct 2023 08:05:47 -0700 Subject: [PATCH] feat(rspack): add generatePackageJson plugin (#341) Co-authored-by: Travis Tarr --- e2e/rspack-e2e/tests/rspack.spec.ts | 19 ++++ package.json | 1 + .../rspack/src/executors/rspack/schema.d.ts | 4 +- .../rspack/src/executors/rspack/schema.json | 8 ++ .../plugins/generate-package-json-plugin.ts | 87 +++++++++++++++++++ packages/rspack/src/utils/with-nx.ts | 31 +++++-- yarn.lock | 14 +++ 7 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 packages/rspack/src/plugins/generate-package-json-plugin.ts diff --git a/e2e/rspack-e2e/tests/rspack.spec.ts b/e2e/rspack-e2e/tests/rspack.spec.ts index 3f18dc24f..6870579ec 100644 --- a/e2e/rspack-e2e/tests/rspack.spec.ts +++ b/e2e/rspack-e2e/tests/rspack.spec.ts @@ -123,5 +123,24 @@ describe('rspack e2e', () => { result = await runNxCommandAsync(`e2e ${app2}-e2e`); expect(result.stdout).toContain('Successfully ran target e2e'); + + // Generate a Nest app and verify build output + const app3 = uniq('app3'); + await runNxCommandAsync( + `generate @nx/rspack:app ${app3} --framework=nest --unitTestRunner=jest` + ); + checkFilesExist(`${app3}/project.json`); + + result = await runNxCommandAsync(`build ${app3}`); + expect(result.stdout).toContain('Successfully ran target build'); + // Make sure expected files are present. + expect(listFiles(`dist/${app3}`)).toHaveLength(3); + + result = await runNxCommandAsync( + `build ${app3} --generatePackageJson=true` + ); + expect(result.stdout).toContain('Successfully ran target build'); + // Make sure expected files are present. + expect(listFiles(`dist/${app3}`)).toHaveLength(5); }, 200_000); }); diff --git a/package.json b/package.json index 1a78dba69..a1d5f9f0b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@types/fs-extra": "^11.0.1", "@types/jest": "29.4.0", "@types/node": "18.11.9", + "@types/webpack-sources": "^3.2.1", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", "chalk": "4.1.2", diff --git a/packages/rspack/src/executors/rspack/schema.d.ts b/packages/rspack/src/executors/rspack/schema.d.ts index 0c6455746..caa5d27ab 100644 --- a/packages/rspack/src/executors/rspack/schema.d.ts +++ b/packages/rspack/src/executors/rspack/schema.d.ts @@ -5,6 +5,7 @@ export interface RspackExecutorSchema { tsConfig: string; typeCheck?: boolean; outputPath: string; + outputFileName?: string; indexHtml?: string; mode?: Mode; watch?: boolean; @@ -15,9 +16,10 @@ export interface RspackExecutorSchema { assets?: any[]; extractLicenses?: boolean; fileReplacements?: FileReplacement[]; + generatePackageJson?: boolean; } export interface FileReplacement { replace: string; with: string; -} \ No newline at end of file +} diff --git a/packages/rspack/src/executors/rspack/schema.json b/packages/rspack/src/executors/rspack/schema.json index 85d4a5da3..31fc3499f 100644 --- a/packages/rspack/src/executors/rspack/schema.json +++ b/packages/rspack/src/executors/rspack/schema.json @@ -18,6 +18,10 @@ "type": "string", "description": "The output path for the bundle." }, + "outputFileName": { + "type": "string", + "description": "The main output entry file" + }, "tsConfig": { "type": "string", "description": "The tsconfig file to build the project." @@ -89,6 +93,10 @@ "type": "string", "description": "Mode to run the build in.", "enum": ["development", "production", "none"] + }, + "generatePackageJson": { + "type": "boolean", + "description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated." } }, "required": ["target", "main", "outputPath", "tsConfig", "rspackConfig"], diff --git a/packages/rspack/src/plugins/generate-package-json-plugin.ts b/packages/rspack/src/plugins/generate-package-json-plugin.ts new file mode 100644 index 000000000..51454264d --- /dev/null +++ b/packages/rspack/src/plugins/generate-package-json-plugin.ts @@ -0,0 +1,87 @@ +import { + ExecutorContext, + detectPackageManager, + serializeJson, + type ProjectGraph, +} from '@nx/devkit'; +import { + HelperDependency, + createLockFile, + createPackageJson, + getHelperDependenciesFromProjectGraph, + getLockFileName, + readTsConfig, +} from '@nx/js'; +import { type Compiler, type RspackPluginInstance } from '@rspack/core'; +import { RawSource } from 'webpack-sources'; + +const pluginName = 'GeneratePackageJsonPlugin'; + +export class GeneratePackageJsonPlugin implements RspackPluginInstance { + private readonly projectGraph: ProjectGraph; + + constructor( + private readonly options: { tsConfig: string; outputFileName: string }, + private readonly context: ExecutorContext + ) { + this.projectGraph = context.projectGraph; + } + + apply(compiler: Compiler): void { + compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { + compilation.hooks.processAssets.tap( + { + name: pluginName, + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, + }, + () => { + const helperDependencies = getHelperDependenciesFromProjectGraph( + this.context.root, + this.context.projectName, + this.projectGraph + ); + + const importHelpers = !!readTsConfig(this.options.tsConfig).options + .importHelpers; + const shouldAddHelperDependency = + importHelpers && + helperDependencies.every( + (dep) => dep.target !== HelperDependency.tsc + ); + + if (shouldAddHelperDependency) { + helperDependencies.push({ + type: 'static', + source: this.context.projectName, + target: HelperDependency.tsc, + }); + } + + const packageJson = createPackageJson( + this.context.projectName, + this.projectGraph, + { + target: this.context.targetName, + root: this.context.root, + isProduction: true, + helperDependencies: helperDependencies.map((dep) => dep.target), + } + ); + packageJson.main = packageJson.main ?? this.options.outputFileName; + + compilation.emitAsset( + 'package.json', + new RawSource(serializeJson(packageJson)) + ); + const packageManager = detectPackageManager(this.context.root); + compilation.emitAsset( + getLockFileName(packageManager), + new RawSource( + createLockFile(packageJson, this.projectGraph, packageManager) + ) + ); + } + ); + }); + } +} diff --git a/packages/rspack/src/utils/with-nx.ts b/packages/rspack/src/utils/with-nx.ts index 3c1ec6c66..b0c7e7867 100644 --- a/packages/rspack/src/utils/with-nx.ts +++ b/packages/rspack/src/utils/with-nx.ts @@ -1,9 +1,15 @@ -import { Configuration, ExternalItem, RspackPluginInstance, ResolveAlias } from '@rspack/core'; +import { + Configuration, + ExternalItem, + ResolveAlias, + RspackPluginInstance, +} from '@rspack/core'; +import { LicenseWebpackPlugin } from 'license-webpack-plugin'; import * as path from 'path'; +import { GeneratePackageJsonPlugin } from '../plugins/generate-package-json-plugin'; import { getCopyPatterns } from './get-copy-patterns'; import { SharedConfigContext } from './model'; import { normalizeAssets } from './normalize-assets'; -import { LicenseWebpackPlugin } from 'license-webpack-plugin'; export function withNx(_opts = {}) { return function makeConfig( @@ -37,10 +43,25 @@ export function withNx(_opts = {}) { ); } - options.fileReplacements.forEach(item => { + if (options.generatePackageJson) { + const mainOutputFile = + options.main.split('/').pop().split('.')[0] + '.js'; + + plugins.push( + new GeneratePackageJsonPlugin( + { + tsConfig: options.tsConfig, + outputFileName: options.outputFileName ?? mainOutputFile, + }, + context + ) + ); + } + + options.fileReplacements.forEach((item) => { alias[item.replace] = item.with; - }) - + }); + const externals: ExternalItem = {}; let externalsType: Configuration['externalsType']; if (options.target === 'node') { diff --git a/yarn.lock b/yarn.lock index be5a3295f..532f002ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4202,6 +4202,11 @@ dependencies: "@types/node" "*" +"@types/source-list-map@*": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.3.tgz#077e15c87fe06520e30396a533bd9848e735ce9b" + integrity sha512-I9R/7fUjzUOyDy6AFkehCK711wWoAXEaBi80AfjZt1lIkbe6AcXKd3ckQc3liMvQExWvfOeh/8CtKzrfUFN5gA== + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" @@ -4217,6 +4222,15 @@ resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/webpack-sources@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.1.tgz#98670b35fa799c44ac235910f3fda9bfdcdbc2c6" + integrity sha512-iLC3Fsx62ejm3ST3PQ8vBMC54Rb3EoCprZjeJGI5q+9QjfDLGt9jeg/k245qz1G9AQnORGk0vqPicJFPT1QODQ== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.7.3" + "@types/ws@^8.5.1": version "8.5.4" resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5"