diff --git a/packages/rspack/package.json b/packages/rspack/package.json index 12e6f7fed0622..3b0de993af8ed 100644 --- a/packages/rspack/package.json +++ b/packages/rspack/package.json @@ -32,22 +32,25 @@ "@rspack/dev-server": "^1.0.4", "@rspack/plugin-react-refresh": "^1.0.0", "autoprefixer": "^10.4.9", + "browserslist": "^4.21.4", "chalk": "~4.1.0", "css-loader": "^6.4.0", "enquirer": "~2.3.6", "express": "^4.19.2", + "fork-ts-checker-webpack-plugin": "7.2.13", "http-proxy-middleware": "^3.0.3", "less-loader": "11.1.0", "license-webpack-plugin": "^4.0.2", "loader-utils": "^2.0.3", "sass": "^1.42.1", "sass-loader": "^12.2.0", + "source-map-loader": "^5.0.0", "style-loader": "^3.3.0", "postcss-import": "~14.1.0", "postcss-loader": "^8.1.1", "postcss": "^8.4.38", - "tsconfig-paths": "^4.1.2", "tslib": "^2.3.0", + "webpack-node-externals": "^3.0.0", "webpack-subresource-integrity": "^5.1.0" }, "peerDependencies": { diff --git a/packages/rspack/src/plugins/utils/apply-base-config.ts b/packages/rspack/src/plugins/utils/apply-base-config.ts new file mode 100644 index 0000000000000..93eb651aaf2aa --- /dev/null +++ b/packages/rspack/src/plugins/utils/apply-base-config.ts @@ -0,0 +1,418 @@ +import * as path from 'path'; +import { type ExecutorContext } from '@nx/devkit'; +import { LicenseWebpackPlugin } from 'license-webpack-plugin'; +import { + Configuration, + ProgressPlugin, + RspackPluginInstance, + SwcJsMinimizerRspackPlugin, + CopyRspackPlugin, + RspackOptionsNormalized, + ExternalItem, +} from '@rspack/core'; +import { getRootTsConfigPath } from '@nx/js'; + +import { StatsJsonPlugin } from './plugins/stats-json-plugin'; +import { GeneratePackageJsonPlugin } from './plugins/generate-package-json-plugin'; +import { getOutputHashFormat } from './hash-format'; +import { NxTsconfigPathsRspackPlugin } from './plugins/nx-tsconfig-paths-rspack-plugin'; +import { getTerserEcmaVersion } from './get-terser-ecma-version'; +import nodeExternals = require('webpack-node-externals'); +import { NormalizedNxAppRspackPluginOptions } from './models'; + +const IGNORED_WEBPACK_WARNINGS = [ + /The comment file/i, + /could not find any license/i, +]; + +const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx']; +const mainFields = ['module', 'main']; + +export function applyBaseConfig( + options: NormalizedNxAppRspackPluginOptions, + config: Partial = {}, + { + useNormalizedEntry, + }: { + // rspack.Configuration allows arrays to be set on a single entry + // rspack then normalizes them to { import: "..." } objects + // This option allows use to preserve existing composePlugins behavior where entry.main is an array. + useNormalizedEntry?: boolean; + } = {} +): void { + // Defaults that was applied from executor schema previously. + options.deleteOutputPath ??= true; + options.externalDependencies ??= 'all'; + options.fileReplacements ??= []; + options.memoryLimit ??= 2048; + options.transformers ??= []; + + applyNxIndependentConfig(options, config); + + // Some of the options only work during actual tasks, not when reading the rspack config during CreateNodes. + if (global.NX_GRAPH_CREATION) return; + + applyNxDependentConfig(options, config, { useNormalizedEntry }); +} + +function applyNxIndependentConfig( + options: NormalizedNxAppRspackPluginOptions, + config: Partial +): void { + const hashFormat = getOutputHashFormat(options.outputHashing as string); + config.context = path.join(options.root, options.projectRoot); + config.target ??= options.target as 'node' | 'web'; + config.node = false; + config.mode = + // When the target is Node avoid any optimizations, such as replacing `process.env.NODE_ENV` with build time value. + config.target === 'node' + ? 'none' + : // Otherwise, make sure it matches `process.env.NODE_ENV`. + // When mode is development or production, rspack will automatically + // configure DefinePlugin to replace `process.env.NODE_ENV` with the + // build-time value. Thus, we need to make sure it's the same value to + // avoid conflicts. + // + // When the NODE_ENV is something else (e.g. test), then set it to none + // to prevent extra behavior from rspack. + process.env.NODE_ENV === 'development' || + process.env.NODE_ENV === 'production' + ? (process.env.NODE_ENV as 'development' | 'production') + : 'none'; + // When target is Node, the Webpack mode will be set to 'none' which disables in memory caching and causes a full rebuild on every change. + // So to mitigate this we enable in memory caching when target is Node and in watch mode. + config.cache = options.target === 'node' && options.watch ? true : undefined; + + config.devtool = + options.sourceMap === 'hidden' + ? 'hidden-source-map' + : options.sourceMap + ? 'source-map' + : false; + + config.output = { + ...(config.output ?? {}), + libraryTarget: + (config as Configuration).output?.libraryTarget ?? + (options.target === 'node' ? 'commonjs' : undefined), + path: + config.output?.path ?? + (options.outputPath + ? // If path is relative, it is relative from project root (aka cwd). + // Otherwise, it is relative to workspace root (legacy behavior). + options.outputPath.startsWith('.') + ? path.join(options.root, options.projectRoot, options.outputPath) + : path.join(options.root, options.outputPath) + : undefined), + filename: + config.output?.filename ?? + (options.outputHashing ? `[name]${hashFormat.script}.js` : '[name].js'), + chunkFilename: + config.output?.chunkFilename ?? + (options.outputHashing ? `[name]${hashFormat.chunk}.js` : '[name].js'), + hashFunction: config.output?.hashFunction ?? 'xxhash64', + // Disabled for performance + pathinfo: config.output?.pathinfo ?? false, + // Use CJS for Node since it has the widest support. + scriptType: + config.output?.scriptType ?? + (options.target === 'node' ? undefined : 'module'), + }; + + config.watch = options.watch; + + config.watchOptions = { + poll: options.poll, + }; + + config.profile = options.statsJson; + + config.performance = { + ...config.performance, + hints: false, + }; + + config.ignoreWarnings = [ + (x) => + IGNORED_WEBPACK_WARNINGS.some((r) => + typeof x === 'string' ? r.test(x) : r.test(x.message) + ), + ]; + + config.optimization = { + ...(config.optimization ?? {}), + sideEffects: true, + minimize: + typeof options.optimization === 'object' + ? !!options.optimization.scripts + : !!options.optimization, + minimizer: [ + new SwcJsMinimizerRspackPlugin({ + extractComments: false, + minimizerOptions: { + module: true, + mangle: { + keep_classnames: true, + }, + format: { + ecma: getTerserEcmaVersion( + path.join(options.root, options.projectRoot) + ), + ascii_only: true, + comments: false, + webkit: true, + safari10: true, + }, + }, + }), + ], + runtimeChunk: false, + concatenateModules: true, + }; + + config.stats = { + hash: true, + timings: false, + cached: false, + cachedAssets: false, + modules: false, + warnings: true, + errors: true, + colors: !options.verbose && !options.statsJson, + chunks: !options.verbose, + assets: !!options.verbose, + chunkOrigins: !!options.verbose, + chunkModules: !!options.verbose, + children: !!options.verbose, + reasons: !!options.verbose, + version: !!options.verbose, + errorDetails: !!options.verbose, + moduleTrace: !!options.verbose, + usedExports: !!options.verbose, + }; + + /** + * Initialize properties that get set when rspack is used during task execution. + * These properties may be used by consumers who expect them to not be undefined. + * + * When @nx/rspack/plugin resolves the config, it is not during a task, and therefore + * these values are not set, which can lead to errors being thrown when reading + * the rspack options from the resolved file. + */ + config.entry ??= {}; + config.resolve ??= {}; + config.module ??= {}; + config.plugins ??= []; + config.externals ??= []; +} + +function applyNxDependentConfig( + options: NormalizedNxAppRspackPluginOptions, + config: Partial, + { useNormalizedEntry }: { useNormalizedEntry?: boolean } = {} +): void { + const tsConfig = options.tsConfig ?? getRootTsConfigPath(); + const plugins: RspackPluginInstance[] = []; + + const executorContext: Partial = { + projectName: options.projectName, + targetName: options.targetName, + projectGraph: options.projectGraph, + configurationName: options.configurationName, + root: options.root, + }; + + plugins.push(new NxTsconfigPathsRspackPlugin({ ...options, tsConfig })); + + if (!options?.skipTypeChecking) { + const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); + plugins.push( + new ForkTsCheckerWebpackPlugin({ + typescript: { + configFile: path.isAbsolute(tsConfig) + ? tsConfig + : path.join(options.root, tsConfig), + memoryLimit: options.memoryLimit || 2018, + }, + }) + ); + } + const entries: Array<{ name: string; import: string[] }> = []; + + if (options.main) { + const mainEntry = options.outputFileName + ? path.parse(options.outputFileName).name + : 'main'; + entries.push({ + name: mainEntry, + import: [path.resolve(options.root, options.main)], + }); + } + + if (options.additionalEntryPoints) { + for (const { entryName, entryPath } of options.additionalEntryPoints) { + entries.push({ + name: entryName, + import: [path.resolve(options.root, entryPath)], + }); + } + } + + if (options.polyfills) { + entries.push({ + name: 'polyfills', + import: [path.resolve(options.root, options.polyfills)], + }); + } + + config.entry ??= {}; + entries.forEach((entry) => { + if (useNormalizedEntry) { + config.entry[entry.name] = { import: entry.import }; + } else { + config.entry[entry.name] = entry.import; + } + }); + + if (options.progress) { + plugins.push(new ProgressPlugin({ profile: options.verbose })); + } + + if (options.extractLicenses) { + plugins.push( + new LicenseWebpackPlugin({ + stats: { + warnings: false, + errors: false, + }, + perChunkOutput: false, + outputFilename: `3rdpartylicenses.txt`, + }) as unknown as RspackPluginInstance + ); + } + + if (Array.isArray(options.assets) && options.assets.length > 0) { + plugins.push( + new CopyRspackPlugin({ + patterns: options.assets.map((asset) => { + return { + context: asset.input, + // Now we remove starting slash to make Webpack place it from the output root. + to: asset.output, + from: asset.glob, + globOptions: { + ignore: [ + '.gitkeep', + '**/.DS_Store', + '**/Thumbs.db', + ...(asset.ignore ?? []), + ], + dot: true, + }, + }; + }), + }) + ); + } + if (options.generatePackageJson && executorContext) { + plugins.push(new GeneratePackageJsonPlugin({ ...options, tsConfig })); + } + + if (options.statsJson) { + plugins.push(new StatsJsonPlugin()); + } + + const externals = []; + if (options.target === 'node' && options.externalDependencies === 'all') { + const modulesDir = `${options.root}/node_modules`; + externals.push(nodeExternals({ modulesDir })); + } else if (Array.isArray(options.externalDependencies)) { + externals.push(function (ctx, callback: Function) { + if (options.externalDependencies.includes(ctx.request)) { + // not bundled + return callback(null, `commonjs ${ctx.request}`); + } + // bundled + callback(); + }); + } + + config.resolve = { + ...config.resolve, + extensions: [...(config?.resolve?.extensions ?? []), ...extensions], + alias: { + ...(config.resolve?.alias ?? {}), + ...(options.fileReplacements?.reduce( + (aliases, replacement) => ({ + ...aliases, + [replacement.replace]: replacement.with, + }), + {} + ) ?? {}), + }, + mainFields: config.resolve?.mainFields ?? mainFields, + }; + + config.externals = externals; + + // Enabled for performance + config.cache = true; + config.module = { + ...config.module, + rules: [ + ...(config?.module?.rules ?? []), + options.sourceMap && { + test: /\.js$/, + enforce: 'pre' as const, + loader: require.resolve('source-map-loader'), + }, + { + // There's an issue resolving paths without fully specified extensions + // See: https://github.com/graphql/graphql-js/issues/2721 + // TODO(jack): Add a flag to turn this option on like Next.js does via experimental flag. + // See: https://github.com/vercel/next.js/pull/29880 + test: /\.m?jsx?$/, + resolve: { + fullySpecified: false, + }, + }, + // There's an issue when using buildable libs and .js files (instead of .ts files), + // where the wrong type is used (commonjs vs esm) resulting in export-imports throwing errors. + // See: https://github.com/nrwl/nx/issues/10990 + { + test: /\.js$/, + type: 'javascript/auto', + }, + // Rspack's docs only suggest swc for TS compilation + //https://rspack.dev/guide/tech/typescript + { + test: /\.([jt])sx?$/, + loader: 'builtin:swc-loader', + exclude: /node_modules/, + options: { + jsc: { + parser: { + syntax: 'typescript', + decorators: true, + tsx: true, + }, + transform: { + react: { + pragma: 'React.createElement', + pragmaFrag: 'React.Fragment', + throwIfNamespace: true, + development: false, + useBuiltins: false, + }, + }, + type: 'javascript/auto', + loose: true, + }, + }, + }, + ].filter((r) => !!r), + }; + + config.plugins ??= []; + config.plugins.push(...plugins); +} diff --git a/packages/rspack/src/plugins/utils/get-terser-ecma-version.ts b/packages/rspack/src/plugins/utils/get-terser-ecma-version.ts new file mode 100644 index 0000000000000..1550020d5fb8e --- /dev/null +++ b/packages/rspack/src/plugins/utils/get-terser-ecma-version.ts @@ -0,0 +1,36 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import browserslist = require('browserslist'); + +const VALID_BROWSERSLIST_FILES = ['.browserslistrc', 'browserslist']; + +const ES5_BROWSERS = [ + 'ie 10', + 'ie 11', + 'safari 11', + 'safari 11.1', + 'safari 12', + 'safari 12.1', + 'safari 13', + 'ios_saf 13.0', + 'ios_saf 13.3', +]; + +export function getTerserEcmaVersion(projectRoot: string): 2020 | 5 { + let pathToBrowserslistFile = ''; + for (const browserslistFile of VALID_BROWSERSLIST_FILES) { + const fullPathToFile = path.join(projectRoot, browserslistFile); + if (fs.existsSync(fullPathToFile)) { + pathToBrowserslistFile = fullPathToFile; + break; + } + } + + if (!pathToBrowserslistFile) { + return 2020; + } + + const env = browserslist.loadConfig({ path: pathToBrowserslistFile }); + const browsers = browserslist(env); + return browsers.some((b) => ES5_BROWSERS.includes(b)) ? 5 : 2020; +} diff --git a/packages/rspack/src/plugins/utils/plugins/generate-package-json-plugin.ts b/packages/rspack/src/plugins/utils/plugins/generate-package-json-plugin.ts new file mode 100644 index 0000000000000..4ca6649c80bed --- /dev/null +++ b/packages/rspack/src/plugins/utils/plugins/generate-package-json-plugin.ts @@ -0,0 +1,97 @@ +import { + type Compiler, + sources, + type RspackPluginInstance, +} from '@rspack/core'; +import { + createLockFile, + createPackageJson, + getHelperDependenciesFromProjectGraph, + getLockFileName, + HelperDependency, + readTsConfig, +} from '@nx/js'; +import { + detectPackageManager, + type ProjectGraph, + serializeJson, +} from '@nx/devkit'; + +const pluginName = 'GeneratePackageJsonPlugin'; + +export class GeneratePackageJsonPlugin implements RspackPluginInstance { + constructor( + private readonly options: { + skipPackageManager?: boolean; + tsConfig: string; + outputFileName: string; + root: string; + projectName: string; + targetName: string; + projectGraph: ProjectGraph; + } + ) {} + + apply(compiler: Compiler): void { + compiler.hooks.thisCompilation.tap(pluginName, (compilation) => { + compilation.hooks.processAssets.tap( + { + name: pluginName, + stage: compiler.rspack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, + }, + () => { + const helperDependencies = getHelperDependenciesFromProjectGraph( + this.options.root, + this.options.projectName, + this.options.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.options.projectName, + target: HelperDependency.tsc, + }); + } + + const packageJson = createPackageJson( + this.options.projectName, + this.options.projectGraph, + { + target: this.options.targetName, + root: this.options.root, + isProduction: true, + helperDependencies: helperDependencies.map((dep) => dep.target), + skipPackageManager: this.options.skipPackageManager, + } + ); + packageJson.main = packageJson.main ?? this.options.outputFileName; + + compilation.emitAsset( + 'package.json', + new sources.RawSource(serializeJson(packageJson)) + ); + const packageManager = detectPackageManager(this.options.root); + compilation.emitAsset( + getLockFileName(packageManager), + new sources.RawSource( + createLockFile( + packageJson, + this.options.projectGraph, + packageManager + ) + ) + ); + } + ); + }); + } +} diff --git a/packages/rspack/src/plugins/utils/plugins/nx-tsconfig-paths-rspack-plugin.ts b/packages/rspack/src/plugins/utils/plugins/nx-tsconfig-paths-rspack-plugin.ts new file mode 100644 index 0000000000000..b2596b2dcab7b --- /dev/null +++ b/packages/rspack/src/plugins/utils/plugins/nx-tsconfig-paths-rspack-plugin.ts @@ -0,0 +1,78 @@ +import * as path from 'path'; +import { + Compiler, + type Configuration, + type RspackOptionsNormalized, +} from '@rspack/core'; +import { workspaceRoot } from '@nx/devkit'; +import { + calculateProjectBuildableDependencies, + createTmpTsConfig, +} from '@nx/js/src/utils/buildable-libs-utils'; +import { NormalizedNxAppRspackPluginOptions } from '../models'; +import { RspackNxBuildCoordinationPlugin } from './rspack-nx-build-coordination-plugin'; + +export class NxTsconfigPathsRspackPlugin { + constructor(private options: NormalizedNxAppRspackPluginOptions) { + if (!this.options.tsConfig) + throw new Error( + `Missing "tsConfig" option. Set this option in your Nx webpack plugin.` + ); + } + + apply(compiler: Compiler): void { + // If we are not building libs from source, we need to remap paths so tsconfig may be updated. + this.handleBuildLibsFromSource(compiler.options, this.options); + + const pathToTsconfig = !path.isAbsolute(this.options.tsConfig) + ? path.join(workspaceRoot, this.options.tsConfig) + : this.options.tsConfig; + + const extensions = new Set([ + ...['.ts', '.tsx', '.mjs', '.js', '.jsx'], + ...(compiler.options?.resolve?.extensions ?? []), + ]); + + compiler.options.resolve = { + ...compiler.options.resolve, + extensions: [...extensions], + tsConfig: pathToTsconfig, + }; + } + + handleBuildLibsFromSource( + config: Partial, + options + ): void { + if (!options.buildLibsFromSource && options.targetName) { + const remappedTarget = + options.targetName === 'serve' ? 'build' : options.targetName; + + const { target, dependencies } = calculateProjectBuildableDependencies( + undefined, + options.projectGraph, + options.root, + options.projectName, + remappedTarget, + options.configurationName + ); + options.tsConfig = createTmpTsConfig( + options.tsConfig, + options.root, + target.data.root, + dependencies + ); + + if (options.targetName === 'serve') { + const buildableDependencies = dependencies + .filter((dependency) => dependency.node.type === 'lib') + .map((dependency) => dependency.node.name) + .join(','); + + const buildCommand = `nx run-many --target=build --projects=${buildableDependencies}`; + + config.plugins.push(new RspackNxBuildCoordinationPlugin(buildCommand)); + } + } + } +} diff --git a/packages/rspack/src/plugins/utils/plugins/rspack-nx-build-coordination-plugin.ts b/packages/rspack/src/plugins/utils/plugins/rspack-nx-build-coordination-plugin.ts new file mode 100644 index 0000000000000..fb13e7589f520 --- /dev/null +++ b/packages/rspack/src/plugins/utils/plugins/rspack-nx-build-coordination-plugin.ts @@ -0,0 +1,105 @@ +import { exec } from 'child_process'; +import type { Compiler } from '@rspack/core'; +import { daemonClient, isDaemonEnabled } from 'nx/src/daemon/client/client'; +import { BatchFunctionRunner } from 'nx/src/command-line/watch/watch'; +import { output } from 'nx/src/utils/output'; + +export class RspackNxBuildCoordinationPlugin { + private currentlyRunning: 'none' | 'nx-build' | 'rspack-build' = 'none'; + private buildCmdProcess: ReturnType | null = null; + + constructor(private readonly buildCmd: string, skipInitialBuild?: boolean) { + if (!skipInitialBuild) { + this.buildChangedProjects(); + } + if (isDaemonEnabled()) { + this.startWatchingBuildableLibs(); + } else { + output.warn({ + title: + 'Nx Daemon is not enabled. Buildable libs will not be rebuilt on file changes.', + }); + } + } + + apply(compiler: Compiler) { + compiler.hooks.beforeCompile.tapPromise( + 'IncrementalDevServerPlugin', + async () => { + while (this.currentlyRunning === 'nx-build') { + await sleep(50); + } + this.currentlyRunning = 'rspack-build'; + } + ); + compiler.hooks.done.tapPromise('IncrementalDevServerPlugin', async () => { + this.currentlyRunning = 'none'; + }); + } + + async startWatchingBuildableLibs() { + const unregisterFileWatcher = await this.createFileWatcher(); + + process.on('exit', () => { + unregisterFileWatcher(); + }); + } + + async buildChangedProjects() { + while (this.currentlyRunning === 'rspack-build') { + await sleep(50); + } + this.currentlyRunning = 'nx-build'; + try { + return await new Promise((res) => { + this.buildCmdProcess = exec(this.buildCmd, { + windowsHide: false, + }); + + this.buildCmdProcess.stdout.pipe(process.stdout); + this.buildCmdProcess.stderr.pipe(process.stderr); + this.buildCmdProcess.on('exit', () => { + res(); + }); + this.buildCmdProcess.on('error', () => { + res(); + }); + }); + } finally { + this.currentlyRunning = 'none'; + this.buildCmdProcess = null; + } + } + + private createFileWatcher() { + const runner = new BatchFunctionRunner(() => this.buildChangedProjects()); + return daemonClient.registerFileWatcher( + { + watchProjects: 'all', + }, + (err, { changedProjects, changedFiles }) => { + if (err === 'closed') { + output.error({ + title: 'Watch connection closed', + bodyLines: [ + 'The daemon has closed the connection to this watch process.', + 'Please restart your watch command.', + ], + }); + process.exit(1); + } + + if (this.buildCmdProcess) { + this.buildCmdProcess.kill(2); + this.buildCmdProcess = null; + } + // Queue a build + runner.enqueue(changedProjects, changedFiles); + } + ); + } +} + +function sleep(time: number) { + return new Promise((resolve) => setTimeout(resolve, time)); +} diff --git a/packages/rspack/src/plugins/utils/plugins/stats-json-plugin.ts b/packages/rspack/src/plugins/utils/plugins/stats-json-plugin.ts new file mode 100644 index 0000000000000..4a2a350019a2b --- /dev/null +++ b/packages/rspack/src/plugins/utils/plugins/stats-json-plugin.ts @@ -0,0 +1,10 @@ +import { Compiler, sources } from '@rspack/core'; + +export class StatsJsonPlugin { + apply(compiler: Compiler) { + compiler.hooks.emit.tap('StatsJsonPlugin', (compilation) => { + const data = JSON.stringify(compilation.getStats().toJson('verbose')); + compilation.assets[`stats.json`] = new sources.RawSource(data); + }); + } +} diff --git a/packages/rspack/src/utils/with-nx.ts b/packages/rspack/src/utils/with-nx.ts index 81461751ea8cf..c9db2bb03583f 100644 --- a/packages/rspack/src/utils/with-nx.ts +++ b/packages/rspack/src/utils/with-nx.ts @@ -1,233 +1,50 @@ -import { - Configuration, - ExternalItem, - ResolveAlias, - RspackPluginInstance, - rspack, -} from '@rspack/core'; -import { existsSync, readFileSync } from 'fs'; -import { LicenseWebpackPlugin } from 'license-webpack-plugin'; -import * as path from 'path'; -import { join } from 'path'; -import { GeneratePackageJsonPlugin } from '../plugins/generate-package-json-plugin'; -import { getCopyPatterns } from './get-copy-patterns'; +import { type Configuration } from '@rspack/core'; import { normalizeAssets } from './normalize-assets'; -import { NxRspackExecutionContext } from './config'; +import { NxAppRspackPluginOptions } from '../plugins/utils/models'; +import { applyBaseConfig } from '../plugins/utils/apply-base-config'; +import { NxRspackExecutionContext, NxComposableRspackPlugin } from './config'; -export function withNx(_opts = {}) { +const processed = new Set(); + +export type WithNxOptions = Partial; + +/** + * @param {WithNxOptions} pluginOptions + * @returns {NxComposableRspackPlugin} + */ +export function withNx( + pluginOptions: WithNxOptions = {} +): NxComposableRspackPlugin { return function makeConfig( config: Configuration, { options, context }: NxRspackExecutionContext ): Configuration { - const isProd = - process.env.NODE_ENV === 'production' || options.mode === 'production'; - - const project = context.projectGraph.nodes[context.projectName]; - const sourceRoot = path.join(context.root, project.data.sourceRoot); - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const tsconfigPaths = require('tsconfig-paths'); - const { paths } = tsconfigPaths.loadConfig(options.tsConfig); - const alias: ResolveAlias = Object.keys(paths).reduce((acc, k) => { - acc[k] = path.join(context.root, paths[k][0]); - return acc; - }, {}); - - const plugins = config.plugins ?? []; - if (options.extractLicenses) { - /** - * Needed to prevent an issue with Rspack and Workspaces where the - * workspace's root package.json file is added to the dependency tree - */ - let rootPackageJsonName; - const pathToRootPackageJson = join(context.root, 'package.json'); - if (existsSync(pathToRootPackageJson)) { - try { - const rootPackageJson = JSON.parse( - readFileSync(pathToRootPackageJson, 'utf-8') - ); - rootPackageJsonName = rootPackageJson.name; - } catch { - // do nothing - } - } - plugins.push( - new LicenseWebpackPlugin({ - stats: { - warnings: false, - errors: false, - }, - outputFilename: `3rdpartylicenses.txt`, - /** - * Needed to prevent an issue with Rspack and Workspaces where the - * workspace's root package.json file is added to the dependency tree - */ - excludedPackageTest: (packageName) => { - if (!rootPackageJsonName) { - return false; - } - return packageName === rootPackageJsonName; - }, - }) as unknown as RspackPluginInstance - ); - } - - if (options.generatePackageJson) { - const mainOutputFile = - options.main.split('/').pop().split('.')[0] + '.js'; - - plugins.push( - new GeneratePackageJsonPlugin( - { - tsConfig: options.tsConfig, - outputFileName: options.outputFileName ?? mainOutputFile, - }, - context - ) - ); - } - - plugins.push( - new rspack.CopyRspackPlugin({ - patterns: getCopyPatterns( - normalizeAssets(options.assets, context.root, sourceRoot) - ), - }) - ); - plugins.push(new rspack.ProgressPlugin()); - - options.fileReplacements.forEach((item) => { - alias[item.replace] = item.with; - }); - - const externals: ExternalItem = {}; - let externalsType: Configuration['externalsType']; - if (options.target === 'node') { - const projectDeps = - context.projectGraph.dependencies[context.projectName]; - for (const dep of Object.values(projectDeps)) { - const externalNode = context.projectGraph.externalNodes[dep.target]; - if (externalNode) { - externals[externalNode.data.packageName] = - externalNode.data.packageName; - } - } - externalsType = 'commonjs'; - } - - const updated: Configuration = { - ...config, - target: options.target, - mode: options.mode, - entry: {}, - context: join( - context.root, - context.projectGraph.nodes[context.projectName].data.root - ), - devtool: - options.sourceMap === 'hidden' - ? ('hidden-source-map' as const) - : options.sourceMap - ? ('source-map' as const) - : (false as const), - output: { - path: path.join(context.root, options.outputPath), - publicPath: '/', - filename: - isProd && options.target !== 'node' - ? '[name].[contenthash:8].js' - : '[name].js', - chunkFilename: - isProd && options.target !== 'node' - ? '[name].[contenthash:8].js' - : '[name].js', - cssFilename: - isProd && options.target !== 'node' - ? '[name].[contenthash:8].css' - : '[name].css', - cssChunkFilename: - isProd && options.target !== 'node' - ? '[name].[contenthash:8].css' - : '[name].css', - assetModuleFilename: - isProd && options.target !== 'node' - ? '[name].[contenthash:8][ext]' - : '[name][ext]', - }, - devServer: { - ...(config.devServer ?? {}), - port: config.devServer?.port ?? 4200, - hot: config.devServer?.hot ?? true, - devMiddleware: { - ...(config.devServer?.devMiddleware ?? {}), - stats: true, - }, - } as any, - module: { - rules: [ - { - test: /\.js$/, - loader: 'builtin:swc-loader', - exclude: /node_modules/, - options: { - jsc: { - parser: { - syntax: 'ecmascript', - }, - externalHelpers: true, - }, - }, - type: 'javascript/auto', - }, - { - test: /\.ts$/, - loader: 'builtin:swc-loader', - exclude: /node_modules/, - options: { - jsc: { - parser: { - syntax: 'typescript', - decorators: true, - }, - transform: { - legacyDecorator: true, - decoratorMetadata: true, - }, - externalHelpers: true, - }, - }, - type: 'javascript/auto', - }, - ], - }, - plugins: plugins, - resolve: { - // There are some issues resolving workspace libs in a monorepo. - // It looks to be an issue with rspack itself, but will check back after Nx 16 release - // once I can reproduce a small example repo with rspack only. - alias, - // We need to define the extensions that rspack can resolve - extensions: ['...', '.ts', '.tsx', '.jsx'], - // tsConfigPath: path.join(context.root, options.tsConfig), + if (processed.has(config)) return config; + + applyBaseConfig( + { + ...options, + ...pluginOptions, + target: options.target ?? 'web', + assets: options.assets + ? options.assets + : pluginOptions.assets + ? normalizeAssets( + pluginOptions.assets, + options.root, + options.sourceRoot + ) + : [], + root: context.root, + projectName: context.projectName, + targetName: context.targetName, + configurationName: context.configurationName, + projectGraph: context.projectGraph, }, - infrastructureLogging: { - debug: false, - }, - externals, - externalsType, - stats: { - colors: true, - preset: 'normal', - }, - }; - - const mainEntry = options.main - ? options.outputFileName - ? path.parse(options.outputFileName).name - : 'main' - : 'main'; - updated.entry[mainEntry] = path.resolve(context.root, options.main); + config + ); - return updated; + processed.add(config); + return config; }; } diff --git a/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts b/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts index 7d2ba347d89d6..1f84310786085 100644 --- a/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts +++ b/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts @@ -51,7 +51,7 @@ export function applyBaseConfig( applyNxIndependentConfig(options, config); // Some of the options only work during actual tasks, not when reading the webpack config during CreateNodes. - if (!process.env['NX_TASK_TARGET_PROJECT']) return; + if (global.NX_GRAPH_CREATION) return; applyNxDependentConfig(options, config, { useNormalizedEntry }); }