UNPKG

@nx/webpack

Version:

The Nx Plugin for Webpack contains executors and generators that support building applications using Webpack.

369 lines (368 loc) • 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.applyBaseConfig = applyBaseConfig; const path = require("path"); const license_webpack_plugin_1 = require("license-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const webpack_1 = require("webpack"); const js_1 = require("@nx/js"); const stats_json_plugin_1 = require("../../stats-json-plugin"); const generate_package_json_plugin_1 = require("../../generate-package-json-plugin"); const hash_format_1 = require("../../../utils/hash-format"); const nx_tsconfig_paths_webpack_plugin_1 = require("../../nx-typescript-webpack-plugin/nx-tsconfig-paths-webpack-plugin"); const get_terser_ecma_version_1 = require("./get-terser-ecma-version"); const compiler_loaders_1 = require("./compiler-loaders"); const TerserPlugin = require("terser-webpack-plugin"); const nodeExternals = require("webpack-node-externals"); const ts_solution_setup_1 = require("@nx/js/src/utils/typescript/ts-solution-setup"); const utils_1 = require("./utils"); const IGNORED_WEBPACK_WARNINGS = [ /The comment file/i, /could not find any license/i, ]; const extensionAlias = { '.js': ['.ts', '.js'], '.mjs': ['.mts', '.mjs'], }; const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx']; const mainFields = ['module', 'main']; function applyBaseConfig(options, config = {}, { useNormalizedEntry, } = {}) { // Defaults that was applied from executor schema previously. options.compiler ??= 'babel'; 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 webpack config during CreateNodes. if (global.NX_GRAPH_CREATION) return; applyNxDependentConfig(options, config, { useNormalizedEntry }); } function applyNxIndependentConfig(options, config) { const hashFormat = (0, hash_format_1.getOutputHashFormat)(options.outputHashing); config.context = path.join(options.root, options.projectRoot); config.target ??= options.target; 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, webpack 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 webpack. process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'production' ? process.env.NODE_ENV : '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 ? { type: 'memory' } : undefined; config.devtool = options.sourceMap === true ? 'source-map' : options.sourceMap; config.output = { ...config.output, libraryTarget: config.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.experiments = { ...config.experiments, cacheUnaffected: true }; config.ignoreWarnings = [ (x) => IGNORED_WEBPACK_WARNINGS.some((r) => typeof x === 'string' ? r.test(x) : r.test(x.message)), ...(config.ignoreWarnings ?? []), ]; config.optimization = { ...config.optimization, sideEffects: true, minimize: typeof options.optimization === 'object' ? !!options.optimization.scripts : !!options.optimization, minimizer: [ options.compiler !== 'swc' ? new TerserPlugin({ parallel: true, terserOptions: { keep_classnames: true, ecma: (0, get_terser_ecma_version_1.getTerserEcmaVersion)(path.join(options.root, options.projectRoot)), safari10: true, format: { ascii_only: true, comments: false, webkit: true, }, }, extractComments: false, }) : new TerserPlugin({ minify: TerserPlugin.swcMinify, // `terserOptions` options will be passed to `swc` terserOptions: { module: true, mangle: false, }, }), ], 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 webpack is used during task execution. * These properties may be used by consumers who expect them to not be undefined. * * When @nx/webpack/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 webpack options from the resolved file. */ config.entry ??= {}; config.resolve ??= {}; config.module ??= {}; config.plugins ??= []; config.externals ??= []; } function applyNxDependentConfig(options, config, { useNormalizedEntry } = {}) { const tsConfig = options.tsConfig ?? (0, js_1.getRootTsConfigPath)(); const plugins = []; const executorContext = { projectName: options.projectName, targetName: options.targetName, projectGraph: options.projectGraph, configurationName: options.configurationName, root: options.root, }; const isUsingTsSolution = (0, ts_solution_setup_1.isUsingTsSolutionSetup)(); options.useTsconfigPaths ??= !isUsingTsSolution; // If the project is using ts solutions setup, the paths are not in tsconfig and we should not use the plugin's paths. if (options.useTsconfigPaths) { plugins.push(new nx_tsconfig_paths_webpack_plugin_1.NxTsconfigPathsWebpackPlugin({ ...options, tsConfig })); } // New TS Solution already has a typecheck target but allow it to run during serve if ((!options?.skipTypeChecking && !isUsingTsSolution) || (isUsingTsSolution && options?.skipTypeChecking === false && process.env['WEBPACK_SERVE'])) { 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 = []; 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 webpack_1.ProgressPlugin({ profile: options.verbose })); } if (options.extractLicenses) { plugins.push(new license_webpack_plugin_1.LicenseWebpackPlugin({ stats: { warnings: false, errors: false, }, perChunkOutput: false, outputFilename: `3rdpartylicenses.txt`, })); } if (Array.isArray(options.assets) && options.assets.length > 0) { plugins.push(new CopyWebpackPlugin({ 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 generate_package_json_plugin_1.GeneratePackageJsonPlugin({ ...options, tsConfig })); } if (options.statsJson) { plugins.push(new stats_json_plugin_1.StatsJsonPlugin()); } const externals = []; if (options.target === 'node' && options.externalDependencies === 'all') { const modulesDir = `${options.root}/node_modules`; const graph = options.projectGraph; const projectName = options.projectName; const deps = graph?.dependencies?.[projectName] ?? []; // Collect non-buildable TS project references so that they are bundled // in the final output. This is needed for projects that are not buildable // but are referenced by buildable projects. This is needed for the new TS // solution setup. const nonBuildableWorkspaceLibs = isUsingTsSolution ? deps .filter((dep) => { const node = graph.nodes?.[dep.target]; if (!node || node.type !== 'lib') return false; const hasBuildTarget = 'build' in (node.data?.targets ?? {}); if (hasBuildTarget) { return false; } // If there is no build target we check the package exports to see if they reference // source files return !(0, utils_1.isBuildableLibrary)(node); }) .map((dep) => graph.nodes?.[dep.target]?.data?.metadata?.js?.packageName) .filter((name) => !!name) : []; externals.push(nodeExternals({ modulesDir, allowlist: nonBuildableWorkspaceLibs })); } else if (Array.isArray(options.externalDependencies)) { externals.push(function (ctx, callback) { 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], extensionAlias: { ...(config.resolve?.extensionAlias ?? {}), ...extensionAlias, }, alias: { ...(config.resolve?.alias ?? {}), ...(options.fileReplacements?.reduce((aliases, replacement) => ({ ...aliases, [replacement.replace]: replacement.with, }), {}) ?? {}), }, mainFields: config.resolve?.mainFields ?? mainFields, }; config.externals = externals; config.module = { ...config.module, // Enabled for performance unsafeCache: true, rules: [ ...(config?.module?.rules ?? []), options.sourceMap && { test: /\.js$/, enforce: 'pre', 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', }, (0, compiler_loaders_1.createLoaderFromCompiler)(options), ].filter((r) => !!r), }; config.plugins ??= []; config.plugins.push(...plugins); }