Skip to content

Commit 5c63d3c

Browse files
committed
move source generation into separate module
allow to pass a Generator to NormalModule change NormalModule constructor to options object :eggplant:
1 parent 02f8c96 commit 5c63d3c

9 files changed

+317
-273
lines changed

lib/JavascriptGenerator.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Tobias Koppers @sokra
4+
*/
5+
"use strict";
6+
7+
const RawSource = require("webpack-sources").RawSource;
8+
const ReplaceSource = require("webpack-sources").ReplaceSource;
9+
10+
// TODO: clean up this file
11+
// replace with newer constructs
12+
13+
// TODO: remove DependencyVariables and replace them with something better
14+
15+
class JavascriptGenerator {
16+
17+
generate(module, dependencyTemplates, runtimeTemplate) {
18+
const originalSource = module.originalSource();
19+
if(!originalSource) {
20+
return new RawSource("throw new Error('No source available');");
21+
}
22+
23+
const source = new ReplaceSource(originalSource);
24+
25+
this.sourceBlock(module, module, [], dependencyTemplates, source, runtimeTemplate);
26+
27+
return source;
28+
}
29+
30+
sourceBlock(module, block, availableVars, dependencyTemplates, source, runtimeTemplate) {
31+
for(const dependency of block.dependencies) {
32+
this.sourceDependency(dependency, dependencyTemplates, source, runtimeTemplate);
33+
}
34+
35+
/**
36+
* Get the variables of all blocks that we need to inject.
37+
* These will contain the variable name and its expression.
38+
* The name will be added as a paramter in a IIFE the expression as its value.
39+
*/
40+
const vars = block.variables.reduce((result, value) => {
41+
const variable = this.sourceVariables(
42+
value, availableVars, dependencyTemplates, runtimeTemplate);
43+
44+
if(variable) {
45+
result.push(variable);
46+
}
47+
48+
return result;
49+
}, []);
50+
51+
/**
52+
* if we actually have variables
53+
* this is important as how #splitVariablesInUniqueNamedChunks works
54+
* it will always return an array in an array which would lead to a IIFE wrapper around
55+
* a module if we do this with an empty vars array.
56+
*/
57+
if(vars.length > 0) {
58+
/**
59+
* Split all variables up into chunks of unique names.
60+
* e.g. imagine you have the following variable names that need to be injected:
61+
* [foo, bar, baz, foo, some, more]
62+
* we can not inject "foo" twice, therefore we just make two IIFEs like so:
63+
* (function(foo, bar, baz){
64+
* (function(foo, some, more){
65+
* ...
66+
* }(...));
67+
* }(...));
68+
*
69+
* "splitVariablesInUniqueNamedChunks" splits the variables shown above up to this:
70+
* [[foo, bar, baz], [foo, some, more]]
71+
*/
72+
const injectionVariableChunks = this.splitVariablesInUniqueNamedChunks(vars);
73+
74+
// create all the beginnings of IIFEs
75+
const functionWrapperStarts = injectionVariableChunks.map((variableChunk) => {
76+
return this.variableInjectionFunctionWrapperStartCode(
77+
variableChunk.map(variable => variable.name)
78+
);
79+
});
80+
81+
// and all the ends
82+
const functionWrapperEnds = injectionVariableChunks.map((variableChunk) => {
83+
return this.variableInjectionFunctionWrapperEndCode(
84+
module,
85+
variableChunk.map(variable => variable.expression), block
86+
);
87+
});
88+
89+
// join them to one big string
90+
const varStartCode = functionWrapperStarts.join("");
91+
92+
// reverse the ends first before joining them, as the last added must be the inner most
93+
const varEndCode = functionWrapperEnds.reverse().join("");
94+
95+
// if we have anything, add it to the source
96+
if(varStartCode && varEndCode) {
97+
const start = block.range ? block.range[0] : -10;
98+
const end = block.range ? block.range[1] : (module.originalSource().size() + 1);
99+
source.insert(start + 0.5, varStartCode);
100+
source.insert(end + 0.5, "\n/* WEBPACK VAR INJECTION */" + varEndCode);
101+
}
102+
}
103+
104+
for(const childBlock of block.blocks) {
105+
this.sourceBlock(
106+
module,
107+
childBlock,
108+
availableVars.concat(vars),
109+
dependencyTemplates,
110+
source,
111+
runtimeTemplate
112+
);
113+
}
114+
}
115+
116+
sourceDependency(dependency, dependencyTemplates, source, runtimeTemplate) {
117+
const template = dependencyTemplates.get(dependency.constructor);
118+
if(!template) throw new Error("No template for dependency: " + dependency.constructor.name);
119+
template.apply(dependency, source, runtimeTemplate, dependencyTemplates);
120+
}
121+
122+
sourceVariables(variable, availableVars, dependencyTemplates, runtimeTemplate) {
123+
const name = variable.name;
124+
const expr = variable.expressionSource(dependencyTemplates, runtimeTemplate);
125+
126+
if(availableVars.some(v => v.name === name && v.expression.source() === expr.source())) {
127+
return;
128+
}
129+
return {
130+
name: name,
131+
expression: expr
132+
};
133+
}
134+
135+
/*
136+
* creates the start part of a IIFE around the module to inject a variable name
137+
* (function(...){ <- this part
138+
* }.call(...))
139+
*/
140+
variableInjectionFunctionWrapperStartCode(varNames) {
141+
const args = varNames.join(", ");
142+
return `/* WEBPACK VAR INJECTION */(function(${args}) {`;
143+
}
144+
145+
contextArgument(module, block) {
146+
if(this === block) {
147+
return module.exportsArgument;
148+
}
149+
return "this";
150+
}
151+
152+
/*
153+
* creates the end part of a IIFE around the module to inject a variable name
154+
* (function(...){
155+
* }.call(...)) <- this part
156+
*/
157+
variableInjectionFunctionWrapperEndCode(module, varExpressions, block) {
158+
const firstParam = this.contextArgument(module, block);
159+
const furtherParams = varExpressions.map(e => e.source()).join(", ");
160+
return `}.call(${firstParam}, ${furtherParams}))`;
161+
}
162+
163+
splitVariablesInUniqueNamedChunks(vars) {
164+
const startState = [
165+
[]
166+
];
167+
return vars.reduce((chunks, variable) => {
168+
const current = chunks[chunks.length - 1];
169+
// check if variable with same name exists already
170+
// if so create a new chunk of variables.
171+
const variableNameAlreadyExists = current.some(v => v.name === variable.name);
172+
173+
if(variableNameAlreadyExists) {
174+
// start new chunk with current variable
175+
chunks.push([variable]);
176+
} else {
177+
// else add it to current chunk
178+
current.push(variable);
179+
}
180+
return chunks;
181+
}, startState);
182+
}
183+
184+
}
185+
186+
module.exports = JavascriptGenerator;

lib/JavascriptModulesPlugin.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const Parser = require("./Parser");
88
const Template = require("./Template");
99
const ConcatSource = require("webpack-sources").ConcatSource;
10+
const JavascriptGenerator = require("./JavascriptGenerator");
1011

1112
class JavascriptModulesPlugin {
1213
apply(compiler) {
@@ -22,6 +23,15 @@ class JavascriptModulesPlugin {
2223
normalModuleFactory.hooks.createParser.for("javascript/esm").tap("JavascriptModulesPlugin", options => {
2324
return new Parser(options, "module");
2425
});
26+
normalModuleFactory.hooks.createGenerator.for("javascript/auto").tap("JavascriptModulesPlugin", options => {
27+
return new JavascriptGenerator(options);
28+
});
29+
normalModuleFactory.hooks.createGenerator.for("javascript/dynamic").tap("JavascriptModulesPlugin", options => {
30+
return new JavascriptGenerator(options);
31+
});
32+
normalModuleFactory.hooks.createGenerator.for("javascript/esm").tap("JavascriptModulesPlugin", options => {
33+
return new JavascriptGenerator(options);
34+
});
2535
compilation.mainTemplate.hooks.renderManifest.tap("JavascriptModulesPlugin", (result, options) => {
2636
const chunk = options.chunk;
2737
const hash = options.hash;

lib/JsonGenerator.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Tobias Koppers @sokra
4+
*/
5+
"use strict";
6+
7+
const ConcatSource = require("webpack-sources").ConcatSource;
8+
9+
const stringifySafe = data => JSON.stringify(data)
10+
.replace(/\u2028|\u2029/g, str => str === "\u2029" ? "\\u2029" : "\\u2028"); // invalid in JavaScript but valid JSON
11+
12+
class JsonGenerator {
13+
generate(module, dependencyTemplates, runtimeTemplate) {
14+
const source = new ConcatSource();
15+
const data = module.buildInfo.jsonData;
16+
if(Array.isArray(module.buildMeta.providedExports) && !module.isUsed("default")) {
17+
// Only some exports are used: We can optimize here, by only generating a part of the JSON
18+
const reducedJson = {};
19+
for(const exportName of module.buildMeta.providedExports) {
20+
if(exportName === "default")
21+
continue;
22+
const used = module.isUsed(exportName);
23+
if(used) {
24+
reducedJson[used] = data[exportName];
25+
}
26+
}
27+
source.add(`${module.moduleArgument}.exports = ${stringifySafe(reducedJson)};`);
28+
} else {
29+
source.add(`${module.moduleArgument}.exports = ${stringifySafe(data)};`);
30+
}
31+
return source;
32+
}
33+
}
34+
35+
module.exports = JsonGenerator;

lib/JsonModulesPlugin.js

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
"use strict";
66

77
const JsonParser = require("./JsonParser");
8-
const ConcatSource = require("webpack-sources").ConcatSource;
9-
10-
const stringifySafe = data => JSON.stringify(data)
11-
.replace(/\u2028|\u2029/g, str => str === "\u2029" ? "\\u2029" : "\\u2028"); // invalid in JavaScript but valid JSON
8+
const JsonGenerator = require("./JsonGenerator");
129

1310
class JsonModulesPlugin {
1411
apply(compiler) {
@@ -18,29 +15,8 @@ class JsonModulesPlugin {
1815
normalModuleFactory.hooks.createParser.for("json").tap("JsonModulesPlugin", () => {
1916
return new JsonParser();
2017
});
21-
compilation.moduleTemplates.javascript.hooks.content.tap("JsonModulesPlugin", (moduleSource, module) => {
22-
if(module.type && module.type.startsWith("json")) {
23-
const source = new ConcatSource();
24-
const data = module.buildInfo.jsonData;
25-
if(Array.isArray(module.buildMeta.providedExports) && !module.isUsed("default")) {
26-
// Only some exports are used: We can optimize here, by only generating a part of the JSON
27-
const reducedJson = {};
28-
for(const exportName of module.buildMeta.providedExports) {
29-
if(exportName === "default")
30-
continue;
31-
const used = module.isUsed(exportName);
32-
if(used) {
33-
reducedJson[used] = data[exportName];
34-
}
35-
}
36-
source.add(`${module.moduleArgument}.exports = ${stringifySafe(reducedJson)};`);
37-
} else {
38-
source.add(`${module.moduleArgument}.exports = ${stringifySafe(data)};`);
39-
}
40-
return source;
41-
} else {
42-
return moduleSource;
43-
}
18+
normalModuleFactory.hooks.createGenerator.for("json").tap("JsonModulesPlugin", () => {
19+
return new JsonGenerator();
4420
});
4521
});
4622
}

0 commit comments

Comments
 (0)