Skip to content

Commit

Permalink
feat(html): add HTML module support (html/experimental)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-ciniawsky committed Mar 3, 2018
1 parent f6e366b commit 1be08a3
Show file tree
Hide file tree
Showing 16 changed files with 476 additions and 2 deletions.
2 changes: 2 additions & 0 deletions lib/Compilation.js
Expand Up @@ -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)
};
Expand Down Expand Up @@ -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);
Expand Down
41 changes: 41 additions & 0 deletions lib/Template.js
Expand Up @@ -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: "<!-- HTML Module removed -->"
});
}
}
sources
.sort()
.reduceRight((result, module, idx) => {
if(idx !== 0) result.add("\n");

result.add(`\n<!-- ${module.id} -->\n`);
result.add(module.source);

return result;
}, result);

result.add("\n" /* + prefix */);

return result;
}
};
8 changes: 8 additions & 0 deletions lib/WebpackOptionsApply.js
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 7 additions & 1 deletion lib/WebpackOptionsDefaulter.js
Expand Up @@ -67,6 +67,10 @@ class WebpackOptionsDefaulter extends OptionsDefaulter {
{
test: /\.wasm$/i,
type: "webassembly/experimental"
},
{
test: /\.html$/,
type: "html/experimental"
}
]);

Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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")
Expand Down
30 changes: 30 additions & 0 deletions 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;
7 changes: 7 additions & 0 deletions lib/html/HTMLGenerator.js
@@ -0,0 +1,7 @@
class HTMLGenerator {
generate(module) {
return module.originalSource();
}
}

module.exports = HTMLGenerator;
99 changes: 99 additions & 0 deletions 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;
88 changes: 88 additions & 0 deletions 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;
35 changes: 35 additions & 0 deletions 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;

0 comments on commit 1be08a3

Please sign in to comment.