├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── codecov.yml ├── commitlint.config.js ├── demo ├── es5-and-es6 │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ └── hello-world.js │ └── webpack.config.js └── mixed-modules │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── app.js │ └── lib │ │ ├── commonjs-lazy.js │ │ ├── commonjs.js │ │ ├── es6-lazy.js │ │ ├── es6.js │ │ ├── forward-es6.js │ │ ├── goog-module.js │ │ └── goog-require.js │ └── webpack.config.js ├── package-lock.json ├── package.json ├── schema ├── closure-compiler.json └── closure-library.json ├── src ├── aggressive-bundle-externs.js ├── basic-runtime.js ├── chunk-sources.js ├── closure-compiler-plugin.js ├── closure-library-plugin.js ├── closure-runtime-template.js ├── common-ancestor.js ├── dependencies │ ├── amd-define-dependency.js │ ├── goog-base-global.js │ ├── goog-dependency.js │ ├── goog-loader-es6-prefix-dependency.js │ ├── goog-loader-es6-suffix-dependency.js │ ├── goog-loader-prefix-dependency.js │ ├── goog-loader-suffix-dependency.js │ ├── harmony-export-dependency.js │ ├── harmony-export-import-dependency.js │ ├── harmony-import-dependency.js │ ├── harmony-marker-dependency.js │ ├── harmony-noop-template.js │ └── harmony-parser-plugin.js ├── goog-require-parser-plugin.js ├── module-name.js ├── runtime.js ├── safe-path.js └── standard-externs.js └── test ├── agressive-mode.test.js ├── fixtures ├── amd │ ├── b.js │ ├── c.js │ └── index.js ├── cjs-common-chunk │ ├── b.js │ ├── c.js │ └── index.js ├── cjs │ ├── b.js │ ├── c.js │ └── index.js ├── esm-common-chunk │ ├── b.js │ ├── c.js │ └── index.js └── esm │ ├── b.js │ ├── c.js │ └── index.js ├── helpers └── compiler.js └── simple-mode.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "useBuiltIns": true, 7 | "targets": { 8 | "node": "6.9.0" 9 | }, 10 | "exclude": [ 11 | "transform-async-to-generator", 12 | "transform-regenerator" 13 | ] 14 | } 15 | ] 16 | ], 17 | "env": { 18 | "test": { 19 | "presets": [ 20 | "env" 21 | ], 22 | "plugins": [] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /test/fixtures 4 | /demo 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | plugins: ['prettier'], 4 | extends: ['@webpack-contrib/eslint-config-webpack'], 5 | rules: { 6 | 'prettier/prettier': [ 7 | 'error', 8 | { singleQuote: true, trailingComma: 'es5', arrowParens: 'always' }, 9 | ], 10 | 'func-names': 0, 11 | 'import/order': 0, 12 | 'no-var': 0, 13 | 'vars-on-top': 0, 14 | 'no-empty': 0, 15 | 'dot-notation': 0, 16 | 'no-underscore-dangle': 0, 17 | 'no-unused-vars': 0, 18 | 'camelcase': 0, 19 | 'no-param-reassign': 0, 20 | 'no-multi-assign': 0, 21 | 'no-unused-expressions': 0, 22 | 'prefer-arrow-callback': 0, 23 | 'prefer-rest-params': 0, 24 | 'prefer-template': 0, 25 | 'class-methods-use-this': 0, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.json text 2 | * text=auto 3 | bin/* eol=lf 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These are the default owners for everything in 2 | # webpack-contrib 3 | @webpack-contrib/org-maintainers 4 | 5 | # Add repository specific users / groups 6 | # below here for libs that are not maintained by the org. 7 | @ChadKillingsworth 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | node-version: [14.x, 16.x, 18.x] 14 | os: [ubuntu-latest, windows-latest] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm install 25 | name: NPM Rebuild 26 | - run: npm run ci:coverage 27 | name: Run unit tests. 28 | - uses: codecov/codecov-action@v3 29 | if: ${{ github.event_name == 'pull_request' && matrix.node-version == '18.x' && matrix.os == 'ubuntu-latest' }} 30 | with: 31 | fail_ci_if_error: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | 61 | /coverage 62 | /dist 63 | /local 64 | /reports 65 | /node_modules 66 | .DS_Store 67 | Thumbs.db 68 | .idea 69 | .vscode 70 | *.sublime-project 71 | *.sublime-workspace -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "arrowParens": "always" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.5.0](https://github.com/webpack-contrib/closure-webpack-plugin/compare/v2.4.0...v2.5.0) (2021-01-08) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * Update google-closure-compiler peer dependency to 20200830 ([1246f6e](https://github.com/webpack-contrib/closure-webpack-plugin/commit/1246f6e51049a5d0afcd3b6ef2ef32a5bf3f9940)) 11 | 12 | ## [2.4.0](https://github.com/webpack-contrib/closure-webpack-plugin/compare/v2.3.0...v2.4.0) (2021-01-04) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * CI by removing duplicate version field ([a8aade6](https://github.com/webpack-contrib/closure-webpack-plugin/commit/a8aade6535b8267eca7784f089c5eac4800a6013)) 18 | * Ignore EPIPE if the compiler input stream is closed ([8e738c1](https://github.com/webpack-contrib/closure-webpack-plugin/commit/8e738c1fa2835d52f2debfd7f9178e83b614125d)) 19 | * include goog.requireType in build dependencies ([6182f55](https://github.com/webpack-contrib/closure-webpack-plugin/commit/6182f55d3afb06444c69b58e2645a884681d1c05)), closes [#140](https://github.com/webpack-contrib/closure-webpack-plugin/issues/140) 20 | * Infinite loops when circular dependency in parent chunks ([c9539ad](https://github.com/webpack-contrib/closure-webpack-plugin/commit/c9539adcc33f14b029e25ceb2cddff0aaf03b947)) 21 | * make json files text ([c187c03](https://github.com/webpack-contrib/closure-webpack-plugin/commit/c187c0370d6b970d2540212f904171c0981b8655)) 22 | * Replace deprecated Buffer with Buffer.from ([e1a402b](https://github.com/webpack-contrib/closure-webpack-plugin/commit/e1a402bc3da3a5d8cd0a0443e2b0aa81419a2aef)) 23 | * sourcemap paths fail to resolve when using loaders ([a6d0b11](https://github.com/webpack-contrib/closure-webpack-plugin/commit/a6d0b114c889be3c190293521de1565b4eba872f)) 24 | * update deps to address security warnings ([479b953](https://github.com/webpack-contrib/closure-webpack-plugin/commit/479b953de3c5a1f139880ec2c3b739d42d6ba3f4)) 25 | * update more deps to address security warnings ([67c5f22](https://github.com/webpack-contrib/closure-webpack-plugin/commit/67c5f2201f0ebf3a6379f822b641bb665095bfb3)) 26 | * use safe path in source maps ([234e437](https://github.com/webpack-contrib/closure-webpack-plugin/commit/234e4375dfcaed516d8c97b1b6ac900d22b737b8)) 27 | 28 | # Change Log 29 | 30 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 31 | 32 | x.x.x / -- 33 | ================== 34 | 35 | * Bug fix - 36 | * Feature - 37 | * Chore - 38 | * Docs - 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # closure-webpack-plugin 2 | 3 | [![npm version](https://badge.fury.io/js/closure-webpack-plugin.svg)](https://badge.fury.io/js/closure-webpack-plugin) 4 | 5 | This plugin supports the use of Google's Closure Tools with webpack. 6 | 7 | **Note: This is the webpack 4 branch.** 8 | 9 | [Closure-Compiler](https://developers.google.com/closure/compiler/) is a full optimizing compiler and transpiler. 10 | It offers unmatched optimizations, provides type checking and can easily target transpilation to different versions of ECMASCRIPT. 11 | 12 | [Closure-Library](https://developers.google.com/closure/library/) is a utility library designed for full compatibility 13 | with Closure-Compiler. 14 | 15 | ## Older Versions 16 | 17 | For webpack 3 support, see https://github.com/webpack-contrib/closure-webpack-plugin/tree/webpack-3 18 | 19 | ## Install 20 | 21 | You must install both the google-closure-compiler package as well as the closure-webpack-plugin. 22 | 23 | ``` 24 | npm install --save-dev closure-webpack-plugin google-closure-compiler 25 | ``` 26 | 27 | ## Usage example 28 | 29 | ```js 30 | const ClosurePlugin = require('closure-webpack-plugin'); 31 | 32 | module.exports = { 33 | optimization: { 34 | minimizer: [ 35 | new ClosurePlugin({mode: 'STANDARD'}, { 36 | // compiler flags here 37 | // 38 | // for debugging help, try these: 39 | // 40 | // formatting: 'PRETTY_PRINT' 41 | // debug: true, 42 | // renaming: false 43 | }) 44 | ] 45 | } 46 | }; 47 | ``` 48 | 49 | ## Options 50 | 51 | * **platform** - `native`, `java` or `javascript`. Controls which version to use of closure-compiler. 52 | By default the plugin will attempt to automatically choose the fastest option available. 53 | - `JAVASCRIPT` does not require the JVM to be installed. Not all flags are supported. 54 | - `JAVA` utilizes the jvm. Utilizes multiple threads for parsing and results in faster compilation for large builds. 55 | - `NATIVE` only available on linux or MacOS. Faster compilation times without requiring a JVM. 56 | * **mode** - `STANDARD` (default) or `AGGRESSIVE_BUNDLE`. Controls how the plugin utilizes the compiler. 57 | - `STANDARD` mode, closure-compiler is used as a direct replacement for other minifiers as well as most Babel transformations. 58 | - `AGGRESSIVE_BUNDLE` mode, the compiler performs additional optimizations of modules to produce a much smaller file 59 | * **childCompilations** - boolean or function. Defaults to `false`. 60 | In order to decrease build times, this plugin by default only operates on the main compilation. 61 | Plugins such as extract-text-plugin and html-webpack-plugin run as child compilations and 62 | usually do not need transpilation or minification. You can enable this for all child compilations 63 | by setting this option to `true`. For specific control, the option can be set to a function which 64 | will be passed a compilation object. 65 | Example: `function(compilation) { return /html-webpack/.test(compilation.name); }`. 66 | * **output** - An object with either `filename` or `chunkfilename` properties. Used to override the 67 | output file naming for a particular compilation. See https://webpack.js.org/configuration/output/ 68 | for details. 69 | * **test** - An optional string or regular expression to determine whether a chunk is included in the compilation 70 | * **extraCommandArgs** - Optional string or Array of strings to pass to the google-closure-compiler plugin. 71 | Can be used to pass flags to the java process. 72 | 73 | ## Compiler Flags 74 | 75 | The plugin controls several compiler flags. The following flags should not be used in any mode: 76 | 77 | * module_resolution 78 | * output_wrapper 79 | * dependency_mode 80 | * create_source_map 81 | * module 82 | * entry_point 83 | 84 | ## Aggressive Bundle Mode 85 | 86 | In this mode, the compiler rewrites CommonJS modules and hoists require calls. Some modules are not compatible with this type of rewriting. In particular, hoisting will cause the following code to execute out of order: 87 | 88 | ```js 89 | const foo = require('foo'); 90 | addPolyfillToFoo(foo); 91 | const bar = require('bar'); 92 | ``` 93 | 94 | Aggressive Bundle Mode utilizes a custom runtime in which modules within a chunk file are all included in the same scope. 95 | This avoids [the cost of small modules](https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/). 96 | 97 | In Aggressive Bundle Mode, a file can only appear in a single output chunk. Use the [Split Chunks Plugin](https://webpack.js.org/plugins/split-chunks-plugin/) 98 | to split duplicated files into a single output chunk. If a module is utilized by more than one chunk, the 99 | plugin will move it up to the first common parent to prevent code duplication. 100 | 101 | The [concatenatedModules optimization](https://webpack.js.org/configuration/optimization/#optimization-concatenatemodules) 102 | is not compatible with this mode since Closure-Compiler performs an equivalent optimization). 103 | The plugin will emit a warning if this optimization is not disabled. 104 | 105 | ## Multiple Output Languages 106 | 107 | You can add the plugin multiple times. This easily allows you to target multiple output languages. 108 | Use `ECMASCRIPT_2015` for modern browsers and `ECMASCRIPT5` for older browsers. 109 | 110 | Use the `output` option to change the filenames of specific plugin instances. 111 | 112 | Use ` 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/es5-and-es6/src/hello-world.js: -------------------------------------------------------------------------------- 1 | import { PolymerElement, html } from '@polymer/polymer/polymer-element'; 2 | 3 | class HelloWorldElement extends PolymerElement { 4 | static get is() { 5 | return 'hello-world'; 6 | } 7 | 8 | static get template() { 9 | return html` 10 | 16 |
[[greeting]]
17 | `; 18 | } 19 | 20 | static get properties() { 21 | return { 22 | greeting: { 23 | type: String, 24 | value: 'hello world' 25 | } 26 | } 27 | } 28 | } 29 | 30 | customElements.define(HelloWorldElement.is, HelloWorldElement); 31 | -------------------------------------------------------------------------------- /demo/es5-and-es6/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ClosureCompilerPlugin = require('../../src/closure-compiler-plugin'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | module.exports = (env, argv) => { 6 | const isProduction = argv.mode === 'production' || !argv.mode; 7 | 8 | return { 9 | entry: { 10 | 'hello-world': './src/hello-world.js' 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, 'public'), 14 | filename: 'js/[name].js', 15 | }, 16 | devServer: { 17 | open: true, 18 | contentBase: path.resolve(__dirname, 'public'), 19 | inline: !isProduction 20 | }, 21 | devtool: 'source-map', 22 | optimization: { 23 | minimize: isProduction, 24 | minimizer: [ 25 | new ClosureCompilerPlugin({ 26 | mode: 'AGGRESSIVE_BUNDLE' 27 | }, { 28 | languageOut: 'ECMASCRIPT_2015' 29 | }), 30 | new ClosureCompilerPlugin({ 31 | mode: 'AGGRESSIVE_BUNDLE', 32 | output: { 33 | filename: 'js/es5-[name].js' 34 | } 35 | }, { 36 | languageOut: 'ECMASCRIPT5' 37 | }) 38 | ], 39 | splitChunks: { 40 | minSize: 0 41 | }, 42 | concatenateModules: false, 43 | }, 44 | plugins: [ 45 | new CopyWebpackPlugin({ 46 | patterns: [{ 47 | from: path.resolve(__dirname, './node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js'), 48 | to: path.resolve(__dirname, 'public', 'js', 'webcomponentsjs/webcomponents-loader.js') 49 | }, { 50 | from: path.resolve(__dirname, './node_modules/@webcomponents/webcomponentsjs/bundles'), 51 | to: path.resolve(__dirname, 'public', 'js', 'webcomponentsjs', 'bundles') 52 | }] 53 | }) 54 | ] 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /demo/mixed-modules/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/*.js 3 | public/*.js.map 4 | -------------------------------------------------------------------------------- /demo/mixed-modules/README.md: -------------------------------------------------------------------------------- 1 | # Mixed modules demo 2 | 3 | This is a demo using mixed modules, which includes the following module systems and libraries. 4 | 5 | * Closure Script(`goog.require`) 6 | * Closure Modules(`goog.module`) 7 | * ES Modules(ES Modules) 8 | 9 | * Closure Library 10 | * npm package 11 | 12 | ## How to use 13 | 14 | * Start development with `webpack-dev-server` 15 | 16 | ``` 17 | npm start 18 | ``` 19 | 20 | * Build 21 | 22 | ``` 23 | npm run build 24 | ``` 25 | -------------------------------------------------------------------------------- /demo/mixed-modules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mixed-modules-example", 3 | "version": "1.0.0", 4 | "description": "An example for mixed modules(goog.require, goog.modules, ES Modules)", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "run-s build:deps start:devserver", 8 | "start:devserver": "webpack-dev-server --mode=development", 9 | "build": "run-s build:deps build:webpack", 10 | "build:webpack": "webpack -p --mode=production", 11 | "build:deps": "./node_modules/google-closure-library/closure/bin/build/depswriter.py --root_with_prefix 'src ../../../../src' > public/deps.js", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "keywords": [], 15 | "author": "koba04", 16 | "license": "MIT", 17 | "dependencies": { 18 | "google-closure-library": "^20201102.0.1" 19 | }, 20 | "devDependencies": { 21 | "mocha": "^6.2.2", 22 | "npm-run-all": "4.x", 23 | "webpack": "4.x", 24 | "webpack-cli": "3.x", 25 | "webpack-dev-server": "4.x" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo/mixed-modules/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/mixed-modules/src/app.js: -------------------------------------------------------------------------------- 1 | import assign from 'object-assign'; 2 | 3 | // ES Module 4 | import esModule from './lib/es6'; 5 | 6 | // goog.xxx 7 | const math = goog.require('goog.math'); 8 | 9 | // Closure Script 10 | const googRequire = goog.require('app.googRequire'); 11 | 12 | // Closure Module 13 | const googModule = goog.require('app.googModule'); 14 | 15 | document.querySelector('#entry').textContent = JSON.stringify( 16 | assign( 17 | {'ES Modules': esModule()}, 18 | {'goog.require': googRequire()}, 19 | {'goog.module': googModule()}, 20 | {'goog.math.average(10, 20, 30, 40)': math.average(10, 20, 30, 40)} 21 | ) 22 | ); 23 | 24 | (async function() { 25 | await import('./lib/commonjs-lazy'); 26 | await import('./lib/es6-lazy'); 27 | })(); 28 | -------------------------------------------------------------------------------- /demo/mixed-modules/src/lib/commonjs-lazy.js: -------------------------------------------------------------------------------- 1 | require.ensure([ 2 | './es6', 3 | './commonjs', 4 | 'object-assign' 5 | ], function(require) { 6 | const assign = require('object-assign'); 7 | const commonJsModule = require('./commonjs'); 8 | const esModule = require('./es6'); 9 | const entry = document.querySelector('#entry'); 10 | entry.textContent += JSON.stringify( 11 | assign( 12 | { 'ES Module Late (require ensure)': esModule.default() }, 13 | { 'CommonJs Module Late (require ensure)': commonJsModule() } 14 | ) 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /demo/mixed-modules/src/lib/commonjs.js: -------------------------------------------------------------------------------- 1 | module.exports = function commonJsModule() { 2 | return 'from CommonJS Module'; 3 | }; 4 | -------------------------------------------------------------------------------- /demo/mixed-modules/src/lib/es6-lazy.js: -------------------------------------------------------------------------------- 1 | Promise.all([ 2 | import('./es6'), 3 | import('object-assign') 4 | ]).then(([esModule, assign]) => { 5 | const entry = document.querySelector('#entry'); 6 | entry.textContent += JSON.stringify( 7 | assign.default( 8 | { 'ES Module Late (import)': esModule.default() }, 9 | ) 10 | ); 11 | }).catch((e) => { 12 | console.error(e); 13 | }); 14 | -------------------------------------------------------------------------------- /demo/mixed-modules/src/lib/es6.js: -------------------------------------------------------------------------------- 1 | export default function esModule() { 2 | return 'from ES Modules'; 3 | } 4 | 5 | export function bar() { 6 | return 'bar'; 7 | } 8 | -------------------------------------------------------------------------------- /demo/mixed-modules/src/lib/forward-es6.js: -------------------------------------------------------------------------------- 1 | goog.module('app.forwardEsModule'); 2 | 3 | const appEs6 = goog.require('app.es6'); 4 | 5 | exports = function() { 6 | return 'forward ' + appEs6.default(); 7 | }; 8 | -------------------------------------------------------------------------------- /demo/mixed-modules/src/lib/goog-module.js: -------------------------------------------------------------------------------- 1 | goog.module('app.googModule'); 2 | 3 | exports = function() { 4 | return 'from goog.module'; 5 | }; 6 | -------------------------------------------------------------------------------- /demo/mixed-modules/src/lib/goog-require.js: -------------------------------------------------------------------------------- 1 | goog.provide('app.googRequire'); 2 | 3 | app.googRequire = function() { 4 | return 'from goog.require'; 5 | }; 6 | -------------------------------------------------------------------------------- /demo/mixed-modules/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ClosureCompilerPlugin = require('../../src/closure-compiler-plugin'); 3 | 4 | module.exports = (env, argv) => { 5 | const isProduction = argv.mode === 'production' || !argv.mode; 6 | 7 | return { 8 | entry: { 9 | app: './src/app.js', 10 | }, 11 | output: { 12 | path: path.resolve(__dirname, 'public'), 13 | filename: isProduction ? '[name].[chunkhash:8].js' : '[name].js?[chunkhash:8]', 14 | chunkFilename: isProduction ? '[name].[chunkhash:8].js' : '[name].js?[chunkhash:8]', 15 | }, 16 | devServer: { 17 | open: true, 18 | contentBase: path.resolve(__dirname, 'public'), 19 | inline: !isProduction 20 | }, 21 | devtool: 'source-map', 22 | optimization: { 23 | minimize: isProduction, 24 | minimizer: [ 25 | new ClosureCompilerPlugin({ mode: 'AGGRESSIVE_BUNDLE' }) 26 | ], 27 | splitChunks: { 28 | minSize: 0 29 | }, 30 | concatenateModules: false, 31 | }, 32 | plugins: [ 33 | new ClosureCompilerPlugin.LibraryPlugin( 34 | { 35 | closureLibraryBase: require.resolve( 36 | 'google-closure-library/closure/goog/base' 37 | ), 38 | deps: [ 39 | require.resolve('google-closure-library/closure/goog/deps'), 40 | './public/deps.js', 41 | ], 42 | }) 43 | ] 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "closure-webpack-plugin", 3 | "version": "2.6.1", 4 | "description": "Webpack Google Closure Compiler and Closure Library plugin", 5 | "author": "Chad Killingsworth (@ChadKillingsworth)", 6 | "license": "MIT", 7 | "main": "src/closure-compiler-plugin.js", 8 | "files": [ 9 | "src", 10 | "schema" 11 | ], 12 | "funding": { 13 | "type": "opencollective", 14 | "url": "https://opencollective.com/webpack" 15 | }, 16 | "scripts": { 17 | "commitlint": "commitlint", 18 | "commitmsg": "commitlint -e $GIT_PARAMS", 19 | "lint": "eslint --cache src test", 20 | "ci:lint:commits": "commitlint --from=${CIRCLE_BRANCH} --to=${CIRCLE_SHA1}", 21 | "lint-staged": "lint-staged", 22 | "release": "standard-version", 23 | "release:ci": "conventional-github-releaser -p angular", 24 | "release:validate": "commitlint --from=$(git describe --tags --abbrev=0) --to=$(git rev-parse HEAD)", 25 | "test": "jest", 26 | "test:watch": "jest --watch", 27 | "test:coverage": "jest --collectCoverageFrom='src/**/*.js' --coverage", 28 | "ci:lint": "npm run lint", 29 | "ci:test": "npm run test -- --runInBand", 30 | "ci:coverage": "npm run test:coverage -- --runInBand" 31 | }, 32 | "dependencies": { 33 | "acorn": "8.x", 34 | "acorn-walk": "^8.2.0", 35 | "schema-utils": "1.x", 36 | "unquoted-property-validator": "^1.0.2", 37 | "webpack-sources": "1.x" 38 | }, 39 | "devDependencies": { 40 | "@commitlint/cli": "^8.2.0", 41 | "@commitlint/config-angular": "^8.2.0", 42 | "@webpack-contrib/eslint-config-webpack": "^3.0.0", 43 | "babel-core": "^6.26.3", 44 | "babel-jest": "^24.9.0", 45 | "babel-preset-env": "^1.7.0", 46 | "conventional-github-releaser": "^3.1.3", 47 | "del": "^5.1.0", 48 | "eslint": "^6.8.0", 49 | "eslint-plugin-import": "^2.19.1", 50 | "eslint-plugin-prettier": "^3.1.2", 51 | "google-closure-compiler": "^20200830.0.0", 52 | "husky": "^3.1.0", 53 | "jest": "^24.9.0", 54 | "lint-staged": "^9.5.0", 55 | "memory-fs": "^0.5.0", 56 | "pre-commit": "^1.2.2", 57 | "prettier": "^1.19.1", 58 | "standard-version": "^8.0.1", 59 | "webpack": "4.x" 60 | }, 61 | "engines": { 62 | "node": ">= 6.9.0 || >= 8.9.0" 63 | }, 64 | "peerDependencies": { 65 | "google-closure-compiler": ">=20200830.0.0", 66 | "webpack": "4.x" 67 | }, 68 | "homepage": "https://github.com/webpack-contrib/closure-webpack-plugin", 69 | "repository": "https://github.com/webpack-contrib/closure-webpack-plugin", 70 | "bugs": "https://github.com/webpack-contrib/closure-webpack-plugin/issues", 71 | "pre-commit": "lint-staged", 72 | "lint-staged": { 73 | "*.js": [ 74 | "eslint --fix", 75 | "git add" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /schema/closure-compiler.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "definitions": { 5 | "platform": { 6 | "type": "string", 7 | "enum": ["java", "javascript", "native"] 8 | }, 9 | "file-conditions": { 10 | "anyOf": [ 11 | { 12 | "instanceof": "RegExp" 13 | }, 14 | { 15 | "type": "string" 16 | } 17 | ] 18 | } 19 | }, 20 | "properties": { 21 | "childCompilations": { 22 | "description": "Whether closure-compiler should run during child compilations", 23 | "oneOf": [{ 24 | "type": "boolean" 25 | }, { 26 | "instanceof": "Function" 27 | }] 28 | }, 29 | "mode": { 30 | "description": "Methodology used to bundle assets in a chunk", 31 | "type": "string", 32 | "enum": ["AGGRESSIVE_BUNDLE", "STANDARD"] 33 | }, 34 | "output": { 35 | "description": "", 36 | "type": "object", 37 | "additionalProperties": false, 38 | "properties": { 39 | "chunkFilename": { 40 | "description": "The filename of non-entry chunks as relative path inside the `output.path` directory.", 41 | "type": "string", 42 | "absolutePath": false 43 | }, 44 | "filename": { 45 | "description": "The filename of non-entry chunks as relative path inside the `output.path` directory.", 46 | "type": "string", 47 | "absolutePath": false 48 | } 49 | } 50 | }, 51 | "platform": { 52 | "description": "Version of closure-compiler used. May be an array of preference order", 53 | "oneOf": [{ 54 | "$ref": "#/definitions/platform" 55 | }, { 56 | "type": "array", 57 | "items": { 58 | "$ref": "#/definitions/platform" 59 | } 60 | }] 61 | }, 62 | "test": { 63 | "anyOf": [ 64 | { 65 | "$ref": "#/definitions/file-conditions" 66 | }, 67 | { 68 | "items": { 69 | "anyOf": [ 70 | { 71 | "$ref": "#/definitions/file-conditions" 72 | } 73 | ] 74 | }, 75 | "type": "array" 76 | } 77 | ] 78 | }, 79 | "include": { 80 | "anyOf": [ 81 | { 82 | "$ref": "#/definitions/file-conditions" 83 | }, 84 | { 85 | "items": { 86 | "anyOf": [ 87 | { 88 | "$ref": "#/definitions/file-conditions" 89 | } 90 | ] 91 | }, 92 | "type": "array" 93 | } 94 | ] 95 | }, 96 | "exclude": { 97 | "anyOf": [ 98 | { 99 | "$ref": "#/definitions/file-conditions" 100 | }, 101 | { 102 | "items": { 103 | "anyOf": [ 104 | { 105 | "$ref": "#/definitions/file-conditions" 106 | } 107 | ] 108 | }, 109 | "type": "array" 110 | } 111 | ] 112 | }, 113 | "extraCommandArgs": { 114 | "description": "Extra command arguments passed to java version of closure-compiler", 115 | "oneOf": [{ 116 | "type": "string" 117 | }, { 118 | "type": "array", 119 | "items": { 120 | "type": "string" 121 | } 122 | }] 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /schema/closure-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "additionalProperties": false, 4 | "definitions": { 5 | "modes": { 6 | "type": "string", 7 | "enum": ["development", "production"] 8 | } 9 | }, 10 | "properties": { 11 | "closureLibraryBase": { 12 | "description": "Path to Closure-Library's base.js file", 13 | "type": "string" 14 | }, 15 | "deps": { 16 | "description": "Path to a Closure-Library deps file", 17 | "type": "array", 18 | "items": { 19 | "type": "string" 20 | } 21 | }, 22 | "extraDeps": { 23 | "description": "A map of name to path of additional closure-library style dependencies", 24 | "type": "object" 25 | }, 26 | "mode": { 27 | "$ref": "#/definitions/modes" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/aggressive-bundle-externs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview externs for closure-compiler webpack 3 | * @externs 4 | */ 5 | 6 | /** @const */ 7 | var __wpcc = {}; 8 | 9 | /** @type {string|undefined} */ 10 | __wpcc.nc; 11 | -------------------------------------------------------------------------------- /src/basic-runtime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview webpack bootstrap for Closure-compiler without 3 | * late-loaded chunk support. 4 | * 5 | * This file is restricted to ES5 syntax so that it does not 6 | * require transpilation. 7 | */ 8 | 9 | /** 10 | * @const 11 | * @type {!Object} 12 | */ 13 | var _WEBPACK_SOURCE_ = {}; 14 | 15 | /** @define {number} */ 16 | var _WEBPACK_TIMEOUT_ = 120000; 17 | 18 | /** @define {string} */ 19 | var _WEBPACK_PUBLIC_PATH_ = ''; 20 | 21 | var __webpack_require__; 22 | if (typeof __webpack_require__ === 'undefined') { 23 | __webpack_require__ = function(m) {}; 24 | } 25 | 26 | /** @const */ 27 | __webpack_require__.p = _WEBPACK_PUBLIC_PATH_; 28 | -------------------------------------------------------------------------------- /src/chunk-sources.js: -------------------------------------------------------------------------------- 1 | const toSafePath = require('./safe-path'); 2 | const getWebpackModuleName = require('./module-name'); 3 | 4 | let uniqueId = 1; 5 | module.exports = function getChunkSources(chunk, compilation) { 6 | if (chunk.isEmpty()) { 7 | const emptyId = uniqueId; 8 | uniqueId += 1; 9 | return [ 10 | { 11 | path: `__empty_${emptyId}__`, 12 | src: '', 13 | }, 14 | ]; 15 | } 16 | 17 | const getModuleSrcObject = (webpackModule) => { 18 | const modulePath = getWebpackModuleName(webpackModule); 19 | let src = ''; 20 | let sourceMap = null; 21 | if (/javascript/.test(webpackModule.type)) { 22 | try { 23 | const souceAndMap = webpackModule 24 | .source(compilation.dependencyTemplates, compilation.runtimeTemplate) 25 | .sourceAndMap(); 26 | src = souceAndMap.source; 27 | if (souceAndMap.map) { 28 | sourceMap = souceAndMap.map; 29 | } 30 | } catch (e) { 31 | compilation.errors.push(e); 32 | } 33 | } 34 | 35 | return { 36 | path: toSafePath(modulePath), 37 | src, 38 | sourceMap, 39 | webpackId: 40 | webpackModule.id !== null && 41 | webpackModule.id !== undefined && // eslint-disable-line no-undefined 42 | webpackModule.id.toString().length > 0 43 | ? `${webpackModule.id}` 44 | : null, 45 | }; 46 | }; 47 | 48 | const getChunkModuleSources = (chunkModules, webpackModule) => { 49 | const moduleDeps = 50 | webpackModule.type === 'multi entry' 51 | ? webpackModule.dependencies 52 | : [webpackModule]; 53 | 54 | // Multi entry modules have no userRequest or id, but they do have multiple 55 | // nested dependencies. Traverse all of them. 56 | moduleDeps.forEach((subModule) => { 57 | chunkModules.push(getModuleSrcObject(subModule)); 58 | }); 59 | 60 | return chunkModules; 61 | }; 62 | 63 | return chunk 64 | .getModules() 65 | .reduce(getChunkModuleSources, []) 66 | .filter( 67 | (moduleJson) => 68 | !( 69 | moduleJson.path === '__unknown__' && 70 | moduleJson.src === '/* (ignored) */' 71 | ) 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/closure-compiler-plugin.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { Readable } = require('stream'); 4 | const googleClosureCompiler = require('google-closure-compiler'); 5 | const { 6 | getFirstSupportedPlatform, 7 | getNativeImagePath, 8 | } = require('google-closure-compiler/lib/utils'); 9 | const { ConcatSource, SourceMapSource, RawSource } = require('webpack-sources'); 10 | const Chunk = require('webpack/lib/Chunk'); 11 | const ChunkGroup = require('webpack/lib/ChunkGroup'); 12 | const RequestShortener = require('webpack/lib/RequestShortener'); 13 | const ModuleTemplate = require('webpack/lib/ModuleTemplate'); 14 | const ClosureRuntimeTemplate = require('./closure-runtime-template'); 15 | const HarmonyParserPlugin = require('./dependencies/harmony-parser-plugin'); 16 | const HarmonyExportDependency = require('./dependencies/harmony-export-dependency'); 17 | const HarmonyExportImportDependency = require('./dependencies/harmony-export-import-dependency'); 18 | const HarmonyImportDependency = require('./dependencies/harmony-import-dependency'); 19 | const HarmonyMarkerDependency = require('./dependencies/harmony-marker-dependency'); 20 | const HarmonyNoopTemplate = require('./dependencies/harmony-noop-template'); 21 | const AMDDefineDependency = require('./dependencies/amd-define-dependency'); 22 | const validateOptions = require('schema-utils'); 23 | const closureCompilerPluginSchema = require('../schema/closure-compiler.json'); 24 | const toSafePath = require('./safe-path'); 25 | const getChunkSources = require('./chunk-sources'); 26 | const getWebpackModuleName = require('./module-name'); 27 | const ClosureLibraryPlugin = require('./closure-library-plugin'); 28 | const findNearestCommonParentChunk = require('./common-ancestor'); 29 | 30 | const ENTRY_CHUNK_WRAPPER = 31 | '(function(__wpcc){%s}).call(this || window, (window.__wpcc = window.__wpcc || {}));'; 32 | 33 | /** 34 | * @typedef {Map, 37 | * sources: !Array<{path: string, source: string: sourceMap: string}>, 38 | * outputWrapper: (string|undefined) 39 | * }>} 40 | */ 41 | var ChunkMap; 42 | 43 | /** 44 | * Find the filename of a chunk which matches either the id or path provided. 45 | * 46 | * @param {!Chunk} chunk 47 | * @param {number} chunkId 48 | * @param {string} outputFilePath 49 | * @return {string|undefined} 50 | */ 51 | function findChunkFile(chunk, chunkId, outputFilePath) { 52 | for (let i = 0; i < chunk.files.length; i++) { 53 | const chunkFile = chunk.files[i]; 54 | let normalizedOutputFilePath = outputFilePath.replace(/^\.\//, ''); 55 | if (!/\.js$/.test(chunkFile)) { 56 | normalizedOutputFilePath = normalizedOutputFilePath.slice(0, -3); 57 | } 58 | 59 | if (normalizedOutputFilePath === chunkFile) { 60 | return chunkFile; 61 | } 62 | } 63 | if (chunk.id === chunkId) { 64 | return chunk.files[0]; 65 | } 66 | return undefined; // eslint-disable-line no-undefined 67 | } 68 | 69 | let baseChunkCount = 1; 70 | const PLUGIN = { name: 'closure-compiler-plugin' }; 71 | 72 | class ClosureCompilerPlugin { 73 | constructor(options, compilerFlags) { 74 | validateOptions( 75 | closureCompilerPluginSchema, 76 | options || {}, 77 | 'closure-webpack-plugin' 78 | ); 79 | this.options = Object.assign( 80 | {}, 81 | ClosureCompilerPlugin.DEFAULT_OPTIONS, 82 | options || {} 83 | ); 84 | if (typeof this.options.childCompilations === 'boolean') { 85 | this.options.childCompilations = function childCompilationSupported( 86 | childrenSupported 87 | ) { 88 | return childrenSupported; 89 | }.bind(this, this.options.childCompilations); 90 | } 91 | 92 | if (!Array.isArray(this.options.platform)) { 93 | this.options.platform = [this.options.platform]; 94 | } 95 | 96 | if (!Array.isArray(this.options.extraCommandArgs)) { 97 | this.options.extraCommandArgs = [this.options.extraCommandArgs]; 98 | } 99 | 100 | if (this.options.mode === 'STANDARD') { 101 | this.compilerFlags = Object.assign( 102 | {}, 103 | ClosureCompilerPlugin.DEFAULT_FLAGS_STANDARD, 104 | compilerFlags || {} 105 | ); 106 | } else if (this.options.mode === 'AGGRESSIVE_BUNDLE') { 107 | this.compilerFlags = Object.assign( 108 | {}, 109 | ClosureCompilerPlugin.DEFAULT_FLAGS_AGGRESSIVE_BUNDLE, 110 | compilerFlags || {} 111 | ); 112 | } 113 | 114 | this.optimizedCompilations = new Set(); 115 | this.BASE_CHUNK_NAME = `required-base-${baseChunkCount}`; 116 | baseChunkCount += 1; 117 | } 118 | 119 | apply(compiler) { 120 | this.requestShortener = new RequestShortener(compiler.context); 121 | 122 | if (this.options.mode === 'AGGRESSIVE_BUNDLE') { 123 | compiler.hooks.thisCompilation.tap( 124 | PLUGIN, 125 | (compilation, { normalModuleFactory }) => { 126 | compilation.runtimeTemplate = new ClosureRuntimeTemplate( 127 | compilation.outputOptions, 128 | compilation.requestShortener 129 | ); 130 | compilation.moduleTemplates = { 131 | javascript: new ModuleTemplate( 132 | compilation.runtimeTemplate, 133 | 'javascript' 134 | ), 135 | }; 136 | 137 | const parserCallback = (parser, parserOptions) => { 138 | // eslint-disable-next-line no-undefined 139 | if (parserOptions.harmony !== undefined && !parserOptions.harmony) { 140 | return; 141 | } 142 | new HarmonyParserPlugin().apply(parser); 143 | }; 144 | normalModuleFactory.hooks.parser 145 | .for('javascript/auto') 146 | .tap(PLUGIN.name, parserCallback); 147 | normalModuleFactory.hooks.parser 148 | .for('javascript/dynamic') 149 | .tap(PLUGIN.name, parserCallback); 150 | normalModuleFactory.hooks.parser 151 | .for('javascript/esm') 152 | .tap(PLUGIN.name, parserCallback); 153 | } 154 | ); 155 | } 156 | compiler.hooks.compilation.tap(PLUGIN, (compilation, params) => 157 | this.compilation_(compilation, params) 158 | ); 159 | } 160 | 161 | compilation_(compilation, { normalModuleFactory }) { 162 | const runFullCompilation = 163 | !compilation.compiler.parentCompilation || 164 | this.options.childCompilations(compilation); 165 | 166 | if (!runFullCompilation) { 167 | return; 168 | } 169 | 170 | if (this.options.mode === 'AGGRESSIVE_BUNDLE') { 171 | // These default webpack optimizations are not compatible with this mode 172 | if (compilation.options.optimization.concatenateModules) { 173 | compilation.warnings.push( 174 | new Error( 175 | PLUGIN.name + 176 | ': The concatenated modules optimization is not compatible with AGGRESSIVE_BUNDLE mode.\n' + 177 | JSON.stringify( 178 | { 179 | optimization: { 180 | concatenateModules: false, 181 | }, 182 | }, 183 | null, 184 | 2 185 | ) 186 | ) 187 | ); 188 | } 189 | 190 | const dependencyFactoriesByName = new Map(); 191 | compilation.dependencyFactories.forEach((val, key) => { 192 | dependencyFactoriesByName.set(key.name, val); 193 | }); 194 | const dependencyTemplatesByName = new Map(); 195 | compilation.dependencyTemplates.forEach((val, key) => { 196 | dependencyTemplatesByName.set(key.name, val); 197 | }); 198 | [ 199 | 'AMDDefineDependency', 200 | 'HarmonyImportSideEffectDependency', 201 | 'HarmonyImportSpecifierDependency', 202 | 'HarmonyExportHeaderDependency', 203 | 'HarmonyExportExpressionDependency', 204 | 'HarmonyExportImportedSpecifierDependency', 205 | 'HarmonyExportSpecifierDependency', 206 | ].forEach((factoryName) => 207 | compilation.dependencyFactories.delete( 208 | dependencyFactoriesByName.get(factoryName) 209 | ) 210 | ); 211 | 212 | [ 213 | 'AMDDefineDependencyTemplate', 214 | 'HarmonyImportSideEffectDependencyTemplate', 215 | 'HarmonyImportSpecifierDependencyTemplate', 216 | 'HarmonyExportHeaderDependencyTemplate', 217 | 'HarmonyExportExpressionDependencyTemplate', 218 | 'HarmonyExportImportedSpecifierDependencyTemplate', 219 | 'HarmonyExportSpecifierDependencyTemplate', 220 | ].forEach((templateName) => 221 | compilation.dependencyTemplates.delete( 222 | dependencyTemplatesByName.get(templateName) 223 | ) 224 | ); 225 | 226 | compilation.dependencyFactories.set( 227 | HarmonyExportDependency, 228 | normalModuleFactory 229 | ); 230 | compilation.dependencyTemplates.set( 231 | HarmonyExportDependency, 232 | new HarmonyExportDependency.Template() 233 | ); 234 | compilation.dependencyFactories.set( 235 | HarmonyExportImportDependency, 236 | normalModuleFactory 237 | ); 238 | compilation.dependencyTemplates.set( 239 | HarmonyExportImportDependency, 240 | new HarmonyExportImportDependency.Template() 241 | ); 242 | compilation.dependencyFactories.set( 243 | HarmonyImportDependency, 244 | normalModuleFactory 245 | ); 246 | compilation.dependencyTemplates.set( 247 | HarmonyImportDependency, 248 | new HarmonyImportDependency.Template() 249 | ); 250 | compilation.dependencyFactories.set( 251 | HarmonyMarkerDependency, 252 | normalModuleFactory 253 | ); 254 | compilation.dependencyTemplates.set( 255 | HarmonyMarkerDependency, 256 | new HarmonyMarkerDependency.Template() 257 | ); 258 | 259 | // It's very difficult to override a specific dependency template without rewriting the entire set. 260 | // Microtask timing is used to ensure that these overrides occur after the main template plugins run. 261 | Promise.resolve().then(() => { 262 | compilation.dependencyTemplates.forEach((val, key) => { 263 | switch (key.name) { 264 | case 'AMDDefineDependency': 265 | compilation.dependencyTemplates.set( 266 | key, 267 | new AMDDefineDependency.Template() 268 | ); 269 | break; 270 | 271 | case 'HarmonyImportSideEffectDependency': 272 | case 'HarmonyImportSpecifierDependency': 273 | case 'HarmonyExportHeaderDependency': 274 | case 'HarmonyExportExpressionDependency': 275 | case 'HarmonyExportImportedSpecifierDependency': 276 | case 'HarmonyExportSpecifierDependency': 277 | compilation.dependencyTemplates.set( 278 | key, 279 | new HarmonyNoopTemplate() 280 | ); 281 | break; 282 | 283 | default: 284 | break; 285 | } 286 | }); 287 | }); 288 | 289 | compilation.hooks.afterOptimizeChunks.tap( 290 | PLUGIN, 291 | (chunks, chunkGroups) => { 292 | if (!this.optimizedCompilations.has(compilation)) { 293 | this.optimizedCompilations.add(compilation); 294 | this.optimizeChunks(compilation, chunks, chunkGroups); 295 | } 296 | } 297 | ); 298 | } 299 | 300 | compilation.mainTemplate.hooks.hash.tap( 301 | 'SetVarMainTemplatePlugin', 302 | (hash) => { 303 | hash.update('set var'); 304 | hash.update(`${this.varExpression}`); 305 | hash.update(`${this.copyObject}`); 306 | } 307 | ); 308 | 309 | compilation.hooks.buildModule.tap(PLUGIN, (moduleArg) => { 310 | // to get detailed location info about errors 311 | moduleArg.useSourceMap = true; 312 | }); 313 | 314 | compilation.hooks.afterOptimizeDependencies.tap(PLUGIN, (webpackModules) => 315 | this.removeMarkers(webpackModules) 316 | ); 317 | 318 | compilation.hooks.optimizeChunkAssets.tapAsync( 319 | PLUGIN, 320 | (originalChunks, cb) => 321 | this.optimizeChunkAssets(compilation, originalChunks, cb) 322 | ); 323 | } 324 | 325 | /** 326 | * The webpack harmony plugin adds constant dependencies to clear 327 | * out parts of both import and export statements. We need to remove those 328 | * dependencies and the associated markers so that closure-compiler sees the 329 | * original import and export statement. 330 | * 331 | * @param {!Array} 332 | */ 333 | removeMarkers(webpackModules) { 334 | webpackModules.forEach((webpackModule) => { 335 | if (!/^javascript\//.test(webpackModule.type)) { 336 | return; 337 | } 338 | const markerDependencies = webpackModule.dependencies.filter( 339 | (dep) => dep instanceof HarmonyMarkerDependency 340 | ); 341 | if (markerDependencies.length > 0) { 342 | webpackModule.dependencies.slice().forEach((dep) => { 343 | if ( 344 | dep.constructor.name === 'ConstDependency' && 345 | markerDependencies.find( 346 | (marker) => 347 | marker.range[0] === dep.range[0] && 348 | marker.range[1] === dep.range[1] 349 | ) 350 | ) { 351 | webpackModule.removeDependency(dep); 352 | } 353 | }); 354 | markerDependencies.forEach((marker) => 355 | webpackModule.removeDependency(marker) 356 | ); 357 | } 358 | }); 359 | } 360 | 361 | /** 362 | * Add the synthetic root chunk and ensure that a module only exists in a single output chunk. 363 | * 364 | * @param {!Compilation} compilation 365 | * @param {!Set} chunks 366 | * @param {!Set} chunkGroups 367 | */ 368 | optimizeChunks(compilation, chunks, chunkGroups) { 369 | const requiredBase = new ChunkGroup(this.BASE_CHUNK_NAME); 370 | 371 | /** @type {!Map>} */ 372 | const moduleChunks = new Map(); 373 | 374 | // Create a map of every module to all chunk groups which 375 | // reference it. 376 | chunkGroups.forEach((chunkGroup) => { 377 | // Add the new synthetic base chunk group as a parent 378 | // for any entrypoints. 379 | if (chunkGroup.getParents().length === 0) { 380 | chunkGroup.addParent(requiredBase); 381 | requiredBase.addChild(chunkGroup); 382 | } 383 | chunkGroup.chunks.forEach((chunk) => { 384 | chunk.getModules().forEach((webpackModule) => { 385 | if (!moduleChunks.has(webpackModule)) { 386 | moduleChunks.set(webpackModule, new Set()); 387 | } 388 | moduleChunks.get(webpackModule).add(chunkGroup); 389 | }); 390 | }); 391 | }); 392 | 393 | // Add the synthetic base chunk group to the compilation 394 | const baseChunk = new Chunk(this.BASE_CHUNK_NAME); 395 | baseChunk.addGroup(requiredBase); 396 | requiredBase.pushChunk(baseChunk); 397 | compilation.chunkGroups.push(requiredBase); 398 | compilation.chunks.push(baseChunk); 399 | 400 | // Find any module with more than 1 chunkGroup and move the module up the graph 401 | // to the nearest common ancestor 402 | moduleChunks.forEach((moduleChunkGroups, duplicatedModule) => { 403 | if (chunkGroups.size < 2) { 404 | return; 405 | } 406 | const commonParent = findNearestCommonParentChunk( 407 | Array.from(moduleChunkGroups) 408 | ); 409 | if (commonParent.distance >= 0) { 410 | const targetChunkGroup = compilation.chunkGroups.find( 411 | (chunkGroup) => chunkGroup === commonParent.chunkGroup 412 | ); 413 | if (!targetChunkGroup) { 414 | return; 415 | } 416 | moduleChunkGroups.forEach((moduleChunkGroup) => { 417 | const targetChunks = moduleChunkGroup.chunks.filter((chunk) => 418 | chunk.getModules().includes(duplicatedModule) 419 | ); 420 | if (targetChunks.length > 0) { 421 | targetChunks.forEach((chunk) => 422 | chunk.removeModule(duplicatedModule) 423 | ); 424 | } 425 | }); 426 | targetChunkGroup.chunks[0].addModule(duplicatedModule); 427 | } 428 | }); 429 | } 430 | 431 | optimizeChunkAssets(compilation, originalChunks, cb) { 432 | // Early exit - don't wait for closure compiler to display errors 433 | if (compilation.errors.length > 0) { 434 | cb(); 435 | return; 436 | } 437 | 438 | if (this.options.mode === 'AGGRESSIVE_BUNDLE') { 439 | this.aggressiveBundle(compilation, originalChunks, cb); 440 | } else { 441 | this.standardBundle(compilation, originalChunks, cb); 442 | } 443 | } 444 | 445 | /** 446 | * Use webpack standard bundles and runtime, but utilize closure-compiler as the minifier. 447 | * 448 | * @param {!Object} compilation 449 | * @param {!Array} originalChunks 450 | * @param {function()} cb 451 | */ 452 | standardBundle(compilation, originalChunks, cb) { 453 | const compilations = []; 454 | // We need to invoke closure compiler for each entry point. Loop through 455 | // each chunk and find any entry points. 456 | // Add the entry point and any descendant chunks to the compilation. 457 | originalChunks.forEach((chunk) => { 458 | if (!chunk.hasEntryModule()) { 459 | return; 460 | } 461 | const chunkDefs = new Map(); 462 | const entrypoints = []; 463 | this.addChunkToCompilationStandard( 464 | compilation, 465 | chunk, 466 | null, 467 | chunkDefs, 468 | entrypoints 469 | ); 470 | const sources = []; 471 | const compilationOptions = this.buildCompilerOptions( 472 | chunkDefs, 473 | entrypoints, 474 | this.compilerFlags.defines || [], 475 | sources 476 | ); 477 | 478 | let externs = []; 479 | externs.push(require.resolve('./standard-externs.js')); 480 | if (Array.isArray(compilationOptions.externs)) { 481 | externs = externs.concat(compilationOptions.externs); 482 | } else if (compilationOptions.externs != null) { 483 | externs.push(compilationOptions.externs); 484 | } 485 | compilationOptions.externs = externs; 486 | 487 | compilations.push( 488 | this.runCompiler(compilation, compilationOptions, sources, chunkDefs) 489 | .then((outputFiles) => { 490 | outputFiles.forEach((outputFile) => { 491 | const chunkIdParts = /chunk-(\d+)\.js/.exec(outputFile.path); 492 | let chunkId; 493 | if (chunkIdParts) { 494 | chunkId = parseInt(chunkIdParts[1], 10); 495 | } 496 | const matchingChunk = compilation.chunks.find((chunk_) => 497 | findChunkFile(chunk_, chunkId, outputFile.path) 498 | ); 499 | if (!matchingChunk) { 500 | return; 501 | } 502 | let [assetName] = chunkIdParts 503 | ? chunk.files 504 | : [outputFile.path.replace(/^\.\//, '')]; 505 | if (chunkIdParts && !/\.js$/.test(chunk.files[0])) { 506 | assetName = assetName.slice(0, -3); 507 | } 508 | const sourceMap = JSON.parse( 509 | outputFile.source_map || outputFile.sourceMap 510 | ); 511 | sourceMap.file = assetName; 512 | const source = outputFile.src; 513 | compilation.assets[assetName] = new SourceMapSource( 514 | source, 515 | assetName, 516 | sourceMap, 517 | null, 518 | null 519 | ); 520 | }); 521 | }) 522 | .catch((e) => { 523 | if (e) { 524 | if (!(e instanceof Error)) { 525 | e = new Error(e); 526 | } 527 | compilation.errors.push(e); 528 | } 529 | }) 530 | ); 531 | }); 532 | 533 | originalChunks.forEach((chunk) => { 534 | const chunkFilename = this.getChunkName(compilation, chunk); 535 | if (!chunk.files.includes(chunkFilename)) { 536 | chunk.files.push(chunkFilename); 537 | if (!compilation.assets[chunkFilename]) { 538 | compilation.assets[chunkFilename] = new RawSource(''); 539 | } 540 | } 541 | }); 542 | 543 | Promise.all(compilations) 544 | .then(() => cb()) 545 | .catch((e) => { 546 | if (e) { 547 | if (!(e instanceof Error)) { 548 | e = new Error(e); 549 | } 550 | compilation.errors.push(e); 551 | } 552 | cb(); 553 | }); 554 | } 555 | 556 | /** 557 | * Rewrite commonjs modules into a global namespace. Output is split into chunks 558 | * based on the dependency graph provided by webpack. Symbols referenced from 559 | * a different output chunk are rewritten to be properties on a __wpcc namespace. 560 | */ 561 | aggressiveBundle(compilation, originalChunks, cb) { 562 | const basicRuntimePath = require.resolve('./basic-runtime.js'); 563 | const externsPath = require.resolve('./aggressive-bundle-externs.js'); 564 | 565 | // Closure compiler requires the chunk graph to have a single root node. 566 | // Since webpack can have multiple entry points, add a synthetic root 567 | // to the graph. 568 | /** @type {!ChunkMap} */ 569 | const chunkDefs = new Map(); 570 | const entrypoints = []; 571 | const baseChunk = originalChunks.find( 572 | (chunk) => chunk.name === this.BASE_CHUNK_NAME 573 | ); 574 | let { chunkGroups } = compilation; 575 | if (baseChunk) { 576 | baseChunk.files.forEach((chunkFile) => { 577 | delete compilation.assets[chunkFile]; 578 | }); 579 | baseChunk.files.splice(0, baseChunk.files.length); 580 | const baseChunkGroup = compilation.chunkGroups.find( 581 | (chunkGroup) => chunkGroup.name === this.BASE_CHUNK_NAME 582 | ); 583 | Array.from(baseChunkGroup.getChildren()) 584 | .slice() 585 | .forEach((childChunk) => { 586 | childChunk.removeParent(baseChunkGroup); 587 | baseChunkGroup.removeChild(childChunk); 588 | }); 589 | 590 | this.addChunkToCompilationAggressive( 591 | compilation, 592 | baseChunk, 593 | [], 594 | chunkDefs, 595 | entrypoints 596 | ); 597 | chunkGroups = chunkGroups.filter( 598 | (chunkGroup) => chunkGroup !== baseChunkGroup 599 | ); 600 | } else { 601 | chunkDefs.set(this.BASE_CHUNK_NAME, { 602 | name: this.BASE_CHUNK_NAME, 603 | parentNames: new Set(), 604 | sources: [], 605 | outputWrapper: ENTRY_CHUNK_WRAPPER, 606 | }); 607 | } 608 | 609 | let jsonpRuntimeRequired = false; 610 | 611 | chunkGroups.forEach((chunkGroup) => { 612 | // If a chunk is split by the SplitChunksPlugin, the original chunk name 613 | // will be set as the chunk group name. 614 | const primaryChunk = chunkGroup.chunks.find( 615 | (chunk) => chunk.name === chunkGroup.options.name 616 | ); 617 | // Add any other chunks in the group to a 2nd array. 618 | // For closure-compiler, the primary chunk will be a descendant of any 619 | // secondary chunks. 620 | const secondaryChunks = chunkGroup.chunks.filter( 621 | (chunk) => chunk !== primaryChunk 622 | ); 623 | const secondaryParentNames = []; 624 | const primaryParentNames = []; 625 | 626 | // Entrypoints are chunk groups with no parents 627 | if (primaryChunk && primaryChunk.entryModule) { 628 | if (!baseChunk || chunkGroup.getParents().length === 0) { 629 | primaryParentNames.push(this.BASE_CHUNK_NAME); 630 | } 631 | const entryModuleDeps = 632 | primaryChunk.entryModule.type === 'multi entry' 633 | ? primaryChunk.entryModule.dependencies 634 | : [primaryChunk.entryModule]; 635 | entryModuleDeps.forEach((entryDep) => { 636 | entrypoints.push(toSafePath(getWebpackModuleName(entryDep))); 637 | }); 638 | } else if (chunkGroup.getParents().length === 0) { 639 | if (!baseChunk) { 640 | if (secondaryChunks.size > 0) { 641 | secondaryParentNames.push(this.BASE_CHUNK_NAME); 642 | } else if (primaryChunk) { 643 | primaryParentNames.push(this.BASE_CHUNK_NAME); 644 | } 645 | } 646 | } else { 647 | jsonpRuntimeRequired = true; 648 | chunkGroup.getParents().forEach((parentGroup) => { 649 | const primaryParentChunk = parentGroup.chunks.find( 650 | (chunk) => chunk.name === parentGroup.options.name 651 | ); 652 | const parentNames = []; 653 | if (primaryParentChunk) { 654 | // Chunks created from a split must be set as the parent of the original chunk. 655 | parentNames.push( 656 | this.getChunkName(compilation, primaryParentChunk).replace( 657 | /\.js$/, 658 | '' 659 | ) 660 | ); 661 | } else { 662 | parentNames.push( 663 | ...parentGroup.chunks.map((parentChunk) => 664 | this.getChunkName(compilation, primaryParentChunk).replace( 665 | /\.js$/, 666 | '' 667 | ) 668 | ) 669 | ); 670 | } 671 | if (secondaryChunks.length > 0) { 672 | secondaryParentNames.push(...parentNames); 673 | } else { 674 | primaryParentNames.push(...parentNames); 675 | } 676 | }); 677 | } 678 | 679 | secondaryChunks.forEach((secondaryChunk) => { 680 | this.addChunkToCompilationAggressive( 681 | compilation, 682 | secondaryChunk, 683 | secondaryParentNames, 684 | chunkDefs, 685 | entrypoints 686 | ); 687 | }); 688 | 689 | // Primary chunks logically depend on modules in the secondary chunks 690 | primaryParentNames.push( 691 | ...secondaryChunks.map((chunk) => 692 | this.getChunkName(compilation, chunk).replace(/\.js$/, '') 693 | ) 694 | ); 695 | 696 | if (primaryChunk) { 697 | this.addChunkToCompilationAggressive( 698 | compilation, 699 | primaryChunk, 700 | primaryParentNames, 701 | chunkDefs, 702 | entrypoints 703 | ); 704 | } 705 | }); 706 | 707 | let baseChunkDef; 708 | if (baseChunk) { 709 | for (const [chunkDefName, chunkDef] of chunkDefs) { 710 | if (chunkDefName.indexOf(this.BASE_CHUNK_NAME) >= 0) { 711 | baseChunkDef = chunkDef; 712 | break; 713 | } 714 | } 715 | } else { 716 | baseChunkDef = chunkDefs.get(this.BASE_CHUNK_NAME); 717 | } 718 | baseChunkDef.sources.unshift( 719 | { 720 | path: externsPath, 721 | src: fs.readFileSync(externsPath, 'utf8'), 722 | }, 723 | { 724 | path: basicRuntimePath, 725 | src: fs.readFileSync(basicRuntimePath, 'utf8'), 726 | } 727 | ); 728 | baseChunkDef.outputWrapper = ENTRY_CHUNK_WRAPPER; 729 | entrypoints.unshift(basicRuntimePath); 730 | 731 | if (jsonpRuntimeRequired) { 732 | const fullRuntimeSource = this.renderRuntime(); 733 | baseChunkDef.sources.push(fullRuntimeSource); 734 | entrypoints.unshift(fullRuntimeSource.path); 735 | } 736 | entrypoints.unshift(basicRuntimePath); 737 | 738 | const defines = []; 739 | if (this.compilerFlags.define) { 740 | if (typeof this.compilerFlags.define === 'string') { 741 | defines.push(this.compilerFlags.define); 742 | } else { 743 | defines.push(...this.compilerFlags.define); 744 | } 745 | } 746 | defines.push( 747 | `_WEBPACK_TIMEOUT_=${compilation.outputOptions.chunkLoadTimeout}` 748 | ); 749 | 750 | const PUBLIC_PATH = compilation.mainTemplate.getPublicPath({ 751 | hash: compilation.hash, 752 | }); 753 | defines.push(`_WEBPACK_PUBLIC_PATH_='${PUBLIC_PATH}'`); 754 | 755 | const allSources = []; 756 | const compilationOptions = this.buildCompilerOptions( 757 | chunkDefs, 758 | entrypoints, 759 | defines, 760 | allSources 761 | ); 762 | 763 | // Invoke the compiler and return a promise of the results. 764 | // Success returns an array of output files. 765 | // Failure returns the exit code. 766 | this.runCompiler(compilation, compilationOptions, allSources) 767 | .then((outputFiles) => { 768 | // Find the synthetic root chunk 769 | const baseFile = outputFiles.find((file) => 770 | file.path.indexOf(this.BASE_CHUNK_NAME) 771 | ); 772 | let baseSrc = `${baseFile.src}\n`; 773 | if (/^['"]use strict['"];\s*$/.test(baseFile.src)) { 774 | baseSrc = ''; 775 | } 776 | 777 | // Remove any assets created by the synthetic base chunk 778 | // They are concatenated on to each entry point. 779 | if (baseChunk) { 780 | baseChunk.files.forEach((filename) => { 781 | delete compilation.assets[filename]; 782 | }); 783 | baseChunk.files.splice(0, baseChunk.files.length); 784 | } 785 | 786 | outputFiles 787 | .filter((outputFile) => outputFile.path !== baseFile.path) 788 | .forEach((outputFile) => { 789 | const chunkIdParts = /chunk-(\d+)\.js/.exec(outputFile.path); 790 | let chunkId; 791 | if (chunkIdParts) { 792 | chunkId = parseInt(chunkIdParts[1], 10); 793 | } 794 | const chunk = compilation.chunks.find((chunk_) => 795 | findChunkFile(chunk_, chunkId, outputFile.path) 796 | ); 797 | if (!chunk || (chunk.isEmpty() && chunk.files.length === 0)) { 798 | return; 799 | } 800 | const assetName = findChunkFile(chunk, chunkId, outputFile.path); 801 | const sourceMap = JSON.parse( 802 | outputFile.source_map || outputFile.sourceMap 803 | ); 804 | sourceMap.file = assetName; 805 | const source = outputFile.src; 806 | let newSource = new SourceMapSource( 807 | source, 808 | assetName, 809 | sourceMap, 810 | null, 811 | null 812 | ); 813 | // Concatenate our synthetic root chunk with an entry point 814 | if (chunk.hasRuntime()) { 815 | newSource = new ConcatSource(baseSrc, newSource); 816 | } 817 | compilation.assets[assetName] = newSource; 818 | }); 819 | 820 | cb(); 821 | }) 822 | .catch((e) => { 823 | if (e) { 824 | if (!(e instanceof Error)) { 825 | e = new Error(e); 826 | } 827 | compilation.errors.push(e); 828 | } 829 | cb(); 830 | }); 831 | } 832 | 833 | /** 834 | * @param {!ChunkMap} chunkDefs 835 | * @param {!Array} entrypoints 836 | * @param {!Array} defines 837 | * @param {!Array<{src: string, path: string, webpackId: number, sourceMap: string}>} allSources 838 | */ 839 | buildCompilerOptions(chunkDefs, entrypoints, defines, allSources) { 840 | const chunkDefinitionStrings = []; 841 | const chunkDefArray = Array.from(chunkDefs.values()); 842 | const chunkNamesProcessed = new Set(); 843 | let chunkWrappers; 844 | // Chunks must be listed in the compiler options in dependency order. 845 | // Loop through the list of chunk definitions and add them to the options 846 | // when all of the parents for that chunk have been added. 847 | while (chunkDefArray.length > 0) { 848 | const startLength = chunkDefArray.length; 849 | for (let i = 0; i < chunkDefArray.length; ) { 850 | if ( 851 | Array.from(chunkDefArray[i].parentNames).every((parentName) => 852 | chunkNamesProcessed.has(parentName) 853 | ) 854 | ) { 855 | chunkNamesProcessed.add(chunkDefArray[i].name); 856 | chunkDefArray[i].sources.forEach((srcInfo) => { 857 | if (srcInfo.sourceMap) { 858 | srcInfo.sourceMap = JSON.stringify({ 859 | ...srcInfo.sourceMap, 860 | sources: srcInfo.sourceMap.sources.map(toSafePath), 861 | }); 862 | } 863 | allSources.push(srcInfo); 864 | }); 865 | let chunkDefinitionString = `${chunkDefArray[i].name}:${chunkDefArray[i].sources.length}`; 866 | if (chunkDefArray[i].parentNames.size > 0) { 867 | chunkDefinitionString += `:${Array.from( 868 | chunkDefArray[i].parentNames 869 | ).join(',')}`; 870 | } 871 | chunkDefinitionStrings.push(chunkDefinitionString); 872 | if (chunkDefArray[i].outputWrapper) { 873 | chunkWrappers = chunkWrappers || []; 874 | chunkWrappers.push( 875 | `${chunkDefArray[i].name}:${chunkDefArray[i].outputWrapper}` 876 | ); 877 | } 878 | chunkDefArray.splice(i, 1); 879 | } else { 880 | i += 1; 881 | } 882 | } 883 | // Sanity check - make sure we added at least one chunk to the output 884 | // in this loop iteration. Prevents infinite loops. 885 | if (startLength === chunkDefArray.length) { 886 | throw new Error('Unable to build chunk map - parent chunks not found'); 887 | } 888 | } 889 | 890 | const options = Object.assign({}, this.compilerFlags, { 891 | entry_point: entrypoints, 892 | chunk: chunkDefinitionStrings, 893 | define: defines, 894 | }); 895 | if (chunkWrappers) { 896 | options.chunkWrapper = chunkWrappers; 897 | } 898 | return options; 899 | } 900 | 901 | /** 902 | * Invoke closure compiler with a set of flags and source files 903 | * 904 | * @param {!Object} compilation 905 | * @param {!Object|boolean)>} flags 906 | * @param {!Array} sources 912 | * @return {Promise>} 917 | */ 918 | runCompiler(compilation, flags, sources) { 919 | return new Promise((resolve, reject) => { 920 | flags = Object.assign({}, flags, { 921 | error_format: 'JSON', 922 | json_streams: 'BOTH', 923 | }); 924 | const { compiler: ClosureCompiler } = googleClosureCompiler; 925 | const compilerRunner = new ClosureCompiler( 926 | flags, 927 | this.options.extraCommandArgs 928 | ); 929 | compilerRunner.spawnOptions = { stdio: 'pipe' }; 930 | const platform = getFirstSupportedPlatform(this.options.platform); 931 | if (platform.toLowerCase() === 'native') { 932 | compilerRunner.JAR_PATH = null; 933 | compilerRunner.javaPath = getNativeImagePath(); 934 | } 935 | const compilerProcess = compilerRunner.run(); 936 | 937 | let stdOutData = ''; 938 | let stdErrData = ''; 939 | compilerProcess.stdout.on('data', (data) => { 940 | stdOutData += data; 941 | }); 942 | 943 | compilerProcess.stderr.on('data', (data) => { 944 | stdErrData += data; 945 | }); 946 | 947 | compilerProcess.on('error', (err) => { 948 | this.reportErrors(compilation, [ 949 | { 950 | level: 'error', 951 | description: `Closure-compiler. Could not be launched.\n${compilerRunner.prependFullCommand( 952 | err.message 953 | )}`, 954 | }, 955 | ]); 956 | reject(); 957 | }); 958 | 959 | compilerProcess.on('close', (exitCode) => { 960 | if (stdErrData instanceof Error) { 961 | this.reportErrors({ 962 | level: 'error', 963 | description: stdErrData.message, 964 | }); 965 | reject(); 966 | return; 967 | } 968 | 969 | if (stdErrData.length > 0) { 970 | let errors = []; 971 | try { 972 | errors = errors.concat(JSON.parse(stdErrData)); 973 | } catch (e1) { 974 | const exceptionIndex = stdErrData.indexOf(']java.lang.'); 975 | if (exceptionIndex > 0) { 976 | try { 977 | errors = errors.concat( 978 | JSON.parse(stdErrData.substring(0, exceptionIndex + 1)) 979 | ); 980 | errors.push({ 981 | level: 'error', 982 | description: stdErrData.slice(exceptionIndex + 1), 983 | }); 984 | } catch (e2) {} 985 | } else { 986 | errors = undefined; // eslint-disable-line no-undefined 987 | } 988 | } 989 | 990 | if (!errors) { 991 | errors = errors || []; 992 | errors.push({ 993 | level: 'error', 994 | description: stdErrData, 995 | }); 996 | } 997 | 998 | this.reportErrors(compilation, errors); 999 | // TODO(ChadKillingsworth) Figure out how to report the stats 1000 | } 1001 | 1002 | if (exitCode > 0) { 1003 | reject(); 1004 | return; 1005 | } 1006 | 1007 | const outputFiles = JSON.parse(stdOutData); 1008 | resolve(outputFiles); 1009 | }); 1010 | 1011 | // Ignore errors (EPIPE) if the compiler input stream is closed 1012 | compilerProcess.stdin.on('error', (err) => {}); 1013 | 1014 | const buffer = Buffer.from(JSON.stringify(sources), 'utf8'); 1015 | const readable = new Readable(); 1016 | readable._read = () => {}; 1017 | readable.push(buffer); 1018 | readable.push(null); 1019 | readable.pipe(compilerProcess.stdin); 1020 | }); 1021 | } 1022 | 1023 | /** 1024 | * Return the filename template for a given chunk 1025 | * 1026 | * @param {!Object} compilation 1027 | * @param {boolean} isEntryModule 1028 | * @return {string} 1029 | */ 1030 | getChunkFilenameTemplate(compilation, isEntrypoint) { 1031 | const outputOptions = this.options.output || {}; 1032 | let { filename } = compilation.outputOptions; 1033 | if (outputOptions.filename) { 1034 | filename = outputOptions.filename; // eslint-disable-line prefer-destructuring 1035 | } 1036 | let { chunkFilename } = compilation.outputOptions; 1037 | if (outputOptions.chunkFilename) { 1038 | chunkFilename = outputOptions.chunkFilename; // eslint-disable-line prefer-destructuring 1039 | } else if (outputOptions.filename) { 1040 | chunkFilename = filename; 1041 | } else { 1042 | chunkFilename = compilation.outputOptions.chunkFilename; // eslint-disable-line prefer-destructuring 1043 | } 1044 | return isEntrypoint ? filename : chunkFilename; 1045 | } 1046 | 1047 | /** 1048 | * For a given chunk, return it's name 1049 | * 1050 | * @param {?} compilation 1051 | * @param {!Chunk} chunk 1052 | */ 1053 | getChunkName(compilation, chunk) { 1054 | const filenameTemplate = this.getChunkFilenameTemplate( 1055 | compilation, 1056 | chunk.hasEntryModule() 1057 | ); 1058 | const useChunkHash = 1059 | !chunk.hasEntryModule() || 1060 | (compilation.mainTemplate.useChunkHash && 1061 | compilation.mainTemplate.useChunkHash(chunk)); 1062 | return compilation.getPath(filenameTemplate, { 1063 | noChunkHash: !useChunkHash, 1064 | chunk, 1065 | hash: useChunkHash ? chunk.hash : compilation.hash, 1066 | contentHash: chunk.contentHash.javascript, 1067 | }); 1068 | } 1069 | 1070 | /** 1071 | * Starting from an entry point, recursively traverse the chunk group tree and add 1072 | * all chunk sources to the compilation. 1073 | * 1074 | * @param {?} compilation 1075 | * @param {!Chunk} initialChunk 1076 | * @param {!Array} initialParentChunkNames - logical chunk parent of this tree 1077 | * @param {!ChunkMap} chunkDefs 1078 | * @param {!Array} entrypoints modules 1079 | */ 1080 | addChunkToCompilationStandard( 1081 | compilation, 1082 | initialChunk, 1083 | initialParentChunkNames, 1084 | chunkDefs, 1085 | entrypoints 1086 | ) { 1087 | const chunkQueue = [ 1088 | { 1089 | chunk: initialChunk, 1090 | parentChunkNames: initialParentChunkNames, 1091 | }, 1092 | ]; 1093 | const chunksEverInQueue = new Set([initialChunk.id]); 1094 | while (chunkQueue.length > 0) { 1095 | const { chunk, parentChunkNames } = chunkQueue.pop(); 1096 | const chunkName = this.getChunkName(compilation, chunk); 1097 | const safeChunkName = chunkName.replace(/\.js$/, ''); 1098 | const chunkSources = []; 1099 | chunk.files.forEach((chunkFile) => { 1100 | if (!chunkFile.match(this.options.test)) { 1101 | return; 1102 | } 1103 | let src = ''; 1104 | let sourceMap = null; 1105 | try { 1106 | const sourceAndMap = compilation.assets[chunkFile].sourceAndMap(); 1107 | src = sourceAndMap.source; 1108 | if (sourceAndMap.map) { 1109 | sourceMap = sourceAndMap.map; 1110 | } 1111 | } catch (e) { 1112 | compilation.errors.push(e); 1113 | } 1114 | if (sourceMap && Array.isArray(sourceMap.sources)) { 1115 | // Closure doesn't support all characters in the loader?ref!filepath "path" format Webpack uses, so trim off the loader prefix. 1116 | sourceMap.sources = sourceMap.sources.map((sourcePath) => { 1117 | const loaderPrefixEndIndex = sourcePath.lastIndexOf('!'); 1118 | let sanitizedPath = 1119 | loaderPrefixEndIndex !== -1 1120 | ? sourcePath.slice(loaderPrefixEndIndex + 1) 1121 | : sourcePath; 1122 | if (sanitizedPath.length === 0) { 1123 | // If a loader created the file (e.g. inject-loader) the original path is empty. Just sanitize the generated name to create a unique name. 1124 | sanitizedPath = toSafePath(sourcePath); 1125 | } 1126 | // Standardize to forward slash in paths as Closure sometimes fails to resolve with back slash. 1127 | return sanitizedPath.replace(/\\/g, '/'); 1128 | }); 1129 | } 1130 | chunkSources.push({ 1131 | path: chunkName, 1132 | src, 1133 | sourceMap, 1134 | }); 1135 | }); 1136 | 1137 | const chunkDef = { 1138 | name: safeChunkName, 1139 | parentNames: new Set(), 1140 | sources: chunkSources, 1141 | outputWrapper: '(function(){%s}).call(this || window)', 1142 | }; 1143 | if (parentChunkNames) { 1144 | parentChunkNames.forEach((parentName) => { 1145 | chunkDef.parentNames.add(parentName); 1146 | }); 1147 | } 1148 | chunkDefs.set(safeChunkName, chunkDef); 1149 | for (const group of chunk.groupsIterable) { 1150 | for (const childGroup of group.childrenIterable) { 1151 | const chunksToAdd = []; 1152 | childGroup.chunks.forEach((childChunk) => { 1153 | if (!chunksEverInQueue.has(childChunk.id)) { 1154 | chunksEverInQueue.add(childChunk.id); 1155 | chunksToAdd.unshift({ 1156 | chunk: childChunk, 1157 | parentChunkNames: [safeChunkName], 1158 | }); 1159 | } 1160 | }); 1161 | chunkQueue.push(...chunksToAdd); 1162 | } 1163 | } 1164 | } 1165 | } 1166 | 1167 | /** 1168 | * Starting from an entry point, recursively traverse the chunk group tree and add 1169 | * all chunk sources to the compilation. 1170 | * 1171 | * @param {?} compilation 1172 | * @param {!Chunk} chunk 1173 | * @param {!Array} sources 1174 | * @param {!Array} parentChunkNames - logical chunk parent of this tree 1175 | * @param {!ChunkMap} chunkDefs 1176 | * @param {!Array} entrypoint modules 1177 | */ 1178 | addChunkToCompilationAggressive( 1179 | compilation, 1180 | chunk, 1181 | parentChunkNames, 1182 | chunkDefs, 1183 | entrypoints 1184 | ) { 1185 | const chunkName = 1186 | chunk.name === this.BASE_CHUNK_NAME 1187 | ? this.BASE_CHUNK_NAME 1188 | : this.getChunkName(compilation, chunk); 1189 | const safeChunkName = chunkName.replace(/\.js$/, ''); 1190 | 1191 | if (chunkDefs.has(safeChunkName)) { 1192 | if (parentChunkNames.length !== 0) { 1193 | parentChunkNames.forEach((parentName) => { 1194 | chunkDefs.get(safeChunkName).parentNames.add(parentName); 1195 | }); 1196 | } 1197 | return; 1198 | } else if ( 1199 | !chunk.files.includes(chunkName) && 1200 | chunk.name !== this.BASE_CHUNK_NAME 1201 | ) { 1202 | chunk.files.push(chunkName); 1203 | if (!compilation.assets[chunkName]) { 1204 | compilation.assets[chunkName] = new RawSource(''); 1205 | } 1206 | } 1207 | 1208 | const chunkSources = []; 1209 | const childChunkIds = Object.keys(chunk.getChunkMaps().hash); 1210 | if (childChunkIds.length > 0) { 1211 | const childChunkPaths = this.getChildChunkPaths( 1212 | compilation.hash, 1213 | chunk, 1214 | 'chunkId', 1215 | compilation, 1216 | this.getChunkFilenameTemplate(compilation, false) 1217 | ); 1218 | const childModulePathRegistrationSource = { 1219 | path: path.resolve('.', `__webpack_register_source_${chunk.id}__.js`), 1220 | src: 1221 | '(function(chunkIds){\n' + 1222 | ' for (var i = 0, chunkId; i < chunkIds.length; i++) {\n' + 1223 | ' chunkId = chunkIds[i];\n' + 1224 | ` __webpack_require__.rs(chunkIds[i], ${childChunkPaths});\n` + 1225 | ' }\n' + 1226 | `})(${JSON.stringify(childChunkIds)});`, 1227 | }; 1228 | chunkSources.push(childModulePathRegistrationSource); 1229 | // put this at the front of the entrypoints so that Closure-compiler sorts the source to the top of the chunk 1230 | entrypoints.unshift(childModulePathRegistrationSource.path); 1231 | } 1232 | chunkSources.push(...getChunkSources(chunk, compilation)); 1233 | 1234 | const chunkDef = { 1235 | name: safeChunkName, 1236 | parentNames: new Set(), 1237 | sources: chunkSources, 1238 | outputWrapper: chunk.hasEntryModule() 1239 | ? ENTRY_CHUNK_WRAPPER 1240 | : `webpackJsonp([${chunk.id}], function(__wpcc){%s});`, 1241 | }; 1242 | if (parentChunkNames) { 1243 | parentChunkNames.forEach((parentName) => { 1244 | chunkDef.parentNames.add(parentName); 1245 | }); 1246 | } 1247 | chunkDefs.set(safeChunkName, chunkDef); 1248 | } 1249 | 1250 | getChildChunkPaths( 1251 | hash, 1252 | chunk, 1253 | chunkIdExpression, 1254 | compilation, 1255 | chunkFilename 1256 | ) { 1257 | const { mainTemplate } = compilation; 1258 | const chunkMaps = chunk.getChunkMaps(); 1259 | return mainTemplate.getAssetPath(JSON.stringify(chunkFilename), { 1260 | hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`, 1261 | hashWithLength: (length) => 1262 | `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`, 1263 | chunk: { 1264 | id: `" + ${chunkIdExpression} + "`, 1265 | hash: `" + ${JSON.stringify(chunkMaps.hash)}[${chunkIdExpression}] + "`, 1266 | hashWithLength(length) { 1267 | const shortChunkHashMap = Object.create(null); 1268 | for (const chunkId of Object.keys(chunkMaps.hash)) { 1269 | if (typeof chunkMaps.hash[chunkId] === 'string') { 1270 | shortChunkHashMap[chunkId] = chunkMaps.hash[chunkId].slice( 1271 | 0, 1272 | length 1273 | ); 1274 | } 1275 | } 1276 | return `" + ${JSON.stringify( 1277 | shortChunkHashMap 1278 | )}[${chunkIdExpression}] + "`; 1279 | }, 1280 | name: `" + (${JSON.stringify( 1281 | chunkMaps.name 1282 | )}[${chunkIdExpression}]||${chunkIdExpression}) + "`, 1283 | contentHash: { 1284 | javascript: `" + ${JSON.stringify( 1285 | chunkMaps.contentHash.javascript 1286 | )}[${chunkIdExpression}] + "`, 1287 | }, 1288 | contentHashWithLength: { 1289 | javascript: (length) => { 1290 | const shortContentHashMap = {}; 1291 | const contentHash = chunkMaps.contentHash.javascript; 1292 | for (const chunkId of Object.keys(contentHash)) { 1293 | if (typeof contentHash[chunkId] === 'string') { 1294 | shortContentHashMap[chunkId] = contentHash[chunkId].slice( 1295 | 0, 1296 | length 1297 | ); 1298 | } 1299 | } 1300 | return `" + ${JSON.stringify( 1301 | shortContentHashMap 1302 | )}[${chunkIdExpression}] + "`; 1303 | }, 1304 | }, 1305 | }, 1306 | contentHashType: 'javascript', 1307 | }); 1308 | } 1309 | 1310 | /** 1311 | * Given the source path of the output destination, return the custom 1312 | * runtime used by AGGRESSIVE_BUNDLE mode. 1313 | * 1314 | * @return {string} 1315 | */ 1316 | renderRuntime() { 1317 | const lateLoadedRuntimePath = require.resolve('./runtime.js'); 1318 | return { 1319 | path: lateLoadedRuntimePath, 1320 | src: fs.readFileSync(lateLoadedRuntimePath, 'utf8'), 1321 | }; 1322 | } 1323 | 1324 | /** 1325 | * Format an array of errors from closure-compiler into webpack style compilation errors 1326 | */ 1327 | reportErrors(compilation, errors) { 1328 | errors.forEach((error) => { 1329 | let formattedMsg; 1330 | if (error.source) { 1331 | formattedMsg = this.requestShortener.shorten(error.source); 1332 | if (error.line === 0 || error.line) { 1333 | formattedMsg += `:${error.line}`; 1334 | } 1335 | if (error.originalLocation) { 1336 | const originalSource = 1337 | error.originalLocation.source === error.source 1338 | ? 'line ' 1339 | : `${this.requestShortener.shorten( 1340 | error.originalLocation.source 1341 | )}:`; 1342 | 1343 | if ( 1344 | error.originalLocation.source !== error.source || 1345 | error.originalLocation.line !== error.line 1346 | ) { 1347 | formattedMsg += ` (originally at ${originalSource}${error.originalLocation.line})`; 1348 | } 1349 | } 1350 | formattedMsg += ` from closure-compiler: ${error.description}`; 1351 | 1352 | if (error.context) { 1353 | formattedMsg += `\n${error.context}`; 1354 | } 1355 | } else { 1356 | formattedMsg = `closure-compiler: ${error.description.trim()}`; 1357 | } 1358 | if (error.level === 'error') { 1359 | compilation.errors.push(new Error(formattedMsg)); 1360 | } else if (error.level !== 'info') { 1361 | compilation.warnings.push(new Error(formattedMsg)); 1362 | } 1363 | }); 1364 | } 1365 | } 1366 | 1367 | /** @const */ 1368 | ClosureCompilerPlugin.DEFAULT_OPTIONS = { 1369 | childCompilations: false, 1370 | mode: 'STANDARD', 1371 | platform: ['native', 'java'], 1372 | test: /\.js(\?.*)?$/i, 1373 | extraCommandArgs: [], 1374 | }; 1375 | 1376 | /** @const */ 1377 | ClosureCompilerPlugin.DEFAULT_FLAGS_AGGRESSIVE_BUNDLE = { 1378 | language_in: 'ECMASCRIPT_NEXT', 1379 | language_out: 'ECMASCRIPT5_STRICT', 1380 | module_resolution: 'WEBPACK', 1381 | rename_prefix_namespace: '__wpcc', 1382 | process_common_js_modules: true, 1383 | dependency_mode: 'PRUNE', 1384 | assume_function_wrapper: true, 1385 | source_map_include_content: true, 1386 | }; 1387 | 1388 | /** @const */ 1389 | ClosureCompilerPlugin.DEFAULT_FLAGS_STANDARD = { 1390 | language_in: 'ECMASCRIPT_NEXT', 1391 | language_out: 'ECMASCRIPT5_STRICT', 1392 | source_map_include_content: true, 1393 | }; 1394 | 1395 | module.exports = ClosureCompilerPlugin; 1396 | module.exports.LibraryPlugin = ClosureLibraryPlugin; 1397 | -------------------------------------------------------------------------------- /src/closure-library-plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Add support for closure-library dependency types to webpack. 3 | * 4 | * Includes: 5 | * 6 | * - goog.require 7 | * - goog.module 8 | * - goog.provide 9 | */ 10 | 11 | const RequestShortener = require('webpack/lib/RequestShortener'); 12 | const GoogRequireParserPlugin = require('./goog-require-parser-plugin'); 13 | const GoogDependency = require('./dependencies/goog-dependency'); 14 | const GoogBaseGlobalDependency = require('./dependencies/goog-base-global'); 15 | const GoogLoaderPrefixDependency = require('./dependencies/goog-loader-prefix-dependency'); 16 | const GoogLoaderSuffixDependency = require('./dependencies/goog-loader-suffix-dependency'); 17 | const GoogLoaderEs6PrefixDependency = require('./dependencies/goog-loader-es6-prefix-dependency'); 18 | const GoogLoaderEs6SuffixDependency = require('./dependencies/goog-loader-es6-suffix-dependency'); 19 | const NullFactory = require('webpack/lib/NullFactory'); 20 | const validateOptions = require('schema-utils'); 21 | const closureLibraryPluginSchema = require('../schema/closure-library.json'); 22 | 23 | const PLUGIN = { name: 'ClosureLibraryPlugin' }; 24 | 25 | class ClosureLibraryPlugin { 26 | constructor(options) { 27 | validateOptions( 28 | closureLibraryPluginSchema, 29 | options || {}, 30 | 'closure-library-plugin' 31 | ); 32 | this.options = Object.assign({}, options || {}); 33 | } 34 | 35 | apply(compiler) { 36 | this.requestShortener = new RequestShortener(compiler.context); 37 | 38 | compiler.hooks.compilation.tap(PLUGIN, (compilation, params) => 39 | this.complation_(compilation, params) 40 | ); 41 | } 42 | 43 | complation_(compilation, params) { 44 | if ( 45 | this.options.closureLibraryBase && 46 | (this.options.deps || this.options.extraDeps) 47 | ) { 48 | const parserPluginOptions = Object.assign( 49 | { mode: compilation.options.mode }, 50 | this.options 51 | ); 52 | 53 | const { normalModuleFactory } = params; 54 | 55 | const parserCallback = (parser) => { 56 | const parserPlugin = new GoogRequireParserPlugin(parserPluginOptions); 57 | parserPlugin.apply(parser); 58 | }; 59 | 60 | normalModuleFactory.hooks.parser 61 | .for('javascript/auto') 62 | .tap(PLUGIN.name, parserCallback); 63 | normalModuleFactory.hooks.parser 64 | .for('javascript/dynamic') 65 | .tap(PLUGIN.name, parserCallback); 66 | normalModuleFactory.hooks.parser 67 | .for('javascript/esm') 68 | .tap(PLUGIN.name, parserCallback); 69 | 70 | compilation.dependencyFactories.set( 71 | GoogDependency, 72 | params.normalModuleFactory 73 | ); 74 | compilation.dependencyTemplates.set( 75 | GoogDependency, 76 | new GoogDependency.Template() 77 | ); 78 | compilation.dependencyFactories.set( 79 | GoogBaseGlobalDependency, 80 | params.normalModuleFactory 81 | ); 82 | compilation.dependencyTemplates.set( 83 | GoogBaseGlobalDependency, 84 | new GoogBaseGlobalDependency.Template() 85 | ); 86 | compilation.dependencyFactories.set( 87 | GoogLoaderPrefixDependency, 88 | params.normalModuleFactory 89 | ); 90 | compilation.dependencyTemplates.set( 91 | GoogLoaderPrefixDependency, 92 | new GoogLoaderPrefixDependency.Template() 93 | ); 94 | compilation.dependencyFactories.set( 95 | GoogLoaderSuffixDependency, 96 | params.normalModuleFactory 97 | ); 98 | compilation.dependencyTemplates.set( 99 | GoogLoaderSuffixDependency, 100 | new GoogLoaderSuffixDependency.Template() 101 | ); 102 | compilation.dependencyFactories.set( 103 | GoogLoaderEs6PrefixDependency, 104 | new NullFactory() 105 | ); 106 | compilation.dependencyTemplates.set( 107 | GoogLoaderEs6PrefixDependency, 108 | new GoogLoaderEs6PrefixDependency.Template() 109 | ); 110 | compilation.dependencyFactories.set( 111 | GoogLoaderEs6SuffixDependency, 112 | new NullFactory() 113 | ); 114 | compilation.dependencyTemplates.set( 115 | GoogLoaderEs6SuffixDependency, 116 | new GoogLoaderEs6SuffixDependency.Template() 117 | ); 118 | } 119 | } 120 | } 121 | 122 | module.exports = ClosureLibraryPlugin; 123 | -------------------------------------------------------------------------------- /src/closure-runtime-template.js: -------------------------------------------------------------------------------- 1 | const RuntimeTemplate = require('webpack/lib/RuntimeTemplate'); 2 | const Template = require('webpack/lib/Template'); 3 | const unquotedValidator = require('unquoted-property-validator'); 4 | 5 | module.exports = class ClosureRuntimeTemplate extends RuntimeTemplate { 6 | moduleNamespacePromise({ block, module, request, message }) { 7 | if (!module) { 8 | return this.missingModulePromise({ 9 | request, 10 | }); 11 | } 12 | if (module.id === null) { 13 | throw new Error( 14 | `RuntimeTemplate.moduleNamespacePromise(): Module ${module.identifier()} has no id. This should not happen.` 15 | ); 16 | } 17 | const promise = this.blockPromise({ 18 | block, 19 | message, 20 | }); 21 | 22 | const idExpr = JSON.stringify(module.id); 23 | const comment = this.comment({ 24 | request, 25 | }); 26 | const getModuleFunction = `function() { return __webpack_require__.t(${comment}${idExpr}); }`; 27 | return `${promise || 'Promise.resolve()'}.then(${getModuleFunction})`; 28 | } 29 | 30 | /** 31 | * 32 | * @param {Object} options options object 33 | * @param {boolean=} options.update whether a new variable should be created or the existing one updated 34 | * @param {Module} options.module the module 35 | * @param {string} options.request the request that should be printed as comment 36 | * @param {string} options.importVar name of the import variable 37 | * @param {Module} options.originModule module in which the statement is emitted 38 | * @returns {string} the import statement 39 | */ 40 | importStatement({ update, module, request, importVar, originModule }) { 41 | if (!module) { 42 | return this.missingModuleStatement({ 43 | request, 44 | }); 45 | } 46 | const moduleId = this.moduleId({ 47 | module, 48 | request, 49 | }); 50 | const optDeclaration = update ? '' : 'var '; 51 | return `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId})\n`; 52 | } 53 | 54 | exportFromImport({ 55 | module, 56 | request, 57 | exportName, 58 | originModule, 59 | asiSafe, 60 | isCall, 61 | callContext, 62 | importVar, 63 | }) { 64 | if (!module) { 65 | return this.missingModule({ 66 | request, 67 | }); 68 | } 69 | const exportsType = module.buildMeta && module.buildMeta.exportsType; 70 | if (!exportsType) { 71 | if (exportName === 'default') { 72 | return importVar; 73 | } else if (originModule.buildMeta.strictHarmonyModule) { 74 | if (exportName) { 75 | return '/* non-default import from non-esm module */undefined'; 76 | } 77 | return `/*#__PURE__*/__webpack_require__(${importVar})`; 78 | } 79 | } 80 | 81 | if (exportName) { 82 | const used = module.isUsed(exportName); 83 | if (!used) { 84 | const comment = Template.toNormalComment(`unused export ${exportName}`); 85 | return `${comment} undefined`; 86 | } 87 | const unquotedAccess = unquotedValidator(exportName); 88 | let access = `.${exportName}`; 89 | if (unquotedAccess.needsQuotes || unquotedAccess.needsBrackets) { 90 | access = `[${unquotedAccess.quotedValue}]`; 91 | } 92 | access = `${importVar}${access}`; 93 | return access; 94 | } 95 | return importVar; 96 | } 97 | 98 | defineEsModuleFlagStatement({ exportsArgument }) { 99 | return ''; 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/common-ancestor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Find an ancestor of a chunk. Return the distance from the target or -1 if not found. 3 | * 4 | * @param {ChunkGroup} initialSrc 5 | * @param {ChunkGroup} target 6 | * @param {number} initialDistance 7 | * @return {number} distance from target of parent or -1 when not found 8 | */ 9 | function findAncestorDistance(initialSrc, target, initialDistance) { 10 | const distances = []; 11 | const parentChunkQueue = [ 12 | { 13 | src: initialSrc, 14 | currentDistance: initialDistance, 15 | }, 16 | ]; 17 | const visitedChunks = new Set([initialSrc]); 18 | while (parentChunkQueue.length > 0) { 19 | const { src, currentDistance } = parentChunkQueue.pop(); 20 | if (target === src) { 21 | if (currentDistance >= 0) { 22 | distances.push(currentDistance); 23 | } 24 | } else { 25 | const parentChunkGroups = src.getParents(); 26 | for (let i = parentChunkGroups.length - 1; i >= 0; i--) { 27 | if (!visitedChunks.has(parentChunkGroups[i])) { 28 | visitedChunks.add(parentChunkGroups[i]); 29 | parentChunkQueue.push({ 30 | src: parentChunkGroups[i], 31 | currentDistance: currentDistance + 1, 32 | }); 33 | } 34 | } 35 | } 36 | } 37 | if (distances.length === 0) { 38 | return -1; 39 | } 40 | return Math.min(...distances); 41 | } 42 | 43 | /** 44 | * Find the closest common parent chunk from a list. 45 | * Since closure-compiler requires a chunk tree to have a single root, 46 | * there will always be a common parent. 47 | * 48 | * @param {!Array} chunkGroups 49 | * @param {number} currentDistance 50 | * @return {{chunkGroup: (!ChunkGroup|undefined), distance: number}} 51 | */ 52 | function findNearestCommonParentChunk(chunkGroups, currentDistance = 0) { 53 | // Map of chunk name to distance from target 54 | const distances = new Map(); 55 | for (let i = 1; i < chunkGroups.length; i++) { 56 | const distance = findAncestorDistance( 57 | chunkGroups[i], 58 | chunkGroups[0], 59 | currentDistance 60 | ); 61 | if (distance < 0) { 62 | distances.delete(chunkGroups[0]); 63 | } else if ( 64 | !distances.has(chunkGroups[0]) || 65 | distance < distances.get(chunkGroups[0]) 66 | ) { 67 | distances.set(chunkGroups[0], distance); 68 | } 69 | } 70 | if (distances.size === 0) { 71 | // chunkGroup[0] was not a parent for the other chunk groups. 72 | // So move up the graph one level and check again. 73 | chunkGroups[0].getParents().forEach((chunkGroupParent) => { 74 | const distanceRecord = findNearestCommonParentChunk( 75 | [chunkGroupParent].concat(chunkGroups.slice(1)), 76 | currentDistance + 1 77 | ); 78 | if ( 79 | distanceRecord.distance >= 0 && 80 | (!distances.has(distanceRecord.chunkGroup) || 81 | distances.get(distanceRecord.chunkGroup) < distanceRecord.distance) 82 | ) { 83 | distances.set(distanceRecord.chunkGroup, distanceRecord.distance); 84 | } 85 | }); 86 | } 87 | 88 | const nearestCommonParent = { 89 | chunkGroup: undefined, // eslint-disable-line no-undefined 90 | distance: -1, 91 | }; 92 | distances.forEach((distance, chunkGroup) => { 93 | if ( 94 | nearestCommonParent.distance < 0 || 95 | distance < nearestCommonParent.distance 96 | ) { 97 | nearestCommonParent.chunkGroup = chunkGroup; 98 | nearestCommonParent.distance = distance; 99 | } 100 | }); 101 | return nearestCommonParent; 102 | } 103 | 104 | module.exports = findNearestCommonParentChunk; 105 | -------------------------------------------------------------------------------- /src/dependencies/amd-define-dependency.js: -------------------------------------------------------------------------------- 1 | const AMDDefineDependency = require('webpack/lib/dependencies/AMDDefineDependency'); 2 | 3 | class ClosureAMDDefineDependency extends AMDDefineDependency { 4 | updateHash(hash) { 5 | hash.update(this.rangeStatement + ''); 6 | hash.update(this.declaration + ''); 7 | hash.update('ClosureAMDDefineDependency'); 8 | } 9 | } 10 | 11 | ClosureAMDDefineDependency.Template = class ClosureAMDDefineDependencyTemplate extends AMDDefineDependency.Template { 12 | get definitions() { 13 | const defs = super.definitions; 14 | for (const value in defs) { 15 | if (Object.prototype.hasOwnProperty.call(defs, value)) { 16 | const valueEntries = defs[value]; 17 | defs[value].forEach((line, index) => { 18 | if (!/^var/.test(line)) { 19 | return; 20 | } 21 | valueEntries[index] = line.replace( 22 | /var __WEBPACK_AMD/g, 23 | '/** @suppress {duplicate} */$&' 24 | ); 25 | }); 26 | } 27 | } 28 | return defs; 29 | } 30 | }; 31 | 32 | module.exports = ClosureAMDDefineDependency; 33 | -------------------------------------------------------------------------------- /src/dependencies/goog-base-global.js: -------------------------------------------------------------------------------- 1 | const Dependency = require('webpack/lib/Dependency'); 2 | 3 | class GoogBaseGlobalDependency extends Dependency {} 4 | 5 | class GoogBaseGlobalDependencyTemplate { 6 | apply(dep, source) { 7 | const sourceContent = source.source(); 8 | const content = `goog.ENABLE_DEBUG_LOADER = false; 9 | module.exports = goog;`; 10 | source.insert(sourceContent.length, content); 11 | 12 | const globalDefIndex = sourceContent.search(/\n\s*goog\.global\s*=\s*/); 13 | let statementEndIndex = -1; 14 | if (globalDefIndex >= 0) { 15 | statementEndIndex = sourceContent.indexOf(';', globalDefIndex); 16 | } 17 | if (statementEndIndex) { 18 | source.insert( 19 | statementEndIndex + 1, 20 | 'goog.global = window; goog.global.CLOSURE_NO_DEPS = true;' 21 | ); 22 | } else { 23 | source.insert(0, 'this.CLOSURE_NO_DEPS = true;\n'); 24 | } 25 | } 26 | } 27 | 28 | module.exports = GoogBaseGlobalDependency; 29 | module.exports.Template = GoogBaseGlobalDependencyTemplate; 30 | -------------------------------------------------------------------------------- /src/dependencies/goog-dependency.js: -------------------------------------------------------------------------------- 1 | const ModuleDependency = require('webpack/lib/dependencies/ModuleDependency'); 2 | 3 | class GoogDependency extends ModuleDependency { 4 | constructor(request, insertPosition, isBase = false, isRequireType = false) { 5 | super(request); 6 | this.insertPosition = insertPosition; 7 | this.isBase = isBase; 8 | this.isRequireType = isRequireType; 9 | } 10 | 11 | get type() { 12 | return 'goog.require or goog.module.get'; 13 | } 14 | 15 | updateHash(hash) { 16 | hash.update(this.insertPosition + ''); 17 | hash.update(this.isBase + ''); 18 | hash.update(this.isRequireType + ''); 19 | } 20 | } 21 | 22 | class GoogDependencyTemplate { 23 | apply(dep, source) { 24 | if (dep.insertPosition === null) { 25 | return; 26 | } 27 | 28 | // goog.requireType is an implicit dependency and shouldn't be loaded 29 | if (dep.isRequireType) { 30 | return; 31 | } 32 | 33 | let content = `__webpack_require__(${JSON.stringify(dep.module.id)});\n`; 34 | if (dep.isBase) { 35 | content = `var goog = ${content}`; 36 | } 37 | source.insert(dep.insertPosition, content); 38 | } 39 | } 40 | 41 | module.exports = GoogDependency; 42 | module.exports.Template = GoogDependencyTemplate; 43 | -------------------------------------------------------------------------------- /src/dependencies/goog-loader-es6-prefix-dependency.js: -------------------------------------------------------------------------------- 1 | const Dependency = require('webpack/lib/Dependency'); 2 | 3 | /** 4 | * Mocks out some methods on the global $jscomp variable that Closure Library 5 | * expects in order to work with ES6 modules. 6 | * 7 | * Closure Library provides goog.module.declareNamespace to associate an ES6 8 | * module with a Closure namespace, enabling it to be goog.require'd. 9 | * 10 | * When goog.module.declareNamespace is called in a bundle, 11 | * $jscomp.getCurrentModulePath is expected to return a non-null value (though 12 | * this value is not stored, just asserted to be non-null). Then 13 | * $jscomp.require($jscomp.getCurrentModulePath()) is called in order to get the 14 | * ES6 module's exports and store it with the namespace passed to the call to 15 | * goog.module.declareNamespace. Closure stores these exports and associates 16 | * them with the namespace so they can later be goog.require'd. 17 | */ 18 | class GoogLoaderEs6PrefixDependency extends Dependency { 19 | constructor(insertPosition) { 20 | super(); 21 | this.insertPosition = insertPosition; 22 | } 23 | 24 | get type() { 25 | return 'goog loader es6 prefix'; 26 | } 27 | 28 | updateHash(hash) { 29 | hash.update(this.insertPosition + ''); 30 | } 31 | } 32 | 33 | class GoogLoaderEs6PrefixDependencyTemplate { 34 | apply(dep, source) { 35 | if (dep.insertPosition === null) { 36 | return; 37 | } 38 | 39 | source.insert( 40 | dep.insertPosition, 41 | `$jscomp.getCurrentModulePath = function() { return ''; };\n` + 42 | '$jscomp.require = function() { return __webpack_exports__ };\n' 43 | ); 44 | } 45 | } 46 | 47 | module.exports = GoogLoaderEs6PrefixDependency; 48 | module.exports.Template = GoogLoaderEs6PrefixDependencyTemplate; 49 | -------------------------------------------------------------------------------- /src/dependencies/goog-loader-es6-suffix-dependency.js: -------------------------------------------------------------------------------- 1 | const Dependency = require('webpack/lib/Dependency'); 2 | 3 | /** 4 | * Cleans up after the prefix dependency. 5 | */ 6 | class GoogLoaderEs6SuffixDependency extends Dependency { 7 | constructor(insertPosition) { 8 | super(); 9 | this.insertPosition = insertPosition; 10 | } 11 | 12 | get type() { 13 | return 'goog loader es6 suffix'; 14 | } 15 | 16 | updateHash(hash) { 17 | hash.update(this.insertPosition + ''); 18 | } 19 | } 20 | 21 | class GoogLoaderes6SuffixDependencyTemplate { 22 | apply(dep, source) { 23 | if (dep.insertPosition === null) { 24 | return; 25 | } 26 | 27 | source.insert( 28 | dep.insertPosition, 29 | `$jscomp.getCurrentModulePath = function() { return null; }; 30 | $jscomp.require = function() { return null; };` 31 | ); 32 | } 33 | } 34 | 35 | module.exports = GoogLoaderEs6SuffixDependency; 36 | module.exports.Template = GoogLoaderes6SuffixDependencyTemplate; 37 | -------------------------------------------------------------------------------- /src/dependencies/goog-loader-prefix-dependency.js: -------------------------------------------------------------------------------- 1 | const ModuleDependency = require('webpack/lib/dependencies/ModuleDependency'); 2 | 3 | class GoogLoaderPrefixDependency extends ModuleDependency { 4 | constructor(request, isModule, insertPosition) { 5 | super(request); 6 | this.insertPosition = insertPosition; 7 | this.isGoogModule = isModule; 8 | } 9 | 10 | get type() { 11 | return 'goog loader prefix'; 12 | } 13 | 14 | updateHash(hash) { 15 | hash.update(this.insertPosition + ''); 16 | hash.update(this.isGoogModule + ''); 17 | } 18 | } 19 | 20 | class GoogLoaderPrefixDependencyTemplate { 21 | apply(dep, source) { 22 | if (dep.insertPosition === null) { 23 | return; 24 | } 25 | 26 | let content = `var googPreviousLoaderState__ = goog.moduleLoaderState_;\n`; 27 | if (dep.isGoogModule) { 28 | content += `goog.moduleLoaderState_ = {moduleName: '', declareLegacyNamespace: false}; 29 | goog.loadModule(function() {\n`; 30 | } else { 31 | content += `goog.moduleLoaderState_ = null;\n`; 32 | } 33 | source.insert(dep.insertPosition, content); 34 | } 35 | } 36 | 37 | module.exports = GoogLoaderPrefixDependency; 38 | module.exports.Template = GoogLoaderPrefixDependencyTemplate; 39 | -------------------------------------------------------------------------------- /src/dependencies/goog-loader-suffix-dependency.js: -------------------------------------------------------------------------------- 1 | const ModuleDependency = require('webpack/lib/dependencies/ModuleDependency'); 2 | 3 | class GoogLoaderSuffixDependency extends ModuleDependency { 4 | constructor(request, isModule, insertPosition) { 5 | super(request); 6 | this.insertPosition = insertPosition; 7 | this.isGoogModule = isModule; 8 | } 9 | 10 | get type() { 11 | return 'goog loader suffix'; 12 | } 13 | 14 | updateHash(hash) { 15 | hash.update(this.insertPosition + ''); 16 | hash.update(this.isGoogModule + ''); 17 | } 18 | } 19 | 20 | class GoogLoaderSuffixDependencyTemplate { 21 | apply(dep, source) { 22 | if (dep.insertPosition === null) { 23 | return; 24 | } 25 | 26 | let content = ''; 27 | if (dep.isGoogModule) { 28 | content = '\nreturn exports; });'; 29 | } 30 | content += ` 31 | goog.moduleLoaderState_ = googPreviousLoaderState__;`; 32 | source.insert(dep.insertPosition, content); 33 | } 34 | } 35 | 36 | module.exports = GoogLoaderSuffixDependency; 37 | module.exports.Template = GoogLoaderSuffixDependencyTemplate; 38 | -------------------------------------------------------------------------------- /src/dependencies/harmony-export-dependency.js: -------------------------------------------------------------------------------- 1 | const NullDependency = require('webpack/lib/dependencies/NullDependency'); 2 | const HarmonyNoopTemplate = require('./harmony-noop-template') 3 | 4 | class ClosureHarmonyExportDependency extends NullDependency { 5 | constructor(declaration, rangeStatement, module, name, id) { 6 | super(); 7 | this.declaration = declaration; 8 | this.rangeStatement = rangeStatement; 9 | this.name = name; 10 | this.id = id; 11 | } 12 | 13 | get type() { 14 | return 'harmony export'; 15 | } 16 | 17 | getExports() { 18 | return { 19 | exports: [this.id], 20 | dependencies: undefined, // eslint-disable-line no-undefined 21 | }; 22 | } 23 | 24 | updateHash(hash) { 25 | hash.update(this.rangeStatement + ''); 26 | hash.update(this.declaration + ''); 27 | hash.update('ClosureHarmonyExportDependency'); 28 | } 29 | } 30 | 31 | ClosureHarmonyExportDependency.Template = HarmonyNoopTemplate; 32 | 33 | module.exports = ClosureHarmonyExportDependency; 34 | -------------------------------------------------------------------------------- /src/dependencies/harmony-export-import-dependency.js: -------------------------------------------------------------------------------- 1 | const HarmonyImportDependency = require('webpack/lib/dependencies/HarmonyImportDependency'); 2 | 3 | class ClosureHarmonyExportImportDependency extends HarmonyImportDependency { 4 | constructor(request, originModule, sourceOrder, parserScope, range) { 5 | super(request, originModule, sourceOrder, parserScope); 6 | this.range = range; 7 | } 8 | updateHash(hash) { 9 | hash.update('ClosureHarmonyExportImportDependency'); 10 | } 11 | } 12 | 13 | ClosureHarmonyExportImportDependency.Template = class ClosureHarmonyExportImportDependencyTemplate { 14 | apply(dep, source, runtime) { 15 | const moduleId = runtime.moduleId({ 16 | module: dep._module, 17 | request: dep._request, 18 | }); 19 | source.replace(dep.range[0], dep.range[1] - 1, JSON.stringify(moduleId)); 20 | } 21 | } 22 | 23 | module.exports = ClosureHarmonyExportImportDependency; 24 | -------------------------------------------------------------------------------- /src/dependencies/harmony-import-dependency.js: -------------------------------------------------------------------------------- 1 | const HarmonyImportSpecifierDependency = require('webpack/lib/dependencies/HarmonyImportSpecifierDependency'); 2 | 3 | class ClosureHarmonyImportDependency extends HarmonyImportSpecifierDependency { 4 | updateHash(hash) { 5 | hash.update('ClosureHarmonyImportDependency'); 6 | } 7 | } 8 | 9 | ClosureHarmonyImportDependency.Template = class ClosureHarmonyImportDependencyTemplate { 10 | apply(dep, source, runtime) { 11 | const moduleId = runtime.moduleId({ 12 | module: dep._module, 13 | request: dep._request, 14 | }); 15 | source.replace(dep.range[0], dep.range[1] - 1, JSON.stringify(moduleId)); 16 | } 17 | }; 18 | 19 | module.exports = ClosureHarmonyImportDependency; 20 | -------------------------------------------------------------------------------- /src/dependencies/harmony-marker-dependency.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview The webpack harmony plugin adds constant dependencies to clear 3 | * out parts of both import and export statements. The dependency acts as a marker 4 | * so that we can locate and remove those constant dependencies later. 5 | */ 6 | 7 | const NullDependency = require('webpack/lib/dependencies/NullDependency'); 8 | const HarmonyNoopTemplate = require('./harmony-noop-template'); 9 | 10 | class ClosureHarmonyMarkerDependency extends NullDependency { 11 | constructor(range) { 12 | super(); 13 | this.range = range; 14 | } 15 | 16 | updateHash(hash) { 17 | hash.update(this.range + ''); 18 | } 19 | } 20 | 21 | ClosureHarmonyMarkerDependency.Template = HarmonyNoopTemplate; 22 | 23 | module.exports = ClosureHarmonyMarkerDependency; 24 | -------------------------------------------------------------------------------- /src/dependencies/harmony-noop-template.js: -------------------------------------------------------------------------------- 1 | class HarmonyNoopTemplate { 2 | apply() {} 3 | 4 | harmonyInit(dep, source, runtime, dependencyTemplates) {} 5 | 6 | getHarmonyInitOrder(dep) { 7 | return dep.sourceOrder; 8 | } 9 | 10 | updateHash(hash) { 11 | hash.update('HarmonyNoopTemplate'); 12 | } 13 | } 14 | 15 | module.exports = HarmonyNoopTemplate; 16 | -------------------------------------------------------------------------------- /src/dependencies/harmony-parser-plugin.js: -------------------------------------------------------------------------------- 1 | const HarmonyExportDependency = require('./harmony-export-dependency'); 2 | const HarmonyExportImportDependency = require('./harmony-export-import-dependency'); 3 | const HarmonyImportDependency = require('./harmony-import-dependency'); 4 | const HarmonyMarkerDependency = require('./harmony-marker-dependency'); 5 | 6 | const PLUGIN_NAME = 'ClosureCompilerPlugin'; 7 | 8 | class HarmonyParserPlugin { 9 | apply(parser) { 10 | parser.hooks.exportSpecifier.tap( 11 | PLUGIN_NAME, 12 | (statement, id, name, idx) => { 13 | const dep = new HarmonyExportDependency( 14 | statement.declaration, 15 | statement.range, 16 | parser.state.module, 17 | id, 18 | name 19 | ); 20 | dep.loc = Object.create(statement.loc); 21 | dep.loc.index = idx; 22 | parser.state.current.addDependency(dep); 23 | return true; 24 | } 25 | ); 26 | 27 | parser.hooks.exportImport.tap(PLUGIN_NAME, (statement, source) => { 28 | parser.state.current.addDependency( 29 | new HarmonyMarkerDependency(statement.range) 30 | ); 31 | // This tap seems to fire twice, but we only want to add a single dependency. 32 | // Check for an existing dep before adding one. 33 | const existingDep = parser.state.current.dependencies.find((dep) => 34 | dep instanceof HarmonyExportImportDependency && dep.range === statement.source.range 35 | ); 36 | if (!existingDep) { 37 | const dep = new HarmonyExportImportDependency( 38 | source, 39 | parser.state.module, 40 | parser.state.lastHarmonyImportOrder, 41 | parser.state.harmonyParserScope, 42 | statement.source.range 43 | ); 44 | parser.state.current.addDependency(dep); 45 | } 46 | }); 47 | 48 | parser.hooks.import.tap('ClosureCompilerPlugin', (statement, source) => { 49 | parser.state.current.addDependency( 50 | new HarmonyMarkerDependency(statement.range) 51 | ); 52 | const dep = new HarmonyImportDependency( 53 | source, 54 | parser.state.module, 55 | parser.state.lastHarmonyImportOrder, 56 | parser.state.harmonyParserScope, 57 | null, 58 | null, 59 | statement.source.range 60 | ); 61 | parser.state.current.addDependency(dep); 62 | return true; 63 | }); 64 | } 65 | } 66 | 67 | module.exports = HarmonyParserPlugin; 68 | -------------------------------------------------------------------------------- /src/goog-require-parser-plugin.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const acorn = require('acorn'); 4 | const walk = require('acorn-walk'); 5 | const GoogDependency = require('./dependencies/goog-dependency'); 6 | const GoogBaseGlobalDependency = require('./dependencies/goog-base-global'); 7 | const GoogLoaderPrefixDependency = require('./dependencies/goog-loader-prefix-dependency'); 8 | const GoogLoaderSuffixDependency = require('./dependencies/goog-loader-suffix-dependency'); 9 | const GoogLoaderEs6PrefixDependency = require('./dependencies/goog-loader-es6-prefix-dependency'); 10 | const GoogLoaderEs6SuffixDependency = require('./dependencies/goog-loader-es6-suffix-dependency'); 11 | 12 | const PLUGIN = { name: 'ClosureLibraryPlugin' }; 13 | 14 | const isProductionLikeMode = (options) => 15 | options.mode === 'production' || !options.mode; 16 | 17 | class GoogRequireParserPlugin { 18 | constructor(options) { 19 | this.options = Object.assign({ deps: [], extraDeps: {} }, options); 20 | 21 | if (Array.isArray(this.options.deps)) { 22 | this.deps = this.options.deps.slice(); 23 | } else { 24 | this.deps = [this.options.deps]; 25 | } 26 | 27 | this.basePath = path.resolve(this.options.closureLibraryBase); 28 | const baseDir = path.dirname(this.basePath); 29 | const googPathsByNamespace = new Map(); 30 | this.googPathsByNamespace = googPathsByNamespace; 31 | const googDepsByPath = new Map(); 32 | 33 | Object.keys(this.options.extraDeps).forEach((namespace) => { 34 | this.googPathsByNamespace.set( 35 | namespace, 36 | this.options.extraDeps[namespace] 37 | ); 38 | }); 39 | 40 | this.deps.forEach((depFilePath) => { 41 | const depFileContents = fs.readFileSync(depFilePath, 'utf8'); 42 | const ast = acorn.parse(depFileContents, { 43 | ranges: true, 44 | locations: false, 45 | ecmaVersion: 'latest', 46 | sourceType: 'module', 47 | }); 48 | walk.simple(ast, { 49 | CallExpression(node) { 50 | if ( 51 | node.callee.type === 'MemberExpression' && 52 | node.callee.object.type === 'Identifier' && 53 | node.callee.object.name === 'goog' && 54 | node.callee.property.type === 'Identifier' && 55 | node.callee.property.name === 'addDependency' 56 | ) { 57 | const filePath = path.resolve(baseDir, node.arguments[0].value); 58 | node.arguments[1].elements.forEach((arg) => 59 | googPathsByNamespace.set(arg.value, filePath) 60 | ); 61 | if ( 62 | !googDepsByPath.has(filePath) && 63 | node.arguments[2] && 64 | node.arguments[2].elements.length > 0 65 | ) { 66 | googDepsByPath.set( 67 | filePath, 68 | node.arguments[2].elements.map((nodeVal) => nodeVal.value) 69 | ); 70 | } 71 | } 72 | }, 73 | }); 74 | }); 75 | } 76 | 77 | apply(parser) { 78 | const googRequireProvideCallback = (expr) => { 79 | if ( 80 | !parser.state.current.hasDependencies( 81 | (dep) => dep.request === this.basePath 82 | ) 83 | ) { 84 | this.addGoogDependency(parser, this.basePath, true); 85 | } 86 | 87 | // For goog.provide calls, add loader code and exit 88 | if (expr.callee.property.name === 'provide') { 89 | if ( 90 | !isProductionLikeMode(this.options) && 91 | !parser.state.current.dependencies.find( 92 | (dep) => dep instanceof GoogLoaderPrefixDependency 93 | ) 94 | ) { 95 | this.addLoaderDependency(parser, false); 96 | } 97 | return false; 98 | } 99 | 100 | try { 101 | const param = expr.arguments[0].value; 102 | const modulePath = this.googPathsByNamespace.get(param); 103 | if (!modulePath) { 104 | parser.state.compilation.warnings.push( 105 | new Error(`Unable to locate module for namespace: ${param}`) 106 | ); 107 | return false; 108 | } 109 | const isRequireType = expr.callee.property.name === 'requireType'; 110 | this.addGoogDependency(parser, modulePath, false, isRequireType); 111 | } catch (e) { 112 | parser.state.compilation.errors.push(e); 113 | } 114 | return false; 115 | }; 116 | parser.hooks.call 117 | .for('goog.require') 118 | .tap(PLUGIN, googRequireProvideCallback); 119 | parser.hooks.call 120 | .for('goog.requireType') 121 | .tap(PLUGIN, googRequireProvideCallback); 122 | parser.hooks.call 123 | .for('goog.provide') 124 | .tap(PLUGIN, googRequireProvideCallback); 125 | 126 | // When closure-compiler is not bundling the output, shim base.js of closure-library 127 | if (!isProductionLikeMode(this.options)) { 128 | parser.hooks.statement.tap(PLUGIN, (expr) => { 129 | if ( 130 | expr.type === 'VariableDeclaration' && 131 | expr.declarations.length === 1 && 132 | expr.declarations[0].id.name === 'goog' && 133 | parser.state.current.userRequest === this.basePath 134 | ) { 135 | parser.state.current.addVariable( 136 | 'goog', 137 | 'window.goog = window.goog || {}', 138 | [] 139 | ); 140 | parser.state.current.contextArgument = function() { 141 | return 'window'; 142 | }; 143 | parser.state.current.addDependency(new GoogBaseGlobalDependency()); 144 | } 145 | }); 146 | parser.hooks.call.for('goog.module').tap(PLUGIN, (expr) => { 147 | if (!isProductionLikeMode(this.options)) { 148 | if ( 149 | !parser.state.current.hasDependencies( 150 | (dep) => dep.request === this.basePath 151 | ) 152 | ) { 153 | this.addGoogDependency(parser, this.basePath); 154 | } 155 | 156 | const prefixDep = parser.state.current.dependencies.find( 157 | (dep) => dep instanceof GoogLoaderPrefixDependency 158 | ); 159 | const suffixDep = parser.state.current.dependencies.find( 160 | (dep) => dep instanceof GoogLoaderSuffixDependency 161 | ); 162 | if (prefixDep && suffixDep) { 163 | prefixDep.isGoogModule = true; 164 | suffixDep.isGoogModule = true; 165 | } else { 166 | this.addLoaderDependency(parser, true); 167 | } 168 | } 169 | }); 170 | const googModuleDeclareCallback = () => { 171 | if ( 172 | !parser.state.current.hasDependencies( 173 | (dep) => dep.request === this.basePath 174 | ) 175 | ) { 176 | this.addGoogDependency(parser, this.basePath); 177 | } 178 | 179 | parser.state.current.addVariable( 180 | '$jscomp', 181 | 'window.$jscomp = window.$jscomp || {}', 182 | [] 183 | ); 184 | 185 | this.addEs6LoaderDependency(parser); 186 | }; 187 | parser.hooks.call 188 | .for('goog.module.declareNamespace') 189 | .tap(PLUGIN, googModuleDeclareCallback); 190 | parser.hooks.call 191 | .for('goog.declareModuleId') 192 | .tap(PLUGIN, googModuleDeclareCallback); 193 | 194 | parser.hooks.import.tap(PLUGIN, () => { 195 | parser.state.current.addVariable( 196 | '$jscomp', 197 | 'window.$jscomp = window.$jscomp || {}', 198 | [] 199 | ); 200 | this.addEs6LoaderDependency(parser); 201 | }); 202 | parser.hooks.export.tap(PLUGIN, () => { 203 | parser.state.current.addVariable( 204 | '$jscomp', 205 | 'window.$jscomp = window.$jscomp || {}', 206 | [] 207 | ); 208 | this.addEs6LoaderDependency(parser); 209 | }); 210 | } 211 | } 212 | 213 | addGoogDependency(parser, request, addAsBaseJs, isRequireType) { 214 | // ES6 prefixing must happen after all requires have loaded otherwise 215 | // Closure library can think an ES6 module is calling goog.provide/module. 216 | const baseInsertPos = !isProductionLikeMode(this.options) ? -1 : null; 217 | parser.state.current.addDependency( 218 | new GoogDependency(request, baseInsertPos, addAsBaseJs, isRequireType) 219 | ); 220 | } 221 | 222 | addLoaderDependency(parser, isModule) { 223 | parser.state.current.addDependency( 224 | new GoogLoaderPrefixDependency(this.basePath, isModule, 0) 225 | ); 226 | const sourceLength = parser.state.current._source.source().length; 227 | parser.state.current.addDependency( 228 | new GoogLoaderSuffixDependency(this.basePath, isModule, sourceLength) 229 | ); 230 | } 231 | 232 | addEs6LoaderDependency(parser) { 233 | if ( 234 | parser.state.current.dependencies.some( 235 | (dep) => dep instanceof GoogLoaderEs6PrefixDependency 236 | ) 237 | ) { 238 | return; 239 | } 240 | 241 | // ES6 prefixing must happen after all requires have loaded otherwise 242 | // Closure library can think an ES6 module is calling goog.provide/module. 243 | const baseInsertPos = !isProductionLikeMode(this.options) ? 0 : null; 244 | const sourceLength = !isProductionLikeMode(this.options) 245 | ? parser.state.current._source.source().length 246 | : null; 247 | parser.state.current.addDependency( 248 | new GoogLoaderEs6PrefixDependency(baseInsertPos) 249 | ); 250 | parser.state.current.addDependency( 251 | new GoogLoaderEs6SuffixDependency(sourceLength) 252 | ); 253 | } 254 | } 255 | 256 | module.exports = GoogRequireParserPlugin; 257 | -------------------------------------------------------------------------------- /src/module-name.js: -------------------------------------------------------------------------------- 1 | let uniqueId = 1; 2 | 3 | function getWebpackModuleName(webpackModule) { 4 | if (webpackModule.userRequest) { 5 | return webpackModule.userRequest; 6 | } 7 | 8 | if (webpackModule.rootModule && webpackModule.rootModule.userRequest) { 9 | return webpackModule.rootModule.userRequest; 10 | } 11 | 12 | if (webpackModule.id) { 13 | return `__missing_path_${webpackModule.id}__`; 14 | } 15 | 16 | if (webpackModule.module) { 17 | return getWebpackModuleName(webpackModule.module); 18 | } 19 | 20 | if (webpackModule.__wpccName) { 21 | return webpackModule.__wpccName; 22 | } 23 | 24 | webpackModule.__wpccName = `__missing_path_no_id_${uniqueId}__`; 25 | uniqueId += 1; 26 | return webpackModule.__wpccName; 27 | } 28 | 29 | module.exports = getWebpackModuleName; 30 | -------------------------------------------------------------------------------- /src/runtime.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | /* global __wpcc, _WEBPACK_SOURCE_, __webpack_require__, _WEBPACK_TIMEOUT_ */ 3 | 4 | /** 5 | * @fileoverview webpack bootstrap for Closure-compiler with 6 | * late-loaded chunk support. 7 | * 8 | * This file is restricted to ES5 syntax so that it does not 9 | * require transpilation. While it does use Promises, they 10 | * can be polyfilled without transpilation. 11 | */ 12 | 13 | /** @const */ 14 | var _WEBPACK_GLOBAL_THIS_ = this; 15 | 16 | var _WEBPACK_MODULE_CACHE_; 17 | if (typeof _WEBPACK_MODULE_CACHE_ === 'undefined') { 18 | _WEBPACK_MODULE_CACHE_ = {}; 19 | } 20 | 21 | // install a JSONP callback for chunk loading 22 | (function() { 23 | /** @type {undefined|function(!Array, function(Object))} */ 24 | var parentJsonpFunction = window['webpackJsonp']; 25 | /** 26 | * @param {!Array} chunkIds 27 | * @param {function(Object)} cb 28 | */ 29 | window['webpackJsonp'] = function(chunkIds, cb) { 30 | var i; 31 | var resolves = []; 32 | 33 | // Register all the new chunks as loaded and then resolve the promise 34 | for (i = 0; i < chunkIds.length; i++) { 35 | if (_WEBPACK_MODULE_CACHE_[chunkIds[i]]) { 36 | resolves.push(_WEBPACK_MODULE_CACHE_[chunkIds[i]][0]); 37 | _WEBPACK_MODULE_CACHE_[chunkIds[i]] = 0; 38 | } 39 | } 40 | if (parentJsonpFunction) { 41 | parentJsonpFunction(chunkIds, function() {}); 42 | } 43 | var executionCallback = cb; 44 | while (resolves.length) { 45 | resolves.shift()(cb); 46 | executionCallback = undefined; // eslint-disable-line no-undefined 47 | } 48 | }; 49 | })(); 50 | 51 | /** 52 | * @param {number} chunkId 53 | * @param {!Promise} basePromise 54 | * @returns {!Promise} 55 | * @private 56 | */ 57 | function _webpack_load_chunk_(chunkId, basePromise) { 58 | var installedChunkData = _WEBPACK_MODULE_CACHE_[chunkId]; 59 | if (installedChunkData === 0) { 60 | return basePromise; 61 | } 62 | 63 | // a Promise means "currently loading". 64 | if (installedChunkData) { 65 | return installedChunkData[2]; 66 | } 67 | 68 | // setup Promise in chunk cache 69 | var promise = new Promise(function(resolve, reject) { 70 | installedChunkData = _WEBPACK_MODULE_CACHE_[chunkId] = [resolve, reject]; 71 | }).then(function(cb) { 72 | return basePromise.then(function() { 73 | if (cb) { 74 | cb.call(_WEBPACK_GLOBAL_THIS_, __wpcc); 75 | } 76 | }); 77 | }); 78 | installedChunkData[2] = promise; 79 | 80 | // start chunk loading 81 | var head = document.getElementsByTagName('head')[0]; // eslint-disable-line prefer-destructuring 82 | var script = document.createElement('script'); 83 | script.type = 'text/javascript'; 84 | script.charset = 'utf-8'; 85 | script.async = true; 86 | script.timeout = _WEBPACK_TIMEOUT_; 87 | 88 | if (__wpcc.nc && __wpcc.nc.length > 0) { 89 | script.setAttribute('nonce', __wpcc.nc); 90 | } 91 | script.src = (__webpack_require__.p || '') + _WEBPACK_SOURCE_[chunkId]; 92 | var timeout = setTimeout(onScriptComplete, _WEBPACK_TIMEOUT_); 93 | script.onerror = script.onload = onScriptComplete; 94 | function onScriptComplete() { 95 | // avoid mem leaks in IE. 96 | script.onerror = script.onload = null; 97 | clearTimeout(timeout); 98 | var chunk = _WEBPACK_MODULE_CACHE_[chunkId]; 99 | if (chunk !== 0) { 100 | if (chunk) { 101 | chunk[1](new Error('Loading chunk ' + chunkId + ' failed.')); 102 | } 103 | _WEBPACK_MODULE_CACHE_[chunkId] = undefined; // eslint-disable-line no-undefined 104 | } 105 | } 106 | head.appendChild(script); 107 | return promise; 108 | } 109 | 110 | /** 111 | * The chunk loading function for additional chunks 112 | * 113 | * @type {function(...number):!Promise} 114 | */ 115 | __webpack_require__.e = function() { 116 | var chunkIds = Array.prototype.slice.call(arguments); 117 | 118 | var promise = Promise.resolve(); 119 | for (var i = 0; i < chunkIds.length; i++) { 120 | promise = _webpack_load_chunk_(chunkIds[i], promise); 121 | } 122 | return promise; 123 | }; 124 | 125 | /** 126 | * on error function for async loading 127 | * @param {Error} err 128 | */ 129 | __webpack_require__.oe = function(err) { 130 | console.error(err); // eslint-disable-line no-console 131 | throw err; 132 | }; 133 | 134 | /** 135 | * Register new child chunk paths 136 | * @param {string} childChunkId 137 | * @param {string} childChunkPath 138 | */ 139 | __webpack_require__.rs = function(childChunkId, childChunkPath) { 140 | _WEBPACK_SOURCE_[childChunkId] = childChunkPath; 141 | }; 142 | -------------------------------------------------------------------------------- /src/safe-path.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Given a path, replace all characters not recognized by closure-compiler with a '$' 4 | */ 5 | module.exports = function toSafePath(originalPath) { 6 | return originalPath.replace(/[:,?|]+/g, '$'); 7 | }; 8 | -------------------------------------------------------------------------------- /src/standard-externs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview externs for closure-compiler webpack 3 | * @externs 4 | */ 5 | 6 | /** 7 | * @param {!Array} chunkIds, 8 | * @param {!Object} moreModules 9 | * @param {?=} executeModules 10 | */ 11 | function webpackJsonp(chunkIds, moreModules, executeModules) {} 12 | -------------------------------------------------------------------------------- /test/agressive-mode.test.js: -------------------------------------------------------------------------------- 1 | describe('Closure: Agressive Mode', () => { 2 | test('should result in the meaning of life', () => { 3 | expect(22 + 20).toBe(42); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixtures/amd/b.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | define([], function () { 3 | return "b export"; 4 | }); 5 | -------------------------------------------------------------------------------- /test/fixtures/amd/c.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | define([], function () { 3 | return { 4 | exportA: "exportA", 5 | exportB: "exportB" 6 | }; 7 | }); 8 | -------------------------------------------------------------------------------- /test/fixtures/amd/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, no-console, func-names, prefer-arrow-callback, import/no-amd */ 2 | define(['./b.js', './c.js'], function (b, c) { 3 | console.log(b, c.exportA, c.exportB); 4 | }); 5 | -------------------------------------------------------------------------------- /test/fixtures/cjs-common-chunk/b.js: -------------------------------------------------------------------------------- 1 | module.exports = 'b export'; 2 | -------------------------------------------------------------------------------- /test/fixtures/cjs-common-chunk/c.js: -------------------------------------------------------------------------------- 1 | exports.exportA = 'exportA'; 2 | exports.exportB = 'exportB'; 3 | -------------------------------------------------------------------------------- /test/fixtures/cjs-common-chunk/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, func-names, prefer-arrow-callback */ 2 | require.ensure(['./b.js', './c.js'], function (require) { 3 | const b = require('./b.js'); 4 | const c = require('./c.js'); 5 | 6 | console.log(b, c.exportA, c.exportB); 7 | }); 8 | -------------------------------------------------------------------------------- /test/fixtures/cjs/b.js: -------------------------------------------------------------------------------- 1 | module.exports = 'b export'; 2 | -------------------------------------------------------------------------------- /test/fixtures/cjs/c.js: -------------------------------------------------------------------------------- 1 | exports.exportA = 'exportA'; 2 | exports.exportB = 'exportB'; 3 | -------------------------------------------------------------------------------- /test/fixtures/cjs/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const b = require('./b.js'); 3 | const c = require('./c.js'); 4 | 5 | console.log(b, c.exportA, c.exportB); 6 | -------------------------------------------------------------------------------- /test/fixtures/esm-common-chunk/b.js: -------------------------------------------------------------------------------- 1 | export const unusedExport = 'unused export'; 2 | export const namedExport = 'named export'; 3 | export default 'b export'; 4 | -------------------------------------------------------------------------------- /test/fixtures/esm-common-chunk/c.js: -------------------------------------------------------------------------------- 1 | export const exportA = 'exportA'; 2 | export const exportB = 'exportB'; 3 | -------------------------------------------------------------------------------- /test/fixtures/esm-common-chunk/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | Promise.all([ 3 | System.import('./b'), 4 | System.import('./c'), 5 | ]).then((b, c) => { 6 | console.log(b, c); 7 | }); 8 | -------------------------------------------------------------------------------- /test/fixtures/esm/b.js: -------------------------------------------------------------------------------- 1 | export const unusedExport = 'unused export'; 2 | export const namedExport = 'named export'; 3 | export default 'b export'; 4 | -------------------------------------------------------------------------------- /test/fixtures/esm/c.js: -------------------------------------------------------------------------------- 1 | export const exportA = 'exportA'; 2 | export const exportB = 'exportB'; 3 | -------------------------------------------------------------------------------- /test/fixtures/esm/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, no-unused-vars */ 2 | import b, { namedExport, unusedExport } from './b'; 3 | import { exportA, exportB } from './c'; 4 | 5 | console.log(b, namedExport, exportA, exportB); 6 | -------------------------------------------------------------------------------- /test/helpers/compiler.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import del from 'del'; 3 | import webpack from 'webpack'; 4 | import MemoryFS from 'memory-fs'; 5 | 6 | const [majorVersion] = require('webpack/package.json').version.split('.'); 7 | 8 | const modules = (config) => { 9 | return { 10 | rules: config.rules, 11 | }; 12 | }; 13 | 14 | const plugins = (config) => 15 | [ 16 | new webpack.optimize.CommonsChunkPlugin({ 17 | names: ['runtime'], 18 | minChunks: Infinity, 19 | }), 20 | ].concat(config.plugins || []); 21 | 22 | const output = (config) => { 23 | return { 24 | path: path.resolve( 25 | __dirname, 26 | `../outputs/${config.output ? config.output : ''}` 27 | ), 28 | filename: '[name].js', 29 | chunkFilename: '[name].chunk.js', 30 | }; 31 | }; 32 | 33 | export default function(fixture, config, options) { 34 | config = { 35 | devtool: config.devtool || 'sourcemap', 36 | context: path.resolve(__dirname, '..', 'fixtures'), 37 | entry: `./${fixture}`, 38 | output: output(config), 39 | module: modules(config), 40 | plugins: plugins(config), 41 | }; 42 | 43 | if (Number(majorVersion) >= 4) { 44 | config.mode = 'development'; 45 | } 46 | 47 | options = Object.assign({ output: false }, options); 48 | 49 | if (options.output) del.sync(config.output.path); 50 | 51 | const compiler = webpack(config); 52 | 53 | if (!options.output) compiler.outputFileSystem = new MemoryFS(); 54 | 55 | return new Promise((resolve, reject) => 56 | compiler.run((err, stats) => { 57 | if (err) reject(err); 58 | 59 | resolve(stats); 60 | }) 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /test/simple-mode.test.js: -------------------------------------------------------------------------------- 1 | describe('Closure: Simple Mode', () => { 2 | test('should result in the meaning of life', () => { 3 | expect(22 + 20).toBe(42); 4 | }); 5 | }); 6 | --------------------------------------------------------------------------------