feat(webpack): add webpack plugin (#11966)

This commit is contained in:
Jack Hsu 2022-09-12 16:19:50 -04:00 committed by GitHub
parent a914f78701
commit f49769a34a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
149 changed files with 4143 additions and 2987 deletions

View File

@ -41,6 +41,7 @@ module.exports = {
description: 'anything testing specific (e.g., jest or cypress)', description: 'anything testing specific (e.g., jest or cypress)',
}, },
{ name: 'web', description: 'anything Web specific' }, { name: 'web', description: 'anything Web specific' },
{ name: 'webpack', description: 'anything Webpack specific' },
], ],
allowTicketNumber: true, allowTicketNumber: true,

View File

@ -26,6 +26,12 @@
"description": "Init Web Plugin.", "description": "Init Web Plugin.",
"type": "object", "type": "object",
"properties": { "properties": {
"bundler": {
"type": "string",
"description": "The bundler to use.",
"enum": ["webpack", "none"],
"default": "webpack"
},
"unitTestRunner": { "unitTestRunner": {
"description": "Adds the specified unit test runner", "description": "Adds the specified unit test runner",
"type": "string", "type": "string",
@ -105,6 +111,12 @@
"enum": ["babel", "swc"], "enum": ["babel", "swc"],
"default": "babel" "default": "babel"
}, },
"bundler": {
"type": "string",
"description": "The bundler to use.",
"enum": ["webpack", "none"],
"default": "webpack"
},
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",
"type": "string", "type": "string",

View File

@ -0,0 +1,780 @@
{
"githubRoot": "https://github.com/nrwl/nx/blob/master",
"name": "webpack",
"packageName": "@nrwl/webpack",
"description": "The Nx Plugin for Webpack contains executors and generators that support building applications using Webpack",
"root": "/packages/webpack",
"source": "/packages/webpack/src",
"documentation": [],
"generators": [
{
"name": "init",
"factory": "./src/generators/init/init#webpackInitGenerator",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxWebpackInit",
"cli": "nx",
"title": "Init Webpack Plugin",
"description": "Init Webpack Plugin.",
"type": "object",
"properties": {
"compiler": {
"type": "string",
"enum": ["babel", "swc", "tsc"],
"description": "The compiler to initialize for.",
"default": "babel"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
}
},
"required": [],
"presets": []
},
"description": "Initialize the `@nrwl/webpack` plugin.",
"aliases": ["ng-add"],
"hidden": true,
"implementation": "/packages/webpack/src/generators/init/init#webpackInitGenerator.ts",
"path": "/packages/webpack/src/generators/init/schema.json"
},
{
"name": "webpack-project",
"factory": "./src/generators/webpack-project/webpack-project#webpackProjectGenerator",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxWebpackProject",
"cli": "nx",
"title": "Add Webpack Configuration to a project",
"description": "Add Webpack Configuration to a project.",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the project.",
"$default": { "$source": "argv", "index": 0 },
"x-dropdown": "project",
"x-prompt": "What is the name of the project to set up a webpack for?"
},
"compiler": {
"type": "string",
"enum": ["babel", "swc", "tsc"],
"description": "The compiler to use to build source.",
"default": "babel"
},
"main": {
"type": "string",
"description": "Path relative to the workspace root for the main entry file. Defaults to '<projectRoot>/src/main.ts'."
},
"tsConfig": {
"type": "string",
"description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '<projectRoot>/tsconfig.app.json'."
},
"target": {
"type": "string",
"description": "Target platform for the build, same as the Webpack config option.",
"enum": ["node", "web"],
"default": "web"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`."
},
"devServer": {
"type": "boolean",
"description": "Add a serve target to run a local webpack dev-server",
"default": false
},
"webpackConfig": {
"type": "string",
"description": "Path relative to workspace root to a custom webpack file that takes a config object and returns an updated config."
}
},
"required": [],
"presets": []
},
"description": "Add webpack configuration to a project.",
"hidden": true,
"implementation": "/packages/webpack/src/generators/webpack-project/webpack-project#webpackProjectGenerator.ts",
"aliases": [],
"path": "/packages/webpack/src/generators/webpack-project/schema.json"
}
],
"executors": [
{
"name": "webpack",
"implementation": "/packages/webpack/src/executors/webpack/webpack.impl.ts",
"schema": {
"title": "Webpack Executor",
"description": "Builds web applications using webpack.",
"cli": "nx",
"type": "object",
"properties": {
"crossOrigin": {
"type": "string",
"description": "The `crossorigin` attribute to use for generated javascript script tags. One of 'none' | 'anonymous' | 'use-credentials'."
},
"main": {
"type": "string",
"description": "The name of the main entry-point file.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.js|.ts|.tsx)"
},
"tsConfig": {
"type": "string",
"description": "The name of the Typescript configuration file.",
"x-completion-type": "file",
"x-completion-glob": "tsconfig.*.json"
},
"compiler": {
"type": "string",
"description": "The compiler to use.",
"enum": ["babel", "swc", "tsc"],
"default": "babel"
},
"outputPath": {
"type": "string",
"description": "The output path of the generated files.",
"x-completion-type": "directory"
},
"target": {
"type": "string",
"description": "Target platform for the build, same as the Webpack config option.",
"enum": ["node", "web"],
"default": "web"
},
"deleteOutputPath": {
"type": "boolean",
"description": "Delete the output path before building.",
"default": true
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": false
},
"baseHref": {
"type": "string",
"description": "Base url for the application being built."
},
"deployUrl": {
"type": "string",
"description": "URL where the application will be deployed."
},
"vendorChunk": {
"type": "boolean",
"description": "Use a separate bundle containing only vendor libraries.",
"default": true
},
"commonChunk": {
"type": "boolean",
"description": "Use a separate bundle containing code used across multiple bundles.",
"default": true
},
"runtimeChunk": {
"type": "boolean",
"description": "Use a separate bundle containing the runtime.",
"default": true
},
"sourceMap": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"default": true,
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"progress": {
"type": "boolean",
"description": "Log progress to the console while building.",
"default": false
},
"assets": {
"type": "array",
"description": "List of static application assets.",
"default": [],
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"glob": {
"type": "string",
"description": "The pattern to match."
},
"input": {
"type": "string",
"description": "The input directory path in which to apply 'glob'. Defaults to the project root."
},
"ignore": {
"description": "An array of globs to ignore.",
"type": "array",
"items": { "type": "string" }
},
"output": {
"type": "string",
"description": "Absolute path within the output."
}
},
"additionalProperties": false,
"required": ["glob", "input", "output"]
},
{ "type": "string" }
]
}
},
"index": {
"type": "string",
"description": "HTML File which will be contain the application.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.html|.htm)"
},
"scripts": {
"type": "array",
"description": "External Scripts which will be included before the main application entry.",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
},
"bundleName": {
"type": "string",
"description": "The bundle name for this extra entry point."
},
"inject": {
"type": "boolean",
"description": "If the bundle will be referenced in the HTML file.",
"default": true
}
},
"additionalProperties": false,
"required": ["input"]
},
{
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
}
]
},
"default": []
},
"styles": {
"type": "array",
"description": "External Styles which will be included with the application",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
},
"bundleName": {
"type": "string",
"description": "The bundle name for this extra entry point."
},
"inject": {
"type": "boolean",
"description": "If the bundle will be referenced in the HTML file.",
"default": true
}
},
"additionalProperties": false,
"required": ["input"]
},
{
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
}
]
},
"default": []
},
"budgets": {
"description": "Budget thresholds to ensure parts of your application stay within boundaries which you set.",
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "The type of budget.",
"enum": [
"all",
"allScript",
"any",
"anyScript",
"bundle",
"initial"
]
},
"name": {
"type": "string",
"description": "The name of the bundle."
},
"baseline": {
"type": "string",
"description": "The baseline size for comparison."
},
"maximumWarning": {
"type": "string",
"description": "The maximum threshold for warning relative to the baseline."
},
"maximumError": {
"type": "string",
"description": "The maximum threshold for error relative to the baseline."
},
"minimumWarning": {
"type": "string",
"description": "The minimum threshold for warning relative to the baseline."
},
"minimumError": {
"type": "string",
"description": "The minimum threshold for error relative to the baseline."
},
"warning": {
"type": "string",
"description": "The threshold for warning relative to the baseline (min & max)."
},
"error": {
"type": "string",
"description": "The threshold for error relative to the baseline (min & max)."
}
},
"additionalProperties": false,
"required": ["type"]
},
"default": []
},
"namedChunks": {
"type": "boolean",
"description": "Names the produced bundles according to their entry file.",
"default": true
},
"outputHashing": {
"type": "string",
"description": "Define the output filename cache-busting hashing mode.",
"default": "none",
"enum": ["none", "all", "media", "bundles"]
},
"stylePreprocessorOptions": {
"description": "Options to pass to style preprocessors.",
"type": "object",
"properties": {
"includePaths": {
"description": "Paths to include. Paths will be resolved to project root.",
"type": "array",
"items": { "type": "string" },
"default": []
}
},
"additionalProperties": false
},
"optimization": {
"description": "Enables optimization of the build output.",
"oneOf": [
{
"type": "object",
"properties": {
"scripts": {
"type": "boolean",
"description": "Enables optimization of the scripts output.",
"default": true
},
"styles": {
"type": "boolean",
"description": "Enables optimization of the styles output.",
"default": true
}
},
"additionalProperties": false
},
{ "type": "boolean" }
]
},
"generatePackageJson": {
"type": "boolean",
"description": "Generates a `package.json` 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.",
"default": false
},
"transformers": {
"type": "array",
"description": "List of TypeScript Compiler Transfomers Plugins.",
"default": [],
"aliases": ["tsPlugins"],
"items": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"options": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": false,
"required": ["name"]
}
]
}
},
"additionalEntryPoints": {
"type": "array",
"items": {
"type": "object",
"properties": {
"entryName": {
"type": "string",
"description": "Name of the additional entry file."
},
"entryPath": {
"type": "string",
"description": "Path to the additional entry file.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.js|.ts)"
}
}
}
},
"outputFileName": {
"type": "string",
"description": "Name of the main output file.",
"default": "main.js"
},
"externalDependencies": {
"oneOf": [
{ "type": "string", "enum": ["none", "all"] },
{ "type": "array", "items": { "type": "string" } }
],
"description": "Dependencies to keep external to the bundle. (`all` (default), `none`, or an array of module names)",
"default": "all"
},
"extractCss": {
"type": "boolean",
"description": "Extract CSS into a `.css` file.",
"default": true
},
"es2015Polyfills": {
"description": "Conditional polyfills loaded in browsers which do not support `ES2015`.",
"type": "string"
},
"subresourceIntegrity": {
"type": "boolean",
"description": "Enables the use of subresource integrity validation.",
"default": false
},
"polyfills": {
"type": "string",
"description": "Polyfills to load before application",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.js|.ts|.tsx)"
},
"verbose": {
"type": "boolean",
"description": "Emits verbose output",
"default": false
},
"statsJson": {
"type": "boolean",
"description": "Generates a 'stats.json' file which can be analyzed using tools such as: 'webpack-bundle-analyzer' or `<https://webpack.github.io/analyse>`.",
"default": false
},
"extractLicenses": {
"type": "boolean",
"description": "Extract all licenses in a separate file, in the case of production builds only.",
"default": false
},
"memoryLimit": {
"type": "number",
"description": "Memory limit for type checking service process in `MB`.",
"default": 2048
},
"maxWorkers": {
"type": "number",
"description": "Number of workers to use for type checking.",
"default": 2
},
"fileReplacements": {
"description": "Replace files with other files in the build.",
"type": "array",
"items": {
"type": "object",
"properties": {
"replace": {
"type": "string",
"description": "The file to be replaced.",
"x-completion-type": "file"
},
"with": {
"type": "string",
"description": "The file to replace with.",
"x-completion-type": "file"
}
},
"additionalProperties": false,
"required": ["replace", "with"]
},
"default": []
},
"buildLibsFromSource": {
"type": "boolean",
"description": "Read buildable libraries from source instead of building them separately.",
"default": true
},
"generateIndexHtml": {
"type": "boolean",
"description": "Generates `index.html` file to the output path. This can be turned off if using a webpack plugin to generate HTML such as `html-webpack-plugin`.",
"default": true
},
"postcssConfig": {
"type": "string",
"description": "Set a path to PostCSS config that applies to the app and all libs. Defaults to `undefined`, which auto-detects postcss.config.js files in each `app`/`lib` directory."
},
"webpackConfig": {
"type": "string",
"description": "Path to a function which takes a webpack config, some context and returns the resulting webpack config. See https://nx.dev/guides/customize-webpack",
"x-completion-type": "file",
"x-completion-glob": "webpack?(*)@(.js|.ts)"
}
},
"required": ["tsConfig", "main"],
"definitions": {
"assetPattern": {
"oneOf": [
{
"type": "object",
"properties": {
"glob": {
"type": "string",
"description": "The pattern to match."
},
"input": {
"type": "string",
"description": "The input directory path in which to apply 'glob'. Defaults to the project root."
},
"ignore": {
"description": "An array of globs to ignore.",
"type": "array",
"items": { "type": "string" }
},
"output": {
"type": "string",
"description": "Absolute path within the output."
}
},
"additionalProperties": false,
"required": ["glob", "input", "output"]
},
{ "type": "string" }
]
},
"budget": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "The type of budget.",
"enum": [
"all",
"allScript",
"any",
"anyScript",
"bundle",
"initial"
]
},
"name": {
"type": "string",
"description": "The name of the bundle."
},
"baseline": {
"type": "string",
"description": "The baseline size for comparison."
},
"maximumWarning": {
"type": "string",
"description": "The maximum threshold for warning relative to the baseline."
},
"maximumError": {
"type": "string",
"description": "The maximum threshold for error relative to the baseline."
},
"minimumWarning": {
"type": "string",
"description": "The minimum threshold for warning relative to the baseline."
},
"minimumError": {
"type": "string",
"description": "The minimum threshold for error relative to the baseline."
},
"warning": {
"type": "string",
"description": "The threshold for warning relative to the baseline (min & max)."
},
"error": {
"type": "string",
"description": "The threshold for error relative to the baseline (min & max)."
}
},
"additionalProperties": false,
"required": ["type"]
},
"extraEntryPoint": {
"oneOf": [
{
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
},
"bundleName": {
"type": "string",
"description": "The bundle name for this extra entry point."
},
"inject": {
"type": "boolean",
"description": "If the bundle will be referenced in the HTML file.",
"default": true
}
},
"additionalProperties": false,
"required": ["input"]
},
{
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
}
]
},
"transformerPattern": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"options": { "type": "object", "additionalProperties": true }
},
"additionalProperties": false,
"required": ["name"]
}
]
}
},
"presets": []
},
"description": "Run webpack build.",
"aliases": [],
"hidden": false,
"path": "/packages/webpack/src/executors/webpack/schema.json"
},
{
"name": "dev-server",
"implementation": "/packages/webpack/src/executors/dev-server/dev-server.impl.ts",
"schema": {
"title": "Web Dev Server",
"description": "Serve a web application.",
"cli": "nx",
"type": "object",
"properties": {
"buildTarget": {
"type": "string",
"description": "Target which builds the application."
},
"port": {
"type": "number",
"description": "Port to listen on.",
"default": 4200
},
"host": {
"type": "string",
"description": "Host to listen on.",
"default": "localhost"
},
"ssl": {
"type": "boolean",
"description": "Serve using `HTTPS`.",
"default": false
},
"sslKey": {
"type": "string",
"description": "SSL key to use for serving `HTTPS`."
},
"sslCert": {
"type": "string",
"description": "SSL certificate to use for serving `HTTPS`."
},
"watch": {
"type": "boolean",
"description": "Watches for changes and rebuilds application.",
"default": true
},
"liveReload": {
"type": "boolean",
"description": "Whether to reload the page on change, using live-reload.",
"default": true
},
"hmr": {
"type": "boolean",
"description": "Enable hot module replacement.",
"default": false
},
"publicHost": {
"type": "string",
"description": "Public URL where the application will be served."
},
"open": {
"type": "boolean",
"description": "Open the application in the browser.",
"default": false
},
"allowedHosts": {
"type": "string",
"description": "This option allows you to whitelist services that are allowed to access the dev server."
},
"memoryLimit": {
"type": "number",
"description": "Memory limit for type checking service process in `MB`."
},
"maxWorkers": {
"type": "number",
"description": "Number of workers to use for type checking."
},
"baseHref": {
"type": "string",
"description": "Base url for the application being built."
}
},
"presets": []
},
"description": "Serve a web application.",
"aliases": [],
"hidden": false,
"path": "/packages/webpack/src/executors/dev-server/schema.json"
}
]
}

View File

@ -303,6 +303,15 @@
"generators": ["init", "application"] "generators": ["init", "application"]
} }
}, },
{
"name": "webpack",
"packageName": "webpack",
"path": "generated/packages/webpack.json",
"schemas": {
"executors": ["webpack", "dev-server"],
"generators": ["init", "webpack-project"]
}
},
{ {
"name": "workspace", "name": "workspace",
"packageName": "workspace", "packageName": "workspace",

View File

@ -29,9 +29,6 @@ describe('js e2e', () => {
expect(libPackageJson.scripts.test).toBeDefined(); expect(libPackageJson.scripts.test).toBeDefined();
expect(libPackageJson.scripts.build).toBeDefined(); expect(libPackageJson.scripts.build).toBeDefined();
expect(runCLI(`test ${npmScriptsLib}`)).toContain('implement test'); expect(runCLI(`test ${npmScriptsLib}`)).toContain('implement test');
expect(runCLI(`test ${npmScriptsLib}`)).toContain(
'existing outputs match the cache, left as is'
);
const tsconfig = readJson(`tsconfig.base.json`); const tsconfig = readJson(`tsconfig.base.json`);
expect(tsconfig.compilerOptions.paths).toEqual({ expect(tsconfig.compilerOptions.paths).toEqual({
@ -47,9 +44,6 @@ describe('js e2e', () => {
expect((await runCLIAsync(`test ${lib}`)).combinedOutput).toContain( expect((await runCLIAsync(`test ${lib}`)).combinedOutput).toContain(
'Ran all test suites' 'Ran all test suites'
); );
expect((await runCLIAsync(`test ${lib}`)).combinedOutput).toContain(
'local cache'
);
const packageJson = readJson('package.json'); const packageJson = readJson('package.json');
const devPackageNames = Object.keys(packageJson.devDependencies); const devPackageNames = Object.keys(packageJson.devDependencies);
@ -115,9 +109,6 @@ describe('js e2e', () => {
expect((await runCLIAsync(`test ${parentLib}`)).combinedOutput).toContain( expect((await runCLIAsync(`test ${parentLib}`)).combinedOutput).toContain(
'Ran all test suites' 'Ran all test suites'
); );
expect((await runCLIAsync(`test ${parentLib}`)).combinedOutput).toContain(
'local cache'
);
expect(runCLI(`build ${parentLib}`)).toContain( expect(runCLI(`build ${parentLib}`)).toContain(
'Done compiling TypeScript files' 'Done compiling TypeScript files'
@ -181,9 +172,6 @@ describe('js e2e', () => {
expect((await runCLIAsync(`test ${lib}`)).combinedOutput).toContain( expect((await runCLIAsync(`test ${lib}`)).combinedOutput).toContain(
'Ran all test suites' 'Ran all test suites'
); );
expect((await runCLIAsync(`test ${lib}`)).combinedOutput).toContain(
'local cache'
);
expect(runCLI(`build ${lib}`)).toContain( expect(runCLI(`build ${lib}`)).toContain(
'Successfully compiled: 2 files with swc' 'Successfully compiled: 2 files with swc'
@ -205,9 +193,6 @@ describe('js e2e', () => {
expect((await runCLIAsync(`test ${parentLib}`)).combinedOutput).toContain( expect((await runCLIAsync(`test ${parentLib}`)).combinedOutput).toContain(
'Ran all test suites' 'Ran all test suites'
); );
expect((await runCLIAsync(`test ${parentLib}`)).combinedOutput).toContain(
'local cache'
);
expect(runCLI(`build ${parentLib}`)).toContain( expect(runCLI(`build ${parentLib}`)).toContain(
'Successfully compiled: 2 files with swc' 'Successfully compiled: 2 files with swc'

View File

@ -352,7 +352,7 @@ ${jslib}();
const nestapp = uniq('nestapp'); const nestapp = uniq('nestapp');
runCLI(`generate @nrwl/nest:app ${nestapp} --linter=eslint`); runCLI(`generate @nrwl/nest:app ${nestapp} --linter=eslint`);
packageInstall('@nestjs/swagger', undefined, '~5.0.0'); packageInstall('@nestjs/swagger', undefined, '^6.0.0');
updateProjectConfig(nestapp, (config) => { updateProjectConfig(nestapp, (config) => {
config.targets.build.options.tsPlugins = ['@nestjs/swagger/plugin']; config.targets.build.options.tsPlugins = ['@nestjs/swagger/plugin'];
@ -396,16 +396,8 @@ ${jslib}();
await runCLIAsync(`build ${nestapp}`); await runCLIAsync(`build ${nestapp}`);
const mainJs = readFile(`dist/apps/${nestapp}/main.js`); const mainJs = readFile(`dist/apps/${nestapp}/main.js`);
expect(stripIndents`${mainJs}`).toContain( expect(mainJs).toContain('FooDto');
stripIndents` expect(mainJs).toContain('_OPENAPI_METADATA_FACTORY');
class FooDto {
static _OPENAPI_METADATA_FACTORY() {
return { foo: { required: true, type: () => String }, bar: { required: true, type: () => Number } };
}
}
exports.FooDto = FooDto;
`
);
}, 300000); }, 300000);
}); });
}); });

View File

@ -80,7 +80,7 @@ describe('React Applications', () => {
checkFilesExist(...filesToCheck); checkFilesExist(...filesToCheck);
expect(readFile(`dist/apps/${appName}/index.html`)).toContain( expect(readFile(`dist/apps/${appName}/index.html`)).toContain(
`<script src="main.esm.js" type="module"></script><script src="main.es5.js" nomodule defer></script>` `<script src="main.js" type="module"></script><script src="main.es5.js" nomodule defer></script>`
); );
}, 250_000); }, 250_000);
@ -125,13 +125,13 @@ describe('React Applications', () => {
); );
const filesToCheck = [ const filesToCheck = [
`dist/apps/${appName}/index.html`, `dist/apps/${appName}/index.html`,
`dist/apps/${appName}/runtime.esm.js`, `dist/apps/${appName}/runtime.js`,
`dist/apps/${appName}/polyfills.esm.js`, `dist/apps/${appName}/polyfills.js`,
`dist/apps/${appName}/main.esm.js`, `dist/apps/${appName}/main.js`,
]; ];
if (opts.checkSourceMap) { if (opts.checkSourceMap) {
filesToCheck.push(`dist/apps/${appName}/main.esm.js.map`); filesToCheck.push(`dist/apps/${appName}/main.js.map`);
} }
if (opts.checkStyles) { if (opts.checkStyles) {
@ -214,9 +214,9 @@ describe('React Applications: additional packages', () => {
checkFilesExist( checkFilesExist(
`dist/apps/${appName}/index.html`, `dist/apps/${appName}/index.html`,
`dist/apps/${appName}/runtime.esm.js`, `dist/apps/${appName}/runtime.js`,
`dist/apps/${appName}/polyfills.esm.js`, `dist/apps/${appName}/polyfills.js`,
`dist/apps/${appName}/main.esm.js` `dist/apps/${appName}/main.js`
); );
}, 250_000); }, 250_000);

View File

@ -301,6 +301,7 @@ export function newProject({
); );
} }
// TODO(jack): we should tag the projects (e.g. tags: ['package']) and filter from that rather than hard-code packages.
const packages = [ const packages = [
`@nrwl/angular`, `@nrwl/angular`,
`@nrwl/eslint-plugin-nx`, `@nrwl/eslint-plugin-nx`,
@ -315,6 +316,7 @@ export function newProject({
`@nrwl/react`, `@nrwl/react`,
`@nrwl/storybook`, `@nrwl/storybook`,
`@nrwl/web`, `@nrwl/web`,
`@nrwl/webpack`,
`@nrwl/react-native`, `@nrwl/react-native`,
]; ];
packageInstall(packages.join(` `), projScope); packageInstall(packages.join(` `), projScope);

View File

@ -31,9 +31,9 @@ describe('Web Components Applications', () => {
runCLI(`build ${appName} --outputHashing none --compiler babel`); runCLI(`build ${appName} --outputHashing none --compiler babel`);
checkFilesExist( checkFilesExist(
`dist/apps/${appName}/index.html`, `dist/apps/${appName}/index.html`,
`dist/apps/${appName}/runtime.esm.js`, `dist/apps/${appName}/runtime.js`,
`dist/apps/${appName}/polyfills.esm.js`, `dist/apps/${appName}/polyfills.js`,
`dist/apps/${appName}/main.esm.js`, `dist/apps/${appName}/main.js`,
`dist/apps/${appName}/styles.css` `dist/apps/${appName}/styles.css`
); );
@ -119,7 +119,7 @@ describe('Web Components Applications', () => {
runCLI(`build ${appName} --outputHashing=none`); runCLI(`build ${appName} --outputHashing=none`);
checkFilesExist( checkFilesExist(
`dist/apps/${appName}/main.esm.js`, `dist/apps/${appName}/main.js`,
`dist/apps/${appName}/main.es5.js` `dist/apps/${appName}/main.es5.js`
); );
}, 120000); }, 120000);
@ -159,7 +159,7 @@ describe('Web Components Applications', () => {
}); });
runCLI(`build ${appName} --outputHashing none`); runCLI(`build ${appName} --outputHashing none`);
expect(readFile(`dist/apps/${appName}/main.esm.js`)).toMatch( expect(readFile(`dist/apps/${appName}/main.js`)).toMatch(
/Reflect\.metadata/ /Reflect\.metadata/
); );
@ -172,7 +172,7 @@ describe('Web Components Applications', () => {
runCLI(`build ${appName} --outputHashing none`); runCLI(`build ${appName} --outputHashing none`);
expect(readFile(`dist/apps/${appName}/main.esm.js`)).not.toMatch( expect(readFile(`dist/apps/${appName}/main.js`)).not.toMatch(
/Reflect\.metadata/ /Reflect\.metadata/
); );
}, 120000); }, 120000);
@ -207,7 +207,7 @@ describe('Web Components Applications', () => {
` `
); );
runCLI(`build ${appName} --outputHashing none`); runCLI(`build ${appName} --outputHashing none`);
checkFilesExist(`dist/apps/${appName}/main.esm.js`); checkFilesExist(`dist/apps/${appName}/main.js`);
rmDist(); rmDist();
@ -221,7 +221,7 @@ describe('Web Components Applications', () => {
` `
); );
runCLI(`build ${appName} --outputHashing none`); runCLI(`build ${appName} --outputHashing none`);
checkFilesExist(`dist/apps/${appName}/main.esm.js`); checkFilesExist(`dist/apps/${appName}/main.js`);
rmDist(); rmDist();
@ -235,7 +235,7 @@ describe('Web Components Applications', () => {
` `
); );
runCLI(`build ${appName} --outputHashing none`); runCLI(`build ${appName} --outputHashing none`);
checkFilesExist(`dist/apps/${appName}/main.esm.js`); checkFilesExist(`dist/apps/${appName}/main.js`);
}, 100000); }, 100000);
}); });

View File

@ -0,0 +1,11 @@
/* eslint-disable */
export default {
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
maxWorkers: 1,
globals: { 'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json' } },
displayName: 'e2e-webpack',
preset: '../../jest.preset.js',
};

34
e2e/webpack/project.json Normal file
View File

@ -0,0 +1,34 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "e2e/webpack",
"projectType": "application",
"targets": {
"e2e": {
"executor": "nx:run-commands",
"options": {
"commands": [
{
"command": "yarn e2e-start-local-registry"
},
{
"command": "yarn e2e-build-package-publish"
},
{
"command": "nx run-e2e-tests e2e-webpack"
}
],
"parallel": false
}
},
"run-e2e-tests": {
"executor": "@nrwl/jest:jest",
"options": {
"jestConfig": "e2e/webpack/jest.config.ts",
"passWithNoTests": true,
"runInBand": true
},
"outputs": ["coverage/e2e/webpack"]
}
},
"implicitDependencies": ["webpack"]
}

View File

@ -0,0 +1,58 @@
import {
cleanupProject,
newProject,
rmDist,
runCLI,
runCommand,
uniq,
updateFile,
updateProjectConfig,
} from '@nrwl/e2e/utils';
describe('Webpack Plugin', () => {
beforeEach(() => newProject());
afterEach(() => cleanupProject());
it('should be able to setup project to build node programs with webpack and different compilers', async () => {
const myPkg = uniq('my-pkg');
runCLI(`generate @nrwl/js:lib ${myPkg} --buildable=false`);
updateFile(`libs/${myPkg}/src/index.ts`, `console.log('Hello');\n`);
// babel (default)
runCLI(
`generate @nrwl/webpack:webpack-project ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts`
);
rmDist();
runCLI(`build ${myPkg}`);
let output = runCommand(`node dist/libs/${myPkg}/main.js`);
expect(output).toMatch(/Hello/);
updateProjectConfig(myPkg, (config) => {
delete config.targets.build;
return config;
});
// swc
runCLI(
`generate @nrwl/webpack:webpack-project ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts --compiler=swc`
);
rmDist();
runCLI(`build ${myPkg}`);
output = runCommand(`node dist/libs/${myPkg}/main.js`);
expect(output).toMatch(/Hello/);
updateProjectConfig(myPkg, (config) => {
delete config.targets.build;
return config;
});
// tsc
runCLI(
`generate @nrwl/webpack:webpack-project ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts --compiler=tsc`
);
rmDist();
runCLI(`build ${myPkg}`);
output = runCommand(`node dist/libs/${myPkg}/main.js`);
expect(output).toMatch(/Hello/);
}, 500000);
});

13
e2e/webpack/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": [],
"files": [],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"**/*.d.ts",
"jest.config.ts"
]
}

View File

@ -6,7 +6,7 @@ import { assertTextOnPage } from './helpers';
*/ */
describe('nx-dev: Packages Section', () => { describe('nx-dev: Packages Section', () => {
(<{ title: string; path: string }[]>[ (<{ title: string; path: string }[]>[
{ title: '@nrwl/', path: '/packages/angular' }, { title: '@nrwl/angular', path: '/packages/angular' },
{ {
title: '@nrwl/angular:add-linting', title: '@nrwl/angular:add-linting',
path: '/packages/angular/generators/add-linting', path: '/packages/angular/generators/add-linting',

View File

@ -0,0 +1,4 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>webpack</title>
<path d="M28.021 24.161l-11.552 6.505v-5.068l7.198-3.943zM28.813 23.448v-13.609l-4.229 2.432v8.745zM3.901 24.161l11.552 6.505v-5.068l-7.198-3.943zM3.109 23.448v-13.609l4.229 2.432v8.745zM3.604 8.958l11.849-6.672v4.901l-7.646 4.188zM28.318 8.958l-11.849-6.672v4.901l7.646 4.188zM15.453 24.448l-7.099-3.891v-7.703l7.099 4.083zM16.469 24.448l7.099-3.891v-7.703l-7.099 4.083zM8.833 11.964l7.13-3.901 7.13 3.901-7.13 4.099z"></path>
</svg>

After

Width:  |  Height:  |  Size: 556 B

View File

@ -18,5 +18,6 @@ export const iconsMap: Record<string, string> = {
'react-native': '/images/icons/react.svg', 'react-native': '/images/icons/react.svg',
storybook: '/images/icons/storybook.svg', storybook: '/images/icons/storybook.svg',
web: '/images/icons/html5.svg', web: '/images/icons/html5.svg',
webpack: '/images/icons/webpack.svg',
workspace: '/images/icons/nx.svg', workspace: '/images/icons/nx.svg',
}; };

View File

@ -89,6 +89,7 @@
"@storybook/react": "~6.5.9", "@storybook/react": "~6.5.9",
"@svgr/webpack": "^6.1.2", "@svgr/webpack": "^6.1.2",
"@swc-node/register": "^1.4.2", "@swc-node/register": "^1.4.2",
"@swc/cli": "~0.1.55",
"@swc/core": "^1.2.173", "@swc/core": "^1.2.173",
"@swc/jest": "^0.2.20", "@swc/jest": "^0.2.20",
"@testing-library/react": "13.3.0", "@testing-library/react": "13.3.0",
@ -122,7 +123,7 @@
"@xstate/react": "^1.6.3", "@xstate/react": "^1.6.3",
"ajv": "^8.11.0", "ajv": "^8.11.0",
"angular": "1.8.0", "angular": "1.8.0",
"autoprefixer": "10.4.8", "autoprefixer": "^10.4.9",
"babel-jest": "28.1.3", "babel-jest": "28.1.3",
"chalk": "4.1.0", "chalk": "4.1.0",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
@ -279,9 +280,11 @@
"@markdoc/markdoc": "0.1.6", "@markdoc/markdoc": "0.1.6",
"@monaco-editor/react": "^4.3.1", "@monaco-editor/react": "^4.3.1",
"@napi-rs/canvas": "^0.1.19", "@napi-rs/canvas": "^0.1.19",
"@swc/helpers": "~0.4.11",
"@tailwindcss/aspect-ratio": "^0.4.0", "@tailwindcss/aspect-ratio": "^0.4.0",
"@tailwindcss/forms": "^0.4.0", "@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.0", "@tailwindcss/typography": "^0.5.0",
"axios": "0.21.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cliui": "^7.0.2", "cliui": "^7.0.2",
"core-js": "^3.6.5", "core-js": "^3.6.5",
@ -305,12 +308,10 @@
"string-width": "^4.2.3", "string-width": "^4.2.3",
"tailwindcss": "3.1.8", "tailwindcss": "3.1.8",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"weak-napi": "^2.0.2", "weak-napi": "^2.0.2"
"axios": "0.21.1"
}, },
"resolutions": { "resolutions": {
"**/xmlhttprequest-ssl": "~1.6.2", "**/xmlhttprequest-ssl": "~1.6.2",
"minimist": "^1.2.6" "minimist": "^1.2.6"
} }
} }

View File

@ -44,11 +44,10 @@
"@nrwl/jest": "file:../jest", "@nrwl/jest": "file:../jest",
"@nrwl/linter": "file:../linter", "@nrwl/linter": "file:../linter",
"@nrwl/storybook": "file:../storybook", "@nrwl/storybook": "file:../storybook",
"@nrwl/web": "file:../web", "@nrwl/webpack": "file:../webpack",
"@nrwl/workspace": "file:../workspace", "@nrwl/workspace": "file:../workspace",
"@phenomnomnominal/tsquery": "4.1.1", "@phenomnomnominal/tsquery": "4.1.1",
"@schematics/angular": "~14.2.0", "@schematics/angular": "~14.2.0",
"@typescript-eslint/type-utils": "^5.36.1",
"chalk": "4.1.0", "chalk": "4.1.0",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"http-server": "^14.1.0", "http-server": "^14.1.0",

View File

@ -9,7 +9,7 @@ import {
parseTargetString, parseTargetString,
readCachedProjectGraph, readCachedProjectGraph,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { WebpackNxBuildCoordinationPlugin } from '@nrwl/web/src/plugins/webpack-nx-build-coordination-plugin'; import { WebpackNxBuildCoordinationPlugin } from '@nrwl/webpack/src/plugins/webpack-nx-build-coordination-plugin';
import { import {
calculateProjectDependencies, calculateProjectDependencies,
createTmpTsConfig, createTmpTsConfig,

View File

@ -2,7 +2,10 @@ import { applyChangesToString, ChangeType, Tree } from '@nrwl/devkit';
import { import {
__String, __String,
CallExpression, CallExpression,
ClassDeclaration,
createSourceFile, createSourceFile,
Decorator,
getDecorators,
ImportDeclaration, ImportDeclaration,
isArrayLiteralExpression, isArrayLiteralExpression,
isCallExpression, isCallExpression,
@ -18,13 +21,6 @@ import {
SourceFile, SourceFile,
} from 'typescript'; } from 'typescript';
/**
* Importing this helper from @typescript-eslint/type-utils to ensure
* compatibility with TS < 4.8 due to the API change in TS4.8.
* This helper allows for support of TS <= 4.8.
*/
import { getDecorators } from '@typescript-eslint/type-utils';
type ngModuleDecoratorProperty = type ngModuleDecoratorProperty =
| 'imports' | 'imports'
| 'providers' | 'providers'
@ -57,14 +53,36 @@ export function insertNgModuleProperty(
const ngModuleName = ngModuleNamedImport.name.escapedText; const ngModuleName = ngModuleNamedImport.name.escapedText;
const ngModuleClassDeclaration = findDecoratedClass(sourceFile, ngModuleName); /**
* Ensure backwards compatibility with TS < 4.8 due to the API change in TS4.8.
const ngModuleDecorator = getDecorators(ngModuleClassDeclaration).find( * The getDecorators util is only in TS 4.8, so we need the previous logic to handle TS < 4.8.
(decorator) => *
isCallExpression(decorator.expression) && * TODO: clean this up by removing the requirement to eslint (can we use typescript instead)?
isIdentifier(decorator.expression.expression) && */
decorator.expression.expression.escapedText === ngModuleName let ngModuleClassDeclaration: ClassDeclaration;
); let ngModuleDecorator: Decorator;
try {
ngModuleClassDeclaration = findDecoratedClass(sourceFile, ngModuleName);
ngModuleDecorator = getDecorators(ngModuleClassDeclaration).find(
(decorator) =>
isCallExpression(decorator.expression) &&
isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName
);
} catch {
// Support for TS < 4.8
ngModuleClassDeclaration = findDecoratedClassLegacy(
sourceFile,
ngModuleName
);
// @ts-ignore
ngModuleDecorator = ngModuleClassDeclaration.decorators.find(
(decorator) =>
isCallExpression(decorator.expression) &&
isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName
);
}
const ngModuleCall = ngModuleDecorator.expression as CallExpression; const ngModuleCall = ngModuleDecorator.expression as CallExpression;
@ -165,7 +183,10 @@ function getNamedImport(coreImport: ImportDeclaration, importName: string) {
); );
} }
function findDecoratedClass(sourceFile: SourceFile, ngModuleName: __String) { function findDecoratedClass(
sourceFile: SourceFile,
ngModuleName: __String
): ClassDeclaration | undefined {
const classDeclarations = sourceFile.statements.filter(isClassDeclaration); const classDeclarations = sourceFile.statements.filter(isClassDeclaration);
return classDeclarations.find((declaration) => { return classDeclarations.find((declaration) => {
const decorators = getDecorators(declaration); const decorators = getDecorators(declaration);
@ -181,6 +202,23 @@ function findDecoratedClass(sourceFile: SourceFile, ngModuleName: __String) {
}); });
} }
function findDecoratedClassLegacy(
sourceFile: SourceFile,
ngModuleName: __String
) {
const classDeclarations = sourceFile.statements.filter(isClassDeclaration);
return classDeclarations.find(
(declaration) =>
declaration.decorators &&
(declaration.decorators as any[]).some(
(decorator) =>
isCallExpression(decorator.expression) &&
isIdentifier(decorator.expression.expression) &&
decorator.expression.expression.escapedText === ngModuleName
)
);
}
function findPropertyAssignment( function findPropertyAssignment(
ngModuleOptions: ObjectLiteralExpression, ngModuleOptions: ObjectLiteralExpression,
propertyName: ngModuleDecoratorProperty propertyName: ngModuleDecoratorProperty

View File

@ -1,3 +1,4 @@
export * from './utils/typescript/load-ts-transformers';
export * from './utils/typescript/print-diagnostics'; export * from './utils/typescript/print-diagnostics';
export * from './utils/typescript/run-type-check'; export * from './utils/typescript/run-type-check';
export { libraryGenerator } from './generators/library/library'; export { libraryGenerator } from './generators/library/library';

View File

@ -39,7 +39,7 @@
"@nrwl/jest": "file:../jest", "@nrwl/jest": "file:../jest",
"@nrwl/linter": "file:../linter", "@nrwl/linter": "file:../linter",
"@nrwl/react": "file:../react", "@nrwl/react": "file:../react",
"@nrwl/web": "file:../web", "@nrwl/webpack": "file:../webpack",
"@nrwl/workspace": "file:../workspace", "@nrwl/workspace": "file:../workspace",
"@svgr/webpack": "^6.1.2", "@svgr/webpack": "^6.1.2",
"chalk": "4.1.0", "chalk": "4.1.0",

View File

@ -5,7 +5,7 @@ import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { PHASE_PRODUCTION_BUILD } from './constants'; import { PHASE_PRODUCTION_BUILD } from './constants';
jest.mock('@nrwl/web/src/utils/config', () => ({ jest.mock('@nrwl/webpack', () => ({
createCopyPlugin: () => {}, createCopyPlugin: () => {},
})); }));
jest.mock('tsconfig-paths-webpack-plugin'); jest.mock('tsconfig-paths-webpack-plugin');

View File

@ -1,7 +1,7 @@
import { import {
ExecutorContext, ExecutorContext,
offsetFromRoot,
joinPathFragments, joinPathFragments,
offsetFromRoot,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
// ignoring while we support both Next 11.1.0 and versions before it // ignoring while we support both Next 11.1.0 and versions before it
// @ts-ignore // @ts-ignore
@ -16,18 +16,14 @@ import type {
import { join, resolve } from 'path'; import { join, resolve } from 'path';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { Configuration } from 'webpack'; import { Configuration } from 'webpack';
import { import { FileReplacement, NextBuildBuilderOptions } from './types';
FileReplacement, import { createCopyPlugin, normalizeAssets } from '@nrwl/webpack';
NextBuildBuilderOptions,
WebpackConfigOptions,
} from './types';
import { normalizeAssets } from '@nrwl/web/src/utils/normalize';
import { createCopyPlugin } from '@nrwl/web/src/utils/config';
import { WithNxOptions } from '../../plugins/with-nx'; import { WithNxOptions } from '../../plugins/with-nx';
import { import {
createTmpTsConfig, createTmpTsConfig,
DependentBuildableProjectNode, DependentBuildableProjectNode,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils'; } from '@nrwl/workspace/src/utilities/buildable-libs-utils';
const loadConfig = require('next/dist/server/config').default; const loadConfig = require('next/dist/server/config').default;
export function createWebpackConfig( export function createWebpackConfig(

View File

@ -33,6 +33,12 @@
"version": "13.8.5-beta.1", "version": "13.8.5-beta.1",
"description": "Renames @nrwl/node:package to @nrwl/js:tsc", "description": "Renames @nrwl/node:package to @nrwl/js:tsc",
"factory": "./src/migrations/update-13-8-5/update-package-to-tsc" "factory": "./src/migrations/update-13-8-5/update-package-to-tsc"
},
"update-webpack-executor": {
"cli": "nx",
"version": "14.7.5-beta.1",
"description": "Update usages of webpack executors to @nrwl/webpack",
"factory": "./src/migrations/update-14-7-5/update-webpack-executor"
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {

View File

@ -34,26 +34,16 @@
"@nrwl/jest": "file:../jest", "@nrwl/jest": "file:../jest",
"@nrwl/js": "file:../js", "@nrwl/js": "file:../js",
"@nrwl/linter": "file:../linter", "@nrwl/linter": "file:../linter",
"@nrwl/webpack": "file:../webpack",
"@nrwl/workspace": "file:../workspace", "@nrwl/workspace": "file:../workspace",
"chalk": "4.1.0", "chalk": "4.1.0",
"copy-webpack-plugin": "^10.2.4",
"dotenv": "~10.0.0", "dotenv": "~10.0.0",
"enhanced-resolve": "^5.8.3",
"fork-ts-checker-webpack-plugin": "7.2.13",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"glob": "7.1.4", "glob": "7.1.4",
"license-webpack-plugin": "^4.0.2",
"rxjs": "^6.5.4", "rxjs": "^6.5.4",
"source-map-support": "0.5.19",
"terser-webpack-plugin": "^5.3.3",
"tree-kill": "1.2.2", "tree-kill": "1.2.2",
"ts-loader": "^9.3.1",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"tsconfig-paths": "^3.9.0", "tsconfig-paths": "^3.9.0",
"tsconfig-paths-webpack-plugin": "3.5.2", "tslib": "^2.3.0"
"tslib": "^2.3.0",
"webpack": "^5.58.1",
"webpack-merge": "^5.8.0",
"webpack-node-externals": "^3.0.0"
} }
} }

View File

@ -2,7 +2,7 @@ import { ExecutorContext } from '@nrwl/devkit';
import type { NodeExecutorOptions } from '@nrwl/js/src/executors/node/schema'; import type { NodeExecutorOptions } from '@nrwl/js/src/executors/node/schema';
import { nodeExecutor as jsNodeExecutor } from '@nrwl/js/src/executors/node/node.impl'; import { nodeExecutor as jsNodeExecutor } from '@nrwl/js/src/executors/node/node.impl';
// TODO(jack): Remove for Nx 15 // TODO(jack): Remove for Nx 16
export async function* nodeExecutor( export async function* nodeExecutor(
options: NodeExecutorOptions, options: NodeExecutorOptions,
context: ExecutorContext context: ExecutorContext

View File

@ -1,162 +0,0 @@
import { ExecutorContext } from '@nrwl/devkit';
import { of } from 'rxjs';
import * as projectGraph from '@nrwl/devkit';
import type { ProjectGraph } from '@nrwl/devkit';
import webpackExecutor from './webpack.impl';
import { BuildNodeBuilderOptions } from '../../utils/types';
jest.mock('tsconfig-paths-webpack-plugin');
jest.mock('../../utils/run-webpack', () => ({
runWebpack: jest.fn(),
}));
import { runWebpack } from '../../utils/run-webpack';
describe('Node Build Executor', () => {
let context: ExecutorContext;
let options: BuildNodeBuilderOptions;
beforeEach(async () => {
(<any>runWebpack).mockReturnValue(of({ hasErrors: () => false }));
context = {
root: '/root',
cwd: '/root',
projectName: 'my-app',
targetName: 'build',
workspace: {
version: 2,
projects: {
'my-app': <any>{
root: 'apps/wibble',
sourceRoot: 'apps/wibble',
},
},
npmScope: 'test',
},
projectGraph: {} as ProjectGraph,
isVerbose: false,
};
options = {
outputPath: 'dist/apps/wibble',
externalDependencies: 'all',
main: 'apps/wibble/src/main.ts',
tsConfig: 'apps/wibble/tsconfig.ts',
buildLibsFromSource: true,
fileReplacements: [],
};
});
afterEach(() => jest.clearAllMocks());
it('should call webpack', async () => {
await webpackExecutor(options, context).next();
expect(runWebpack).toHaveBeenCalledWith(
expect.objectContaining({
output: expect.objectContaining({
filename: 'main.js',
libraryTarget: 'commonjs',
path: '/root/dist/apps/wibble',
}),
})
);
});
it('should use outputFileName if passed in', async () => {
await webpackExecutor(
{ ...options, outputFileName: 'index.js' },
context
).next();
expect(runWebpack).toHaveBeenCalledWith(
expect.objectContaining({
entry: expect.objectContaining({
index: ['/root/apps/wibble/src/main.ts'],
}),
output: expect.objectContaining({
filename: 'index.js',
libraryTarget: 'commonjs',
path: '/root/dist/apps/wibble',
}),
})
);
});
it('should use watchOptions if passed in', async () => {
await webpackExecutor(
{
...options,
watchOptions: {
ignored: ['path1'],
},
},
context
).next();
expect(runWebpack).toHaveBeenCalledWith(
expect.objectContaining({
watchOptions: {
ignored: ['path1'],
aggregateTimeout: 200,
},
})
);
});
describe('webpackConfig', () => {
it('should handle custom path', async () => {
jest.mock(
'/root/config.js',
() => (options) => ({ ...options, prop: 'my-val' }),
{ virtual: true }
);
await webpackExecutor(
{ ...options, webpackConfig: 'config.js' },
context
).next();
expect(runWebpack).toHaveBeenCalledWith(
expect.objectContaining({
output: expect.objectContaining({
filename: 'main.js',
libraryTarget: 'commonjs',
path: '/root/dist/apps/wibble',
}),
prop: 'my-val',
})
);
});
it('should handle multiple custom paths in order', async () => {
jest.mock(
'/root/config1.js',
() => (o) => ({ ...o, prop1: 'my-val-1' }),
{ virtual: true }
);
jest.mock(
'/root/config2.js',
() => (o) => ({
...o,
prop1: o.prop1 + '-my-val-2',
prop2: 'my-val-2',
}),
{ virtual: true }
);
await webpackExecutor(
{ ...options, webpackConfig: ['config1.js', 'config2.js'] },
context
).next();
expect(runWebpack).toHaveBeenCalledWith(
expect.objectContaining({
output: expect.objectContaining({
filename: 'main.js',
libraryTarget: 'commonjs',
path: '/root/dist/apps/wibble',
}),
prop1: 'my-val-1-my-val-2',
prop2: 'my-val-2',
})
);
});
});
});

View File

@ -1,109 +1,23 @@
import 'dotenv/config'; /**
* For backwards compat.
* TODO(jack): Remove in Nx 16.
*/
import { ExecutorContext } from '@nrwl/devkit'; import { ExecutorContext } from '@nrwl/devkit';
import { eachValueFrom } from '@nrwl/devkit/src/utils/rxjs-for-await'; import type { WebpackExecutorOptions } from '@nrwl/webpack';
import { import { webpackExecutor as baseWebpackExecutor } from '@nrwl/webpack';
calculateProjectDependencies,
createTmpTsConfig,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { getRootTsConfigPath } from '@nrwl/workspace/src/utilities/typescript';
import { map, tap } from 'rxjs/operators';
import { resolve } from 'path';
import { register } from 'ts-node';
import { getNodeWebpackConfig } from '../../utils/node.config';
import { BuildNodeBuilderOptions } from '../../utils/types';
import { normalizeBuildOptions } from '../../utils/normalize';
import { deleteOutputDir } from '../../utils/fs';
import { runWebpack } from '../../utils/run-webpack';
export type NodeBuildEvent = {
outfile: string;
success: boolean;
};
export async function* webpackExecutor( export async function* webpackExecutor(
rawOptions: BuildNodeBuilderOptions, options: WebpackExecutorOptions,
context: ExecutorContext context: ExecutorContext
) { ) {
const { sourceRoot, root } = context.workspace.projects[context.projectName]; yield* baseWebpackExecutor(
{
if (!sourceRoot) { ...options,
throw new Error(`${context.projectName} does not have a sourceRoot.`); target: 'node',
} compiler: 'tsc',
if (!root) {
throw new Error(`${context.projectName} does not have a root.`);
}
const options = normalizeBuildOptions(
rawOptions,
context.root,
sourceRoot,
root
);
if (options.webpackConfig.some((x) => x.endsWith('.ts'))) {
registerTsNode();
}
if (!options.buildLibsFromSource) {
const { target, dependencies } = calculateProjectDependencies(
context.projectGraph,
context.root,
context.projectName,
context.targetName,
context.configurationName
);
options.tsConfig = createTmpTsConfig(
options.tsConfig,
context.root,
target.data.root,
dependencies
);
}
// Delete output path before bundling
if (options.deleteOutputPath) {
deleteOutputDir(context.root, options.outputPath);
}
const config = await options.webpackConfig.reduce(
async (currentConfig, plugin) => {
return require(plugin)(await currentConfig, {
options,
configuration: context.configurationName,
});
}, },
Promise.resolve( context
getNodeWebpackConfig(context, context.projectGraph, options)
)
); );
return yield* eachValueFrom(
runWebpack(config).pipe(
tap((stats) => {
console.info(stats.toString(config.stats));
}),
map((stats) => {
return {
success: !stats.hasErrors(),
outfile: resolve(
context.root,
options.outputPath,
options.outputFileName
),
} as NodeBuildEvent;
})
)
);
}
function registerTsNode() {
const rootTsConfig = getRootTsConfigPath();
register({
...(rootTsConfig ? { project: rootTsConfig } : null),
});
} }
export default webpackExecutor; export default webpackExecutor;

View File

@ -1,5 +1,5 @@
import { NxJsonConfiguration, readJson, Tree, getProjects } from '@nrwl/devkit';
import * as devkit from '@nrwl/devkit'; import * as devkit from '@nrwl/devkit';
import { getProjects, NxJsonConfiguration, readJson, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
// nx-ignore-next-line // nx-ignore-next-line
@ -44,9 +44,11 @@ describe('app', () => {
expect(project.architect).toEqual( expect(project.architect).toEqual(
expect.objectContaining({ expect.objectContaining({
build: { build: {
builder: '@nrwl/node:webpack', builder: '@nrwl/webpack:webpack',
outputs: ['{options.outputPath}'], outputs: ['{options.outputPath}'],
options: { options: {
target: 'node',
compiler: 'tsc',
outputPath: 'dist/apps/my-node-app', outputPath: 'dist/apps/my-node-app',
main: 'apps/my-node-app/src/main.ts', main: 'apps/my-node-app/src/main.ts',
tsConfig: 'apps/my-node-app/tsconfig.app.json', tsConfig: 'apps/my-node-app/tsconfig.app.json',
@ -67,7 +69,7 @@ describe('app', () => {
}, },
}, },
serve: { serve: {
builder: '@nrwl/node:node', builder: '@nrwl/js:node',
options: { options: {
buildTarget: 'my-node-app:build', buildTarget: 'my-node-app:build',
}, },

View File

@ -40,9 +40,11 @@ function getBuildConfig(
options: NormalizedSchema options: NormalizedSchema
): TargetConfiguration { ): TargetConfiguration {
return { return {
executor: '@nrwl/node:webpack', executor: '@nrwl/webpack:webpack',
outputs: ['{options.outputPath}'], outputs: ['{options.outputPath}'],
options: { options: {
target: 'node',
compiler: 'tsc',
outputPath: joinPathFragments('dist', options.appProjectRoot), outputPath: joinPathFragments('dist', options.appProjectRoot),
main: joinPathFragments( main: joinPathFragments(
project.sourceRoot, project.sourceRoot,
@ -75,7 +77,7 @@ function getBuildConfig(
function getServeConfig(options: NormalizedSchema): TargetConfiguration { function getServeConfig(options: NormalizedSchema): TargetConfiguration {
return { return {
executor: '@nrwl/node:node', executor: '@nrwl/js:node',
options: { options: {
buildTarget: `${options.name}:build`, buildTarget: `${options.name}:build`,
}, },

View File

@ -0,0 +1,52 @@
import { readJson } from '@nrwl/devkit';
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
import update from './update-webpack-executor';
describe('Migration: @nrwl/webpack', () => {
it(`should update usage of webpack executor`, async () => {
let tree = createTreeWithEmptyV1Workspace();
tree.write(
'workspace.json',
JSON.stringify({
version: 2,
projects: {
myapp: {
root: 'apps/myapp',
sourceRoot: 'apps/myapp/src',
projectType: 'application',
targets: {
build: {
executor: '@nrwl/node:webpack',
options: {},
},
},
},
},
})
);
await update(tree);
expect(readJson(tree, 'workspace.json')).toEqual({
version: 2,
projects: {
myapp: {
root: 'apps/myapp',
sourceRoot: 'apps/myapp/src',
projectType: 'application',
targets: {
build: {
executor: '@nrwl/webpack:webpack',
options: {
compiler: 'tsc',
target: 'node',
},
},
},
},
},
});
});
});

View File

@ -0,0 +1,21 @@
import {
formatFiles,
getProjects,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
export default async function update(host: Tree) {
const projects = getProjects(host);
for (const [name, config] of projects.entries()) {
if (config?.targets?.build?.executor === '@nrwl/node:webpack') {
config.targets.build.executor = '@nrwl/webpack:webpack';
config.targets.build.options.target = 'node';
config.targets.build.options.compiler = 'tsc';
updateProjectConfiguration(host, name, config);
}
}
await formatFiles(host);
}

View File

@ -1 +0,0 @@
export const before = () => {};

View File

@ -1 +0,0 @@
export const after = () => {};

View File

@ -1,222 +0,0 @@
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import * as ts from 'typescript';
import type { Configuration, WebpackPluginInstance } from 'webpack';
import * as webpack from 'webpack';
import { loadTsTransformers } from './load-ts-transformers';
import { BuildBuilderOptions } from './types';
import CopyWebpackPlugin = require('copy-webpack-plugin');
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import { removeExt } from '@nrwl/workspace/src/utils/runtime-lint-utils';
export const OUT_FILENAME_TEMPLATE = '[name].js';
export function getBaseWebpackPartial(
options: BuildBuilderOptions
): Configuration {
const { options: compilerOptions } = readTsConfig(options.tsConfig);
const supportsEs2015 =
compilerOptions.target !== ts.ScriptTarget.ES3 &&
compilerOptions.target !== ts.ScriptTarget.ES5;
const mainFields = [...(supportsEs2015 ? ['es2015'] : []), 'module', 'main'];
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
const { compilerPluginHooks, hasPlugin } = loadTsTransformers(
options.transformers
);
const additionalEntryPoints =
options.additionalEntryPoints?.reduce(
(obj, current) => ({
...obj,
[current.entryName]: current.entryPath,
}),
{} as { [entryName: string]: string }
) ?? {};
const mainEntry = options.outputFileName
? removeExt(options.outputFileName)
: 'main';
const webpackConfig: Configuration = {
entry: {
[mainEntry]: [options.main],
...additionalEntryPoints,
},
devtool: options.sourceMap ? 'source-map' : false,
mode: options.optimization ? 'production' : 'development',
output: {
path: options.outputPath,
filename:
options.additionalEntryPoints?.length > 0
? OUT_FILENAME_TEMPLATE
: options.outputFileName,
hashFunction: 'xxhash64',
// Disabled for performance
pathinfo: false,
},
module: {
// Enabled for performance
unsafeCache: true,
rules: [
{
test: /\.([jt])sx?$/,
loader: require.resolve(`ts-loader`),
exclude: /node_modules/,
options: {
configFile: options.tsConfig,
transpileOnly: !hasPlugin,
// https://github.com/TypeStrong/ts-loader/pull/685
experimentalWatchApi: true,
getCustomTransformers: (program) => ({
before: compilerPluginHooks.beforeHooks.map((hook) =>
hook(program)
),
after: compilerPluginHooks.afterHooks.map((hook) =>
hook(program)
),
afterDeclarations: compilerPluginHooks.afterDeclarationsHooks.map(
(hook) => hook(program)
),
}),
},
},
],
},
resolve: {
extensions,
alias: getAliases(options),
plugins: [
new TsconfigPathsPlugin({
configFile: options.tsConfig,
extensions,
mainFields,
}) as never, // TODO: Remove never type when 'tsconfig-paths-webpack-plugin' types fixed
],
mainFields,
},
performance: {
hints: false,
},
plugins: [
new ForkTsCheckerWebpackPlugin({
// For watch mode, type errors should result in failure.
async: false,
typescript: {
configFile: options.tsConfig,
memoryLimit: options.memoryLimit || 2018,
},
}),
],
watch: options.watch,
watchOptions: {
// Delay the next rebuild from first file change, otherwise can lead to
// two builds on a single file change.
aggregateTimeout: 200,
poll: options.poll,
...options.watchOptions,
},
stats: getStatsConfig(options),
experiments: {
cacheUnaffected: true,
},
};
const extraPlugins: WebpackPluginInstance[] = [];
if (options.progress) {
extraPlugins.push(new webpack.ProgressPlugin());
}
if (options.extractLicenses) {
extraPlugins.push(
new LicenseWebpackPlugin({
stats: {
errors: false,
},
perChunkOutput: false,
outputFilename: `3rdpartylicenses.txt`,
}) as unknown as WebpackPluginInstance
);
}
// process asset entries
if (Array.isArray(options.assets) && options.assets.length > 0) {
const copyWebpackPluginInstance = 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,
},
};
}),
});
new CopyWebpackPlugin({
patterns: options.assets.map((asset: any) => {
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,
},
};
}),
});
extraPlugins.push(copyWebpackPluginInstance);
}
webpackConfig.plugins = [...webpackConfig.plugins, ...extraPlugins];
return webpackConfig;
}
function getAliases(options: BuildBuilderOptions): { [key: string]: string } {
return options.fileReplacements.reduce(
(aliases, replacement) => ({
...aliases,
[replacement.replace]: replacement.with,
}),
{}
);
}
function getStatsConfig(options: BuildBuilderOptions) {
return {
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,
};
}

View File

@ -1,40 +0,0 @@
import { loadTsTransformers } from './load-ts-transformers';
jest.mock('plugin-a');
jest.mock('plugin-b');
const mockRequireResolve = jest.fn((path) => path);
describe('loadTsTransformers', () => {
it('should return empty hooks if plugins is falsy', () => {
const result = loadTsTransformers(undefined);
assertEmptyResult(result);
});
it('should return empty hooks if plugins is []', () => {
const result = loadTsTransformers([]);
assertEmptyResult(result);
});
it('should return correct compiler hooks', () => {
const result = loadTsTransformers(
['plugin-a', 'plugin-b'],
mockRequireResolve as any
);
expect(result.hasPlugin).toEqual(true);
expect(result.compilerPluginHooks).toEqual({
beforeHooks: [expect.any(Function)],
afterHooks: [expect.any(Function)],
afterDeclarationsHooks: [],
});
});
function assertEmptyResult(result: ReturnType<typeof loadTsTransformers>) {
expect(result.hasPlugin).toEqual(false);
expect(result.compilerPluginHooks).toEqual({
beforeHooks: [],
afterHooks: [],
afterDeclarationsHooks: [],
});
}
});

View File

@ -1,86 +0,0 @@
import { logger } from '@nrwl/devkit';
import { join } from 'path';
import {
CompilerPlugin,
CompilerPluginHooks,
TransformerEntry,
TransformerPlugin,
} from './types';
export function loadTsTransformers(
plugins: TransformerEntry[],
moduleResolver: typeof require.resolve = require.resolve
): {
compilerPluginHooks: CompilerPluginHooks;
hasPlugin: boolean;
} {
const beforeHooks: CompilerPluginHooks['beforeHooks'] = [];
const afterHooks: CompilerPluginHooks['afterHooks'] = [];
const afterDeclarationsHooks: CompilerPluginHooks['afterDeclarationsHooks'] =
[];
if (!plugins || !plugins.length)
return {
compilerPluginHooks: {
beforeHooks,
afterHooks,
afterDeclarationsHooks,
},
hasPlugin: false,
};
const normalizedPlugins: TransformerPlugin[] = plugins.map((plugin) =>
typeof plugin === 'string' ? { name: plugin, options: {} } : plugin
);
const nodeModulePaths = [
join(process.cwd(), 'node_modules'),
...module.paths,
];
const pluginRefs: CompilerPlugin[] = normalizedPlugins.map(({ name }) => {
try {
const binaryPath = moduleResolver(name, {
paths: nodeModulePaths,
});
return require(binaryPath);
} catch (e) {
logger.warn(`"${name}" plugin could not be found!`);
return {};
}
});
for (let i = 0; i < pluginRefs.length; i++) {
const { name: pluginName, options: pluginOptions } = normalizedPlugins[i];
const { before, after, afterDeclarations } = pluginRefs[i];
if (!before && !after && !afterDeclarations) {
logger.warn(
`${pluginName} is not a Transformer Plugin. It does not provide neither before(), after(), nor afterDeclarations()`
);
continue;
}
if (before) {
beforeHooks.push(before.bind(before, pluginOptions));
}
if (after) {
afterHooks.push(after.bind(after, pluginOptions));
}
if (afterDeclarations) {
afterDeclarationsHooks.push(
afterDeclarations.bind(afterDeclarations, pluginOptions)
);
}
}
return {
compilerPluginHooks: {
beforeHooks,
afterHooks,
afterDeclarationsHooks,
},
hasPlugin: true,
};
}

View File

@ -1,137 +0,0 @@
import { ProjectGraph, ExecutorContext } from '@nrwl/devkit';
import { join } from 'path';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { GeneratePackageJsonWebpackPlugin } from './generate-package-json-webpack-plugin';
import { getNodeWebpackConfig } from './node.config';
import { BuildNodeBuilderOptions } from './types';
jest.mock('tsconfig-paths-webpack-plugin');
jest.mock('@nrwl/devkit', () => ({
get workspaceRoot() {
return join(__dirname, '../../../..');
},
}));
describe('getNodePartial', () => {
let context: ExecutorContext;
let projectGraph: ProjectGraph;
let input: BuildNodeBuilderOptions;
beforeEach(() => {
context = {
projectName: 'sample-project',
root: '/root',
cwd: '',
target: {} as any,
isVerbose: false,
workspace: {} as any,
};
projectGraph = {} as any;
input = {
main: 'main.ts',
outputPath: 'dist',
tsConfig: 'tsconfig.json',
externalDependencies: 'all',
fileReplacements: [],
statsJson: false,
};
(<any>TsconfigPathsPlugin).mockImplementation(
function MockPathsPlugin() {}
);
});
describe('unconditionally', () => {
it('should target commonjs', () => {
const result = getNodeWebpackConfig(context, projectGraph, input);
expect(result.output.libraryTarget).toEqual('commonjs');
});
it('should target node', () => {
const result = getNodeWebpackConfig(context, projectGraph, input);
expect(result.target).toEqual('node');
});
it('should not polyfill node apis', () => {
const result = getNodeWebpackConfig(context, projectGraph, input);
expect(result.node).toEqual(false);
});
});
describe('the optimization option when true', () => {
it('should minify', () => {
const result = getNodeWebpackConfig(context, projectGraph, {
...input,
optimization: true,
});
expect(result.optimization.minimize).toEqual(true);
expect(result.optimization.minimizer).toBeDefined();
});
it('should concatenate modules', () => {
const result = getNodeWebpackConfig(context, projectGraph, {
...input,
optimization: true,
});
expect(result.optimization.concatenateModules).toEqual(true);
});
});
describe('the externalDependencies option', () => {
it('should change all node_modules to commonjs imports', () => {
const result = getNodeWebpackConfig(context, projectGraph, input);
const callback = jest.fn();
result.externals[0](null, '@nestjs/core', callback);
expect(callback).toHaveBeenCalledWith(null, 'commonjs @nestjs/core');
});
it('should change given module names to commonjs imports but not others', () => {
const result = getNodeWebpackConfig(context, projectGraph, {
...input,
externalDependencies: ['module1'],
});
const callback = jest.fn();
result.externals[0]({ request: 'module1' }, callback);
expect(callback).toHaveBeenCalledWith(null, 'commonjs module1');
result.externals[0]({ request: '@nestjs/core' }, callback);
expect(callback).toHaveBeenCalledWith();
});
it('should not change any modules to commonjs imports', () => {
const result = getNodeWebpackConfig(context, projectGraph, {
...input,
externalDependencies: 'none',
});
expect(result.externals).not.toBeDefined();
});
});
describe('the generatePackageJson option', () => {
it('should add the GeneratePackageJsonWebpackPlugin plugin', () => {
const result = getNodeWebpackConfig(context, projectGraph, {
...input,
generatePackageJson: true,
});
expect(
result.plugins.find(
(plugin) => plugin instanceof GeneratePackageJsonWebpackPlugin
)
).toBeTruthy();
});
it('should not add the GeneratePackageJsonWebpackPlugin plugin', () => {
const result = getNodeWebpackConfig(context, projectGraph, {
...input,
generatePackageJson: false,
});
expect(
result.plugins.find(
(plugin) => plugin instanceof GeneratePackageJsonWebpackPlugin
)
).toBeFalsy();
});
});
});

View File

@ -1,74 +0,0 @@
import { ExecutorContext, ProjectGraph, workspaceRoot } from '@nrwl/devkit';
import { Configuration } from 'webpack';
import { merge } from 'webpack-merge';
import { getBaseWebpackPartial } from './config';
import { GeneratePackageJsonWebpackPlugin } from './generate-package-json-webpack-plugin';
import { BuildNodeBuilderOptions } from './types';
import nodeExternals = require('webpack-node-externals');
import TerserPlugin = require('terser-webpack-plugin');
function getNodePartial(
context: ExecutorContext,
projectGraph: ProjectGraph,
options: BuildNodeBuilderOptions
) {
const webpackConfig: Configuration = {
output: {
libraryTarget: 'commonjs',
},
target: 'node',
node: false,
};
if (options.optimization) {
webpackConfig.optimization = {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
mangle: false,
keep_classnames: true,
},
}),
],
concatenateModules: true,
};
}
if (options.externalDependencies === 'all') {
const modulesDir = `${workspaceRoot}/node_modules`;
webpackConfig.externals = [nodeExternals({ modulesDir })];
} else if (Array.isArray(options.externalDependencies)) {
webpackConfig.externals = [
function (context, callback: Function) {
if (options.externalDependencies.includes(context.request)) {
// not bundled
return callback(null, `commonjs ${context.request}`);
}
// bundled
callback();
},
];
}
if (options.generatePackageJson) {
webpackConfig.plugins ??= [];
webpackConfig.plugins.push(
new GeneratePackageJsonWebpackPlugin(context, projectGraph, options)
);
}
return webpackConfig;
}
export function getNodeWebpackConfig(
context: ExecutorContext,
projectGraph: ProjectGraph,
options: BuildNodeBuilderOptions
) {
return merge([
getBaseWebpackPartial(options),
getNodePartial(context, projectGraph, options),
]);
}

View File

@ -1,167 +0,0 @@
import { normalizeBuildOptions } from './normalize';
import { BuildNodeBuilderOptions } from './types';
import * as fs from 'fs';
describe('normalizeBuildOptions', () => {
let testOptions: BuildNodeBuilderOptions;
let root: string;
let sourceRoot: string;
let projectRoot: string;
beforeEach(() => {
testOptions = {
main: 'apps/nodeapp/src/main.ts',
tsConfig: 'apps/nodeapp/tsconfig.app.json',
outputPath: 'dist/apps/nodeapp',
fileReplacements: [
{
replace: 'apps/environment/environment.ts',
with: 'apps/environment/environment.prod.ts',
},
{
replace: 'module1.ts',
with: 'module2.ts',
},
],
assets: [],
statsJson: false,
externalDependencies: 'all',
};
root = '/root';
sourceRoot = 'apps/nodeapp/src';
projectRoot = 'apps/nodeapp';
});
it('should add the root', () => {
const result = normalizeBuildOptions(
testOptions,
root,
sourceRoot,
projectRoot
);
expect(result.root).toEqual('/root');
});
it('should resolve main from root', () => {
const result = normalizeBuildOptions(
testOptions,
root,
sourceRoot,
projectRoot
);
expect(result.main).toEqual('/root/apps/nodeapp/src/main.ts');
});
it('should resolve additional entries from root', () => {
const result = normalizeBuildOptions(
{
...testOptions,
additionalEntryPoints: [
{ entryName: 'test', entryPath: 'some/path.ts' },
],
},
root,
sourceRoot,
projectRoot
);
expect(result.additionalEntryPoints[0].entryPath).toEqual(
'/root/some/path.ts'
);
});
it('should resolve the output path', () => {
const result = normalizeBuildOptions(
testOptions,
root,
sourceRoot,
projectRoot
);
expect(result.outputPath).toEqual('/root/dist/apps/nodeapp');
});
it('should resolve the tsConfig path', () => {
const result = normalizeBuildOptions(
testOptions,
root,
sourceRoot,
projectRoot
);
expect(result.tsConfig).toEqual('/root/apps/nodeapp/tsconfig.app.json');
});
it('should normalize asset patterns', () => {
jest.spyOn(fs, 'statSync').mockReturnValue({
isDirectory: () => true,
} as any);
const result = normalizeBuildOptions(
{
...testOptions,
root,
assets: [
'apps/nodeapp/src/assets',
{
input: 'outsideproj',
output: 'output',
glob: '**/*',
ignore: ['**/*.json'],
},
],
},
root,
sourceRoot,
projectRoot
);
expect(result.assets).toEqual([
{
input: '/root/apps/nodeapp/src/assets',
output: 'assets',
glob: '**/*',
},
{
input: '/root/outsideproj',
output: 'output',
glob: '**/*',
ignore: ['**/*.json'],
},
]);
});
it('should resolve the file replacement paths', () => {
const result = normalizeBuildOptions(
testOptions,
root,
sourceRoot,
projectRoot
);
expect(result.fileReplacements).toEqual([
{
replace: '/root/apps/environment/environment.ts',
with: '/root/apps/environment/environment.prod.ts',
},
{
replace: '/root/module1.ts',
with: '/root/module2.ts',
},
]);
});
it('should resolve outputFileName correctly', () => {
const result = normalizeBuildOptions(
testOptions,
root,
sourceRoot,
projectRoot
);
expect(result.outputFileName).toEqual('main.js');
});
it('should resolve outputFileName to "main.js" if not passed in', () => {
const result = normalizeBuildOptions(
{ ...testOptions, outputFileName: 'index.js' },
root,
sourceRoot,
projectRoot
);
expect(result.outputFileName).toEqual('index.js');
});
});

View File

@ -1,114 +0,0 @@
import type {
CustomTransformerFactory,
Node,
Program,
TransformerFactory as TypescriptTransformerFactory,
} from 'typescript';
export interface FileReplacement {
replace: string;
with: string;
}
export interface OptimizationOptions {
scripts: boolean;
styles: boolean;
}
export interface SourceMapOptions {
scripts: boolean;
styles: boolean;
vendors: boolean;
hidden: boolean;
}
type TransformerFactory =
| TypescriptTransformerFactory<Node>
| CustomTransformerFactory;
export interface TransformerPlugin {
name: string;
options: Record<string, unknown>;
}
export type TransformerEntry = string | TransformerPlugin;
export interface CompilerPlugin {
before?: (
options?: Record<string, unknown>,
program?: Program
) => TransformerFactory;
after?: (
options?: Record<string, unknown>,
program?: Program
) => TransformerFactory;
afterDeclarations?: (
options?: Record<string, unknown>,
program?: Program
) => TransformerFactory;
}
export interface CompilerPluginHooks {
beforeHooks: Array<(program?: Program) => TransformerFactory>;
afterHooks: Array<(program?: Program) => TransformerFactory>;
afterDeclarationsHooks: Array<(program?: Program) => TransformerFactory>;
}
export interface AdditionalEntryPoint {
entryName: string;
entryPath: string;
}
export interface WebpackWatchOptions {
aggregateTimeout?: number;
ignored?: Array<string> | string;
poll?: number;
followSymlinks?: boolean;
stdin?: boolean;
}
export interface BuildBuilderOptions {
main: string;
outputPath: string;
tsConfig: string;
watch?: boolean;
watchOptions?: WebpackWatchOptions;
sourceMap?: boolean | SourceMapOptions;
optimization?: boolean | OptimizationOptions;
maxWorkers?: number;
memoryLimit?: number;
poll?: number;
fileReplacements: FileReplacement[];
assets?: any[];
progress?: boolean;
statsJson?: boolean;
extractLicenses?: boolean;
verbose?: boolean;
webpackConfig?: string | string[];
root?: string;
sourceRoot?: string;
projectRoot?: string;
transformers?: TransformerEntry[];
additionalEntryPoints?: AdditionalEntryPoint[];
outputFileName?: string;
}
export interface BuildNodeBuilderOptions extends BuildBuilderOptions {
optimization?: boolean;
sourceMap?: boolean;
externalDependencies: 'all' | 'none' | string[];
buildLibsFromSource?: boolean;
generatePackageJson?: boolean;
deleteOutputPath?: boolean;
}
export interface NormalizedBuildNodeBuilderOptions
extends BuildNodeBuilderOptions {
webpackConfig: string[];
}

View File

@ -39,6 +39,7 @@
"@nrwl/linter": "file:../linter", "@nrwl/linter": "file:../linter",
"@nrwl/storybook": "file:../storybook", "@nrwl/storybook": "file:../storybook",
"@nrwl/web": "file:../web", "@nrwl/web": "file:../web",
"@nrwl/webpack": "file:../webpack",
"@nrwl/workspace": "file:../workspace", "@nrwl/workspace": "file:../workspace",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@svgr/webpack": "^6.1.2", "@svgr/webpack": "^6.1.2",

View File

@ -16,9 +16,9 @@ import {
Target, Target,
workspaceRoot, workspaceRoot,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import type { WebWebpackExecutorOptions } from '@nrwl/web/src/executors/webpack/webpack.impl'; import type { WebpackExecutorOptions } from '@nrwl/webpack/src/executors/webpack/schema';
import { normalizeWebBuildOptions } from '@nrwl/web/src/utils/normalize'; import { normalizeOptions } from '@nrwl/webpack/src/executors/webpack/lib/normalize-options';
import { getWebConfig } from '@nrwl/web/src/utils/web.config'; import { getWebpackConfig } from '@nrwl/webpack/src/executors/webpack/lib/get-webpack-config';
import { buildBaseWebpackConfig } from './webpack-fallback'; import { buildBaseWebpackConfig } from './webpack-fallback';
/** /**
@ -108,8 +108,8 @@ export function nxComponentTestingPreset(
function withSchemaDefaults( function withSchemaDefaults(
target: Target, target: Target,
context: ExecutorContext context: ExecutorContext
): WebWebpackExecutorOptions { ): WebpackExecutorOptions {
const options = readTargetOptions<WebWebpackExecutorOptions>(target, context); const options = readTargetOptions<WebpackExecutorOptions>(target, context);
options.compiler ??= 'babel'; options.compiler ??= 'babel';
options.deleteOutputPath ??= true; options.deleteOutputPath ??= true;
@ -149,18 +149,16 @@ function buildTargetWebpack(
Has component config? ${!!ctProjectConfig} Has component config? ${!!ctProjectConfig}
`); `);
} }
const context = createExecutorContext(
graph,
buildableProjectConfig.targets,
parsed.project,
parsed.target,
parsed.target
);
const options = normalizeWebBuildOptions( const options = normalizeOptions(
withSchemaDefaults( withSchemaDefaults(parsed, context),
parsed,
createExecutorContext(
graph,
buildableProjectConfig.targets,
parsed.project,
parsed.target,
parsed.target
)
),
workspaceRoot, workspaceRoot,
buildableProjectConfig.sourceRoot! buildableProjectConfig.sourceRoot!
); );
@ -171,13 +169,10 @@ function buildTargetWebpack(
: options.optimization && options.optimization.scripts : options.optimization && options.optimization.scripts
? options.optimization.scripts ? options.optimization.scripts
: false; : false;
return getWebConfig(
workspaceRoot, return getWebpackConfig(context, options, true, isScriptOptimizeOn, {
ctProjectConfig.root, root: ctProjectConfig.root,
ctProjectConfig.sourceRoot, sourceRoot: ctProjectConfig.sourceRoot,
options, configuration: parsed.configuration,
true, });
isScriptOptimizeOn,
parsed.configuration
);
} }

View File

@ -1,4 +1,4 @@
import { getCSSModuleLocalIdent } from '@nrwl/web/src/utils/web.config'; import { getCSSModuleLocalIdent } from '@nrwl/webpack/src/executors/webpack/lib/get-webpack-config';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { Configuration } from 'webpack'; import { Configuration } from 'webpack';

View File

@ -1,7 +1,7 @@
import { webpack } from './index'; import { webpack } from './index';
import { join } from 'path'; import { join } from 'path';
jest.mock('@nrwl/web/src/utils/web.config', () => { jest.mock('@nrwl/webpack/src/executors/webpack/lib/get-webpack-config', () => {
return { return {
getStylesPartial: () => ({}), getStylesPartial: () => ({}),
}; };

View File

@ -1,17 +1,22 @@
import { import {
ExecutorContext,
joinPathFragments, joinPathFragments,
readJsonFile,
logger, logger,
ProjectGraph,
readJsonFile,
readNxJson,
TargetConfiguration,
workspaceRoot, workspaceRoot,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { getBaseWebpackPartial } from '@nrwl/web/src/utils/config'; import { getBaseWebpackPartial } from '@nrwl/webpack/src/utils/config';
import { getStylesPartial } from '@nrwl/web/src/utils/web.config'; import { getStylesPartial } from '@nrwl/webpack/src/executors/webpack/lib/get-webpack-config';
import { checkAndCleanWithSemver } from '@nrwl/workspace/src/utilities/version-utils'; import { checkAndCleanWithSemver } from '@nrwl/workspace/src/utilities/version-utils';
import { join } from 'path'; import { join } from 'path';
import { gte } from 'semver'; import { gte } from 'semver';
import { Configuration, WebpackPluginInstance, DefinePlugin } from 'webpack'; import { Configuration, DefinePlugin, WebpackPluginInstance } from 'webpack';
import * as mergeWebpack from 'webpack-merge'; import * as mergeWebpack from 'webpack-merge';
import { mergePlugins } from './merge-plugins'; import { mergePlugins } from './merge-plugins';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
const reactWebpackConfig = require('../webpack'); const reactWebpackConfig = require('../webpack');

View File

@ -1,7 +1,6 @@
import { ExecutorContext, logger, runExecutor } from '@nrwl/devkit'; import { ExecutorContext, logger, runExecutor } from '@nrwl/devkit';
import devServerExecutor, { import devServerExecutor from '@nrwl/webpack/src/executors/dev-server/dev-server.impl';
WebDevServerOptions, import { WebDevServerOptions } from '@nrwl/webpack/src/executors/dev-server/schema';
} from '@nrwl/web/src/executors/dev-server/dev-server.impl';
import { join } from 'path'; import { join } from 'path';
import { import {
combineAsyncIterators, combineAsyncIterators,

View File

@ -324,7 +324,7 @@ describe('app', () => {
const workspaceJson = getProjects(appTree); const workspaceJson = getProjects(appTree);
const targetConfig = workspaceJson.get('my-app').targets; const targetConfig = workspaceJson.get('my-app').targets;
expect(targetConfig.build.executor).toEqual('@nrwl/web:webpack'); expect(targetConfig.build.executor).toEqual('@nrwl/webpack:webpack');
expect(targetConfig.build.outputs).toEqual(['{options.outputPath}']); expect(targetConfig.build.outputs).toEqual(['{options.outputPath}']);
expect(targetConfig.build.options).toEqual({ expect(targetConfig.build.options).toEqual({
compiler: 'babel', compiler: 'babel',
@ -360,7 +360,7 @@ describe('app', () => {
const workspaceJson = getProjects(appTree); const workspaceJson = getProjects(appTree);
const targetConfig = workspaceJson.get('my-app').targets; const targetConfig = workspaceJson.get('my-app').targets;
expect(targetConfig.serve.executor).toEqual('@nrwl/web:dev-server'); expect(targetConfig.serve.executor).toEqual('@nrwl/webpack:dev-server');
expect(targetConfig.serve.options).toEqual({ expect(targetConfig.serve.options).toEqual({
buildTarget: 'my-app:build', buildTarget: 'my-app:build',
hmr: true, hmr: true,

View File

@ -25,7 +25,7 @@ import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-ser
import reactInitGenerator from '../init/init'; import reactInitGenerator from '../init/init';
import { lintProjectGenerator } from '@nrwl/linter'; import { lintProjectGenerator } from '@nrwl/linter';
import { swcCoreVersion } from '@nrwl/js/src/utils/versions'; import { swcCoreVersion } from '@nrwl/js/src/utils/versions';
import { swcLoaderVersion } from '@nrwl/web/src/utils/versions'; import { swcLoaderVersion } from '@nrwl/webpack/src/utils/versions';
async function addLinting(host: Tree, options: NormalizedSchema) { async function addLinting(host: Tree, options: NormalizedSchema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];

View File

@ -36,7 +36,7 @@ function maybeJs(options: NormalizedSchema, path: string): string {
function createBuildTarget(options: NormalizedSchema): TargetConfiguration { function createBuildTarget(options: NormalizedSchema): TargetConfiguration {
return { return {
executor: '@nrwl/web:webpack', executor: '@nrwl/webpack:webpack',
outputs: ['{options.outputPath}'], outputs: ['{options.outputPath}'],
defaultConfiguration: 'production', defaultConfiguration: 'production',
options: { options: {
@ -102,7 +102,7 @@ function createBuildTarget(options: NormalizedSchema): TargetConfiguration {
function createServeTarget(options: NormalizedSchema): TargetConfiguration { function createServeTarget(options: NormalizedSchema): TargetConfiguration {
return { return {
executor: '@nrwl/web:dev-server', executor: '@nrwl/webpack:dev-server',
defaultConfiguration: 'development', defaultConfiguration: 'development',
options: { options: {
buildTarget: `${options.projectName}:build`, buildTarget: `${options.projectName}:build`,

View File

@ -16,7 +16,7 @@ export async function updateProjectConfig(
const found = await findBuildConfig(tree, { const found = await findBuildConfig(tree, {
project: options.project, project: options.project,
buildTarget: options.buildTarget, buildTarget: options.buildTarget,
validExecutorNames: new Set<string>(['@nrwl/web:webpack']), validExecutorNames: new Set<string>(['@nrwl/webpack:webpack']),
}); });
assetValidConfig(found.config); assetValidConfig(found.config);

View File

@ -8,7 +8,7 @@ export function updateProject(
config: ProjectConfiguration, config: ProjectConfiguration,
options: SetupTailwindOptions options: SetupTailwindOptions
) { ) {
if (config?.targets?.build?.executor === '@nrwl/web:webpack') { if (config?.targets?.build?.executor === '@nrwl/webpack:webpack') {
config.targets.build.options ??= {}; config.targets.build.options ??= {};
config.targets.build.options.postcssConfig = joinPathFragments( config.targets.build.options.postcssConfig = joinPathFragments(
config.root, config.root,

View File

@ -49,7 +49,7 @@ describe('setup-tailwind', () => {
sourceRoot: 'apps/example/src', sourceRoot: 'apps/example/src',
targets: { targets: {
build: { build: {
executor: '@nrwl/web:webpack', executor: '@nrwl/webpack:webpack',
options: {}, options: {},
}, },
}, },

View File

@ -1,13 +1,12 @@
import { import {
Tree,
readProjectConfiguration, readProjectConfiguration,
Tree,
updateProjectConfiguration, updateProjectConfiguration,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { WebRollupOptions } from '@nrwl/web/src/executors/rollup/schema';
import { forEachExecutorOptions } from '@nrwl/workspace/src/utilities/executor-options-utils'; import { forEachExecutorOptions } from '@nrwl/workspace/src/utilities/executor-options-utils';
export async function updateExternalEmotionJsxRuntime(tree: Tree) { export async function updateExternalEmotionJsxRuntime(tree: Tree) {
forEachExecutorOptions<WebRollupOptions>( forEachExecutorOptions<any>(
tree, tree,
'@nrwl/web:rollup', '@nrwl/web:rollup',
(options: any, projectName, targetName, configurationName) => { (options: any, projectName, targetName, configurationName) => {

View File

@ -62,6 +62,12 @@
"version": "13.8.0-beta.1", "version": "13.8.0-beta.1",
"description": "Add a postcss config option to apps to load a single config file for all libs", "description": "Add a postcss config option to apps to load a single config file for all libs",
"factory": "./src/migrations/update-13-8-0/add-postcss-config-option" "factory": "./src/migrations/update-13-8-0/add-postcss-config-option"
},
"update-webpack-executor": {
"cli": "nx",
"version": "14.7.5-beta.1",
"description": "Update usages of webpack executors to @nrwl/webpack",
"factory": "./src/migrations/update-14-7-5/update-webpack-executor"
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {

View File

@ -42,46 +42,28 @@
"@nrwl/jest": "file:../jest", "@nrwl/jest": "file:../jest",
"@nrwl/js": "file:../js", "@nrwl/js": "file:../js",
"@nrwl/linter": "file:../linter", "@nrwl/linter": "file:../linter",
"@nrwl/webpack": "file:../webpack",
"@nrwl/workspace": "file:../workspace", "@nrwl/workspace": "file:../workspace",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^20.0.0", "@rollup/plugin-commonjs": "^20.0.0",
"@rollup/plugin-image": "^2.1.0", "@rollup/plugin-image": "^2.1.0",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.4", "@rollup/plugin-node-resolve": "^13.0.4",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.9",
"babel-loader": "^8.2.2",
"babel-plugin-const-enum": "^1.0.1", "babel-plugin-const-enum": "^1.0.1",
"babel-plugin-macros": "^2.8.0", "babel-plugin-macros": "^2.8.0",
"babel-plugin-transform-async-to-promises": "^0.8.15", "babel-plugin-transform-async-to-promises": "^0.8.15",
"babel-plugin-transform-typescript-metadata": "^0.3.1", "babel-plugin-transform-typescript-metadata": "^0.3.1",
"browserslist": "^4.16.6",
"bytes": "^3.1.0", "bytes": "^3.1.0",
"caniuse-lite": "^1.0.30001251",
"chalk": "4.1.0", "chalk": "4.1.0",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"copy-webpack-plugin": "^10.2.4",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"css-loader": "^6.4.0",
"css-minimizer-webpack-plugin": "^3.4.1",
"enhanced-resolve": "^5.8.3",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "7.2.13",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"http-server": "14.1.0", "http-server": "14.1.0",
"identity-obj-proxy": "3.0.0",
"ignore": "^5.0.4", "ignore": "^5.0.4",
"less": "3.12.2", "less": "3.12.2",
"less-loader": "^10.1.0",
"license-webpack-plugin": "^4.0.2",
"loader-utils": "1.2.3",
"mini-css-extract-plugin": "~2.4.7",
"parse5": "4.0.0",
"parse5-html-rewriting-stream": "6.0.1",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"postcss-import": "~14.1.0", "postcss-import": "~14.1.0",
"postcss-loader": "^6.1.1",
"raw-loader": "^4.0.2",
"react-refresh": "^0.10.0", "react-refresh": "^0.10.0",
"rollup": "^2.56.2", "rollup": "^2.56.2",
"rollup-plugin-copy": "^3.4.0", "rollup-plugin-copy": "^3.4.0",
@ -90,23 +72,11 @@
"rollup-plugin-typescript2": "^0.31.1", "rollup-plugin-typescript2": "^0.31.1",
"rxjs": "^6.5.4", "rxjs": "^6.5.4",
"sass": "^1.42.1", "sass": "^1.42.1",
"sass-loader": "^12.2.0",
"semver": "7.3.4", "semver": "7.3.4",
"source-map": "0.7.3", "source-map": "0.7.3",
"source-map-loader": "^3.0.0",
"style-loader": "^3.3.0",
"stylus": "^0.55.0", "stylus": "^0.55.0",
"stylus-loader": "^6.2.0",
"terser-webpack-plugin": "^5.3.3",
"ts-loader": "^9.3.1",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"tsconfig-paths": "^3.9.0", "tsconfig-paths": "^3.9.0",
"tsconfig-paths-webpack-plugin": "3.5.2", "tslib": "^2.3.0"
"tslib": "^2.3.0",
"webpack": "^5.58.1",
"webpack-dev-server": "^4.9.3",
"webpack-merge": "^5.8.0",
"webpack-sources": "^3.2.3",
"webpack-subresource-integrity": "^5.1.0"
} }
} }

View File

@ -1,134 +1,8 @@
import * as webpack from 'webpack'; /**
import { * This is here for backwards-compat.
ExecutorContext, * TODO(jack): remove in Nx 16.
parseTargetString, */
readTargetOptions, import { devServerExecutor, WebDevServerOptions } from '@nrwl/webpack';
} from '@nrwl/devkit';
import { eachValueFrom } from '@nrwl/devkit/src/utils/rxjs-for-await'; export { devServerExecutor, WebDevServerOptions };
import { map, tap } from 'rxjs/operators'; export default devServerExecutor;
import * as WebpackDevServer from 'webpack-dev-server';
import { normalizeWebBuildOptions } from '../../utils/normalize';
import { WebWebpackExecutorOptions } from '../webpack/webpack.impl';
import { getDevServerConfig } from '../../utils/devserver.config';
import {
calculateProjectDependencies,
createTmpTsConfig,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { getEmittedFiles, runWebpackDevServer } from '../../utils/run-webpack';
import { resolveCustomWebpackConfig } from '../../utils/webpack/custom-webpack';
export interface WebDevServerOptions {
host: string;
port: number;
publicHost?: string;
ssl: boolean;
sslKey?: string;
sslCert?: string;
proxyConfig?: string;
buildTarget: string;
open: boolean;
liveReload: boolean;
hmr: boolean;
watch: boolean;
allowedHosts: string;
maxWorkers?: number;
memoryLimit?: number;
baseHref?: string;
}
export default async function* devServerExecutor(
serveOptions: WebDevServerOptions,
context: ExecutorContext
) {
const { root: projectRoot, sourceRoot } =
context.workspace.projects[context.projectName];
const buildOptions = normalizeWebBuildOptions(
getBuildOptions(serveOptions, context),
context.root,
sourceRoot
);
if (!buildOptions.buildLibsFromSource) {
const { target, dependencies } = calculateProjectDependencies(
context.projectGraph,
context.root,
context.projectName,
'build', // should be generalized
context.configurationName
);
buildOptions.tsConfig = createTmpTsConfig(
buildOptions.tsConfig,
context.root,
target.data.root,
dependencies
);
}
let webpackConfig = getDevServerConfig(
context.root,
projectRoot,
sourceRoot,
buildOptions,
serveOptions
);
if (buildOptions.webpackConfig) {
let customWebpack = resolveCustomWebpackConfig(
buildOptions.webpackConfig,
buildOptions.tsConfig
);
if (typeof customWebpack.then === 'function') {
customWebpack = await customWebpack;
}
webpackConfig = await customWebpack(webpackConfig, {
buildOptions,
configuration: serveOptions.buildTarget.split(':')[2],
});
}
return yield* eachValueFrom(
runWebpackDevServer(webpackConfig, webpack, WebpackDevServer).pipe(
tap(({ stats }) => {
console.info(stats.toString((webpackConfig as any).stats));
}),
map(({ baseUrl, stats }) => {
return {
baseUrl,
emittedFiles: getEmittedFiles(stats),
success: !stats.hasErrors(),
};
})
)
);
}
function getBuildOptions(
options: WebDevServerOptions,
context: ExecutorContext
): WebWebpackExecutorOptions {
const target = parseTargetString(options.buildTarget);
const overrides: Partial<WebWebpackExecutorOptions> = {
watch: false,
};
if (options.maxWorkers) {
overrides.maxWorkers = options.maxWorkers;
}
if (options.memoryLimit) {
overrides.memoryLimit = options.memoryLimit;
}
if (options.baseHref) {
overrides.baseHref = options.baseHref;
}
const buildOptions = readTargetOptions(target, context);
return {
...buildOptions,
...overrides,
};
}

View File

@ -1,7 +1,7 @@
import { dirname } from 'path'; import { dirname } from 'path';
import { AssetGlobPattern } from '../../../utils/shared-models'; import { AssetGlobPattern } from '@nrwl/webpack';
import { normalizeAssets, normalizePluginPath } from '../../../utils/normalize'; import { normalizeAssets, normalizePluginPath } from '@nrwl/webpack';
import { WebRollupOptions } from '../schema'; import { WebRollupOptions } from '../schema';
export interface NormalizedWebRollupOptions extends WebRollupOptions { export interface NormalizedWebRollupOptions extends WebRollupOptions {

View File

@ -15,7 +15,7 @@ import {
} from '@nrwl/workspace/src/utilities/buildable-libs-utils'; } from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import resolve from '@rollup/plugin-node-resolve'; import resolve from '@rollup/plugin-node-resolve';
import { AssetGlobPattern } from '../../utils/shared-models'; import { AssetGlobPattern } from '@nrwl/webpack';
import { WebRollupOptions } from './schema'; import { WebRollupOptions } from './schema';
import { runRollup } from './lib/run-rollup'; import { runRollup } from './lib/run-rollup';
import { import {

View File

@ -1,5 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit'; import { convertNxExecutor } from '@nrwl/devkit';
import { run } from './webpack.impl'; import { webpackExecutor } from './webpack.impl';
export default convertNxExecutor(run); export default convertNxExecutor(webpackExecutor);

View File

@ -1,249 +1,11 @@
import { ExecutorContext, logger } from '@nrwl/devkit'; /**
import { eachValueFrom } from '@nrwl/devkit/src/utils/rxjs-for-await'; * This is here for backwards-compat.
import type { Configuration, Stats } from 'webpack'; * TODO(jack): remove in Nx 16.
import { from, of } from 'rxjs'; */
import { import {
bufferCount, webpackExecutor,
mergeMap, WebpackExecutorOptions as WebWebpackExecutorOptions,
mergeScan, } from '@nrwl/webpack';
switchMap,
tap,
} from 'rxjs/operators';
import { execSync } from 'child_process';
import { Range, satisfies } from 'semver';
import { basename, join } from 'path';
import {
calculateProjectDependencies,
createTmpTsConfig,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { normalizeWebBuildOptions } from '../../utils/normalize'; export { webpackExecutor, WebWebpackExecutorOptions };
import { getWebConfig } from '../../utils/web.config'; export default webpackExecutor;
import type { BuildBuilderOptions } from '../../utils/shared-models';
import { ExtraEntryPoint } from '../../utils/shared-models';
import { getEmittedFiles, runWebpack } from '../../utils/run-webpack';
import { BuildBrowserFeatures } from '../../utils/webpack/build-browser-features';
import { deleteOutputDir } from '../../utils/fs';
import {
CrossOriginValue,
writeIndexHtml,
} from '../../utils/webpack/write-index-html';
import { resolveCustomWebpackConfig } from '../../utils/webpack/custom-webpack';
export interface WebWebpackExecutorOptions extends BuildBuilderOptions {
index: string;
budgets?: any[];
baseHref?: string;
deployUrl?: string;
crossOrigin?: CrossOriginValue;
polyfills?: string;
es2015Polyfills?: string;
scripts: ExtraEntryPoint[];
styles: ExtraEntryPoint[];
vendorChunk?: boolean;
commonChunk?: boolean;
runtimeChunk?: boolean;
namedChunks?: boolean;
stylePreprocessorOptions?: any;
subresourceIntegrity?: boolean;
verbose?: boolean;
buildLibsFromSource?: boolean;
deleteOutputPath?: boolean;
generateIndexHtml?: boolean;
postcssConfig?: string;
extractCss?: boolean;
}
async function getWebpackConfigs(
options: WebWebpackExecutorOptions,
context: ExecutorContext
): Promise<Configuration[]> {
const metadata = context.workspace.projects[context.projectName];
const sourceRoot = metadata.sourceRoot;
const projectRoot = metadata.root;
options = normalizeWebBuildOptions(options, context.root, sourceRoot);
const isScriptOptimizeOn =
typeof options.optimization === 'boolean'
? options.optimization
: options.optimization && options.optimization.scripts
? options.optimization.scripts
: false;
const tsConfig = readTsConfig(options.tsConfig);
const scriptTarget = tsConfig.options.target;
const buildBrowserFeatures = new BuildBrowserFeatures(
projectRoot,
scriptTarget
);
let customWebpack = null;
if (options.webpackConfig) {
customWebpack = resolveCustomWebpackConfig(
options.webpackConfig,
options.tsConfig
);
if (typeof customWebpack.then === 'function') {
customWebpack = await customWebpack;
}
}
return await Promise.all(
[
// ESM build for modern browsers.
getWebConfig(
context.root,
projectRoot,
sourceRoot,
options,
true,
isScriptOptimizeOn,
context.configurationName
),
// ES5 build for legacy browsers.
isScriptOptimizeOn && buildBrowserFeatures.isDifferentialLoadingNeeded()
? getWebConfig(
context.root,
projectRoot,
sourceRoot,
options,
false,
isScriptOptimizeOn,
context.configurationName
)
: undefined,
]
.filter(Boolean)
.map(async (config) => {
if (customWebpack) {
return await customWebpack(config, {
options,
configuration: context.configurationName,
});
} else {
return config;
}
})
);
}
export async function* run(
options: WebWebpackExecutorOptions,
context: ExecutorContext
) {
// Node versions 12.2-12.8 has a bug where prod builds will hang for 2-3 minutes
// after the program exits.
const nodeVersion = execSync(`node --version`).toString('utf-8').trim();
const supportedRange = new Range('10 || >=12.9');
if (!satisfies(nodeVersion, supportedRange)) {
throw new Error(
`Node version ${nodeVersion} is not supported. Supported range is "${supportedRange.raw}".`
);
}
const isScriptOptimizeOn =
typeof options.optimization === 'boolean'
? options.optimization
: options.optimization && options.optimization.scripts
? options.optimization.scripts
: false;
process.env.NODE_ENV ||= isScriptOptimizeOn ? 'production' : 'development';
const metadata = context.workspace.projects[context.projectName];
if (options.compiler === 'swc') {
try {
require.resolve('swc-loader');
require.resolve('@swc/core');
} catch {
logger.error(
`Missing SWC dependencies: @swc/core, swc-loader. Make sure you install them first.`
);
return { success: false };
}
}
if (!options.buildLibsFromSource && context.targetName) {
const { dependencies } = calculateProjectDependencies(
context.projectGraph,
context.root,
context.projectName,
context.targetName,
context.configurationName
);
options.tsConfig = createTmpTsConfig(
join(context.root, options.tsConfig),
context.root,
metadata.root,
dependencies
);
}
// Delete output path before bundling
if (options.deleteOutputPath) {
deleteOutputDir(context.root, options.outputPath);
}
const configs = await getWebpackConfigs(options, context);
return yield* eachValueFrom(
from(configs).pipe(
mergeMap((config) => (Array.isArray(config) ? from(config) : of(config))),
// Run build sequentially and bail when first one fails.
mergeScan(
(acc, config) => {
if (!acc.hasErrors()) {
return runWebpack(config).pipe(
tap((stats) => {
console.info(stats.toString(config.stats));
})
);
} else {
return of();
}
},
{ hasErrors: () => false } as Stats,
1
),
// Collect build results as an array.
bufferCount(configs.length),
switchMap(async ([result1, result2]) => {
const success =
result1 && !result1.hasErrors() && (!result2 || !result2.hasErrors());
const emittedFiles1 = getEmittedFiles(result1);
const emittedFiles2 = result2 ? getEmittedFiles(result2) : [];
if (options.generateIndexHtml) {
await writeIndexHtml({
crossOrigin: options.crossOrigin,
sri: options.subresourceIntegrity,
outputPath: join(options.outputPath, basename(options.index)),
indexPath: join(context.root, options.index),
files: emittedFiles1.filter((x) => x.extension === '.css'),
noModuleFiles: emittedFiles2,
moduleFiles: emittedFiles1,
baseHref: options.baseHref,
deployUrl: options.deployUrl,
scripts: options.scripts,
styles: options.styles,
});
}
return { success, emittedFiles: [...emittedFiles1, ...emittedFiles2] };
})
)
);
}
export default run;

View File

@ -299,7 +299,7 @@ describe('app', () => {
}); });
const workspaceJson = readJson(tree, 'workspace.json'); const workspaceJson = readJson(tree, 'workspace.json');
const architectConfig = workspaceJson.projects['my-app'].architect; const architectConfig = workspaceJson.projects['my-app'].architect;
expect(architectConfig.build.builder).toEqual('@nrwl/web:webpack'); expect(architectConfig.build.builder).toEqual('@nrwl/webpack:webpack');
expect(architectConfig.build.outputs).toEqual(['{options.outputPath}']); expect(architectConfig.build.outputs).toEqual(['{options.outputPath}']);
expect(architectConfig.build.options).toEqual({ expect(architectConfig.build.options).toEqual({
compiler: 'babel', compiler: 'babel',
@ -336,7 +336,7 @@ describe('app', () => {
}); });
const workspaceJson = readJson(tree, 'workspace.json'); const workspaceJson = readJson(tree, 'workspace.json');
const architectConfig = workspaceJson.projects['my-app'].architect; const architectConfig = workspaceJson.projects['my-app'].architect;
expect(architectConfig.serve.builder).toEqual('@nrwl/web:dev-server'); expect(architectConfig.serve.builder).toEqual('@nrwl/webpack:dev-server');
expect(architectConfig.serve.options).toEqual({ expect(architectConfig.serve.options).toEqual({
buildTarget: 'my-app:build', buildTarget: 'my-app:build',
}); });

View File

@ -1,3 +1,5 @@
import { join } from 'path';
import { webpackProjectGenerator } from '@nrwl/webpack';
import { cypressProjectGenerator } from '@nrwl/cypress'; import { cypressProjectGenerator } from '@nrwl/cypress';
import { import {
addDependenciesToPackageJson, addDependenciesToPackageJson,
@ -10,10 +12,11 @@ import {
joinPathFragments, joinPathFragments,
names, names,
offsetFromRoot, offsetFromRoot,
ProjectConfiguration, readProjectConfiguration,
readWorkspaceConfiguration, readWorkspaceConfiguration,
TargetConfiguration, TargetConfiguration,
Tree, Tree,
updateProjectConfiguration,
updateWorkspaceConfiguration, updateWorkspaceConfiguration,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { jestProjectGenerator } from '@nrwl/jest'; import { jestProjectGenerator } from '@nrwl/jest';
@ -22,11 +25,7 @@ import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { getRelativePathToRootTsConfig } from '@nrwl/workspace/src/utilities/typescript'; import { getRelativePathToRootTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { join } from 'path';
import { WebWebpackExecutorOptions } from '../../executors/webpack/webpack.impl';
import { swcLoaderVersion } from '../../utils/versions'; import { swcLoaderVersion } from '../../utils/versions';
import { webInitGenerator } from '../init/init'; import { webInitGenerator } from '../init/init';
import { Schema } from './schema'; import { Schema } from './schema';
@ -54,29 +53,43 @@ function createApplicationFiles(tree: Tree, options: NormalizedSchema) {
} }
} }
function addBuildTarget( async function setupBundler(tree: Tree, options: NormalizedSchema) {
project: ProjectConfiguration, const main = joinPathFragments(options.appProjectRoot, 'src/main.ts');
options: NormalizedSchema const tsConfig = joinPathFragments(
): ProjectConfiguration { options.appProjectRoot,
const buildOptions: WebWebpackExecutorOptions = { 'tsconfig.app.json'
outputPath: joinPathFragments('dist', options.appProjectRoot), );
compiler: options.compiler ?? 'babel', const assets = [
index: joinPathFragments(options.appProjectRoot, 'src/index.html'), joinPathFragments(options.appProjectRoot, 'src/favicon.ico'),
baseHref: '/', joinPathFragments(options.appProjectRoot, 'src/assets'),
main: joinPathFragments(options.appProjectRoot, 'src/main.ts'), ];
polyfills: joinPathFragments(options.appProjectRoot, 'src/polyfills.ts'),
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'), if (options.bundler === 'webpack') {
assets: [ await webpackProjectGenerator(tree, {
joinPathFragments(options.appProjectRoot, 'src/favicon.ico'), project: options.projectName,
joinPathFragments(options.appProjectRoot, 'src/assets'), main,
], tsConfig,
styles: [ compiler: options.compiler ?? 'babel',
devServer: true,
});
const project = readProjectConfiguration(tree, options.projectName);
const prodConfig = project.targets.build.configurations.production;
const buildOptions = project.targets.build.options;
buildOptions.assets = assets;
buildOptions.index = joinPathFragments(
options.appProjectRoot,
'src/index.html'
);
buildOptions.baseHref = '/';
buildOptions.polyfills = joinPathFragments(
options.appProjectRoot,
'src/polyfills.ts'
);
buildOptions.styles = [
joinPathFragments(options.appProjectRoot, `src/styles.${options.style}`), joinPathFragments(options.appProjectRoot, `src/styles.${options.style}`),
], ];
scripts: [], buildOptions.scripts = [];
}; prodConfig.fileReplacements = [
const productionBuildOptions: Partial<WebWebpackExecutorOptions> = {
fileReplacements: [
{ {
replace: joinPathFragments( replace: joinPathFragments(
options.appProjectRoot, options.appProjectRoot,
@ -87,77 +100,51 @@ function addBuildTarget(
`src/environments/environment.prod.ts` `src/environments/environment.prod.ts`
), ),
}, },
], ];
optimization: true, prodConfig.optimization = true;
outputHashing: 'all', prodConfig.outputHashing = 'all';
sourceMap: false, prodConfig.sourceMap = false;
namedChunks: false, prodConfig.namedChunks = false;
extractLicenses: true, prodConfig.extractLicenses = true;
vendorChunk: false, prodConfig.vendorChunk = false;
}; updateProjectConfiguration(tree, options.projectName, project);
} else if (options.bundler === 'none') {
return { // TODO(jack): Flush this out... no bundler should be possible for web but the experience isn't holistic due to missing features (e.g. writing index.html).
...project, const project = readProjectConfiguration(tree, options.projectName);
targets: { project.targets.build = {
...project.targets, executor: `@nrwl/js:${options.compiler}`,
build: { outputs: ['{options.outputPath}'],
executor: '@nrwl/web:webpack', options: {
outputs: ['{options.outputPath}'], main,
defaultConfiguration: 'production', outputPath: joinPathFragments('dist', options.appProjectRoot),
options: buildOptions, tsConfig,
configurations: { assets,
production: productionBuildOptions,
},
}, },
}, };
}; updateProjectConfiguration(tree, options.projectName, project);
} else {
throw new Error('Unsupported bundler type');
}
} }
function addServeTarget( async function addProject(tree: Tree, options: NormalizedSchema) {
project: ProjectConfiguration,
options: NormalizedSchema
) {
const serveTarget: TargetConfiguration = {
executor: '@nrwl/web:dev-server',
options: {
buildTarget: `${options.projectName}:build`,
},
configurations: {
production: {
buildTarget: `${options.projectName}:build:production`,
},
},
};
return {
...project,
targets: {
...project.targets,
serve: serveTarget,
},
};
}
function addProject(tree: Tree, options: NormalizedSchema) {
const targets: Record<string, TargetConfiguration> = {}; const targets: Record<string, TargetConfiguration> = {};
let project: ProjectConfiguration = {
projectType: 'application',
root: options.appProjectRoot,
sourceRoot: joinPathFragments(options.appProjectRoot, 'src'),
tags: options.parsedTags,
targets,
};
project = addBuildTarget(project, options);
project = addServeTarget(project, options);
addProjectConfiguration( addProjectConfiguration(
tree, tree,
options.projectName, options.projectName,
project, {
projectType: 'application',
root: options.appProjectRoot,
sourceRoot: joinPathFragments(options.appProjectRoot, 'src'),
tags: options.parsedTags,
targets,
},
options.standaloneConfig options.standaloneConfig
); );
await setupBundler(tree, options);
const workspace = readWorkspaceConfiguration(tree); const workspace = readWorkspaceConfiguration(tree);
if (!workspace.defaultProject) { if (!workspace.defaultProject) {
@ -198,7 +185,7 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
tasks.push(webTask); tasks.push(webTask);
createApplicationFiles(host, options); createApplicationFiles(host, options);
addProject(host, options); await addProject(host, options);
const lintTask = await lintProjectGenerator(host, { const lintTask = await lintProjectGenerator(host, {
linter: options.linter, linter: options.linter,
@ -275,6 +262,8 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
...options, ...options,
prefix: options.prefix ?? npmScope, prefix: options.prefix ?? npmScope,
name: names(options.name).fileName, name: names(options.name).fileName,
compiler: options.compiler ?? 'babel',
bundler: options.bundler ?? 'webpack',
projectName: appProjectName, projectName: appProjectName,
appProjectRoot, appProjectRoot,
e2eProjectRoot, e2eProjectRoot,

View File

@ -4,6 +4,7 @@ export interface Schema {
name: string; name: string;
prefix?: string; prefix?: string;
style?: string; style?: string;
bundler?: 'webpack' | 'none';
compiler?: 'babel' | 'swc'; compiler?: 'babel' | 'swc';
skipFormat?: boolean; skipFormat?: boolean;
directory?: string; directory?: string;

View File

@ -50,6 +50,12 @@
"enum": ["babel", "swc"], "enum": ["babel", "swc"],
"default": "babel" "default": "babel"
}, },
"bundler": {
"type": "string",
"description": "The bundler to use.",
"enum": ["webpack", "none"],
"default": "webpack"
},
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",
"type": "string", "type": "string",

View File

@ -45,58 +45,4 @@ describe('init', () => {
}); });
expect(tree.exists('jest.config.js')).toBe(false); expect(tree.exists('jest.config.js')).toBe(false);
}); });
describe('babel config', () => {
it('should create babel config if not present', async () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs = {
sharedGlobals: ['{workspaceRoot}/exiting-file.json'],
};
return json;
});
await webInitGenerator(tree, {
unitTestRunner: 'none',
});
expect(tree.exists('babel.config.json')).toBe(true);
const sharedGloabls = readJson<NxJsonConfiguration>(tree, 'nx.json')
.namedInputs.sharedGlobals;
expect(sharedGloabls).toContain('{workspaceRoot}/exiting-file.json');
expect(sharedGloabls).toContain('{workspaceRoot}/babel.config.json');
});
it('should not overwrite existing babel config', async () => {
tree.write('babel.config.json', '{ "preset": ["preset-awesome"] }');
await webInitGenerator(tree, {
unitTestRunner: 'none',
});
const existing = readJson(tree, 'babel.config.json');
expect(existing).toEqual({ preset: ['preset-awesome'] });
});
it('should not overwrite existing babel config (.js)', async () => {
tree.write('/babel.config.js', 'module.exports = () => {};');
await webInitGenerator(tree, {
unitTestRunner: 'none',
});
expect(tree.exists('babel.config.json')).toBe(false);
});
it('should not fail when dependencies is missing from package.json and no other init generators are invoked', async () => {
updateJson(tree, 'package.json', (json) => {
delete json.dependencies;
return json;
});
expect(
webInitGenerator(tree, {
e2eTestRunner: 'none',
unitTestRunner: 'none',
})
).resolves.toBeTruthy();
});
});
}); });

View File

@ -19,9 +19,18 @@ import {
} from '../../utils/versions'; } from '../../utils/versions';
import { Schema } from './schema'; import { Schema } from './schema';
function updateDependencies(tree: Tree) { function updateDependencies(tree: Tree, schema: Schema) {
removeDependenciesFromPackageJson(tree, ['@nrwl/web'], []); removeDependenciesFromPackageJson(tree, ['@nrwl/web'], []);
const devDependencies = {
'@nrwl/web': nxVersion,
'@types/node': typesNodeVersion,
};
if (schema.bundler === 'webpack') {
devDependencies['@nrwl/webpack'] = nxVersion;
}
return addDependenciesToPackageJson( return addDependenciesToPackageJson(
tree, tree,
{ {
@ -29,10 +38,7 @@ function updateDependencies(tree: Tree) {
'regenerator-runtime': '0.13.7', 'regenerator-runtime': '0.13.7',
tslib: tsLibVersion, tslib: tsLibVersion,
}, },
{ devDependencies
'@nrwl/web': nxVersion,
'@types/node': typesNodeVersion,
}
); );
} }
@ -66,7 +72,7 @@ export async function webInitGenerator(tree: Tree, schema: Schema) {
const cypressTask = cypressInitGenerator(tree, {}); const cypressTask = cypressInitGenerator(tree, {});
tasks.push(cypressTask); tasks.push(cypressTask);
} }
const installTask = updateDependencies(tree); const installTask = updateDependencies(tree, schema);
tasks.push(installTask); tasks.push(installTask);
initRootBabelConfig(tree); initRootBabelConfig(tree);
if (!schema.skipFormat) { if (!schema.skipFormat) {

View File

@ -1,4 +1,5 @@
export interface Schema { export interface Schema {
bundler?: 'webpack' | 'none';
unitTestRunner?: 'jest' | 'none'; unitTestRunner?: 'jest' | 'none';
e2eTestRunner?: 'cypress' | 'none'; e2eTestRunner?: 'cypress' | 'none';
skipFormat?: boolean; skipFormat?: boolean;

View File

@ -6,6 +6,12 @@
"description": "Init Web Plugin.", "description": "Init Web Plugin.",
"type": "object", "type": "object",
"properties": { "properties": {
"bundler": {
"type": "string",
"description": "The bundler to use.",
"enum": ["webpack", "none"],
"default": "webpack"
},
"unitTestRunner": { "unitTestRunner": {
"description": "Adds the specified unit test runner", "description": "Adds the specified unit test runner",
"type": "string", "type": "string",

View File

@ -1,6 +1,6 @@
import { getProjects, ProjectGraph, DependencyType } from '@nrwl/devkit'; import { getProjects, ProjectGraph, DependencyType } from '@nrwl/devkit';
import { reverse } from '@nrwl/devkit'; import { reverse } from '@nrwl/devkit';
import { hasDependentAppUsingWebBuild } from '@nrwl/web/src/migrations/update-11-5-2/utils'; import { hasDependentAppUsingWebBuild } from './utils';
describe('hasDependentAppUsingWebBuild', () => { describe('hasDependentAppUsingWebBuild', () => {
const graph: ProjectGraph = reverse({ const graph: ProjectGraph = reverse({

View File

@ -0,0 +1,92 @@
import { readJson } from '@nrwl/devkit';
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
import update from './update-webpack-executor';
describe('Migration: @nrwl/webpack', () => {
it(`should update usage of webpack executor`, async () => {
let tree = createTreeWithEmptyV1Workspace();
tree.write(
'workspace.json',
JSON.stringify({
version: 2,
projects: {
myapp: {
root: 'apps/myapp',
sourceRoot: 'apps/myapp/src',
projectType: 'application',
targets: {
build: {
executor: '@nrwl/web:webpack',
options: {},
},
},
},
},
})
);
await update(tree);
expect(readJson(tree, 'workspace.json')).toEqual({
version: 2,
projects: {
myapp: {
root: 'apps/myapp',
sourceRoot: 'apps/myapp/src',
projectType: 'application',
targets: {
build: {
executor: '@nrwl/webpack:webpack',
options: {},
},
},
},
},
});
});
it(`should update usage of dev-server executor`, async () => {
let tree = createTreeWithEmptyV1Workspace();
tree.write(
'workspace.json',
JSON.stringify({
version: 2,
projects: {
myapp: {
root: 'apps/myapp',
sourceRoot: 'apps/myapp/src',
projectType: 'application',
targets: {
serve: {
executor: '@nrwl/web:dev-server',
options: {},
},
},
},
},
})
);
await update(tree);
expect(readJson(tree, 'workspace.json')).toEqual({
version: 2,
projects: {
myapp: {
root: 'apps/myapp',
sourceRoot: 'apps/myapp/src',
projectType: 'application',
targets: {
serve: {
executor: '@nrwl/webpack:dev-server',
options: {},
},
},
},
},
});
});
});

View File

@ -0,0 +1,27 @@
import {
formatFiles,
getProjects,
Tree,
updateProjectConfiguration,
} from '@nrwl/devkit';
export default async function update(host: Tree) {
const projects = getProjects(host);
for (const [name, config] of projects.entries()) {
let updated = false;
if (config?.targets?.build?.executor === '@nrwl/web:webpack') {
config.targets.build.executor = '@nrwl/webpack:webpack';
updated = true;
}
if (config?.targets?.serve?.executor === '@nrwl/web:dev-server') {
config.targets.serve.executor = '@nrwl/webpack:dev-server';
updated = true;
}
if (updated) {
updateProjectConfiguration(host, name, config);
}
}
await formatFiles(host);
}

View File

@ -1,305 +0,0 @@
import { join } from 'path';
import * as webpack from 'webpack';
import { Configuration, WebpackPluginInstance } from 'webpack';
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
import { AssetGlobPattern, BuildBuilderOptions } from './shared-models';
import { getOutputHashFormat } from './hash-format';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import TerserPlugin = require('terser-webpack-plugin');
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const IGNORED_WEBPACK_WARNINGS = [
/The comment file/i,
/could not find any license/i,
];
export function getBaseWebpackPartial(
builderOptions: BuildBuilderOptions,
extraOptions: {
esm?: boolean;
isScriptOptimizeOn?: boolean;
emitDecoratorMetadata?: boolean;
configuration?: string;
skipTypeCheck?: boolean;
}
): Configuration {
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
const mainFields = [
...(extraOptions.esm ? ['es2015'] : []),
'module',
'main',
];
const hashFormat = getOutputHashFormat(builderOptions.outputHashing);
const suffixFormat = extraOptions.esm ? '.esm' : '.es5';
const filename = extraOptions.isScriptOptimizeOn
? `[name]${hashFormat.script}${suffixFormat}.js`
: '[name].js';
const chunkFilename = extraOptions.isScriptOptimizeOn
? `[name]${hashFormat.chunk}${suffixFormat}.js`
: '[name].js';
const mode = extraOptions.isScriptOptimizeOn ? 'production' : 'development';
const webpackConfig: Configuration = {
target: 'web', // webpack defaults to 'browserslist' which breaks Fast Refresh
entry: {
main: [builderOptions.main],
},
devtool:
builderOptions.sourceMap === 'hidden'
? 'hidden-source-map'
: builderOptions.sourceMap
? 'source-map'
: false,
mode,
output: {
path: builderOptions.outputPath,
filename,
chunkFilename,
hashFunction: 'xxhash64',
// Disabled for performance
pathinfo: false,
},
module: {
// Enabled for performance
unsafeCache: true,
rules: [
{
test: /\.(bmp|png|jpe?g|gif|webp|avif)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10_000, // 10 kB
},
},
},
{
// 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,
},
},
builderOptions.compiler === 'babel' && {
test: /\.([jt])sx?$/,
loader: join(__dirname, 'web-babel-loader'),
exclude: /node_modules/,
options: {
rootMode: 'upward',
cwd: join(builderOptions.root, builderOptions.sourceRoot),
emitDecoratorMetadata: extraOptions.emitDecoratorMetadata,
isModern: extraOptions.esm,
envName: extraOptions.isScriptOptimizeOn
? 'production'
: extraOptions.configuration,
babelrc: true,
cacheDirectory: true,
cacheCompression: false,
},
},
builderOptions.compiler === 'swc' && {
test: /\.([jt])sx?$/,
loader: require.resolve('swc-loader'),
exclude: /node_modules/,
options: {
jsc: {
parser: {
syntax: 'typescript',
decorators: true,
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
loose: true,
},
},
},
].filter(Boolean),
},
resolve: {
extensions,
alias: getAliases(builderOptions),
plugins: [
new TsconfigPathsPlugin({
configFile: builderOptions.tsConfig,
extensions,
mainFields,
}) as never, // TODO: Remove never type when 'tsconfig-paths-webpack-plugin' types fixed
],
mainFields,
},
performance: {
hints: false,
},
plugins: [new webpack.DefinePlugin(getClientEnvironment(mode).stringified)],
watch: builderOptions.watch,
watchOptions: {
poll: builderOptions.poll,
},
stats: getStatsConfig(builderOptions),
ignoreWarnings: [
(x) =>
IGNORED_WEBPACK_WARNINGS.some((r) =>
typeof x === 'string' ? r.test(x) : r.test(x.message)
),
],
experiments: {
cacheUnaffected: true,
},
};
if (builderOptions.compiler !== 'swc' && extraOptions.isScriptOptimizeOn) {
webpackConfig.optimization = {
sideEffects: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
ecma: (extraOptions.esm ? 2016 : 5) as TerserPlugin.TerserECMA,
safari10: true,
output: {
ascii_only: true,
comments: false,
webkit: true,
},
},
}),
],
runtimeChunk: true,
};
}
const extraPlugins: WebpackPluginInstance[] = [];
if (!extraOptions.skipTypeCheck && extraOptions.esm) {
extraPlugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: builderOptions.tsConfig,
memoryLimit: builderOptions.memoryLimit || 2018,
},
})
);
}
if (builderOptions.progress) {
extraPlugins.push(new webpack.ProgressPlugin());
}
// TODO LicenseWebpackPlugin needs a PR for proper typing
if (builderOptions.extractLicenses) {
extraPlugins.push(
new LicenseWebpackPlugin({
stats: {
errors: false,
},
perChunkOutput: false,
outputFilename: `3rdpartylicenses.txt`,
}) as unknown as WebpackPluginInstance
);
}
if (
Array.isArray(builderOptions.assets) &&
builderOptions.assets.length > 0
) {
extraPlugins.push(createCopyPlugin(builderOptions.assets));
}
webpackConfig.plugins = [...webpackConfig.plugins, ...extraPlugins];
return webpackConfig;
}
function getAliases(options: BuildBuilderOptions): { [key: string]: string } {
return options.fileReplacements.reduce(
(aliases, replacement) => ({
...aliases,
[replacement.replace]: replacement.with,
}),
{}
);
}
function getStatsConfig(options: BuildBuilderOptions) {
return {
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,
};
}
// This is shamelessly taken from CRA and modified for NX use
// https://github.com/facebook/create-react-app/blob/4784997f0682e75eb32a897b4ffe34d735912e6c/packages/react-scripts/config/env.js#L71
function getClientEnvironment(mode) {
// Grab NODE_ENV and NX_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const NX_APP = /^NX_/i;
const raw = Object.keys(process.env)
.filter((key) => NX_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
// Useful for determining whether were running in production mode.
NODE_ENV: process.env.NODE_ENV || mode,
}
);
// Stringify all values so we can feed into webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { stringified };
}
export function createCopyPlugin(assets: AssetGlobPattern[]) {
return new CopyWebpackPlugin({
patterns: 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,
},
};
}),
});
}

View File

@ -1,84 +0,0 @@
import { getDevServerConfig } from '@nrwl/web/src/utils/devserver.config';
jest.mock('./webpack/partials/common', () => ({
getCommonConfig: () => ({
output: {},
resolve: {},
}),
getCommonPartial: () => ({}),
}));
jest.mock('tsconfig-paths-webpack-plugin', () => ({
TsconfigPathsPlugin: class TsconfigPathsPlugin {},
}));
describe('getDevServerConfig', () => {
it('should set mode to production when optimization is on', () => {
const result = getDevServerConfig(
'/',
'/app',
'/app/src',
{
optimization: true,
root: '/app',
sourceRoot: '/app/src',
main: '/app/src/main.ts',
outputPath: '/dist/app',
tsConfig: '/app/tsconfig.app.json',
compiler: 'babel',
assets: [],
fileReplacements: [],
index: '/app/src/index.html',
scripts: [],
styles: [],
},
{
buildTarget: 'app:build:production',
allowedHosts: '',
liveReload: false,
hmr: false,
ssl: false,
watch: false,
open: false,
host: 'localhost',
port: 4200,
}
);
expect(result.mode).toEqual('production');
});
it('should set mode to development when optimization is off', () => {
const result = getDevServerConfig(
'/',
'/app',
'/app/src',
{
root: '/app',
sourceRoot: '/app/src',
main: '/app/src/main.ts',
outputPath: '/dist/app',
tsConfig: '/app/tsconfig.app.json',
compiler: 'babel',
assets: [],
fileReplacements: [],
index: '/app/src/index.html',
scripts: [],
styles: [],
},
{
buildTarget: 'app:build:development',
allowedHosts: '',
liveReload: false,
hmr: false,
ssl: false,
watch: false,
open: false,
host: 'localhost',
port: 4200,
}
);
expect(result.mode).toEqual('development');
});
});

View File

@ -1,67 +1 @@
import * as path from 'path'; export * from '@nrwl/webpack/src/utils/fs';
import { existsSync, removeSync } from 'fs-extra';
import { directoryExists } from 'nx/src/utils/fileutils';
export function findUp(
names: string | string[],
from: string,
stopOnNodeModules = false
) {
if (!Array.isArray(names)) {
names = [names];
}
const root = path.parse(from).root;
let currentDir = from;
while (currentDir && currentDir !== root) {
for (const name of names) {
const p = path.join(currentDir, name);
if (existsSync(p)) {
return p;
}
}
if (stopOnNodeModules) {
const nodeModuleP = path.join(currentDir, 'node_modules');
if (existsSync(nodeModuleP)) {
return null;
}
}
currentDir = path.dirname(currentDir);
}
return null;
}
export function findAllNodeModules(from: string, root?: string) {
const nodeModules: string[] = [];
let current = from;
while (current && current !== root) {
const potential = path.join(current, 'node_modules');
if (directoryExists(potential)) {
nodeModules.push(potential);
}
const next = path.dirname(current);
if (next === current) {
break;
}
current = next;
}
return nodeModules;
}
/**
* Delete an output directory, but error out if it's the root of the project.
*/
export function deleteOutputDir(root: string, outputPath: string) {
const resolvedOutputPath = path.resolve(root, outputPath);
if (resolvedOutputPath === root) {
throw new Error('Output path MUST not be project root directory!');
}
removeSync(resolvedOutputPath);
}

View File

@ -1,115 +0,0 @@
import { normalizeBuildOptions } from './normalize';
import { BuildBuilderOptions } from './shared-models';
import * as fs from 'fs';
describe('normalizeBuildOptions', () => {
let testOptions: BuildBuilderOptions;
let root: string;
let sourceRoot: string;
beforeEach(() => {
testOptions = {
compiler: 'babel',
main: 'apps/nodeapp/src/main.ts',
tsConfig: 'apps/nodeapp/tsconfig.app.json',
outputPath: 'dist/apps/nodeapp',
fileReplacements: [
{
replace: 'apps/environment/environment.ts',
with: 'apps/environment/environment.prod.ts',
},
{
replace: 'module1.ts',
with: 'module2.ts',
},
],
assets: [],
statsJson: false,
webpackConfig: 'apps/nodeapp/webpack.config',
};
root = '/root';
sourceRoot = 'apps/nodeapp/src';
});
it('should resolve main from root', () => {
const result = normalizeBuildOptions(testOptions, root, sourceRoot);
expect(result.main).toEqual('/root/apps/nodeapp/src/main.ts');
});
it('should resolve the output path', () => {
const result = normalizeBuildOptions(testOptions, root, sourceRoot);
expect(result.outputPath).toEqual('/root/dist/apps/nodeapp');
});
it('should resolve the tsConfig path', () => {
const result = normalizeBuildOptions(testOptions, root, sourceRoot);
expect(result.tsConfig).toEqual('/root/apps/nodeapp/tsconfig.app.json');
});
it('should normalize asset patterns', () => {
jest.spyOn(fs, 'statSync').mockReturnValue({
isDirectory: () => true,
} as any);
const result = normalizeBuildOptions(
<BuildBuilderOptions>{
...testOptions,
root,
assets: [
'apps/nodeapp/src/assets',
{
input: 'outsideproj',
output: 'output',
glob: '**/*',
ignore: ['**/*.json'],
},
],
},
root,
sourceRoot
);
expect(result.assets).toEqual([
{
input: '/root/apps/nodeapp/src/assets',
output: 'assets',
glob: '**/*',
},
{
input: '/root/outsideproj',
output: 'output',
glob: '**/*',
ignore: ['**/*.json'],
},
]);
});
it('should resolve the file replacement paths', () => {
const result = normalizeBuildOptions(testOptions, root, sourceRoot);
expect(result.fileReplacements).toEqual([
{
replace: '/root/apps/environment/environment.ts',
with: '/root/apps/environment/environment.prod.ts',
},
{
replace: '/root/module1.ts',
with: '/root/module2.ts',
},
]);
});
it('should resolve both node modules and relative path for webpackConfig', () => {
let result = normalizeBuildOptions(testOptions, root, sourceRoot);
expect(result.webpackConfig).toEqual('/root/apps/nodeapp/webpack.config');
result = normalizeBuildOptions(
{
...testOptions,
webpackConfig: 'react', // something that exists in node_modules
},
root,
sourceRoot
);
expect(result.webpackConfig).toMatch('react');
expect(result.webpackConfig).not.toMatch(root);
});
});

View File

@ -1,173 +1 @@
import { WebWebpackExecutorOptions } from '../executors/webpack/webpack.impl'; export * from '@nrwl/webpack/src/executors/webpack/lib/normalize-options';
import { normalizePath } from '@nrwl/devkit';
import { basename, dirname, relative, resolve } from 'path';
import {
AssetGlobPattern,
BuildBuilderOptions,
ExtraEntryPoint,
ExtraEntryPointClass,
} from './shared-models';
import { statSync } from 'fs';
export interface FileReplacement {
replace: string;
with: string;
}
export function normalizeBuildOptions<T extends BuildBuilderOptions>(
options: T,
root: string,
sourceRoot: string
): T {
return {
...options,
root,
sourceRoot,
main: resolve(root, options.main),
outputPath: resolve(root, options.outputPath),
tsConfig: resolve(root, options.tsConfig),
fileReplacements: normalizeFileReplacements(root, options.fileReplacements),
assets: normalizeAssets(options.assets, root, sourceRoot),
webpackConfig: normalizePluginPath(options.webpackConfig, root),
};
}
export function normalizePluginPath(pluginPath: void | string, root: string) {
if (!pluginPath) {
return '';
}
try {
return require.resolve(pluginPath);
} catch {
return resolve(root, pluginPath);
}
}
export function normalizeAssets(
assets: any[],
root: string,
sourceRoot: string
): AssetGlobPattern[] {
return assets.map((asset) => {
if (typeof asset === 'string') {
const assetPath = normalizePath(asset);
const resolvedAssetPath = resolve(root, assetPath);
const resolvedSourceRoot = resolve(root, sourceRoot);
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
throw new Error(
`The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}`
);
}
const isDirectory = statSync(resolvedAssetPath).isDirectory();
const input = isDirectory
? resolvedAssetPath
: dirname(resolvedAssetPath);
const output = relative(resolvedSourceRoot, resolve(root, input));
const glob = isDirectory ? '**/*' : basename(resolvedAssetPath);
return {
input,
output,
glob,
};
} else {
if (asset.output.startsWith('..')) {
throw new Error(
'An asset cannot be written to a location outside of the output path.'
);
}
const assetPath = normalizePath(asset.input);
const resolvedAssetPath = resolve(root, assetPath);
return {
...asset,
input: resolvedAssetPath,
// Now we remove starting slash to make Webpack place it from the output root.
output: asset.output.replace(/^\//, ''),
};
}
});
}
function normalizeFileReplacements(
root: string,
fileReplacements: FileReplacement[]
): FileReplacement[] {
return fileReplacements.map((fileReplacement) => ({
replace: resolve(root, fileReplacement.replace),
with: resolve(root, fileReplacement.with),
}));
}
export function normalizeWebBuildOptions(
options: WebWebpackExecutorOptions,
root: string,
sourceRoot: string
): WebWebpackExecutorOptions {
return {
...normalizeBuildOptions(options, root, sourceRoot),
optimization:
typeof options.optimization !== 'object'
? {
scripts: options.optimization,
styles: options.optimization,
}
: options.optimization,
polyfills: options.polyfills ? resolve(root, options.polyfills) : undefined,
es2015Polyfills: options.es2015Polyfills
? resolve(root, options.es2015Polyfills)
: undefined,
};
}
export function convertBuildOptions(
buildOptions: WebWebpackExecutorOptions
): any {
const options = buildOptions as any;
return <any>{
...options,
buildOptimizer: options.optimization,
forkTypeChecker: false,
lazyModules: [] as string[],
};
}
export type NormalizedEntryPoint = Required<Omit<ExtraEntryPointClass, 'lazy'>>;
export function normalizeExtraEntryPoints(
extraEntryPoints: ExtraEntryPoint[],
defaultBundleName: string
): NormalizedEntryPoint[] {
return extraEntryPoints.map((entry) => {
let normalizedEntry;
if (typeof entry === 'string') {
normalizedEntry = {
input: entry,
inject: true,
bundleName: defaultBundleName,
};
} else {
const { lazy, inject = true, ...newEntry } = entry;
const injectNormalized = entry.lazy !== undefined ? !entry.lazy : inject;
let bundleName;
if (entry.bundleName) {
bundleName = entry.bundleName;
} else if (!injectNormalized) {
// Lazy entry points use the file name as bundle name.
bundleName = basename(
normalizePath(
entry.input.replace(/\.(js|css|scss|sass|less|styl)$/i, '')
)
);
} else {
bundleName = defaultBundleName;
}
normalizedEntry = { ...newEntry, inject: injectNormalized, bundleName };
}
return normalizedEntry;
});
}

View File

@ -1,120 +0,0 @@
import * as webpack from 'webpack';
import type { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server';
import { Observable } from 'rxjs';
import { extname } from 'path';
export function runWebpack(config: webpack.Configuration): Observable<any> {
return new Observable((subscriber) => {
// Passing `watch` option here will result in a warning due to missing callback.
// We manually call `.watch` or `.run` later so this option isn't needed here.
const { watch, ...normalizedConfig } = config;
const webpackCompiler = webpack(normalizedConfig);
const callback = (err: Error, stats: webpack.Stats) => {
if (err) {
subscriber.error(err);
}
subscriber.next(stats);
};
if (config.watch) {
const watchOptions = config.watchOptions || {};
const watching = webpackCompiler.watch(watchOptions, callback);
return () => watching.close(() => subscriber.complete());
} else {
webpackCompiler.run((err, stats) => {
callback(err, stats);
webpackCompiler.close((closeErr) => {
if (closeErr) subscriber.error(closeErr);
subscriber.complete();
});
});
}
});
}
export function runWebpackDevServer(
config: any,
webpack: typeof import('webpack'),
WebpackDevServer: typeof import('webpack-dev-server')
): Observable<{ stats: any; baseUrl: string }> {
return new Observable((subscriber) => {
const webpackCompiler: any = webpack(config);
let baseUrl: string;
webpackCompiler.hooks.done.tap('build-webpack', (stats) => {
subscriber.next({ stats, baseUrl });
});
const devServerConfig = (config as any).devServer || {};
const originalOnListen = devServerConfig.onListening;
devServerConfig.onListening = function (server: any) {
originalOnListen(server);
const devServerOptions: WebpackDevServerConfiguration = server.options;
baseUrl = `${server.options.https ? 'https' : 'http'}://${
server.options.host
}:${server.options.port}${devServerOptions.devMiddleware.publicPath}`;
};
const webpackServer = new WebpackDevServer(
devServerConfig,
webpackCompiler as any
);
try {
webpackServer.start().catch((err) => subscriber.error(err));
return () => webpackServer.stop();
} catch (e) {
throw new Error('Could not start start dev server');
}
});
}
export interface EmittedFile {
id?: string;
name?: string;
file: string;
extension: string;
initial: boolean;
asset?: boolean;
}
export function getEmittedFiles(stats: webpack.Stats) {
const { compilation } = stats;
const files: EmittedFile[] = [];
// adds all chunks to the list of emitted files such as lazy loaded modules
for (const chunk of compilation.chunks) {
for (const file of chunk.files) {
files.push({
// The id is guaranteed to exist at this point in the compilation process
// tslint:disable-next-line: no-non-null-assertion
id: chunk.id.toString(),
name: chunk.name,
file,
extension: extname(file),
initial: chunk.isOnlyInitial(),
});
}
}
// other all files
for (const file of Object.keys(compilation.assets)) {
files.push({
file,
extension: extname(file),
initial: false,
asset: true,
});
}
// dedupe
return files.filter(
({ file, name }, index) =>
files.findIndex((f) => f.file === file && (!name || name === f.name)) ===
index
);
}

View File

@ -1,121 +0,0 @@
import { WebWebpackExecutorOptions } from '../executors/webpack/webpack.impl';
import { FileReplacement } from './normalize';
export interface OptimizationOptions {
scripts: boolean;
styles: boolean;
}
export interface BuildBuilderOptions {
main: string;
outputPath: string;
compiler: 'babel' | 'swc';
tsConfig: string;
watch?: boolean;
sourceMap?: boolean | 'hidden';
optimization?: boolean | OptimizationOptions;
memoryLimit?: number;
maxWorkers?: number;
poll?: number;
fileReplacements?: FileReplacement[];
assets?: any[];
progress?: boolean;
statsJson?: boolean;
extractLicenses?: boolean;
verbose?: boolean;
outputHashing?: any;
webpackConfig?: string;
root?: string;
sourceRoot?: string;
}
export interface AssetGlobPattern {
glob: string;
input: string;
output: string;
ignore?: string[];
}
export type AssetPattern = AssetPatternClass | string;
export interface AssetPatternClass {
glob: string;
ignore?: string[];
input: string;
output: string;
}
export enum Type {
All = 'all',
AllScript = 'allScript',
Any = 'any',
AnyComponentStyle = 'anyComponentStyle',
AnyScript = 'anyScript',
Bundle = 'bundle',
Initial = 'initial',
}
export enum CrossOrigin {
Anonymous = 'anonymous',
None = 'none',
UseCredentials = 'use-credentials',
}
export type IndexUnion = IndexObject | string;
export interface IndexObject {
input: string;
output?: string;
}
export type Localize = string[] | boolean;
export type OptimizationUnion = boolean | OptimizationClass;
export interface OptimizationClass {
scripts?: boolean;
styles?: boolean;
}
export enum OutputHashing {
All = 'all',
Bundles = 'bundles',
Media = 'media',
None = 'none',
}
export type ExtraEntryPoint = ExtraEntryPointClass | string;
export interface ExtraEntryPointClass {
bundleName?: string;
inject?: boolean;
input: string;
lazy?: boolean;
}
export type SourceMapUnion = boolean | SourceMapClass;
export interface SourceMapClass {
hidden?: boolean;
scripts?: boolean;
styles?: boolean;
vendor?: boolean;
}
export interface StylePreprocessorOptions {
includePaths?: string[];
}
export interface WebpackConfigOptions<T = WebWebpackExecutorOptions> {
root: string;
projectRoot: string;
sourceRoot?: string;
buildOptions: T;
tsConfig: any;
tsConfigPath: string;
supportES2015: boolean;
}

View File

@ -0,0 +1,25 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["./package.json", "./generators.json", "./executors.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nrwl/nx/nx-plugin-checks": "error"
}
}
]
}

View File

@ -0,0 +1,13 @@
<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx.png" width="600" alt="Nx - Smart, Fast and Extensible Build System"></p>
{{links}}
<hr>
# Nx: Smart, Fast and Extensible Build System
Nx is a next generation build system with first class monorepo support and powerful integrations.
This package is a [Webpack plugin for Nx](https://nx.dev/packages/webpack).
{{content}}

View File

@ -0,0 +1,26 @@
{
"builders": {
"webpack": {
"implementation": "./src/executors/webpack/compat",
"schema": "./src/executors/webpack/schema.json",
"description": "Run webpack build."
},
"dev-server": {
"implementation": "./src/executors/dev-server/compat",
"schema": "./src/executors/dev-server/schema.json",
"description": "Serve a web application."
}
},
"executors": {
"webpack": {
"implementation": "./src/executors/webpack/webpack.impl",
"schema": "./src/executors/webpack/schema.json",
"description": "Run webpack build."
},
"dev-server": {
"implementation": "./src/executors/dev-server/dev-server.impl",
"schema": "./src/executors/dev-server/schema.json",
"description": "Serve a web application."
}
}
}

View File

@ -0,0 +1,33 @@
{
"name": "Nx Webpack",
"version": "0.1",
"schematics": {
"init": {
"factory": "./src/generators/init/init#webpackInitSchematic",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the `@nrwl/webpack` plugin.",
"hidden": true
},
"webpack-project": {
"factory": "./src/generators/webpack-project/webpack-project#webpackProjectSchematic",
"schema": "./src/generators/webpack-project/schema.json",
"description": "Add webpack configuration to a project.",
"hidden": true
}
},
"generators": {
"init": {
"factory": "./src/generators/init/init#webpackInitGenerator",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the `@nrwl/webpack` plugin.",
"aliases": ["ng-add"],
"hidden": true
},
"webpack-project": {
"factory": "./src/generators/webpack-project/webpack-project#webpackProjectGenerator",
"schema": "./src/generators/webpack-project/schema.json",
"description": "Add webpack configuration to a project.",
"hidden": true
}
}
}

View File

@ -0,0 +1,7 @@
export * from './src/utils/config';
export * from './src/generators/webpack-project/webpack-project';
export * from './src/executors/dev-server/schema';
export * from './src/executors/dev-server/dev-server.impl';
export * from './src/executors/webpack/lib/normalize-options';
export * from './src/executors/webpack/schema';
export * from './src/executors/webpack/webpack.impl';

View File

@ -0,0 +1,11 @@
/* eslint-disable */
export default {
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: { 'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json' } },
displayName: 'webpack',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};

View File

@ -0,0 +1,79 @@
{
"name": "@nrwl/webpack",
"version": "0.0.1",
"description": "The Nx Plugin for Webpack contains executors and generators that support building applications using Webpack",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
"directory": "packages/webpack"
},
"keywords": [
"Monorepo",
"Webpack",
"Web",
"CLI"
],
"main": "./index.js",
"typings": "./index.d.ts",
"author": "Victor Savkin",
"license": "MIT",
"bugs": {
"url": "https://github.com/nrwl/nx/issues"
},
"homepage": "https://nx.dev",
"schematics": "./generators.json",
"builders": "./executors.json",
"ng-update": {
"requirements": {},
"migrations": "./migrations.json"
},
"dependencies": {
"@nrwl/devkit": "file:../devkit",
"@nrwl/js": "file:../js",
"@nrwl/workspace": "file:../workspace",
"autoprefixer": "^10.4.9",
"babel-loader": "^8.2.2",
"browserslist": "^4.16.6",
"caniuse-lite": "^1.0.30001394",
"chalk": "4.1.0",
"chokidar": "^3.5.1",
"copy-webpack-plugin": "^10.2.4",
"css-loader": "^6.4.0",
"css-minimizer-webpack-plugin": "^3.4.1",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "7.2.13",
"fs-extra": "^10.1.0",
"ignore": "^5.0.4",
"less": "3.12.2",
"less-loader": "^10.1.0",
"license-webpack-plugin": "^4.0.2",
"loader-utils": "1.2.3",
"mini-css-extract-plugin": "~2.4.7",
"parse5": "4.0.0",
"parse5-html-rewriting-stream": "6.0.1",
"postcss": "^8.4.14",
"postcss-import": "~14.1.0",
"postcss-loader": "^6.1.1",
"raw-loader": "^4.0.2",
"rxjs": "^6.5.4",
"sass": "^1.42.1",
"sass-loader": "^12.2.0",
"source-map": "0.7.3",
"source-map-loader": "^3.0.0",
"style-loader": "^3.3.0",
"stylus": "^0.55.0",
"stylus-loader": "^6.2.0",
"terser-webpack-plugin": "^5.3.3",
"ts-loader": "^9.3.1",
"ts-node": "10.9.1",
"tsconfig-paths": "^3.9.0",
"tsconfig-paths-webpack-plugin": "3.5.2",
"tslib": "^2.3.0",
"webpack": "^5.58.1",
"webpack-dev-server": "^4.9.3",
"webpack-merge": "^5.8.0",
"webpack-node-externals": "^3.0.0",
"webpack-sources": "^3.2.3",
"webpack-subresource-integrity": "^5.1.0"
}
}

View File

@ -0,0 +1,87 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/webpack",
"projectType": "library",
"targets": {
"test": {
"executor": "@nrwl/jest:jest",
"options": {
"jestConfig": "packages/webpack/jest.config.ts",
"passWithNoTests": true
},
"outputs": ["coverage/packages/webpack"]
},
"build-base": {
"executor": "@nrwl/js:tsc",
"options": {
"outputPath": "build/packages/webpack",
"tsConfig": "packages/webpack/tsconfig.lib.json",
"main": "packages/webpack/index.ts",
"updateBuildableProjectDepsInPackageJson": false,
"assets": [
{
"input": "packages/webpack",
"glob": "**/files/**",
"output": "/"
},
{
"input": "packages/webpack",
"glob": "**/files/**/.gitkeep",
"output": "/"
},
{
"input": "packages/webpack",
"glob": "**/*.json",
"ignore": ["**/tsconfig*.json", "project.json", ".eslintrc.json"],
"output": "/"
},
{
"input": "packages/webpack",
"glob": "**/*.js",
"ignore": ["**/jest.config.js"],
"output": "/"
},
{
"input": "packages/webpack",
"glob": "**/*.d.ts",
"output": "/"
},
{
"input": "",
"glob": "LICENSE",
"output": "/"
}
]
},
"outputs": ["{options.outputPath}"]
},
"build": {
"executor": "nx:run-commands",
"outputs": ["build/packages/webpack"],
"options": {
"command": "node ./scripts/copy-readme.js webpack"
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": [
"packages/webpack/**/*.ts",
"packages/webpack/**/*.spec.ts",
"packages/webpack/**/*_spec.ts",
"packages/webpack/**/*.spec.tsx",
"packages/webpack/**/*.spec.js",
"packages/webpack/**/*.spec.jsx",
"packages/webpack/**/*.d.ts",
"packages/webpack/**/executors/**/schema.json",
"packages/webpack/**/generators/**/schema.json",
"packages/webpack/generators.json",
"packages/webpack/executors.json",
"packages/webpack/package.json",
"packages/webpack/migrations.json"
]
},
"outputs": ["{options.outputFile}"]
}
}
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import devServerExecutor from './dev-server.impl';
export default convertNxExecutor(devServerExecutor);

View File

@ -0,0 +1,119 @@
import * as webpack from 'webpack';
import {
ExecutorContext,
parseTargetString,
readTargetOptions,
} from '@nrwl/devkit';
import { eachValueFrom } from '@nrwl/devkit/src/utils/rxjs-for-await';
import { map, tap } from 'rxjs/operators';
import * as WebpackDevServer from 'webpack-dev-server';
import { getDevServerConfig } from './lib/get-dev-server-config';
import {
calculateProjectDependencies,
createTmpTsConfig,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { runWebpackDevServer } from '../../utils/run-webpack';
import { resolveCustomWebpackConfig } from '../../utils/webpack/custom-webpack';
import { normalizeOptions } from '../webpack/lib/normalize-options';
import { getEmittedFiles } from '../webpack/lib/get-emitted-files';
import { WebpackExecutorOptions } from '../webpack/schema';
import { WebDevServerOptions } from './schema';
export async function* devServerExecutor(
serveOptions: WebDevServerOptions,
context: ExecutorContext
) {
const { root: projectRoot, sourceRoot } =
context.workspace.projects[context.projectName];
const buildOptions = normalizeOptions(
getBuildOptions(serveOptions, context),
context.root,
sourceRoot
);
if (!buildOptions.index) {
throw new Error(
`Cannot run dev-server without "index" option. Check the build options for ${context.projectName}.`
);
}
if (!buildOptions.buildLibsFromSource) {
const { target, dependencies } = calculateProjectDependencies(
context.projectGraph,
context.root,
context.projectName,
'build', // should be generalized
context.configurationName
);
buildOptions.tsConfig = createTmpTsConfig(
buildOptions.tsConfig,
context.root,
target.data.root,
dependencies
);
}
let webpackConfig = getDevServerConfig(context, buildOptions, serveOptions);
if (buildOptions.webpackConfig) {
let customWebpack = resolveCustomWebpackConfig(
buildOptions.webpackConfig,
buildOptions.tsConfig
);
if (typeof customWebpack.then === 'function') {
customWebpack = await customWebpack;
}
webpackConfig = await customWebpack(webpackConfig, {
buildOptions,
configuration: serveOptions.buildTarget.split(':')[2],
});
}
return yield* eachValueFrom(
runWebpackDevServer(webpackConfig, webpack, WebpackDevServer).pipe(
tap(({ stats }) => {
console.info(stats.toString((webpackConfig as any).stats));
}),
map(({ baseUrl, stats }) => {
return {
baseUrl,
emittedFiles: getEmittedFiles(stats),
success: !stats.hasErrors(),
};
})
)
);
}
function getBuildOptions(
options: WebDevServerOptions,
context: ExecutorContext
): WebpackExecutorOptions {
const target = parseTargetString(options.buildTarget);
const overrides: Partial<WebpackExecutorOptions> = {
watch: false,
};
if (options.maxWorkers) {
overrides.maxWorkers = options.maxWorkers;
}
if (options.memoryLimit) {
overrides.memoryLimit = options.memoryLimit;
}
if (options.baseHref) {
overrides.baseHref = options.baseHref;
}
const buildOptions = readTargetOptions(target, context);
return {
...buildOptions,
...overrides,
};
}
export default devServerExecutor;

View File

@ -1,29 +1,27 @@
import { logger } from '@nrwl/devkit'; import { ExecutorContext, logger } from '@nrwl/devkit';
import type { Configuration as WebpackConfiguration } from 'webpack'; import type { Configuration as WebpackConfiguration } from 'webpack';
import type { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server'; import type { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server';
import * as path from 'path'; import * as path from 'path';
import { basename, resolve } from 'path'; import { basename, resolve } from 'path';
import { getWebConfig } from './web.config'; import { getWebpackConfig } from '../../webpack/lib/get-webpack-config';
import { WebWebpackExecutorOptions } from '../executors/webpack/webpack.impl'; import { WebDevServerOptions } from '../schema';
import { WebDevServerOptions } from '../executors/dev-server/dev-server.impl';
import { buildServePath } from './serve-path'; import { buildServePath } from './serve-path';
import { OptimizationOptions } from './shared-models';
import { readFileSync } from 'fs-extra'; import { readFileSync } from 'fs-extra';
import { IndexHtmlWebpackPlugin } from './/webpack/plugins/index-html-webpack-plugin'; import { generateEntryPoints } from '../../../utils//webpack/package-chunk-sort';
import { generateEntryPoints } from './webpack/package-chunk-sort'; import { IndexHtmlWebpackPlugin } from '../../../utils/webpack/plugins/index-html-webpack-plugin';
import { NormalizedWebpackExecutorOptions } from '../../webpack/schema';
export function getDevServerConfig( export function getDevServerConfig(
workspaceRoot: string, context: ExecutorContext,
projectRoot: string, buildOptions: NormalizedWebpackExecutorOptions,
sourceRoot: string,
buildOptions: WebWebpackExecutorOptions,
serveOptions: WebDevServerOptions serveOptions: WebDevServerOptions
): Partial<WebpackConfiguration> { ): Partial<WebpackConfiguration> {
const webpackConfig = getWebConfig( const workspaceRoot = context.root;
workspaceRoot, const { root: projectRoot, sourceRoot } =
projectRoot, context.workspace.projects[context.projectName];
sourceRoot, const webpackConfig = getWebpackConfig(
context,
buildOptions, buildOptions,
true, true,
typeof buildOptions.optimization === 'boolean' typeof buildOptions.optimization === 'boolean'
@ -65,12 +63,20 @@ export function getDevServerConfig(
function getDevServerPartial( function getDevServerPartial(
root: string, root: string,
options: WebDevServerOptions, options: WebDevServerOptions,
buildOptions: WebWebpackExecutorOptions buildOptions: NormalizedWebpackExecutorOptions
): WebpackDevServerConfiguration { ): WebpackDevServerConfiguration {
const servePath = buildServePath(buildOptions); const servePath = buildServePath(buildOptions);
const { scripts: scriptsOptimization, styles: stylesOptimization } = let scriptsOptimization: boolean;
(buildOptions.optimization || {}) as OptimizationOptions; let stylesOptimization: boolean;
if (typeof buildOptions.optimization === 'boolean') {
scriptsOptimization = stylesOptimization = buildOptions.optimization;
} else if (buildOptions.optimization) {
scriptsOptimization = buildOptions.optimization.scripts;
stylesOptimization = buildOptions.optimization.styles;
} else {
scriptsOptimization = stylesOptimization = false;
}
const config: WebpackDevServerConfiguration = { const config: WebpackDevServerConfiguration = {
host: options.host, host: options.host,

View File

@ -1,6 +1,8 @@
import { WebWebpackExecutorOptions } from '../executors/webpack/webpack.impl'; import type { NormalizedWebpackExecutorOptions } from '../../webpack/schema';
export function buildServePath(browserOptions: WebWebpackExecutorOptions) { export function buildServePath(
browserOptions: NormalizedWebpackExecutorOptions
) {
let servePath = let servePath =
_findDefaultServePath(browserOptions.baseHref, browserOptions.deployUrl) || _findDefaultServePath(browserOptions.baseHref, browserOptions.deployUrl) ||
'/'; '/';

View File

@ -0,0 +1,18 @@
export interface WebDevServerOptions {
host: string;
port: number;
publicHost?: string;
ssl: boolean;
sslKey?: string;
sslCert?: string;
proxyConfig?: string;
buildTarget: string;
open: boolean;
liveReload: boolean;
hmr: boolean;
watch: boolean;
allowedHosts: string;
maxWorkers?: number;
memoryLimit?: number;
baseHref?: string;
}

View File

@ -0,0 +1,75 @@
{
"title": "Web Dev Server",
"description": "Serve a web application.",
"cli": "nx",
"type": "object",
"properties": {
"buildTarget": {
"type": "string",
"description": "Target which builds the application."
},
"port": {
"type": "number",
"description": "Port to listen on.",
"default": 4200
},
"host": {
"type": "string",
"description": "Host to listen on.",
"default": "localhost"
},
"ssl": {
"type": "boolean",
"description": "Serve using `HTTPS`.",
"default": false
},
"sslKey": {
"type": "string",
"description": "SSL key to use for serving `HTTPS`."
},
"sslCert": {
"type": "string",
"description": "SSL certificate to use for serving `HTTPS`."
},
"watch": {
"type": "boolean",
"description": "Watches for changes and rebuilds application.",
"default": true
},
"liveReload": {
"type": "boolean",
"description": "Whether to reload the page on change, using live-reload.",
"default": true
},
"hmr": {
"type": "boolean",
"description": "Enable hot module replacement.",
"default": false
},
"publicHost": {
"type": "string",
"description": "Public URL where the application will be served."
},
"open": {
"type": "boolean",
"description": "Open the application in the browser.",
"default": false
},
"allowedHosts": {
"type": "string",
"description": "This option allows you to whitelist services that are allowed to access the dev server."
},
"memoryLimit": {
"type": "number",
"description": "Memory limit for type checking service process in `MB`."
},
"maxWorkers": {
"type": "number",
"description": "Number of workers to use for type checking."
},
"baseHref": {
"type": "string",
"description": "Base url for the application being built."
}
}
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import { webpackExecutor } from './webpack.impl';
export default convertNxExecutor(webpackExecutor);

View File

@ -0,0 +1,38 @@
import type { Stats } from 'webpack';
import { extname } from 'path';
import { EmittedFile } from '../../../utils/models';
export function getEmittedFiles(stats: Stats): EmittedFile[] {
const { compilation } = stats;
const files: EmittedFile[] = [];
// adds all chunks to the list of emitted files such as lazy loaded modules
for (const chunk of compilation.chunks) {
for (const file of chunk.files) {
files.push({
// The id is guaranteed to exist at this point in the compilation process
// tslint:disable-next-line: no-non-null-assertion
id: chunk.id.toString(),
name: chunk.name,
file,
extension: extname(file),
initial: chunk.isOnlyInitial(),
});
}
}
// other all files
for (const file of Object.keys(compilation.assets)) {
files.push({
file,
extension: extname(file),
initial: false,
asset: true,
});
}
// dedupe
return files.filter(
({ file, name }, index) =>
files.findIndex((f) => f.file === file && (!name || name === f.name)) ===
index
);
}

View File

@ -3,16 +3,16 @@ import { posix, resolve } from 'path';
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript'; import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { ScriptTarget } from 'typescript'; import { ScriptTarget } from 'typescript';
import { getHashDigest, interpolateName } from 'loader-utils'; import { getHashDigest, interpolateName } from 'loader-utils';
import { Configuration } from 'webpack'; import type { Configuration } from 'webpack';
import { WebWebpackExecutorOptions } from '../executors/webpack/webpack.impl'; import { NormalizedWebpackExecutorOptions } from '../schema';
import { convertBuildOptions } from './normalize';
// TODO(jack): These should be inlined in a single function so it is easier to understand // TODO(jack): These should be inlined in a single function so it is easier to understand
import { getBaseWebpackPartial } from './config'; import { getBaseWebpackPartial } from '../../../utils/config';
import { getBrowserConfig } from './webpack/partials/browser'; import { getBrowserConfig } from '../../../utils/webpack/partials/browser';
import { getCommonConfig } from './webpack/partials/common'; import { getCommonConfig } from '../../../utils/webpack/partials/common';
import { getStylesConfig } from './webpack/partials/styles'; import { getStylesConfig } from '../../../utils/webpack/partials/styles';
import { ExecutorContext } from '@nrwl/devkit';
import MiniCssExtractPlugin = require('mini-css-extract-plugin'); import MiniCssExtractPlugin = require('mini-css-extract-plugin');
import webpackMerge = require('webpack-merge'); import webpackMerge = require('webpack-merge');
import postcssImports = require('postcss-import'); import postcssImports = require('postcss-import');
@ -25,16 +25,32 @@ interface PostcssOptions {
config?: string; config?: string;
} }
export function getWebConfig( interface GetWebpackConfigOverrides {
workspaceRoot, root: string;
projectRoot, sourceRoot: string;
sourceRoot, configuration?: string;
options: WebWebpackExecutorOptions, }
export function getWebpackConfig(
context: ExecutorContext,
options: NormalizedWebpackExecutorOptions,
esm?: boolean, esm?: boolean,
isScriptOptimizeOn?: boolean, isScriptOptimizeOn?: boolean,
configuration?: string overrides?: GetWebpackConfigOverrides
) { ): Configuration {
const tsConfig = readTsConfig(options.tsConfig); const tsConfig = readTsConfig(options.tsConfig);
const workspaceRoot = context.root;
let sourceRoot: string;
let projectRoot: string;
if (overrides) {
projectRoot = overrides.root;
sourceRoot = overrides.sourceRoot;
} else {
const project = context.workspace.projects[context.projectName];
projectRoot = project.root;
sourceRoot = project.sourceRoot;
}
if (isScriptOptimizeOn) { if (isScriptOptimizeOn) {
// Angular CLI uses an environment variable (NG_BUILD_DIFFERENTIAL_FULL) // Angular CLI uses an environment variable (NG_BUILD_DIFFERENTIAL_FULL)
@ -56,43 +72,53 @@ export function getWebConfig(
// TODO(jack): Replace merge behavior with an inlined config so it is easier to understand. // TODO(jack): Replace merge behavior with an inlined config so it is easier to understand.
return webpackMerge.merge([ return webpackMerge.merge([
_getBaseWebpackPartial( _getBaseWebpackPartial(
context,
options, options,
esm, esm,
isScriptOptimizeOn, isScriptOptimizeOn,
tsConfig.options.emitDecoratorMetadata, tsConfig.options.emitDecoratorMetadata,
configuration overrides
),
getPolyfillsPartial(
options.polyfills,
options.es2015Polyfills,
esm,
isScriptOptimizeOn
),
getStylesPartial(
wco.root,
wco.projectRoot,
wco.buildOptions,
options.extractCss,
options.postcssConfig
), ),
options.target === 'web'
? getPolyfillsPartial(
options.polyfills,
options.es2015Polyfills,
esm,
isScriptOptimizeOn
)
: {},
options.target === 'web'
? getStylesPartial(
wco.root,
wco.projectRoot,
wco.buildOptions,
options.extractCss,
options.postcssConfig
)
: {},
getCommonPartial(wco), getCommonPartial(wco),
getBrowserConfig(wco), options.target === 'web' ? getBrowserConfig(wco) : {},
]); ]);
} }
function _getBaseWebpackPartial( function _getBaseWebpackPartial(
options: WebWebpackExecutorOptions, context: ExecutorContext,
options: NormalizedWebpackExecutorOptions,
esm: boolean, esm: boolean,
isScriptOptimizeOn: boolean, isScriptOptimizeOn: boolean,
emitDecoratorMetadata: boolean, emitDecoratorMetadata: boolean,
configuration?: string overrides?: GetWebpackConfigOverrides
) { ) {
let partial = getBaseWebpackPartial(options, { let partial = getBaseWebpackPartial(
esm, options,
isScriptOptimizeOn, {
emitDecoratorMetadata, esm,
configuration, isScriptOptimizeOn,
}); emitDecoratorMetadata,
configuration: overrides?.configuration ?? context.configurationName,
},
context
);
delete partial.resolve.mainFields; delete partial.resolve.mainFields;
return partial; return partial;
} }
@ -256,7 +282,7 @@ export function getPolyfillsPartial(
// Safari 10.1 supports <script type="module"> but not <script nomodule>. // Safari 10.1 supports <script type="module"> but not <script nomodule>.
// Need to patch it up so the browser doesn't load both sets. // Need to patch it up so the browser doesn't load both sets.
config.entry.polyfills = [ config.entry.polyfills = [
require.resolve('@nrwl/web/src/utils/webpack/safari-nomodule.js'), require.resolve('@nrwl/webpack/src/utils/webpack/safari-nomodule.js'),
...(polyfills ? [polyfills] : []), ...(polyfills ? [polyfills] : []),
]; ];
} else if (es2015Polyfills && !esm && isScriptOptimizeOn) { } else if (es2015Polyfills && !esm && isScriptOptimizeOn) {
@ -304,3 +330,15 @@ export function getCSSModuleLocalIdent(
// Remove the .module that appears in every classname when based on the file and replace all "." with "_". // Remove the .module that appears in every classname when based on the file and replace all "." with "_".
return className.replace('.module_', '_').replace(/\./g, '_'); return className.replace('.module_', '_').replace(/\./g, '_');
} }
export function convertBuildOptions(
buildOptions: NormalizedWebpackExecutorOptions
): any {
const options = buildOptions as any;
return {
...options,
buildOptimizer: options.optimization,
forkTypeChecker: false,
lazyModules: [] as string[],
};
}

View File

@ -1,57 +1,73 @@
import { resolve, dirname, relative, basename } from 'path'; import { basename, dirname, relative, resolve } from 'path';
import {
AdditionalEntryPoint,
BuildNodeBuilderOptions,
NormalizedBuildNodeBuilderOptions,
} from './types';
import { statSync } from 'fs'; import { statSync } from 'fs';
import { normalizePath } from '@nrwl/devkit';
export interface FileReplacement { import type {
replace: string; AssetGlobPattern,
with: string; FileReplacement,
} WebpackExecutorOptions,
NormalizedWebpackExecutorOptions,
} from '../schema';
export function normalizeBuildOptions( export function normalizeOptions(
options: BuildNodeBuilderOptions, options: WebpackExecutorOptions,
root: string, root: string,
sourceRoot: string, sourceRoot: string
projectRoot: string ): NormalizedWebpackExecutorOptions {
): NormalizedBuildNodeBuilderOptions {
return { return {
...options, ...options,
root, root,
sourceRoot, sourceRoot,
projectRoot, target: options.target ?? 'web',
main: resolve(root, options.main), main: resolve(root, options.main),
outputPath: resolve(root, options.outputPath), outputPath: resolve(root, options.outputPath),
tsConfig: resolve(root, options.tsConfig), tsConfig: resolve(root, options.tsConfig),
fileReplacements: normalizeFileReplacements(root, options.fileReplacements), fileReplacements: normalizeFileReplacements(root, options.fileReplacements),
assets: normalizeAssets(options.assets, root, sourceRoot), assets: normalizeAssets(options.assets, root, sourceRoot),
webpackConfig: options.webpackConfig webpackConfig: normalizePluginPath(options.webpackConfig, root),
? [] optimization:
.concat(options.webpackConfig) typeof options.optimization !== 'object'
.map((path) => normalizePluginPath(path, root)) ? {
: [], scripts: options.optimization,
additionalEntryPoints: normalizeAdditionalEntries( styles: options.optimization,
root, }
options.additionalEntryPoints ?? [] : options.optimization,
), polyfills: options.polyfills ? resolve(root, options.polyfills) : undefined,
outputFileName: options.outputFileName ?? 'main.js', es2015Polyfills: options.es2015Polyfills
deleteOutputPath: options.deleteOutputPath ?? true, ? resolve(root, options.es2015Polyfills)
: undefined,
}; };
} }
function normalizeFileReplacements(
root: string,
fileReplacements: FileReplacement[]
): FileReplacement[] {
return fileReplacements.map((fileReplacement) => ({
replace: resolve(root, fileReplacement.replace),
with: resolve(root, fileReplacement.with),
}));
}
function normalizeAssets( export function normalizePluginPath(pluginPath: void | string, root: string) {
if (!pluginPath) {
return '';
}
try {
return require.resolve(pluginPath);
} catch {
return resolve(root, pluginPath);
}
}
export function normalizeAssets(
assets: any[], assets: any[],
root: string, root: string,
sourceRoot: string sourceRoot: string
): any[] { ): AssetGlobPattern[] {
if (!Array.isArray(assets)) {
return [];
}
return assets.map((asset) => { return assets.map((asset) => {
if (typeof asset === 'string') { if (typeof asset === 'string') {
const resolvedAssetPath = resolve(root, asset); const assetPath = normalizePath(asset);
const resolvedAssetPath = resolve(root, assetPath);
const resolvedSourceRoot = resolve(root, sourceRoot); const resolvedSourceRoot = resolve(root, sourceRoot);
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) { if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
@ -78,7 +94,8 @@ function normalizeAssets(
); );
} }
const resolvedAssetPath = resolve(root, asset.input); const assetPath = normalizePath(asset.input);
const resolvedAssetPath = resolve(root, assetPath);
return { return {
...asset, ...asset,
input: resolvedAssetPath, input: resolvedAssetPath,
@ -88,34 +105,3 @@ function normalizeAssets(
} }
}); });
} }
function normalizeFileReplacements(
root: string,
fileReplacements: FileReplacement[]
): FileReplacement[] {
return fileReplacements.map((fileReplacement) => ({
replace: resolve(root, fileReplacement.replace),
with: resolve(root, fileReplacement.with),
}));
}
function normalizePluginPath(path: string, root: string) {
try {
return require.resolve(path);
} catch {
return resolve(root, path);
}
}
function normalizeAdditionalEntries(
root: string,
additionalEntries: AdditionalEntryPoint[]
) {
return additionalEntries.map(
({ entryName, entryPath }) =>
({
entryName,
entryPath: resolve(root, entryPath),
} as AdditionalEntryPoint)
);
}

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