diff --git a/examples/module-code-splitting/README.md b/examples/module-code-splitting/README.md new file mode 100644 index 000000000..6e6120c6e --- /dev/null +++ b/examples/module-code-splitting/README.md @@ -0,0 +1,274 @@ +# example.js + +```javascript +import { resetCounter, print } from "./methods"; + +setTimeout(async () => { + const counter = await import("./counter"); + print(counter.value); + counter.increment(); + counter.increment(); + counter.increment(); + print(counter.value); + await resetCounter(); + print(counter.value); +}, 100); +``` + +# methods.js + +```javascript +export const resetCounter = async () => { + (await import("./counter")).reset(); +}; + +export const print = value => console.log(value); +``` + +# counter.js + +```javascript +export let value = 0; +export function increment() { + value++; +} +export function decrement() { + value--; +} +export function reset() { + value = 0; +} +``` + +# dist/output.js + +```javascript +/******/ "use strict"; +/******/ var __webpack_modules__ = ({}); +``` + +
/* webpack runtime code */ + +``` js +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = __webpack_modules__; +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/ensure chunk */ +/******/ (() => { +/******/ __webpack_require__.f = {}; +/******/ // This file contains only the entry chunk. +/******/ // The chunk loading function for additional chunks +/******/ __webpack_require__.e = (chunkId) => { +/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => { +/******/ __webpack_require__.f[key](chunkId, promises); +/******/ return promises; +/******/ }, [])); +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/get javascript chunk filename */ +/******/ (() => { +/******/ // This function allow to reference async chunks +/******/ __webpack_require__.u = (chunkId) => { +/******/ // return url for filenames based on template +/******/ return "" + chunkId + ".output.js"; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/import chunk loading */ +/******/ (() => { +/******/ // no baseURI +/******/ +/******/ // object to store loaded and loading chunks +/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched +/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded +/******/ var installedChunks = { +/******/ 0: 0 +/******/ }; +/******/ +/******/ __webpack_require__.f.j = (chunkId, promises) => { +/******/ // JSONP chunk loading for javascript +/******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined; +/******/ if(installedChunkData !== 0) { // 0 means "already installed". +/******/ +/******/ // a Promise means "currently loading". +/******/ if(installedChunkData) { +/******/ promises.push(installedChunkData[1]); +/******/ } else { +/******/ if(true) { // all chunks have JS +/******/ // setup Promise in chunk cache +/******/ var promise = import("./" + __webpack_require__.u(chunkId)).then((data) => { +/******/ var {ids, modules, runtime} = data; +/******/ // add "modules" to the modules object, +/******/ // then flag all "ids" as loaded and fire callback +/******/ var moduleId, chunkId, i = 0; +/******/ for(moduleId in modules) { +/******/ if(__webpack_require__.o(modules, moduleId)) { +/******/ __webpack_require__.m[moduleId] = modules[moduleId]; +/******/ } +/******/ } +/******/ if(runtime) runtime(__webpack_require__); +/******/ for(;i < ids.length; i++) { +/******/ chunkId = ids[i]; +/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { +/******/ installedChunks[chunkId][0](); +/******/ } +/******/ installedChunks[ids[i]] = 0; +/******/ } +/******/ +/******/ }, (e) => { +/******/ if(installedChunks[chunkId] !== 0) installedChunks[chunkId] = undefined; +/******/ throw e; +/******/ }); +/******/ var promise = Promise.race([promise, new Promise((resolve) => (installedChunkData = installedChunks[chunkId] = [resolve]))]) +/******/ promises.push(installedChunkData[1] = promise); +/******/ } else installedChunks[chunkId] = 0; +/******/ } +/******/ } +/******/ }; +/******/ +/******/ // no on chunks loaded +/******/ })(); +/******/ +/************************************************************************/ +``` + +
+ +``` js +var __webpack_exports__ = {}; +/*!********************************!*\ + !*** ./example.js + 1 modules ***! + \********************************/ +/*! namespace exports */ +/*! runtime requirements: __webpack_require__.e, __webpack_require__, __webpack_require__.* */ + +;// CONCATENATED MODULE: ./methods.js +const resetCounter = async () => { + (await __webpack_require__.e(/*! import() */ 1).then(__webpack_require__.bind(__webpack_require__, /*! ./counter */ 1))).reset(); +}; + +const print = value => console.log(value); + +;// CONCATENATED MODULE: ./example.js + + +setTimeout(async () => { + const counter = await __webpack_require__.e(/*! import() */ 1).then(__webpack_require__.bind(__webpack_require__, /*! ./counter */ 1)); + print(counter.value); + counter.increment(); + counter.increment(); + counter.increment(); + print(counter.value); + await resetCounter(); + print(counter.value); +}, 100); +``` + +# dist/output.js (production) + +```javascript +var e,o={},t={};function r(e){var n=t[e];if(void 0!==n)return n.exports;var i=t[e]={exports:{}};return o[e](i,i.exports,r),i.exports}r.m=o,r.d=(e,o)=>{for(var t in o)r.o(o,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((o,t)=>(r.f[t](e,o),o)),[])),r.u=e=>e+".output.js",r.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},e={179:0},r.f.j=(o,t)=>{var n=r.o(e,o)?e[o]:void 0;if(0!==n)if(n)t.push(n[1]);else{var i=import("./"+r.u(o)).then((o=>{var t,n,{ids:i,modules:a,runtime:s}=o,u=0;for(t in a)r.o(a,t)&&(r.m[t]=a[t]);for(s&&s(r);u{throw 0!==e[o]&&(e[o]=void 0),t}));i=Promise.race([i,new Promise((t=>n=e[o]=[t]))]),t.push(n[1]=i)}};const n=e=>console.log(e);setTimeout((async()=>{const e=await r.e(946).then(r.bind(r,946));n(e.value),e.increment(),e.increment(),e.increment(),n(e.value),await(async()=>{(await r.e(946).then(r.bind(r,946))).reset()})(),n(e.value)}),100); +``` + +# Info + +## Unoptimized + +``` +asset output.js 6.35 KiB [emitted] [javascript module] (name: main) +asset 1.output.js 1.36 KiB [emitted] [javascript module] +chunk (runtime: main) output.js (main) 420 bytes (javascript) 2.89 KiB (runtime) [entry] [rendered] + > ./example.js main + runtime modules 2.89 KiB 6 modules + ./example.js + 1 modules 420 bytes [built] [code generated] + [no exports] + [no exports used] + entry ./example.js main + used as library export +chunk (runtime: main) 1.output.js 146 bytes [rendered] + > ./counter ./methods.js 2:8-27 + > ./counter ./example.js 4:23-42 + ./counter.js 146 bytes [built] [code generated] + [exports: decrement, increment, reset, value] + import() ./counter ./example.js + 1 modules ./example.js 4:23-42 + import() ./counter ./example.js + 1 modules ./methods.js 2:8-27 +webpack 5.40.0 compiled successfully +``` + +## Production mode + +``` +asset output.js 1.15 KiB [emitted] [javascript module] [minimized] (name: main) +asset 946.output.js 213 bytes [emitted] [javascript module] [minimized] +chunk (runtime: main) output.js (main) 420 bytes (javascript) 2.89 KiB (runtime) [entry] [rendered] + > ./example.js main + runtime modules 2.89 KiB 6 modules + ./example.js + 1 modules 420 bytes [built] [code generated] + [no exports] + [no exports used] + entry ./example.js main + used as library export +chunk (runtime: main) 946.output.js 146 bytes [rendered] + > ./counter ./methods.js 2:8-27 + > ./counter ./example.js 4:23-42 + ./counter.js 146 bytes [built] [code generated] + [exports: decrement, increment, reset, value] + import() ./counter ./example.js + 1 modules ./example.js 4:23-42 + import() ./counter ./example.js + 1 modules ./methods.js 2:8-27 +webpack 5.40.0 compiled successfully +``` diff --git a/examples/module-code-splitting/build.js b/examples/module-code-splitting/build.js new file mode 100644 index 000000000..41c29c9d1 --- /dev/null +++ b/examples/module-code-splitting/build.js @@ -0,0 +1 @@ +require("../build-common"); \ No newline at end of file diff --git a/examples/module-code-splitting/counter.js b/examples/module-code-splitting/counter.js new file mode 100644 index 000000000..7009896e2 --- /dev/null +++ b/examples/module-code-splitting/counter.js @@ -0,0 +1,10 @@ +export let value = 0; +export function increment() { + value++; +} +export function decrement() { + value--; +} +export function reset() { + value = 0; +} diff --git a/examples/module-code-splitting/example.js b/examples/module-code-splitting/example.js new file mode 100644 index 000000000..d9dc73232 --- /dev/null +++ b/examples/module-code-splitting/example.js @@ -0,0 +1,12 @@ +import { resetCounter, print } from "./methods"; + +setTimeout(async () => { + const counter = await import("./counter"); + print(counter.value); + counter.increment(); + counter.increment(); + counter.increment(); + print(counter.value); + await resetCounter(); + print(counter.value); +}, 100); diff --git a/examples/module-code-splitting/index.html b/examples/module-code-splitting/index.html new file mode 100644 index 000000000..5ce1e0f88 --- /dev/null +++ b/examples/module-code-splitting/index.html @@ -0,0 +1,10 @@ + + + + + Worker example + + + + + diff --git a/examples/module-code-splitting/methods.js b/examples/module-code-splitting/methods.js new file mode 100644 index 000000000..81140850b --- /dev/null +++ b/examples/module-code-splitting/methods.js @@ -0,0 +1,5 @@ +export const resetCounter = async () => { + (await import("./counter")).reset(); +}; + +export const print = value => console.log(value); diff --git a/examples/module-code-splitting/template.md b/examples/module-code-splitting/template.md new file mode 100644 index 000000000..98d06e62e --- /dev/null +++ b/examples/module-code-splitting/template.md @@ -0,0 +1,43 @@ +# example.js + +```javascript +_{{example.js}}_ +``` + +# methods.js + +```javascript +_{{methods.js}}_ +``` + +# counter.js + +```javascript +_{{counter.js}}_ +``` + +# dist/output.js + +```javascript +_{{dist/output.js}}_ +``` + +# dist/output.js (production) + +```javascript +_{{production:dist/output.js}}_ +``` + +# Info + +## Unoptimized + +``` +_{{stdout}}_ +``` + +## Production mode + +``` +_{{production:stdout}}_ +``` diff --git a/examples/module-code-splitting/webpack.config.js b/examples/module-code-splitting/webpack.config.js new file mode 100644 index 000000000..f5141e5ff --- /dev/null +++ b/examples/module-code-splitting/webpack.config.js @@ -0,0 +1,16 @@ +module.exports = { + output: { + module: true, + library: { + type: "module" + } + }, + optimization: { + usedExports: true, + concatenateModules: true + }, + target: "browserslist: last 2 chrome versions", + experiments: { + outputModule: true + } +}; diff --git a/examples/module-library/README.md b/examples/module-library/README.md new file mode 100644 index 000000000..cf605d69d --- /dev/null +++ b/examples/module-library/README.md @@ -0,0 +1,170 @@ +# example.js + +```javascript +export * from "./counter"; +export * from "./methods"; +``` + +# methods.js + +```javascript +export { reset as resetCounter } from "./counter"; + +export const print = value => console.log(value); +``` + +# counter.js + +```javascript +export let value = 0; +export function increment() { + value++; +} +export function decrement() { + value--; +} +export function reset() { + value = 0; +} +``` + +# dist/output.js + +```javascript +/******/ "use strict"; +/******/ // The require scope +/******/ var __webpack_require__ = {}; +/******/ +``` + +
/* webpack runtime code */ + +``` js +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +``` + +
+ +``` js +var __webpack_exports__ = {}; +/*!********************************!*\ + !*** ./example.js + 2 modules ***! + \********************************/ +/*! namespace exports */ +/*! export decrement [provided] [used in main] [missing usage info prevents renaming] -> ./counter.js .decrement */ +/*! export increment [provided] [used in main] [missing usage info prevents renaming] -> ./counter.js .increment */ +/*! export print [provided] [used in main] [missing usage info prevents renaming] -> ./methods.js .print */ +/*! export reset [provided] [used in main] [missing usage info prevents renaming] -> ./counter.js .reset */ +/*! export resetCounter [provided] [used in main] [missing usage info prevents renaming] -> ./counter.js .reset */ +/*! export value [provided] [used in main] [missing usage info prevents renaming] -> ./counter.js .value */ +/*! other exports [not provided] [no usage info] */ +/*! runtime requirements: __webpack_require__.r, __webpack_exports__, __webpack_require__.d, __webpack_require__.* */ +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + "decrement": () => (/* reexport */ decrement), + "increment": () => (/* reexport */ increment), + "print": () => (/* reexport */ print), + "reset": () => (/* reexport */ counter_reset), + "resetCounter": () => (/* reexport */ counter_reset), + "value": () => (/* reexport */ value) +}); + +;// CONCATENATED MODULE: ./counter.js +let value = 0; +function increment() { + value++; +} +function decrement() { + value--; +} +function counter_reset() { + value = 0; +} + +;// CONCATENATED MODULE: ./methods.js + + +const print = value => console.log(value); + +;// CONCATENATED MODULE: ./example.js + + + +var __webpack_exports__decrement = __webpack_exports__.decrement; +var __webpack_exports__increment = __webpack_exports__.increment; +var __webpack_exports__print = __webpack_exports__.print; +var __webpack_exports__reset = __webpack_exports__.reset; +var __webpack_exports__resetCounter = __webpack_exports__.resetCounter; +var __webpack_exports__value = __webpack_exports__.value; +export { __webpack_exports__decrement as decrement, __webpack_exports__increment as increment, __webpack_exports__print as print, __webpack_exports__reset as reset, __webpack_exports__resetCounter as resetCounter, __webpack_exports__value as value }; +``` + +# dist/output.js (production) + +```javascript +var e={d:(n,t)=>{for(var o in t)e.o(t,o)&&!e.o(n,o)&&Object.defineProperty(n,o,{enumerable:!0,get:t[o]})},o:(e,n)=>Object.prototype.hasOwnProperty.call(e,n)},n={};e.d(n,{Mj:()=>r,nP:()=>o,S0:()=>c,mc:()=>a,Uh:()=>a,S3:()=>t});let t=0;function o(){t++}function r(){t--}function a(){t=0}const c=e=>console.log(e);var s=n.Mj,i=n.nP,l=n.S0,p=n.mc,u=n.Uh,f=n.S3;export{s as decrement,i as increment,l as print,p as reset,u as resetCounter,f as value}; +``` + +# Info + +## Unoptimized + +``` +asset output.js 3.63 KiB [emitted] [javascript module] (name: main) +chunk (runtime: main) output.js (main) 302 bytes (javascript) 670 bytes (runtime) [entry] [rendered] + > ./example.js main + runtime modules 670 bytes 3 modules + ./example.js + 2 modules 302 bytes [built] [code generated] + [exports: decrement, increment, print, reset, resetCounter, value] + [used exports unknown] + entry ./example.js main + used as library export +webpack 5.40.0 compiled successfully +``` + +## Production mode + +``` +asset output.js 446 bytes [emitted] [javascript module] [minimized] (name: main) +chunk (runtime: main) output.js (main) 302 bytes (javascript) 396 bytes (runtime) [entry] [rendered] + > ./example.js main + runtime modules 396 bytes 2 modules + ./example.js + 2 modules 302 bytes [built] [code generated] + [exports: decrement, increment, print, reset, resetCounter, value] + [all exports used] + entry ./example.js main + used as library export +webpack 5.40.0 compiled successfully +``` diff --git a/examples/module-library/build.js b/examples/module-library/build.js new file mode 100644 index 000000000..41c29c9d1 --- /dev/null +++ b/examples/module-library/build.js @@ -0,0 +1 @@ +require("../build-common"); \ No newline at end of file diff --git a/examples/module-library/counter.js b/examples/module-library/counter.js new file mode 100644 index 000000000..7009896e2 --- /dev/null +++ b/examples/module-library/counter.js @@ -0,0 +1,10 @@ +export let value = 0; +export function increment() { + value++; +} +export function decrement() { + value--; +} +export function reset() { + value = 0; +} diff --git a/examples/module-library/example.js b/examples/module-library/example.js new file mode 100644 index 000000000..ef58a21ff --- /dev/null +++ b/examples/module-library/example.js @@ -0,0 +1,2 @@ +export * from "./counter"; +export * from "./methods"; diff --git a/examples/module-library/methods.js b/examples/module-library/methods.js new file mode 100644 index 000000000..4be8f10f7 --- /dev/null +++ b/examples/module-library/methods.js @@ -0,0 +1,3 @@ +export { reset as resetCounter } from "./counter"; + +export const print = value => console.log(value); diff --git a/examples/module-library/template.md b/examples/module-library/template.md new file mode 100644 index 000000000..98d06e62e --- /dev/null +++ b/examples/module-library/template.md @@ -0,0 +1,43 @@ +# example.js + +```javascript +_{{example.js}}_ +``` + +# methods.js + +```javascript +_{{methods.js}}_ +``` + +# counter.js + +```javascript +_{{counter.js}}_ +``` + +# dist/output.js + +```javascript +_{{dist/output.js}}_ +``` + +# dist/output.js (production) + +```javascript +_{{production:dist/output.js}}_ +``` + +# Info + +## Unoptimized + +``` +_{{stdout}}_ +``` + +## Production mode + +``` +_{{production:stdout}}_ +``` diff --git a/examples/module-library/webpack.config.js b/examples/module-library/webpack.config.js new file mode 100644 index 000000000..d7f45aa69 --- /dev/null +++ b/examples/module-library/webpack.config.js @@ -0,0 +1,14 @@ +module.exports = { + output: { + module: true, + library: { + type: "module" + } + }, + optimization: { + concatenateModules: true + }, + experiments: { + outputModule: true + } +}; diff --git a/lib/RuntimeTemplate.js b/lib/RuntimeTemplate.js index 4199e9d05..d2da8af15 100644 --- a/lib/RuntimeTemplate.js +++ b/lib/RuntimeTemplate.js @@ -133,6 +133,14 @@ class RuntimeTemplate { ); } + destructureObject(items, value) { + return this.supportsDestructuring() + ? `var {${items.join(", ")}} = ${value};` + : Template.asString( + items.map(item => `var ${item} = ${value}${propertyAccess([item])};`) + ); + } + iife(args, body) { return `(${this.basicFunction(args, body)})()`; } diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index e3822f05b..044300140 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -139,10 +139,11 @@ class WebpackOptionsApply extends OptionsApply { new CommonJsChunkFormatPlugin().apply(compiler); break; } - case "module": - throw new Error( - "EcmaScript Module Chunk Format is not implemented yet" - ); + case "module": { + const ModuleChunkFormatPlugin = require("./esm/ModuleChunkFormatPlugin"); + new ModuleChunkFormatPlugin().apply(compiler); + break; + } default: throw new Error( "Unsupported chunk format '" + options.output.chunkFormat + "'." diff --git a/lib/config/defaults.js b/lib/config/defaults.js index 7a75b8395..8f348324d 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -182,6 +182,11 @@ const applyWebpackOptionsDefaults = options => { applyOutputDefaults(options.output, { context: options.context, targetProperties, + isAffectedByBrowserslist: + target === undefined || + (typeof target === "string" && target.startsWith("browserslist")) || + (Array.isArray(target) && + target.some(target => target.startsWith("browserslist"))), outputModule: options.experiments.outputModule, development, entry: options.entry, @@ -543,6 +548,7 @@ const applyModuleDefaults = ( * @param {Object} options options * @param {string} options.context context * @param {TargetProperties | false} options.targetProperties target properties + * @param {boolean} options.isAffectedByBrowserslist is affected by browserslist * @param {boolean} options.outputModule is outputModule experiment enabled * @param {boolean} options.development is development mode * @param {Entry} options.entry entry option @@ -551,7 +557,15 @@ const applyModuleDefaults = ( */ const applyOutputDefaults = ( output, - { context, targetProperties: tp, outputModule, development, entry, module } + { + context, + targetProperties: tp, + isAffectedByBrowserslist, + outputModule, + development, + entry, + module + } ) => { /** * @param {Library=} library the library option @@ -591,8 +605,8 @@ const applyOutputDefaults = ( } }); - D(output, "filename", "[name].js"); F(output, "module", () => !!outputModule); + D(output, "filename", output.module ? "[name].mjs" : "[name].js"); F(output, "iife", () => !output.module); D(output, "importFunctionName", "import"); D(output, "importMetaName", "import.meta"); @@ -608,7 +622,7 @@ const applyOutputDefaults = ( // Otherwise prefix "[id]." in front of the basename to make it changing return filename.replace(/(^|\/)([^/]*(?:\?|$))/, "$1[id].$2"); } - return "[id].js"; + return output.module ? "[id].mjs" : "[id].js"; }); D(output, "assetModuleFilename", "[hash][ext][query]"); D(output, "webassemblyModuleFilename", "[hash].module.wasm"); @@ -633,13 +647,34 @@ const applyOutputDefaults = ( }); F(output, "chunkFormat", () => { if (tp) { - if (tp.document) return "array-push"; - if (tp.require) return "commonjs"; - if (tp.nodeBuiltins) return "commonjs"; - if (tp.importScripts) return "array-push"; - if (tp.dynamicImport && output.module) return "module"; + const helpMessage = isAffectedByBrowserslist + ? "Make sure that your 'browserslist' includes only platforms that support these features or select an appropriate 'target' to allow selecting a chunk format by default. Alternatively specify the 'output.chunkFormat' directly." + : "Select an appropriate 'target' to allow selecting one by default, or specify the 'output.chunkFormat' directly."; + if (output.module) { + if (tp.dynamicImport) return "module"; + if (tp.document) return "array-push"; + throw new Error( + "For the selected environment is no default ESM chunk format available:\n" + + "ESM exports can be chosen when 'import()' is available.\n" + + "JSONP Array push can be chosen when 'document' is available.\n" + + helpMessage + ); + } else { + if (tp.document) return "array-push"; + if (tp.require) return "commonjs"; + if (tp.nodeBuiltins) return "commonjs"; + if (tp.importScripts) return "array-push"; + throw new Error( + "For the selected environment is no default script chunk format available:\n" + + "JSONP Array push can be chosen when 'document' or 'importScripts' is available.\n" + + "CommonJs exports can be chosen when 'require' or node builtins are available.\n" + + helpMessage + ); + } } - return false; + throw new Error( + "Chunk format can't be selected by default when no target is specified" + ); }); F(output, "chunkLoading", () => { if (tp) { @@ -709,7 +744,11 @@ const applyOutputDefaults = ( F(output, "path", () => path.join(process.cwd(), "dist")); F(output, "pathinfo", () => development); D(output, "sourceMapFilename", "[file].map[query]"); - D(output, "hotUpdateChunkFilename", "[id].[fullhash].hot-update.js"); + D( + output, + "hotUpdateChunkFilename", + `[id].[fullhash].hot-update.${output.module ? "mjs" : "js"}` + ); D(output, "hotUpdateMainFilename", "[runtime].[fullhash].hot-update.json"); D(output, "crossOriginLoading", false); F(output, "scriptType", () => (output.module ? "module" : false)); diff --git a/lib/esm/ModuleChunkFormatPlugin.js b/lib/esm/ModuleChunkFormatPlugin.js new file mode 100644 index 000000000..0602ec6be --- /dev/null +++ b/lib/esm/ModuleChunkFormatPlugin.js @@ -0,0 +1,97 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const { ConcatSource } = require("webpack-sources"); +const { RuntimeGlobals } = require(".."); +const HotUpdateChunk = require("../HotUpdateChunk"); +const Template = require("../Template"); +const { + getCompilationHooks +} = require("../javascript/JavascriptModulesPlugin"); + +/** @typedef {import("../Compiler")} Compiler */ + +class ModuleChunkFormatPlugin { + /** + * Apply the plugin + * @param {Compiler} compiler the compiler instance + * @returns {void} + */ + apply(compiler) { + compiler.hooks.thisCompilation.tap( + "ModuleChunkFormatPlugin", + compilation => { + compilation.hooks.additionalChunkRuntimeRequirements.tap( + "ModuleChunkFormatPlugin", + (chunk, set) => { + if (chunk.hasRuntime()) return; + if (compilation.chunkGraph.getNumberOfEntryModules(chunk) > 0) { + set.add(RuntimeGlobals.onChunksLoaded); + set.add(RuntimeGlobals.require); + } + } + ); + const hooks = getCompilationHooks(compilation); + hooks.renderChunk.tap( + "ModuleChunkFormatPlugin", + (modules, renderContext) => { + const { chunk, chunkGraph } = renderContext; + const hotUpdateChunk = + chunk instanceof HotUpdateChunk ? chunk : null; + const source = new ConcatSource(); + if (hotUpdateChunk) { + throw new Error( + "HMR is not implemented for module chunk format yet" + ); + } else { + source.add(`export const id = ${JSON.stringify(chunk.id)};\n`); + source.add(`export const ids = ${JSON.stringify(chunk.ids)};\n`); + source.add(`export const modules = `); + source.add(modules); + source.add(`;\n`); + const runtimeModules = + chunkGraph.getChunkRuntimeModulesInOrder(chunk); + if (runtimeModules.length > 0) { + source.add("export const runtime =\n"); + source.add( + Template.renderChunkRuntimeModules( + runtimeModules, + renderContext + ) + ); + } + const entries = Array.from( + chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk) + ); + if (entries.length > 0) { + throw new Error( + "Entry modules in chunk is not implemented for module chunk format yet" + ); + } + } + return source; + } + ); + hooks.chunkHash.tap( + "ModuleChunkFormatPlugin", + (chunk, hash, { chunkGraph, runtimeTemplate }) => { + if (chunk.hasRuntime()) return; + hash.update("ModuleChunkFormatPlugin"); + hash.update("1"); + // TODO + // const entries = Array.from( + // chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk) + // ); + // updateHashForEntryStartup(hash, chunkGraph, entries, chunk); + } + ); + } + ); + } +} + +module.exports = ModuleChunkFormatPlugin; diff --git a/lib/esm/ModuleChunkLoadingPlugin.js b/lib/esm/ModuleChunkLoadingPlugin.js new file mode 100644 index 000000000..35fdd726d --- /dev/null +++ b/lib/esm/ModuleChunkLoadingPlugin.js @@ -0,0 +1,63 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const RuntimeGlobals = require("../RuntimeGlobals"); +const ModuleChunkLoadingRuntimeModule = require("./ModuleChunkLoadingRuntimeModule"); + +/** @typedef {import("../Compiler")} Compiler */ + +class ModuleChunkLoadingPlugin { + /** + * Apply the plugin + * @param {Compiler} compiler the compiler instance + * @returns {void} + */ + apply(compiler) { + compiler.hooks.thisCompilation.tap( + "ModuleChunkLoadingPlugin", + compilation => { + const globalChunkLoading = compilation.outputOptions.chunkLoading; + const isEnabledForChunk = chunk => { + const options = chunk.getEntryOptions(); + const chunkLoading = + (options && options.chunkLoading) || globalChunkLoading; + return chunkLoading === "import"; + }; + const onceForChunkSet = new WeakSet(); + const handler = (chunk, set) => { + if (onceForChunkSet.has(chunk)) return; + onceForChunkSet.add(chunk); + if (!isEnabledForChunk(chunk)) return; + set.add(RuntimeGlobals.moduleFactoriesAddOnly); + set.add(RuntimeGlobals.hasOwnProperty); + compilation.addRuntimeModule( + chunk, + new ModuleChunkLoadingRuntimeModule(set) + ); + }; + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.ensureChunkHandlers) + .tap("ModuleChunkLoadingPlugin", handler); + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.baseURI) + .tap("ModuleChunkLoadingPlugin", handler); + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.onChunksLoaded) + .tap("ModuleChunkLoadingPlugin", handler); + + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.ensureChunkHandlers) + .tap("ModuleChunkLoadingPlugin", (chunk, set) => { + if (!isEnabledForChunk(chunk)) return; + set.add(RuntimeGlobals.getChunkScriptFilename); + }); + } + ); + } +} + +module.exports = ModuleChunkLoadingPlugin; diff --git a/lib/esm/ModuleChunkLoadingRuntimeModule.js b/lib/esm/ModuleChunkLoadingRuntimeModule.js new file mode 100644 index 000000000..ebeadc5d9 --- /dev/null +++ b/lib/esm/ModuleChunkLoadingRuntimeModule.js @@ -0,0 +1,208 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php +*/ + +"use strict"; + +const { SyncWaterfallHook } = require("tapable"); +const Compilation = require("../Compilation"); +const RuntimeGlobals = require("../RuntimeGlobals"); +const RuntimeModule = require("../RuntimeModule"); +const Template = require("../Template"); +const { + getChunkFilenameTemplate, + chunkHasJs +} = require("../javascript/JavascriptModulesPlugin"); +const { getInitialChunkIds } = require("../javascript/StartupHelpers"); +const compileBooleanMatcher = require("../util/compileBooleanMatcher"); +const { getUndoPath } = require("../util/identifier"); + +/** @typedef {import("../Chunk")} Chunk */ + +/** + * @typedef {Object} JsonpCompilationPluginHooks + * @property {SyncWaterfallHook<[string, Chunk]>} linkPreload + * @property {SyncWaterfallHook<[string, Chunk]>} linkPrefetch + */ + +/** @type {WeakMap} */ +const compilationHooksMap = new WeakMap(); + +class ModuleChunkLoadingRuntimeModule extends RuntimeModule { + /** + * @param {Compilation} compilation the compilation + * @returns {JsonpCompilationPluginHooks} hooks + */ + static getCompilationHooks(compilation) { + if (!(compilation instanceof Compilation)) { + throw new TypeError( + "The 'compilation' argument must be an instance of Compilation" + ); + } + let hooks = compilationHooksMap.get(compilation); + if (hooks === undefined) { + hooks = { + linkPreload: new SyncWaterfallHook(["source", "chunk"]), + linkPrefetch: new SyncWaterfallHook(["source", "chunk"]) + }; + compilationHooksMap.set(compilation, hooks); + } + return hooks; + } + + constructor(runtimeRequirements) { + super("import chunk loading", RuntimeModule.STAGE_ATTACH); + this._runtimeRequirements = runtimeRequirements; + } + + /** + * @returns {string} runtime code + */ + generate() { + const { compilation, chunk } = this; + const { + runtimeTemplate, + chunkGraph, + outputOptions: { importFunctionName, importMetaName } + } = compilation; + const fn = RuntimeGlobals.ensureChunkHandlers; + const withBaseURI = this._runtimeRequirements.has(RuntimeGlobals.baseURI); + const withLoading = this._runtimeRequirements.has( + RuntimeGlobals.ensureChunkHandlers + ); + const withOnChunkLoad = this._runtimeRequirements.has( + RuntimeGlobals.onChunksLoaded + ); + const conditionMap = chunkGraph.getChunkConditionMap(chunk, chunkHasJs); + const hasJsMatcher = compileBooleanMatcher(conditionMap); + const initialChunkIds = getInitialChunkIds(chunk, chunkGraph); + + const outputName = this.compilation.getPath( + getChunkFilenameTemplate(chunk, this.compilation.outputOptions), + { + chunk, + contentHashType: "javascript" + } + ); + const rootOutputDir = getUndoPath( + outputName, + this.compilation.outputOptions.path, + true + ); + + return Template.asString([ + withBaseURI + ? Template.asString([ + `${RuntimeGlobals.baseURI} = new URL(${JSON.stringify( + rootOutputDir + )}, ${importMetaName}.url);` + ]) + : "// no baseURI", + "", + "// object to store loaded and loading chunks", + "// undefined = chunk not loaded, null = chunk preloaded/prefetched", + "// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded", + "var installedChunks = {", + Template.indent( + Array.from(initialChunkIds, id => `${JSON.stringify(id)}: 0`).join( + ",\n" + ) + ), + "};", + "", + withLoading + ? Template.asString([ + `${fn}.j = ${runtimeTemplate.basicFunction( + "chunkId, promises", + hasJsMatcher !== false + ? Template.indent([ + "// JSONP chunk loading for javascript", + `var installedChunkData = ${RuntimeGlobals.hasOwnProperty}(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;`, + 'if(installedChunkData !== 0) { // 0 means "already installed".', + Template.indent([ + "", + '// a Promise means "currently loading".', + "if(installedChunkData) {", + Template.indent([ + "promises.push(installedChunkData[1]);" + ]), + "} else {", + Template.indent([ + hasJsMatcher === true + ? "if(true) { // all chunks have JS" + : `if(${hasJsMatcher("chunkId")}) {`, + Template.indent([ + "// setup Promise in chunk cache", + `var promise = ${importFunctionName}(${JSON.stringify( + rootOutputDir + )} + ${ + RuntimeGlobals.getChunkScriptFilename + }(chunkId)).then(${runtimeTemplate.basicFunction( + "data", + [ + runtimeTemplate.destructureObject( + ["ids", "modules", "runtime"], + "data" + ), + '// add "modules" to the modules object,', + '// then flag all "ids" as loaded and fire callback', + "var moduleId, chunkId, i = 0;", + "for(moduleId in modules) {", + Template.indent([ + `if(${RuntimeGlobals.hasOwnProperty}(modules, moduleId)) {`, + Template.indent( + `${RuntimeGlobals.moduleFactories}[moduleId] = modules[moduleId];` + ), + "}" + ]), + "}", + "if(runtime) runtime(__webpack_require__);", + "for(;i < ids.length; i++) {", + Template.indent([ + "chunkId = ids[i];", + `if(${RuntimeGlobals.hasOwnProperty}(installedChunks, chunkId) && installedChunks[chunkId]) {`, + Template.indent( + "installedChunks[chunkId][0]();" + ), + "}", + "installedChunks[ids[i]] = 0;" + ]), + "}", + withOnChunkLoad + ? `${RuntimeGlobals.onChunksLoaded}();` + : "" + ] + )}, ${runtimeTemplate.basicFunction("e", [ + "if(installedChunks[chunkId] !== 0) installedChunks[chunkId] = undefined;", + "throw e;" + ])});`, + `var promise = Promise.race([promise, new Promise(${runtimeTemplate.expressionFunction( + `installedChunkData = installedChunks[chunkId] = [resolve]`, + "resolve" + )})])`, + `promises.push(installedChunkData[1] = promise);` + ]), + "} else installedChunks[chunkId] = 0;" + ]), + "}" + ]), + "}" + ]) + : Template.indent(["installedChunks[chunkId] = 0;"]) + )};` + ]) + : "// no chunk on demand loading", + "", + withOnChunkLoad + ? `${ + RuntimeGlobals.onChunksLoaded + }.j = ${runtimeTemplate.returningFunction( + "installedChunks[chunkId] === 0", + "chunkId" + )};` + : "// no on chunks loaded" + ]); + } +} + +module.exports = ModuleChunkLoadingRuntimeModule; diff --git a/lib/javascript/EnableChunkLoadingPlugin.js b/lib/javascript/EnableChunkLoadingPlugin.js index b1e53df06..2d938d2da 100644 --- a/lib/javascript/EnableChunkLoadingPlugin.js +++ b/lib/javascript/EnableChunkLoadingPlugin.js @@ -96,9 +96,11 @@ class EnableChunkLoadingPlugin { }).apply(compiler); break; } - case "import": - // TODO implement import chunk loading - throw new Error("Chunk Loading via import() is not implemented yet"); + case "import": { + const ModuleChunkLoadingPlugin = require("../esm/ModuleChunkLoadingPlugin"); + new ModuleChunkLoadingPlugin().apply(compiler); + break; + } case "universal": // TODO implement universal chunk loading throw new Error("Universal Chunk Loading is not implemented yet"); diff --git a/package.json b/package.json index 5de67d6d2..c26c94051 100644 --- a/package.json +++ b/package.json @@ -131,11 +131,11 @@ ], "scripts": { "setup": "node ./setup/setup.js", - "test": "node --max-old-space-size=4096 --trace-deprecation node_modules/jest-cli/bin/jest", + "test": "node --max-old-space-size=4096 --experimental-vm-modules --trace-deprecation node_modules/jest-cli/bin/jest", "test:update-snapshots": "yarn jest -u", - "test:integration": "node --max-old-space-size=4096 --trace-deprecation node_modules/jest-cli/bin/jest --testMatch \"/test/*.test.js\"", - "test:basic": "node --max-old-space-size=4096 --trace-deprecation node_modules/jest-cli/bin/jest --testMatch \"/te{st/TestCasesNormal,st/StatsTestCases,st/ConfigTestCases}.test.js\"", - "test:unit": "node --max-old-space-size=4096 --trace-deprecation node_modules/jest-cli/bin/jest --testMatch \"/test/*.unittest.js\"", + "test:integration": "node --max-old-space-size=4096 --experimental-vm-modules --trace-deprecation node_modules/jest-cli/bin/jest --testMatch \"/test/*.test.js\"", + "test:basic": "node --max-old-space-size=4096 --experimental-vm-modules --trace-deprecation node_modules/jest-cli/bin/jest --testMatch \"/te{st/TestCasesNormal,st/StatsTestCases,st/ConfigTestCases}.test.js\"", + "test:unit": "node --max-old-space-size=4096 --experimental-vm-modules --trace-deprecation node_modules/jest-cli/bin/jest --testMatch \"/test/*.unittest.js\"", "travis:integration": "yarn cover:integration --ci $JEST", "travis:basic": "yarn cover:basic --ci $JEST", "travis:lintunit": "yarn lint && yarn cover:unit --ci $JEST", @@ -162,13 +162,13 @@ "pretty-lint": "yarn pretty-lint-base --check", "yarn-lint": "yarn-deduplicate --fail --list -s highest yarn.lock", "yarn-lint-fix": "yarn-deduplicate -s highest yarn.lock", - "benchmark": "node --max-old-space-size=4096 --trace-deprecation node_modules/jest-cli/bin/jest --testMatch \"/test/*.benchmark.js\" --runInBand", + "benchmark": "node --max-old-space-size=4096 --experimental-vm-modules --trace-deprecation node_modules/jest-cli/bin/jest --testMatch \"/test/*.benchmark.js\" --runInBand", "cover": "yarn cover:all && yarn cover:report", "cover:clean": "rimraf .nyc_output coverage", - "cover:all": "node --max-old-space-size=4096 node_modules/jest-cli/bin/jest --coverage", - "cover:basic": "node --max-old-space-size=4096 node_modules/jest-cli/bin/jest --testMatch \"/te{st/TestCasesNormal,st/StatsTestCases,st/ConfigTestCases}.test.js\" --coverage", - "cover:integration": "node --max-old-space-size=4096 node_modules/jest-cli/bin/jest --testMatch \"/test/*.test.js\" --coverage", - "cover:unit": "node --max-old-space-size=4096 node_modules/jest-cli/bin/jest --testMatch \"/test/*.unittest.js\" --coverage", + "cover:all": "node --max-old-space-size=4096 --experimental-vm-modules node_modules/jest-cli/bin/jest --coverage", + "cover:basic": "node --max-old-space-size=4096 --experimental-vm-modules node_modules/jest-cli/bin/jest --testMatch \"/te{st/TestCasesNormal,st/StatsTestCases,st/ConfigTestCases}.test.js\" --coverage", + "cover:integration": "node --max-old-space-size=4096 --experimental-vm-modules node_modules/jest-cli/bin/jest --testMatch \"/test/*.test.js\" --coverage", + "cover:unit": "node --max-old-space-size=4096 --experimental-vm-modules node_modules/jest-cli/bin/jest --testMatch \"/test/*.unittest.js\" --coverage", "cover:types": "node node_modules/tooling/type-coverage", "cover:merge": "nyc merge .nyc_output coverage/coverage-nyc.json && rimraf .nyc_output", "cover:report": "nyc report -t coverage" diff --git a/test/ConfigTestCases.template.js b/test/ConfigTestCases.template.js index 79d7e7931..324f1b284 100644 --- a/test/ConfigTestCases.template.js +++ b/test/ConfigTestCases.template.js @@ -84,7 +84,12 @@ const describeCases = config => { if (typeof options.output.pathinfo === "undefined") options.output.pathinfo = true; if (!options.output.filename) - options.output.filename = "bundle" + idx + ".js"; + options.output.filename = + "bundle" + + idx + + (options.experiments && options.experiments.outputModule + ? ".mjs" + : ".js"); if (config.cache) { options.cache = { cacheDirectory, @@ -295,7 +300,12 @@ const describeCases = config => { const requireCache = Object.create(null); // eslint-disable-next-line no-loop-func - const _require = (currentDirectory, options, module) => { + const _require = ( + currentDirectory, + options, + module, + esmModule + ) => { if (Array.isArray(module) || /^\.\.?\//.test(module)) { let content; let p; @@ -339,35 +349,13 @@ const describeCases = config => { }; requireCache[p] = m; let runInNewContext = false; - let oldCurrentScript = document.currentScript; - document.currentScript = new CurrentScript(subPath); const moduleScope = { - require: _require.bind( - null, - path.dirname(p), - options - ), - importScripts: url => { - expect(url).toMatch( - /^https:\/\/test\.cases\/path\// - ); - _require( - outputDirectory, - options, - `.${url.slice("https://test.cases/path".length)}` - ); - }, - module: m, - exports: m.exports, - __dirname: path.dirname(p), - __filename: p, it: _it, beforeEach: _beforeEach, afterEach: _afterEach, expect, jest, - _globalAssign: { expect }, __STATS__: jsonStats, nsObj: m => { Object.defineProperty(m, Symbol.toStringTag, { @@ -376,6 +364,36 @@ const describeCases = config => { return m; } }; + const isModule = + p.endsWith(".mjs") && + options.experiments && + options.experiments.outputModule; + if (!isModule) { + Object.assign(moduleScope, { + require: _require.bind( + null, + path.dirname(p), + options + ), + importScripts: url => { + expect(url).toMatch( + /^https:\/\/test\.cases\/path\// + ); + _require( + outputDirectory, + options, + `.${url.slice( + "https://test.cases/path".length + )}` + ); + }, + module: m, + exports: m.exports, + __dirname: path.dirname(p), + __filename: p, + _globalAssign: { expect } + }); + } if ( options.target === "web" || options.target === "webworker" @@ -392,21 +410,88 @@ const describeCases = config => { if (testConfig.moduleScope) { testConfig.moduleScope(moduleScope); } - const args = Object.keys(moduleScope); - const argValues = args.map(arg => moduleScope[arg]); - if (!runInNewContext) - content = `Object.assign(global, _globalAssign); ${content}`; - const code = `(function(${args.join( - ", " - )}) {${content}\n})`; - const fn = runInNewContext - ? vm.runInNewContext(code, globalContext, p) - : vm.runInThisContext(code, p); - fn.call(m.exports, ...argValues); - - //restore state - document.currentScript = oldCurrentScript; + if (isModule) { + if (!vm.SourceTextModule) + throw new Error( + "Running this test requires '--experimental-vm-modules'.\nRun with 'node --experimental-vm-modules node_modules/jest-cli/bin/jest'." + ); + const esm = new vm.SourceTextModule(content, { + identifier: p, + context: vm.createContext(moduleScope, { + name: `context for ${p}` + }), + importModuleDynamically: async ( + specifier, + module + ) => { + const result = await _require( + path.dirname(p), + options, + specifier, + "evaluated" + ); + if ( + result instanceof + (vm.Module || + /* node.js 10 */ vm.SourceTextModule) + ) { + return result; + } + if (!vm.SyntheticModule) return result; + return new vm.SyntheticModule( + [ + ...new Set([ + "default", + ...Object.keys(result) + ]) + ], + function () { + for (const key in result) { + this.setExport(key, result[key]); + } + this.setExport("default", result); + } + ); + } + }); + if (esmModule === "unlinked") return esm; + return (async () => { + await esm.link( + async (specifier, referencingModule) => { + return _require( + path.dirname(referencingModule.identfier), + options, + specifier, + "unlinked" + ); + } + ); + // node.js 10 needs instantiate + if (esm.instantiate) esm.instantiate(); + await esm.evaluate(); + if (esmModule === "evaluated") return esm; + const ns = esm.namespace; + return ns.default && ns.default instanceof Promise + ? ns.default + : ns; + })(); + } else { + if (!runInNewContext) + content = `Object.assign(global, _globalAssign); ${content}`; + const args = Object.keys(moduleScope); + const argValues = args.map(arg => moduleScope[arg]); + const code = `(function(${args.join( + ", " + )}) {${content}\n})`; + let oldCurrentScript = document.currentScript; + document.currentScript = new CurrentScript(subPath); + const fn = runInNewContext + ? vm.runInNewContext(code, globalContext, p) + : vm.runInThisContext(code, p); + fn.call(m.exports, ...argValues); + document.currentScript = oldCurrentScript; + } return m.exports; } else if ( testConfig.modules && diff --git a/test/Defaults.unittest.js b/test/Defaults.unittest.js index 1403be5aa..a0644aa66 100644 --- a/test/Defaults.unittest.js +++ b/test/Defaults.unittest.js @@ -861,6 +861,15 @@ describe("Defaults", () => { - "externalsType": "var", + "externalsType": "module", @@ ... @@ + - "chunkFilename": "[name].js", + + "chunkFilename": "[name].mjs", + @@ ... @@ + - "filename": "[name].js", + + "filename": "[name].mjs", + @@ ... @@ + - "hotUpdateChunkFilename": "[id].[fullhash].hot-update.js", + + "hotUpdateChunkFilename": "[id].[fullhash].hot-update.mjs", + @@ ... @@ - "iife": true, + "iife": false, @@ ... @@ diff --git a/test/TestCases.template.js b/test/TestCases.template.js index c54a57d93..cbe9e1b41 100644 --- a/test/TestCases.template.js +++ b/test/TestCases.template.js @@ -3,6 +3,7 @@ const path = require("path"); const fs = require("graceful-fs"); const vm = require("vm"); +const { pathToFileURL } = require("url"); const rimraf = require("rimraf"); const webpack = require(".."); const TerserPlugin = require("terser-webpack-plugin"); @@ -122,7 +123,7 @@ const describeCases = config => { output: { pathinfo: true, path: outputDirectory, - filename: "bundle.js" + filename: config.module ? "bundle.mjs" : "bundle.js" }, resolve: { modules: ["web_modules", "node_modules"], @@ -186,7 +187,8 @@ const describeCases = config => { }), experiments: { asyncWebAssembly: true, - topLevelAwait: true + topLevelAwait: true, + ...(config.module ? { outputModule: true } : {}) } }; beforeAll(done => { @@ -309,36 +311,73 @@ const describeCases = config => { function _require(module) { if (module.substr(0, 2) === "./") { const p = path.join(outputDirectory, module); - const fn = vm.runInThisContext( - "(function(require, module, exports, __dirname, __filename, it, expect) {" + - "global.expect = expect;" + - 'function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; }' + - fs.readFileSync(p, "utf-8") + - "\n})", - p - ); - const m = { - exports: {}, - webpackTestSuiteModule: true - }; - fn.call( - m.exports, - _require, - m, - m.exports, - outputDirectory, - p, - _it, - expect - ); - return m.exports; + if (p.endsWith(".mjs")) { + const module = new vm.SourceTextModule( + `import { it, expect } from "TEST_ENV"; +function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; } +${fs.readFileSync(p, "utf-8")}`, + { + identifier: p, + lineOffset: 1, + initializeImportMeta: (meta, module) => { + meta.url = pathToFileURL(p); + }, + importModuleDynamically: (specifier, module) => { + return _require(specifier); + } + } + ); + return module + .link((specifier, module) => { + if (specifier === "TEST_ENV") { + const m = new vm.SyntheticModule( + ["it", "expect"], + function () { + this.setExport("it", _it); + this.setExport("expect", expect); + } + ); + return m; + } + }) + .then(() => module.evaluate()) + .then(() => module.namespace); + } else { + const fn = vm.runInThisContext( + "(function(require, module, exports, __dirname, __filename, it, expect) {" + + "global.expect = expect;" + + 'function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; }' + + fs.readFileSync(p, "utf-8") + + "\n})", + p + ); + const m = { + exports: {}, + webpackTestSuiteModule: true + }; + fn.call( + m.exports, + _require, + m, + m.exports, + outputDirectory, + p, + _it, + expect + ); + return m.exports; + } } else return require(module); } _require.webpackTestSuiteRequire = true; - _require("./bundle.js"); - if (getNumberOfTests() === 0) - return done(new Error("No tests exported by test case")); - done(); + const promise = _require("./" + options.output.filename); + if (promise && promise.then) promise.then(finish); + else finish(); + function finish() { + if (getNumberOfTests() === 0) + return done(new Error("No tests exported by test case")); + done(); + } }, 10000 ); diff --git a/test/TestCasesModule.test.js b/test/TestCasesModule.test.js new file mode 100644 index 000000000..16d6b288e --- /dev/null +++ b/test/TestCasesModule.test.js @@ -0,0 +1,13 @@ +const { describeCases } = require("./TestCases.template"); +const vm = require("vm"); + +describe("TestCases", () => { + if (!vm.SourceTextModule) { + it("module can't run without --experimental-vm-modules"); + return; + } + describeCases({ + name: "module", + module: true + }); +}); diff --git a/test/__snapshots__/StatsTestCases.test.js.snap b/test/__snapshots__/StatsTestCases.test.js.snap index 8335cbae2..09034942f 100644 --- a/test/__snapshots__/StatsTestCases.test.js.snap +++ b/test/__snapshots__/StatsTestCases.test.js.snap @@ -1673,8 +1673,8 @@ webpack x.x.x compiled successfully in X ms" `; exports[`StatsTestCases should print correct stats for output-module 1`] = ` -"asset main.js 9.95 KiB [emitted] [javascript module] (name: main) -asset 52.js 417 bytes [emitted] [javascript module] +"asset main.mjs 9.95 KiB [emitted] [javascript module] (name: main) +asset 52.mjs 417 bytes [emitted] [javascript module] runtime modules 6.01 KiB 8 modules orphan modules 38 bytes [orphan] 1 module cacheable modules 263 bytes diff --git a/test/configCases/library/0-create-library/webpack.config.js b/test/configCases/library/0-create-library/webpack.config.js index 61ada5eb1..f77134bc1 100644 --- a/test/configCases/library/0-create-library/webpack.config.js +++ b/test/configCases/library/0-create-library/webpack.config.js @@ -2,6 +2,21 @@ const path = require("path"); const webpack = require("../../../../"); /** @type {function(any, any): import("../../../../").Configuration[]} */ module.exports = (env, { testPath }) => [ + { + output: { + filename: "esm.js", + libraryTarget: "module" + }, + target: "node14", + resolve: { + alias: { + external: "./non-external" + } + }, + experiments: { + outputModule: true + } + }, { output: { filename: "commonjs.js", diff --git a/test/configCases/library/1-use-library/webpack.config.js b/test/configCases/library/1-use-library/webpack.config.js index 0ca795138..4fc8cdb19 100644 --- a/test/configCases/library/1-use-library/webpack.config.js +++ b/test/configCases/library/1-use-library/webpack.config.js @@ -2,6 +2,18 @@ var webpack = require("../../../../"); var path = require("path"); /** @type {function(any, any): import("../../../../").Configuration[]} */ module.exports = (env, { testPath }) => [ + { + resolve: { + alias: { + library: path.resolve(testPath, "../0-create-library/esm.js") + } + }, + plugins: [ + new webpack.DefinePlugin({ + NAME: JSON.stringify("esm") + }) + ] + }, { resolve: { alias: { diff --git a/test/configCases/output-module/simple/chunk.js b/test/configCases/output-module/simple/chunk.js new file mode 100644 index 000000000..7a4e8a723 --- /dev/null +++ b/test/configCases/output-module/simple/chunk.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/configCases/output-module/simple/index.js b/test/configCases/output-module/simple/index.js new file mode 100644 index 000000000..bdf68397f --- /dev/null +++ b/test/configCases/output-module/simple/index.js @@ -0,0 +1,12 @@ +it("should execute as module", () => { + expect( + (function () { + return !this; + })() + ).toBe(true); +}); + +it("should be able to load a chunk", async () => { + const module = await import("./chunk"); + expect(module.default).toBe(42); +}); diff --git a/test/configCases/output-module/simple/webpack.config.js b/test/configCases/output-module/simple/webpack.config.js new file mode 100644 index 000000000..b8e5da8c1 --- /dev/null +++ b/test/configCases/output-module/simple/webpack.config.js @@ -0,0 +1,7 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + experiments: { + outputModule: true + }, + target: "node14" +}; diff --git a/types.d.ts b/types.d.ts index 3ce91a4ed..d7b4bc201 100644 --- a/types.d.ts +++ b/types.d.ts @@ -9671,6 +9671,7 @@ declare abstract class RuntimeTemplate { expressionFunction(expression?: any, args?: string): string; emptyFunction(): "x => {}" | "function() {}"; destructureArray(items?: any, value?: any): string; + destructureObject(items?: any, value?: any): string; iife(args?: any, body?: any): string; forEach(variable?: any, array?: any, body?: any): string;