diff --git a/lib/Compilation.js b/lib/Compilation.js index 836d71437e7..c5673da8c44 100644 --- a/lib/Compilation.js +++ b/lib/Compilation.js @@ -220,6 +220,7 @@ class Compilation extends Tapable { this.requestShortener ); this.moduleTemplates = { + html: new ModuleTemplate(this.outputOptions), javascript: new ModuleTemplate(this.runtimeTemplate), webassembly: new ModuleTemplate(this.runtimeTemplate) }; @@ -1673,6 +1674,7 @@ class Compilation extends Tapable { if (!aEntry && bEntry) return -1; return 0; }); + for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; const chunkHash = createHash(hashFunction); diff --git a/lib/Template.js b/lib/Template.js index d045e3241c2..eb9c02b7a89 100644 --- a/lib/Template.js +++ b/lib/Template.js @@ -202,4 +202,45 @@ module.exports = class Template { } return source; } + + static renderHTMLChunk(chunk, filterFn, moduleTemplate, dependencyTemplates) { + // if (!prefix) prefix = ""; + + const result = new ConcatSource(); + + const modules = chunk.getModules().filter(filterFn); + const removedModules = chunk.removedModules; + + const sources = modules.map((module) => { + return { + id: module.id, + source: moduleTemplate.render(module, dependencyTemplates, { + chunk + }) + }; + }); + + if(removedModules && removedModules.length > 0) { + for(const id of removedModules) { + sources.push({ + id: id, + source: "" + }); + } + } + sources + .sort() + .reduceRight((result, module, idx) => { + if(idx !== 0) result.add("\n"); + + result.add(`\n\n`); + result.add(module.source); + + return result; + }, result); + + result.add("\n" /* + prefix */); + + return result; + } }; diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index 4c89a071e9f..c5aebe6169b 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -6,9 +6,14 @@ const OptionsApply = require("./OptionsApply"); +// Module Types const JavascriptModulesPlugin = require("./JavascriptModulesPlugin"); const JsonModulesPlugin = require("./JsonModulesPlugin"); const WebAssemblyModulesPlugin = require("./WebAssemblyModulesPlugin"); +const HTMLModulesPlugin = require("./html/HTMLModulesPlugin"); + +// Module Type Dependencies +const HTMLDependencyPlugin = require("./html/HTMLDependency"); const LoaderTargetPlugin = require("./LoaderTargetPlugin"); const FunctionModulePlugin = require("./FunctionModulePlugin"); @@ -270,6 +275,9 @@ class WebpackOptionsApply extends OptionsApply { new JavascriptModulesPlugin().apply(compiler); new JsonModulesPlugin().apply(compiler); new WebAssemblyModulesPlugin().apply(compiler); + // HTML + new HTMLModulesPlugin().apply(compiler); + new HTMLDependencyPlugin().apply(compiler); new EntryOptionPlugin().apply(compiler); compiler.hooks.entryOption.call(options.context, options.entry); diff --git a/lib/WebpackOptionsDefaulter.js b/lib/WebpackOptionsDefaulter.js index 3c7b1f83af7..a4932d744d6 100644 --- a/lib/WebpackOptionsDefaulter.js +++ b/lib/WebpackOptionsDefaulter.js @@ -67,6 +67,10 @@ class WebpackOptionsDefaulter extends OptionsDefaulter { { test: /\.wasm$/i, type: "webassembly/experimental" + }, + { + test: /\.html$/, + type: "html/experimental" } ]); @@ -94,6 +98,8 @@ class WebpackOptionsDefaulter extends OptionsDefaulter { // Elsewise prefix "[id]." in front of the basename to make it changing return filename.replace(/(^|\/)([^/]*(?:\?|$))/, "$1[id].$2"); }); + // HTML + this.set("output.HTMLModuleFilename", "[modulehash].module.html"); this.set("output.webassemblyModuleFilename", "[modulehash].module.wasm"); this.set("output.library", ""); this.set("output.hotUpdateFunction", "make", options => { @@ -281,7 +287,7 @@ class WebpackOptionsDefaulter extends OptionsDefaulter { this.set("resolve", "call", value => Object.assign({}, value)); this.set("resolve.unsafeCache", true); this.set("resolve.modules", ["node_modules"]); - this.set("resolve.extensions", [".wasm", ".mjs", ".js", ".json"]); + this.set("resolve.extensions", [".html", ".wasm", ".mjs", ".js", ".json"]); this.set("resolve.mainFiles", ["index"]); this.set("resolve.aliasFields", "make", options => { if (options.target === "web" || options.target === "webworker") diff --git a/lib/html/HTMLDependency.js b/lib/html/HTMLDependency.js new file mode 100644 index 00000000000..2c8834dd90c --- /dev/null +++ b/lib/html/HTMLDependency.js @@ -0,0 +1,30 @@ +const { + HTMLURLDependency, + HTMLImportDependency +} = require("./dependencies"); + +class HTMLDependencyPlugin { + constructor(options) { + this.plugin = "HTMLDependencyPlugin"; + this.options = options; + } + + apply(compiler) { + const { plugin } = this; + const { compilation } = compiler.hooks; + + compilation.tap(plugin, (compilation, { normalModuleFactory }) => { + const { dependencyFactories, dependencyTemplates } = compilation; + + dependencyFactories.set(HTMLURLDependency, normalModuleFactory); + dependencyFactories.set(HTMLImportDependency, normalModuleFactory); + + dependencyTemplates.set( + HTMLImportDependency, + new HTMLImportDependency.Template() + ); + }); + } +} + +module.exports = HTMLDependencyPlugin; diff --git a/lib/html/HTMLGenerator.js b/lib/html/HTMLGenerator.js new file mode 100644 index 00000000000..ba65781b646 --- /dev/null +++ b/lib/html/HTMLGenerator.js @@ -0,0 +1,7 @@ +class HTMLGenerator { + generate(module) { + return module.originalSource(); + } +} + +module.exports = HTMLGenerator; diff --git a/lib/html/HTMLModulesPlugin.js b/lib/html/HTMLModulesPlugin.js new file mode 100644 index 00000000000..e81ce581065 --- /dev/null +++ b/lib/html/HTMLModulesPlugin.js @@ -0,0 +1,99 @@ +const HTMLParser = require("./HTMLParser"); +const HTMLGenerator = require("./HTMLGenerator"); + +const Template = require("webpack/lib/Template"); +const { + ConcatSource +} = require("webpack-sources"); + +class HTMLModulesPlugin { + constructor() { + this.plugin = { + name: "HTMLModulesPlugin" + }; + } + + apply(compiler) { + const { plugin } = this; + const { compilation } = compiler.hooks; + + compilation.tap(plugin, (compilation, { normalModuleFactory }) => { + const { createParser, createGenerator } = normalModuleFactory.hooks; + + createParser.for("html/experimental").tap(plugin, () => { + return new HTMLParser(); + }); + + createGenerator.for("html/experimental").tap(plugin, () => { + return new HTMLGenerator(); + }); + + const { chunkTemplate } = compilation; + + chunkTemplate.hooks.renderManifest.tap(plugin, (result, options) => { + const chunk = options.chunk; + const output = options.outputOptions; + + const { moduleTemplates, dependencyTemplates } = options; + + for(const module of chunk.modulesIterable) { + if(module.type && module.type.startsWith("html")) { + const filenameTemplate = output.HTMLModuleFilename; + + result.push({ + render: () => this.renderHTML( + chunkTemplate, + chunk, + moduleTemplates.html, + dependencyTemplates + ), + filenameTemplate, + pathOptions: { + module + }, + identifier: `HTMLModule ${module.id}`, + hash: module.hash + }); + } + } + + return result; + }); + }); + } + + renderHTMLModules(module, moduleTemplate, dependencyTemplates) { + return moduleTemplate.render(module, dependencyTemplates, {}); + } + + renderHTML(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) { + const { modules, /* render */ } = chunkTemplate.hooks; + + const sources = Template.renderHTMLChunk( + chunk, + module => module.type.startsWith("html"), + moduleTemplate, + dependencyTemplates + ); + + const core = modules.call( + sources, + chunk, + moduleTemplate, + dependencyTemplates + ); + + // let source = render.call( + // core, + // chunk, + // moduleTemplate, + // dependencyTemplates + // ); + + chunk.rendered = true; + + return new ConcatSource(core); + } +} + +module.exports = HTMLModulesPlugin; diff --git a/lib/html/HTMLParser.js b/lib/html/HTMLParser.js new file mode 100644 index 00000000000..47f5dee5608 --- /dev/null +++ b/lib/html/HTMLParser.js @@ -0,0 +1,88 @@ +const posthtml = require("posthtml"); +const { + imports, + urls +} = require("@posthtml/esm"); + +const { + HTMLURLDependency, + HTMLImportDependency, + HTMLExportDependency +} = require("./dependencies"); + +const { + OriginalSource +} = require("webpack-sources"); + +const isDependency = (msg) => { + return msg.type.includes("import") || msg.type.includes("export"); +}; + +class HTMLParser { + constructor(options = {}) { + this.options = options; + } + + parse(source, state, cb) { + const plugins = [ + urls({ url: true }), + imports({ imports: true }) + ]; + + const options = { + to: state.module.resource, + from: state.module.resource + }; + + posthtml(plugins) + .process(source, options) + .then(({ tree, html, messages }) => { + state.module._ast = tree; + state.module._source = new OriginalSource(html); + + const dependencies = messages.filter(isDependency); + + // HACK PostHTML Bug (#250) + messages.length = 0; + + return dependencies + .reduce((done, dep) => new Promise((resolve, reject) => { + if(dep.name.includes("HTML__URL")) { + const dependency = new HTMLURLDependency(dep.url, dep.name); + + state.module.addDependency(dependency, (err) => { + if(err) reject(err); + + resolve(); + }); + } + + if(dep.name.includes("HTML__IMPORT")) { + const dependency = new HTMLImportDependency(dep.url, dep.name); + + state.module.addDependency(dependency, (err) => { + if(err) reject(err); + + resolve(); + }); + } + + if(dep.name.includes("HTML__EXPORT")) { + const dependency = new HTMLExportDependency(dep.export(), dep.name); + + state.module.addDependency(dependency, (err) => { + if(err) reject(err); + + resolve(); + }); + } + + resolve(); + }), Promise.resolve()); + }) + .then(() => cb(null, state)) + .catch((err) => cb(err)); + } +} + +module.exports = HTMLParser; diff --git a/lib/html/HTMLTemplate.js b/lib/html/HTMLTemplate.js new file mode 100644 index 00000000000..435a9522964 --- /dev/null +++ b/lib/html/HTMLTemplate.js @@ -0,0 +1,35 @@ +const { + RawSource +} = require("webpack-sources"); + +class HTMLModulesTemplatePlugin { + constructor() { + this.pluginName = "HTMLModulesTemplatePlugin"; + } + + apply(moduleTemplate) { + const { + content, + hash + } = moduleTemplate.hooks; + + content.tap(this.pluginName, (source, module, { + chunk + }) => { + if(module.type && module.type.startsWith("html")) { + const html = new RawSource(source); + + return html; + } else { + return source; + } + }); + + hash.tap(this.pluginName, (hash) => { + hash.update(this.pluginName); + hash.update("1"); + }); + } +} + +module.exports = HTMLModulesTemplatePlugin; diff --git a/lib/html/dependencies/HTMLExportDependency.js b/lib/html/dependencies/HTMLExportDependency.js new file mode 100644 index 00000000000..b198d46c744 --- /dev/null +++ b/lib/html/dependencies/HTMLExportDependency.js @@ -0,0 +1,21 @@ +const NullDependency = require("../../dependencies/NullDependency"); + +class HTMLExportDependency extends NullDependency { + constructor(exports) { + super(); + + this.exports = exports; + } + + get type() { + return "html exports"; + } + + getExports() { + return { + exports: this.exports + }; + } +} + +module.exports = HTMLExportDependency; diff --git a/lib/html/dependencies/HTMLImportDependency.js b/lib/html/dependencies/HTMLImportDependency.js new file mode 100644 index 00000000000..1fb601ca493 --- /dev/null +++ b/lib/html/dependencies/HTMLImportDependency.js @@ -0,0 +1,33 @@ +const ModuleDependency = require("../../dependencies/ModuleDependency"); + +class HTMLImportDependency extends ModuleDependency { + constructor(request, name) { + super(request); + + this.name = name; + } + + get type() { + return "html import"; + } + + getReference() { + if(!this.module) { + return null; + } + + return { + module: this.module, + importedNames: [ this.name ] + }; + } +} + +HTMLImportDependency.Template = class HTMLImportDependencyTemplate { + apply(dependency, source, runtime) { + console.log(dependency); + console.log(source); + } +}; + +module.exports = HTMLImportDependency; diff --git a/lib/html/dependencies/HTMLURLDependency.js b/lib/html/dependencies/HTMLURLDependency.js new file mode 100644 index 00000000000..cd272021240 --- /dev/null +++ b/lib/html/dependencies/HTMLURLDependency.js @@ -0,0 +1,26 @@ +const ModuleDependency = require("../../dependencies/ModuleDependency"); + +class HTMLURLDependency extends ModuleDependency { + constructor(request, name) { + super(request); + + this.name = name; + } + + get type() { + return "html url"; + } + + getReference() { + if(!this.module) { + return null; + } + + return { + module: this.module, + importedNames: [this.name] + }; + } +} + +module.exports = HTMLURLDependency; diff --git a/lib/html/dependencies/index.js b/lib/html/dependencies/index.js new file mode 100644 index 00000000000..84ee3c4fc8c --- /dev/null +++ b/lib/html/dependencies/index.js @@ -0,0 +1,9 @@ +const HTMLURLDependency = require("./HTMLURLDependency"); +const HTMLImportDependency = require("./HTMLImportDependency"); +const HTMLExportDependency = require("./HTMLExportDependency"); + +module.exports = { + HTMLURLDependency, + HTMLImportDependency, + HTMLExportDependency +}; diff --git a/package.json b/package.json index 9679b8baf20..98d4642cc31 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "mkdirp": "~0.5.0", "neo-async": "^2.5.0", "node-libs-browser": "^2.0.0", + "@posthtml/esm": "^1.0.0", + "posthtml": "^0.11.0", "schema-utils": "^0.4.2", "tapable": "^1.0.0", "uglifyjs-webpack-plugin": "^1.1.1", diff --git a/schemas/WebpackOptions.json b/schemas/WebpackOptions.json index 62b8ee07745..6504e70a587 100644 --- a/schemas/WebpackOptions.json +++ b/schemas/WebpackOptions.json @@ -947,6 +947,7 @@ "javascript/dynamic", "javascript/esm", "json", + "html/experimental", "webassembly/experimental" ] }, diff --git a/yarn.lock b/yarn.lock index 745dc02aad8..b40b04416bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@posthtml/esm@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@posthtml/esm/-/esm-1.0.0.tgz#09bcb28a02438dcee22ad1970ca1d85a000ae0cf" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1178,10 +1182,38 @@ doctrine@^2.0.2: dependencies: esutils "^2.0.2" +dom-serializer@0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" +domelementtype@1, domelementtype@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + +domelementtype@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + +domhandler@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" + dependencies: + domelementtype "1" + +domutils@^1.5.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" + dependencies: + dom-serializer "0" + domelementtype "1" + duplexify@^3.1.2, duplexify@^3.4.2: version "3.5.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.1.tgz#4e1516be68838bc90a49994f0b39a6e5960befcd" @@ -1247,6 +1279,10 @@ enhanced-resolve@^4.0.0: memory-fs "^0.4.0" tapable "^1.0.0" +entities@^1.1.1, entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + errno@^0.1.1, errno@^0.1.3, errno@^0.1.4: version "0.1.6" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026" @@ -1985,6 +2021,17 @@ html-comment-regex@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" +htmlparser2@^3.9.2: + version "3.9.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" + dependencies: + domelementtype "^1.3.0" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^2.0.2" + http-errors@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.3.1.tgz#197e22cdebd4198585e8694ef6786197b91ed942" @@ -2309,7 +2356,7 @@ isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" -isobject@^2.0.0: +isobject@^2.0.0, isobject@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" dependencies: @@ -3495,6 +3542,25 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.1.0" +posthtml-parser@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.3.3.tgz#3fe986fca9f00c0f109d731ba590b192f26e776d" + dependencies: + htmlparser2 "^3.9.2" + isobject "^2.1.0" + object-assign "^4.1.1" + +posthtml-render@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.1.0.tgz#854fcaaf3d4b9c8c1dc736fd5d80e52b709d98b7" + +posthtml@^0.11.0: + version "0.11.2" + resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.11.2.tgz#4c1755838da624c460ae0e5528391a445fc414d9" + dependencies: + posthtml-parser "^0.3.3" + posthtml-render "^1.1.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"