From 0ab55ea4126840431a382ec947816ef9170193ed Mon Sep 17 00:00:00 2001 From: Danielle Church Date: Fri, 11 Oct 2019 22:14:34 -0400 Subject: [PATCH 1/2] Add dist option for omitting 'dist' et al from output path --- README.md | 45 ++++++++++++++++++++++++- src/HtmlWebpackExternalsPlugin.js | 19 ++++++----- test/HtmlWebpackExternalsPlugin.spec.js | 42 +++++++++++++++++++++++ 3 files changed, 97 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9787031..b56932e 100644 --- a/README.md +++ b/README.md @@ -43,14 +43,17 @@ The constructor takes a configuration object with the following properties. | --- | --- | --- | --- | | `externals` | array<object> | An array of vendor modules that will be excluded from your Webpack bundle and added as `script` or `link` tags in your HTML output. | *None* | | `externals[].module` | string | The name of the vendor module. This should match the package name, e.g. if you are writing `import React from 'react'`, this would be `react`. | *None* | -| `externals[].entry` | string \| array<string> \| object \| array<object \| string> | The path, relative to the vendor module directory, to its pre-bundled distro file. e.g. for React, use `dist/react.js`, since the file exists at `node_modules/react/dist/react.js`. Specify an array if there are multiple CSS/JS files to inject. To use a CDN instead, simply use a fully qualified URL beginning with `http://`, `https://`, or `//`.

For entries whose type (JS or CSS) cannot be inferred by file extension, pass an object such as `{ path: 'https://some/url', type: 'css' }` (or `type: 'js'`). | *None* | +| `externals[].dist` | string | The relative path of the vendor distribution directory. All entry paths are resolved relative to this directory, but it is *not* included in the output path. | *None* | +| `externals[].entry` | string \| array<string> \| object \| array<object \| string> | The path, relative to the vendor distribution directory, to its pre-bundled distro file. e.g. for React, use `dist/react.js` (or, if you have set `.dist: "dist"`, just use `react.js`), since the file exists at `node_modules/react/dist/react.js`. Specify an array if there are multiple CSS/JS files to inject. To use a CDN instead, simply use a fully qualified URL beginning with `http://`, `https://`, or `//`.

For entries whose type (JS or CSS) cannot be inferred by file extension, pass an object such as `{ path: 'https://some/url', type: 'css' }` (or `type: 'js'`). | *None* | | `externals[].entry.path` | string | If entry is an object, the path to the asset. | *None* | +| `externals[].entry.dist` | string | The path of the vendor distribution directory, for this entry alone. | *Inherits from `externals[].dist`* | | `externals[].entry.type` | `'js'`\|`'css'` | The asset type, if it cannot be inferred. | *Inferred by extension when possible* | | `externals[].entry`
`  .cwpPatternConfig` | object | The properties to set on the [`copy-webpack-plugin` pattern object](https://github.com/webpack-contrib/copy-webpack-plugin#patterns). This object is merged in with the default `from` and `to` properties which are generated by the externals plugin. | `{}` | | `externals[].entry`
`  .attributes` | object.<string,string> | Additional attributes to add to the injected tag. | `{}` | | `externals[].global` | string \| null | For JavaScript modules, this is the name of the object globally exported by the vendor's dist file. e.g. for React, use `React`, since `react.js` creates a `window.React` global. For modules without an export (such as CSS), omit this property or use `null`. | `null` | | `externals[].supplements` | array<string> \| array<object> | For modules that require additional resources, specify globs of files to copy over to the output. e.g. for Bootstrap CSS, use `['dist/fonts/']`, since Glyphicon fonts are referenced in the CSS and exist at `node_modules/bootstrap/dist/fonts/`. Supplements can be specified as just an array of paths, or an array of objects with a path and copy plugin pattern object. | `[]` | | `externals[].supplements[]`
`  .path` | string | The glob path to copy assets from. | *None* | +| `externals[].supplements[]`
`  .dist` | string | The path of the vendor distribution directory, for this entry alone. | *Inherits from `externals[].dist`* | | `externals[].supplements[]`
`  .cwpPatternConfig` | object | The properties to set on the [`copy-webpack-plugin` pattern object](https://github.com/webpack-contrib/copy-webpack-plugin#patterns). This object is merged in with the default `from` and `to` properties which are generated by the externals plugin. | `{}` | | `externals[].append` | boolean | Set to true to inject this module after your Webpack bundles. | `false` | | `hash` | boolean | Set to true to append the injected module distro paths with a unique hash for cache-busting. | `false` | @@ -250,6 +253,46 @@ new HtmlWebpackExternalsPlugin({ }) ``` +### Omitting distribution path example + +By default, the Webpack output directory includes the full relative path of all copied files relative to their module root, including top-level directories like `dist` or `build`. You can omit these top-level directories in your output. + +Do not include a trailing slash or leading slash in the distribution path, they are concatenated automatically by the plugin. + +This example assumes `bootstrap` is installed in the app. It: + +1. copies `node_modules/bootstrap/dist/css/bootstrap.min.css` to `/vendor/bootstrap/css/bootstrap.min.css` +1. copies `node_modules/bootstrap/dist/css/bootstrap-theme.min.css` to `/vendor/bootstrap/bootstrap-theme.min.css` +1. copies all contents of `node_modules/bootstrap/dist/fonts/` to `/vendor/bootstrap/fonts/` +1. copies `node_modules/bootstrap/js/dist/dropdown.js` to `/vendor/bootstrap/dropdown.js` +1. adds `` to your HTML file, before your chunks +1. adds `` to your HTML file, before your chunks + +```js +new HtmlWebpackExternalsPlugin({ + externals: [ + { + module: 'bootstrap', + dist: 'dist', + entry: [ + 'css/bootstrap.min.css', + { + dist: 'dist/css', + path: 'bootstrap-theme.min.css', + }, + ], + supplements: [ + 'fonts/', + { + dist: 'js/dist', + path: 'dropdown.js', + }, + ], + }, + ], +}) +``` + ### Customizing public path example By default, local externals are resolved from the same root path as your Webpack configuration file's `output.publicPath`, concatenated with the `outputPath` variable. This is configurable. diff --git a/src/HtmlWebpackExternalsPlugin.js b/src/HtmlWebpackExternalsPlugin.js index 95dad13..11f1d0a 100644 --- a/src/HtmlWebpackExternalsPlugin.js +++ b/src/HtmlWebpackExternalsPlugin.js @@ -33,7 +33,7 @@ export default class HtmlWebpackExternalsPlugin { this.enabled = enabled this.cwpOptions = cwpOptions - externals.forEach(({ module, entry, global, supplements, append }) => { + externals.forEach(({ module, dist, entry, global, supplements, append }) => { this.externals.push(global ? { [module]: global } : module) const localEntries = [] @@ -46,7 +46,7 @@ export default class HtmlWebpackExternalsPlugin { return entry } - const result = { ...entry, path: `${module}/${entry.path}` } + const result = { module, dist, ...entry } localEntries.push(result) return result }) @@ -62,8 +62,8 @@ export default class HtmlWebpackExternalsPlugin { ...localEntries, ...supplements.map(asset => typeof asset === 'string' - ? { path: `${module}/${asset}`, cwpPatternConfig: {} } - : { ...asset, path: `${module}/${asset.path}` } + ? { module, dist, path: asset, cwpPatternConfig: {} } + : { module, dist, ...asset } ), ] }) @@ -96,9 +96,12 @@ export default class HtmlWebpackExternalsPlugin { pluginsToApply.push( new CopyWebpackPlugin( - this.assetsToCopy.map(({ path, cwpPatternConfig }) => ({ - from: path, - to: `${this.outputPath}/${path}`, + this.assetsToCopy.map(({ dist, ...entry }) => ({ + dist: dist ? `/${dist}` : '', + ...entry + })).map(({ module, path, dist, cwpPatternConfig }) => ({ + from: `${module}${dist}/${path}`, + to: `${this.outputPath}/${module}/${path}`, ...cwpPatternConfig, })), this.cwpOptions @@ -115,7 +118,7 @@ export default class HtmlWebpackExternalsPlugin { ? asset : { ...asset, - path: `${publicPath}${this.outputPath}/${asset.path}`, + path: `${publicPath}${this.outputPath}/${asset.module}/${asset.path}`, } ), append, diff --git a/test/HtmlWebpackExternalsPlugin.spec.js b/test/HtmlWebpackExternalsPlugin.spec.js index 28bd273..8c22a78 100644 --- a/test/HtmlWebpackExternalsPlugin.spec.js +++ b/test/HtmlWebpackExternalsPlugin.spec.js @@ -234,6 +234,48 @@ describe('HtmlWebpackExternalsPlugin', function() { ) }) + it('Omitting distribution path example', function() { + return runWebpack( + new HtmlWebpackPlugin(), + new HtmlWebpackExternalsPlugin({ + externals: [ + { + module: 'bootstrap', + dist: 'dist', + entry: [ + 'css/bootstrap.min.css', + { + dist: 'dist/css', + path: 'bootstrap-reboot.min.css', + }, + ], + supplements: [ + 'js/', + { + dist: 'js/dist', + path: 'dropdown.js', + }, + ], + }, + ], + }) + ) + .then(() => checkCopied('vendor/bootstrap/css/bootstrap.min.css')) + .then(() => checkCopied('vendor/bootstrap/bootstrap-reboot.min.css')) + .then(() => checkCopied('vendor/bootstrap/js/bootstrap.bundle.js')) + .then(() => checkCopied('vendor/bootstrap/dropdown.js')) + .then(() => + checkHtmlIncludes( + 'vendor/bootstrap/css/bootstrap.min.css', + 'css' + )) + .then(() => + checkHtmlIncludes( + 'vendor/bootstrap/bootstrap-reboot.min.css', + 'css' + )) + }) + it('Customizing public path example', function() { return runWebpack( new HtmlWebpackPlugin(), From 98c801e259dc7129efd541b7ea7bb6f2967abd25 Mon Sep 17 00:00:00 2001 From: Danielle Church Date: Sat, 12 Oct 2019 02:32:31 -0400 Subject: [PATCH 2/2] Fixing output paths for glob supplementals This requires copy-webpack-plugin 4.6.0 for the transformPath property, which also requires working around a yet-unfixed regression in c-w-p that was causing a test failure - the "nobraces" option no longer works properly when the input is an explicit file or directory. So, if there's a cwpPatternConfig.fromArgs option, we explicitly tell c-w-p to treat this as a glob. We could probably safely do this no matter what, but better safe than sorry. --- package.json | 2 +- src/HtmlWebpackExternalsPlugin.js | 15 +++++-- test/HtmlWebpackExternalsPlugin.spec.js | 56 +++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 030838f..0c443fe 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ }, "dependencies": { "ajv": "^6.1.1", - "copy-webpack-plugin": "^4.4.1", + "copy-webpack-plugin": "^4.6.0", "html-webpack-include-assets-plugin": "^1.0.2" } } diff --git a/src/HtmlWebpackExternalsPlugin.js b/src/HtmlWebpackExternalsPlugin.js index 11f1d0a..550154c 100644 --- a/src/HtmlWebpackExternalsPlugin.js +++ b/src/HtmlWebpackExternalsPlugin.js @@ -2,6 +2,7 @@ import CopyWebpackPlugin from 'copy-webpack-plugin' import HtmlWebpackIncludeAssetsPlugin from 'html-webpack-include-assets-plugin' import Ajv from 'ajv' import configSchema from './configSchema.json' +import path from 'path' export default class HtmlWebpackExternalsPlugin { static validateArguments = (() => { @@ -94,14 +95,22 @@ export default class HtmlWebpackExternalsPlugin { const pluginsToApply = [] + const context = path.isAbsolute(this.cwpOptions.context) + ? this.cwpOptions.context + : path.resolve(compiler.options.context, this.cwpOptions.context || ".") + pluginsToApply.push( new CopyWebpackPlugin( - this.assetsToCopy.map(({ dist, ...entry }) => ({ + this.assetsToCopy.map(({ dist, cwpPatternConfig, ...entry }) => ({ dist: dist ? `/${dist}` : '', + cwpPatternConfig: cwpPatternConfig || {}, ...entry })).map(({ module, path, dist, cwpPatternConfig }) => ({ - from: `${module}${dist}/${path}`, - to: `${this.outputPath}/${module}/${path}`, + from: cwpPatternConfig.fromArgs + ? {...cwpPatternConfig.fromArgs, glob:`${module}${dist}/${path}`} + : `${module}${dist}/${path}`, + to: 'UNEXPECTED_ERROR/', // We're going to be overwriting this below + transformPath: (_,absolutePath) => (absolutePath.replace(`${cwpPatternConfig.context||context}/${module}${dist}`,`${this.outputPath}/${module}`)), ...cwpPatternConfig, })), this.cwpOptions diff --git a/test/HtmlWebpackExternalsPlugin.spec.js b/test/HtmlWebpackExternalsPlugin.spec.js index 8c22a78..2d5f937 100644 --- a/test/HtmlWebpackExternalsPlugin.spec.js +++ b/test/HtmlWebpackExternalsPlugin.spec.js @@ -402,6 +402,62 @@ describe('HtmlWebpackExternalsPlugin', function() { .then(() => checkHtmlIncludes('vendor/context_test/dist/contextTest.css', 'css')) }) + it('works with glob filename supplements, matching single', function() { + const externals = [ + { + module: 'bootstrap', + entry: ['dist/js/bootstrap.min.js'], + supplements: [ + 'dist/css/bootstrap.min.c*s', + ] + }, + ] + + return runWebpack( + new HtmlWebpackPlugin(), + new HtmlWebpackExternalsPlugin({ externals }) + ) + .then(() => checkCopied('vendor/bootstrap/dist/css/bootstrap.min.css')) + }) + + it('works with glob filename supplements, matching multiple', function() { + const externals = [ + { + module: 'bootstrap', + entry: ['dist/js/bootstrap.min.js'], + supplements: [ + 'dist/css/*.min.css', + ] + }, + ] + + return runWebpack( + new HtmlWebpackPlugin(), + new HtmlWebpackExternalsPlugin({ externals }) + ) + .then(() => checkCopied('vendor/bootstrap/dist/css/bootstrap.min.css')) + .then(() => checkCopied('vendor/bootstrap/dist/css/bootstrap-reboot.min.css')) + }) + + it('works with recursive glob supplements', function() { + const externals = [ + { + module: 'bootstrap', + entry: ['dist/js/bootstrap.min.js'], + supplements: [ + '**/dropdown.js', + ] + }, + ] + + return runWebpack( + new HtmlWebpackPlugin(), + new HtmlWebpackExternalsPlugin({ externals }) + ) + .then(() => checkCopied('vendor/bootstrap/js/dist/dropdown.js')) + .then(() => checkCopied('vendor/bootstrap/js/src/dropdown.js')) + }) + it('Specifying which HTML files to affect example', function() { return runWebpack( new HtmlWebpackPlugin({