├── .nvmrc ├── index.d.ts ├── CHANGELOG.md ├── example └── src │ ├── index.ts │ └── tsconfig.json ├── tslint.json ├── .yarnrc ├── .npmrc ├── .gitignore ├── dynamicImporter.d.ts ├── src ├── tsconfig.json ├── Stats.ts └── DynamicVendorPlugin.ts ├── LICENSE ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 10 -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/DynamicVendorPlugin'; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 4 | ### Feature 5 | - Add plugin and dynamic importer -------------------------------------------------------------------------------- /example/src/index.ts: -------------------------------------------------------------------------------- 1 | import { dynamicImporter } from 'dynamic-vendor-webpack-plugin/dynamicImporter'; 2 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@wynd/tslint-config-wynd" 4 | ], 5 | "rules": { 6 | "no-console": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | always-auth false 2 | registry "https://registry.npmjs.org/" 3 | "@wynd:always-auth" true 4 | "@wynd:registry" "https://nexus.wynd.eu/repository/npm/" 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | always-auth=false 2 | registry=https://registry.npmjs.org/ 3 | @wynd:always-auth=true 4 | @wynd:registry=https://nexus.wynd.eu/repository/npm/ 5 | package-lock=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/* 3 | !/dist/.gitkeep 4 | /example/compiled/ 5 | 6 | tslint-ouput.txt 7 | /example/compiled/* 8 | 9 | /coverage/* 10 | /doc/* 11 | !/doc/.gitkeep 12 | 13 | /*.tgz 14 | -------------------------------------------------------------------------------- /dynamicImporter.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the dynamic importer. 3 | * It will return an array of lazy import to load by calling the wrapping function. 4 | */ 5 | export const dynamicImporter: Array<() => Promise>; 6 | -------------------------------------------------------------------------------- /example/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "pretty": true, 7 | "sourceMap": true, 8 | "lib": [ 9 | "dom", 10 | "es2017" 11 | ], 12 | "baseUrl": "./", 13 | "paths": { 14 | "dynamic-vendor-webpack-plugin/*": ["../../*"] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2016", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "lib": [ 9 | "es2017" 10 | ], 11 | "declaration": true, 12 | "declarationDir": "../dist", 13 | "outDir": "../dist", 14 | "pretty": true, 15 | "removeComments": false 16 | } 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lilian Saget-Lethias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamic-vendor-webpack-plugin", 3 | "version": "1.0.0", 4 | "description": "A Webpack plugin that gives you a way to import vendors with dynamic variable and specific code splitting", 5 | "main": "dist/DynamicVendorPlugin.js", 6 | "repository": "git@github.com:bios21/dynamic-vendor-webpack-plugin.git", 7 | "author": "Lilian Saget-Lethias ", 8 | "license": "MIT", 9 | "files": [ 10 | "dist/", 11 | "dynamicImporter.d.ts", 12 | "index.d.ts" 13 | ], 14 | "types": "./index.d.ts", 15 | "typings": "./index.d.ts", 16 | "scripts": { 17 | "build": "rimraf dist/* && tsc -P src/" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^10.12.3", 21 | "@types/tapable": "^1.0.4", 22 | "@types/webpack": "^4.4.18", 23 | "@wynd/tslint-config-wynd": "^1.1.5", 24 | "jest": "^23.6.0", 25 | "prettier": "^1.15.1", 26 | "rimraf": "^2.6.2", 27 | "ts-jest": "^23.10.4", 28 | "ts-node": "^7.0.1", 29 | "tsconfig-paths": "^3.6.0", 30 | "tslint": "^5.11.0", 31 | "typescript": "^3.1.6", 32 | "webpack": "^4.25.1", 33 | "webpack-cli": "^3.1.2" 34 | }, 35 | "peerDependencies": { 36 | "webpack": "4.x" 37 | }, 38 | "keywords": [ 39 | "webpack", 40 | "plugin", 41 | "vendor", 42 | "vendors", 43 | "import", 44 | "dynamic-import", 45 | "dynamic-vendor-webpack-plugin" 46 | ], 47 | "engines": { 48 | "node": ">=10" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Stats.ts: -------------------------------------------------------------------------------- 1 | import constants from 'constants'; 2 | import { Stats as BaseStats } from 'fs'; 3 | 4 | // tslint:disable-next-line:ban-types 5 | type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]; 6 | type NonFunctionProperties = Pick>; 7 | 8 | type StatsConstructorConfig = Partial>; 9 | 10 | export class Stats implements BaseStats { 11 | public dev: number; 12 | public ino: number; 13 | public mode: number; 14 | public nlink: number; 15 | public uid: number; 16 | public gid: number; 17 | public rdev: number; 18 | public size: number; 19 | public blksize: number; 20 | public blocks: number; 21 | public atimeMs: number; 22 | public mtimeMs: number; 23 | public ctimeMs: number; 24 | public birthtimeMs: number; 25 | public atime: Date; 26 | public mtime: Date; 27 | public ctime: Date; 28 | public birthtime: Date; 29 | 30 | constructor(config: StatsConstructorConfig) { 31 | Object.keys(config).forEach(k => { 32 | this[k] = config[k]; 33 | }); 34 | } 35 | 36 | public static genericStats(content: string): Stats { 37 | const t = new Date(); 38 | return new Stats({ 39 | atime: t, 40 | birthtime: t, 41 | blksize: 4096, 42 | ctime: t, 43 | dev: 8675309, 44 | gid: 20, 45 | ino: 44700000, 46 | mode: 33188, 47 | mtime: t, 48 | nlink: 1, 49 | rdev: 0, 50 | size: content.length, 51 | uid: 501, 52 | }); 53 | } 54 | 55 | private checkModeProperty(p: number) { 56 | // tslint:disable-next-line:no-bitwise 57 | return (this.mode & constants.S_IFMT) === p; 58 | } 59 | 60 | public isDirectory() { 61 | return this.checkModeProperty(constants.S_IFDIR); 62 | } 63 | 64 | public isFile() { 65 | return this.checkModeProperty(constants.S_IFREG); 66 | } 67 | 68 | public isBlockDevice() { 69 | return this.checkModeProperty(constants.S_IFBLK); 70 | } 71 | 72 | public isCharacterDevice() { 73 | return this.checkModeProperty(constants.S_IFCHR); 74 | } 75 | 76 | public isSymbolicLink() { 77 | return this.checkModeProperty(constants.S_IFLNK); 78 | } 79 | 80 | public isFIFO() { 81 | return this.checkModeProperty(constants.S_IFIFO); 82 | } 83 | 84 | public isSocket() { 85 | return this.checkModeProperty(constants.S_IFSOCK); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | [![npm][npm]][npm-url] 5 | 6 | [![deps][deps]][deps-url] 7 | [![node][node]][node-url] 8 | 9 |
10 | 11 | 12 | 13 |

dynamic-vendor-webpack-plugin

14 |

This is a webpack plugin that gives you a way to import vendors with dynamic variable and specific code splitting.

15 |
16 | 17 |

Requirements

18 | 19 | `dynamic-vendor-webpack-plugin` relies on [webpack] 4. It will be updated as needed on major updates of webpack. 20 | 21 | 22 |

Install

23 | 24 | ```bash 25 | yarn add -D dynamic-vendor-webpack-plugin 26 | # or 27 | npm i --save-dev dynamic-vendor-webpack-plugin 28 | ``` 29 | 30 | 31 |

Usage

32 | 33 | Dynamic vendor code splitting is a two steps code process. First, you need to setup the plugin in your `webpack` config with desired "lazy" vendors, then import the dynamic importer wherever you need in your code. 34 | FYI, the following examples are based on a Typescript code based application. 35 | 36 | **webpack.config.t-s** 37 | ```ts 38 | import { DynamicVendorPlugin } from 'dynamic-vendor-webpack-plugin'; 39 | import { Configuration } from 'webpack'; 40 | 41 | const config: Configuration = { 42 | // ... your webpack configuration 43 | plugins: [ 44 | new DynamicVendorPlugin({ 45 | vendors: ['my-vendor'], 46 | }), 47 | ], 48 | } 49 | export default config; 50 | ``` 51 | 52 | **index.ts** 53 | ```ts 54 | // fetch the array of your vendors 55 | // the module is not load by default but wrapped in a pure function 56 | import { dynamicImporter } from 'dynamic-vendor-webpack-plugin/dynamicImporter'; 57 | 58 | (async () => { 59 | // run through it 60 | for (const fn of dynamicImporter) { 61 | // get the module with the function 62 | const m = await fn(); 63 | 64 | // use it 65 | new (m.default)(); 66 | } 67 | })(); 68 | ``` 69 | 70 | This will generate a separated chunk with this vendor (and its exclusive dependencies) loaded on demand by you application. 71 | 72 | 73 |

Options

74 | 75 | - `options.vendors: Array`: The list of vendors by their name of by a more detailed object. 76 | - `options.vendors[].name: string`: The name of the vendor (dependecy name). 77 | - `options.vendors[].magicComment: WebpackMagicComment`: List of webpack magic comment import configuration. (see https://webpack.js.org/api/module-methods/#import- ) 78 | - `options.webpackChunkName: string`: A name for the dynamic vendor chunk. `'dynamic-vendor'` by default, you can atomically override this for each vendors with a vendor object. 79 | 80 | ### Conditional vendor 81 | **webpack.config.ts** 82 | ```ts 83 | const DEV = process.env.NODE_ENV === 'development'; 84 | { 85 | mode: DEV ? 'development' : 'production', 86 | plugins: [ 87 | new DynamicVendorPlugin({ 88 | vendors: [ 89 | { 90 | // admiting that you have a services third party library 91 | name: DEV ? 'mock-service-lib' : 'service-lib', 92 | }, 93 | ], 94 | }), 95 | ], 96 | } 97 | ``` 98 | 99 | ### Group similar specific vendor together (i.e. plugins) 100 | **webpack.config.ts** 101 | ```ts 102 | import packageJson from './package.json'; 103 | 104 | { 105 | plugins: [ 106 | new DynamicVendorPlugin({ 107 | // admiting that you want to lazy blind load all vendors under a specific pattern 108 | // in this case '@mylib/*' 109 | vendors: Object.keys(packageJson.dependencies).filter(d => d.startsWith('@mylib/')), 110 | }), 111 | ], 112 | } 113 | ``` 114 | 115 |

Maintainers

116 | 117 | 118 | 119 | 125 | 126 | 127 |
120 | 122 |
123 | Lilian Saget-Lethias 124 |
128 | 129 | 130 | [npm]: https://img.shields.io/npm/v/dynamic-vendor-webpack-plugin.svg 131 | [npm-url]: https://npmjs.com/package/dynamic-vendor-webpack-plugin 132 | 133 | [node]: https://img.shields.io/node/v/dynamic-vendor-webpack-plugin.svg 134 | [node-url]: https://nodejs.org 135 | 136 | [deps]: https://img.shields.io/david/bios21/dynamic-vendor-webpack-plugin.svg 137 | [deps-url]: https://david-dm.org/bios21/dynamic-vendor-webpack-plugin 138 | 139 | [webpack]: https://webpack.org -------------------------------------------------------------------------------- /src/DynamicVendorPlugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { Compiler, Plugin } from 'webpack'; 3 | import { Stats } from './Stats'; 4 | 5 | interface WebpackMagicComment { 6 | /** 7 | * Disables dynamic import parsing when set to `true`. 8 | */ 9 | webpackIgnore?: boolean; 10 | /** 11 | * A name for the new chunk. The placeholders `[index]` and `[request]` are supported within the given string to an incremented number or the actual resolved filename respectively. 12 | */ 13 | webpackChunkName?: string; 14 | /** 15 | * Different modes for resolving dynamic imports can be specified. The following options are supported: 16 | * - "lazy" (default): Generates a lazy-loadable chunk for each `import()`ed module. 17 | * - "eager": Generates no extra chunk. All modules are included in the current chunk and no additional network requests are made. A `Promise` is still returned but is already resolved. In contrast to a static import, the module isn't executed until the call to `import()` is made. 18 | * - "weak": Tries to load the module if the module function has already been loaded in some other way (i. e. another chunk imported it or a script containing the module was loaded). A `Promise` is still returned but, only successfully resolves if the chunks are already on the client. If the module is not available, the `Promise` is rejected. A network request will never be performed. This is useful for universal rendering when required chunks are always manually served in initial requests (embedded within the page), but not in cases where app navigation will trigger an import not initially served. 19 | */ 20 | webpackMode?: 'lazy' | 'eager' | 'weak'; 21 | /** 22 | * A regular expression that will be matched against during import resolution. 23 | * Only modules that match **will be bundled.** 24 | */ 25 | webpackInclude?: RegExp; 26 | /** 27 | * A regular expression that will be matched against during import resolution. 28 | * Any module that matches **will not be bundled.** 29 | */ 30 | webpackExclude?: RegExp; 31 | webpackPrefetch?: boolean | number; 32 | webpackPreload?: boolean | number; 33 | } 34 | 35 | interface VendorEntry { 36 | /** 37 | * The name of the vendor (dependecy name). 38 | */ 39 | name: string; 40 | /** 41 | * List of webpack magic comment import configuration. 42 | */ 43 | magicComment?: WebpackMagicComment; 44 | } 45 | 46 | export interface Options { 47 | /** 48 | * The list of vendors by their name of by a more detailed object. 49 | */ 50 | vendors: Array; 51 | /** 52 | * A name for the new chunk. The placeholders `[index]` and `[request]` are supported within the given string to an incremented number or the actual resolved filename respectively. 53 | */ 54 | webpackChunkName?: string; 55 | } 56 | 57 | export class DynamicVendorPlugin implements Plugin { 58 | private readonly pluginName = this.constructor.name; 59 | private readonly options: Partial = { 60 | webpackChunkName: 'dynamic-vendor', 61 | }; 62 | 63 | public constructor(options: Options) { 64 | this.options = { ...this.options, ...options }; 65 | } 66 | 67 | private buildContent(): string { 68 | return this.options.vendors.reduce((previous: string, current): string => { 69 | let imp: string; 70 | if (typeof current === 'string') { 71 | imp = `() => import(/* webpackChunkName: "${this.options.webpackChunkName}" */ '${current}')`; 72 | } else { 73 | const chunkName = 74 | current.magicComment && current.magicComment.webpackChunkName 75 | ? current.magicComment.webpackChunkName 76 | : this.options.webpackChunkName; 77 | const magicComments = current.magicComment 78 | ? Object.keys(current.magicComment) 79 | .filter(c => c !== 'webpackChunkName') 80 | .map(c => { 81 | const comment = current.magicComment[c]; 82 | return `${c}: ${typeof comment === 'string' ? `"${comment}"` : comment}`; 83 | }) 84 | : []; 85 | magicComments.push(`webpackChunkName: "${chunkName}"`); 86 | 87 | imp = `() => import(/* ${magicComments.join(', ')} */ '${current.name}')`; 88 | } 89 | if (previous) { 90 | return `${previous}, ${imp}`; 91 | } 92 | return imp; 93 | }, '') as string; 94 | } 95 | 96 | public apply(compiler: Compiler): void { 97 | const ifs = compiler.inputFileSystem; 98 | 99 | const statStorage: { data: Map } = (ifs as any)._statStorage; 100 | const readFileStorage: { data: Map } = (ifs as any)._readFileStorage; 101 | 102 | const CONTENT = `export const dynamicImporter = [${this.buildContent()}];\n`; 103 | 104 | const PATH = path.resolve(compiler['context'], 'node_modules/dynamic-vendor-webpack-plugin/dynamicImporter'); 105 | 106 | compiler.hooks.normalModuleFactory.tap(this.pluginName, nmf => { 107 | nmf.hooks.beforeResolve.tap(this.pluginName, () => { 108 | if (readFileStorage.data.has(PATH)) { 109 | return; 110 | } 111 | 112 | statStorage.data.set(PATH, [null, Stats.genericStats(CONTENT)]); 113 | readFileStorage.data.set(PATH, [null, CONTENT]); 114 | }); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------