Skip to content

Commit 5d1ff60

Browse files
author
Chuck Dumont
authored
Merge pull request #144 from chuckdumont/work2
Improve client-side support for relative module ids with global require
2 parents 25bd479 + 6d8beef commit 5d1ff60

10 files changed

+160
-30
lines changed

lib/DojoAMDChunkTemplatePlugin.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,6 @@ module.exports = class DojoAMDChunkTemplatePlugin {
8787

8888
hash(hash) {
8989
hash.update("DojoAMDChunkTemplate");
90-
hash.update("2"); // Increment this whenever the template code above changes
90+
hash.update("3"); // Increment this whenever the template code above changes
9191
}
9292
};

lib/DojoAMDMainTemplatePlugin.js

+81-13
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
const path = require('path');
2525
const util = require('util');
26+
const commondir = require('commondir');
2627
const Template = require("webpack/lib/Template");
2728
const stringify = require("node-stringify");
2829
const {plugin} = require("./pluginHelper");
@@ -43,12 +44,12 @@ module.exports = class DojoAMDMainTemplatePlugin {
4344
compilation:{value: compilation},
4445
params:{value: params}
4546
});
47+
compilation.plugin("before-chunk-assets", this.relativizeAbsoluteAbsMids.bind(context));
4648
plugin(compilation.mainTemplate, {
4749
"bootstrap" : this.bootstrap,
4850
"require-extensions" : this.requireExtensions,
4951
"dojo-require-extensions" : this.dojoRequireExtensions, // plugin specific event
5052
"render-dojo-config-vars" : this.renderDojoConfigVars, // plugin specific event
51-
"set-emit-build-context" : this.setEmitBuildContext, // plugin specific event
5253
"hash" : this.hash
5354
}, context);
5455
});
@@ -131,13 +132,8 @@ but the loader specified at ${this.embeddedLoaderFilename} was built without the
131132
}
132133
}
133134
buf.push(mainTemplate.applyPluginsWaterfall("render-dojo-config-vars", loaderScope, "", ...rest));
134-
if (this.emitBuildContext) {
135-
var context = path.resolve(this.compiler.context, this.options.globalContext || '.').replace(/\\/g,'/');
136-
/* istanbul ignore else */
137-
if (!context.endsWith('/')) {
138-
context += '/';
139-
}
140-
buf.push(`loaderScope.buildContext = "${context}"`);
135+
if (this.buildContext) {
136+
buf.push(`loaderScope.buildContext = "${this.buildContext}"`);
141137
}
142138
buf.push("var dojoLoader = " + mainTemplate.requireFn + "(" + JSON.stringify(dojoLoaderModule.id) + ");");
143139
buf.push("dojoLoader.call(loaderScope, userConfig, defaultConfig, loaderScope, loaderScope);");
@@ -205,14 +201,10 @@ but the loader specified at ${this.embeddedLoaderFilename} was built without the
205201
return mainTemplate.asString(buf);
206202
}
207203

208-
setEmitBuildContext() {
209-
this.emitBuildContext = true;
210-
}
211-
212204
hash(hash) {
213205
const {options} = this;
214206
hash.update("DojoAMDMainTemplate");
215-
hash.update("9"); // Increment this whenever the template code above changes
207+
hash.update("10"); // Increment this whenever the template code above changes
216208
if (util.isString(options.loaderConfig)) {
217209
hash.update(options.loaderConfig); // loading the config as a module, so any any changes to the
218210
// content will be detected at the module level
@@ -222,7 +214,83 @@ but the loader specified at ${this.embeddedLoaderFilename} was built without the
222214
hash.update(stringify(options.loaderConfig));
223215
}
224216
}
217+
225218
getDefaultFeatures() {
226219
return require("./defaultFeatures");
227220
}
221+
222+
/**
223+
* Relativize absolute absMids. Absolute absMids are created when global require is used with relative module ids.
224+
* They have the form '$$root$$/<absolute-path>' where '$$root$$' can be changed using the globalContext.varName
225+
* option. So as not to expose absolute paths on the client for security reasons, we determine the closest common
226+
* directory for all the absolute absMids and rewrite the absMids in the form '$$root$$/<relative-path>', where the
227+
* path is relative to the buildContext path, and the buildContext path is calculated as the globalContext
228+
* directory relative to the closest common directory. For example, if the globalContext path is '/a/b/c/d/e'
229+
* and the closest common directory is '/a/b/c', then the buildContext path would be'$$root$$/d/e', and the
230+
* modified absMid for the module with absolute absMid '$$root$$/a/b/c/d/f/foo' would be '$$root$$/d/f/foo'. The
231+
* buildContext path is emitted to the client so that runtime global require calls specifying relative module ids
232+
* can be resolved on the client. If at runtime, the client calls global require with the module id '../f/foo',
233+
* then the absMid for the module will be computed by resolving the specified module id against the buildContext
234+
* path ('$$root$$/d/e'), resulting in '$$root$$/d/f/foo' and the client will locate the module by looking
235+
* it up in the table of registered absMids.
236+
*
237+
* In the event that runtime global require calls attempt to traverse the buildContext parent hierarchy
238+
* beyond the level of the closest common directory, the globalContext.numParents option can be specified
239+
* to indicate the number of parent directories beyond the closest common directory to include in
240+
* the buildContext path. In the example above, a numParents value of 1 would result in the buildContext
241+
* path changing from '$$root$$/d/e' to '$$root$$/c/d/e' and the absMid for the module with absolute path
242+
* '/a/b/c/d/f/foo' changing from '$$root$$/d/f/foo' to '$$root$$/c/d/f/foo'.
243+
*/
244+
relativizeAbsoluteAbsMids() {
245+
const relAbsMids = [];
246+
const relMods = [];
247+
var rootVarName = this.options.getGlobalContextVarName();
248+
// Gather the absMids that need to be rewritten, as well as the modules that contain them
249+
this.compilation.modules.forEach(module => {
250+
if (module.filterAbsMids) {
251+
var foundSome = false;
252+
module.filterAbsMids(absMid => {
253+
if (absMid.startsWith(rootVarName + "/")) {
254+
relAbsMids.push(path.dirname(absMid.substring(rootVarName.length + 1)));
255+
foundSome = true;
256+
}
257+
return true;
258+
});
259+
if (foundSome) {
260+
relMods.push(module);
261+
}
262+
}
263+
});
264+
if (relAbsMids.length) {
265+
// Determine the closest common directory for all the root relative modules
266+
var commonRoot = commondir(relAbsMids);
267+
// Get the global context
268+
var context = this.options.getGlobalContext(this.compiler);
269+
// Adjust the closest common directory as specified by config
270+
for (var i = 0; i < this.options.getGlobalContextNumParents(); i++) {
271+
commonRoot = path.resolve(commonRoot, '..');
272+
}
273+
// Determine the relative path from the adjusted common root to the global context and set it
274+
// as the build context that gets adorned and emitted to the client.
275+
var relative = path.relative(commonRoot, context);
276+
this.buildContext = path.join(rootVarName, relative).replace(/\\/g,'/');
277+
if (!this.buildContext.endsWith('/')) {
278+
this.buildContext += '/';
279+
}
280+
// Now rewrite the absMids to be relative to the computed build context
281+
relMods.forEach(module => {
282+
const toFix = [];
283+
module.filterAbsMids(absMid => {
284+
if (absMid.startsWith(rootVarName + "/")) {
285+
toFix.push(absMid.substring(rootVarName.length + 1));
286+
return false;
287+
}
288+
return true;
289+
});
290+
toFix.forEach(absMid => {
291+
module.addAbsMid(path.normalize(path.join(rootVarName, relative, path.relative(context, absMid))).replace(/\\/g,'/'));
292+
});
293+
});
294+
}
295+
}
228296
};

lib/DojoAMDModuleFactoryPlugin.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,8 @@ module.exports = class DojoAMDModuleFactoryPlugin {
170170
if (dep && dep.usingGlobalRequire && data.request.startsWith('.')) {
171171
// Global require with relative path. Dojo resolves against the page.
172172
// We'll resolve against the compiler context.
173-
data.request = path.resolve(this.compiler.context, this.options.globalContext||'.', data.request);
174-
this.addAbsMid(data, data.request.replace(/\\/g,'/'));
175-
this.compilation.mainTemplate.applyPlugins("set-emit-build-context");
173+
data.request = path.resolve(this.options.getGlobalContext(this.compiler), data.request);
174+
this.addAbsMid(data, this.options.getGlobalContextVarName() + "/" + data.request);
176175
}
177176
this.factory.applyPluginsWaterfall("add absMids from request", data);
178177
return callback(null, data);

lib/DojoAMDPlugin.js

+29
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ const ScopedRequirePlugin = require("./ScopedRequirePlugin");
3434
module.exports = class DojoAMDPlugin {
3535
constructor(options) {
3636
this.options = options;
37+
this.options.getGlobalContext = this.getGlobalContext.bind(this);
38+
this.options.getGlobalContextVarName = this.getGlobalContextVarName.bind(this);
39+
this.options.getGlobalContextNumParents = this.getGlobalContextNumParents.bind(this);
3740
}
3841

3942
apply(compiler) {
@@ -97,6 +100,32 @@ module.exports = class DojoAMDPlugin {
97100
alias['dojo/loaderProxy'] = path.resolve(__dirname, "..", "loaders", "dojo", "loaderProxy");
98101
}
99102

103+
getGlobalContextVarName() {
104+
var result = '$$root$$';
105+
if (this.options.globalContext && this.options.globalContext.varName) {
106+
result = this.options.globalContext.varName;
107+
}
108+
return result;
109+
}
110+
111+
getGlobalContext(compiler) {
112+
var context = '.';
113+
if (typeof this.options.globalContext === 'string') {
114+
context = this.options.globalContext;
115+
} else if (typeof this.options.globalContext === 'object') {
116+
context = this.options.globalContext.context || '.';
117+
}
118+
return path.resolve(compiler.context, context);
119+
}
120+
121+
getGlobalContextNumParents() {
122+
var result = 0;
123+
if (this.options.globalContext && this.options.globalContext.numParents) {
124+
result = this.options.globalContext.numParents;
125+
}
126+
return result;
127+
}
128+
100129
// Factories
101130
newDojoLoaderPlugin(options) {
102131
return new DojoLoaderPlugin(options);

package-lock.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls"
1313
},
1414
"dependencies": {
15+
"commondir": "1.0.1",
1516
"es6-class-mixin": "1.0.5",
1617
"file-loader": "0.9.0",
1718
"loader-utils": "1.1.0",

test/DojoAMDMainTemplatePlugin.test.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ describe("DojoAMDMainTemplatePlugin tests", function() {
2424
beforeEach(function() {
2525
mainTemplate = new MainTemplate();
2626
const compiler = new Tapable();
27+
const compilation = new Tapable();
2728
plugin.apply(compiler);
28-
compiler.applyPlugins("compilation", { // compilation object
29-
mainTemplate: mainTemplate,
30-
modules: {
31-
find: function() { return null; }
32-
}
33-
});
29+
compilation.mainTemplate = mainTemplate;
30+
compilation.modules = {
31+
find: function() { return null; }
32+
};
33+
compiler.applyPlugins("compilation", compilation);
3434
});
3535

3636
describe("dojo-require-extensions test", function() {

test/TestCases/dependencies/constGlobalRequire/index.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ define("require".split(','), function(req) {
55
asyncDep1.should.be.eql("global asyncDep");
66
asyncDep2.should.be.eql("local asyncDep");
77
require("./asyncDep").should.be.eql("global asyncDep");
8-
require("../globalContext/asyncDep").should.be.eql("global asyncDep");
8+
try {
9+
// parent traversal should fail because we don't specify numParents
10+
require("../globalContext/asyncDep");
11+
return done(new Error("Shouldn't get here"));
12+
} catch(e) {}
913
require("asyncDep").should.be.eql("local asyncDep");
1014
done();
1115
} catch (e) {

test/TestCases/dependencies/globalRequire/index.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
define(["require"], function(req) {
22
it("should load dep from global context", function(done) {
3-
require(["./asyncDep", "asyncDep"], function(asyncDep1, asyncDep2) {
3+
require(["./asyncDep", "asyncDep", "../asyncDep"], function(asyncDep1, asyncDep2, asyncDep3) {
44
try {
55
asyncDep1.should.be.eql("global asyncDep");
66
asyncDep2.should.be.eql("local asyncDep");
7+
asyncDep3.should.be.eql("local asyncDep");
78
require("./asyncDep").should.be.eql("global asyncDep");
89
require("../globalContext/asyncDep").should.be.eql("global asyncDep");
910
require("asyncDep").should.be.eql("local asyncDep");
11+
require("../asyncDep").should.be.eql("local asyncDep");
1012
done();
1113
} catch (e) {
1214
done(e);
@@ -27,4 +29,13 @@ define(["require"], function(req) {
2729
}
2830
});
2931
});
32+
it("should use " + require.rawConfig.testVarName + " for root relative module absMids", function() {
33+
var error;
34+
try {
35+
require("./missing");
36+
} catch(e) {
37+
error = e;
38+
}
39+
error.message.should.containEql("not found: " + require.rawConfig.testVarName + "/");
40+
});
3041
});

test/TestCases/dependencies/globalRequire/webpack.config.js

+18-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ module.exports = [contextdir, contextdir+'/'].map(context => {
99
new DojoWebpackPlugin({
1010
loaderConfig: {
1111
baseUrl: "../",
12-
paths:{test: "."}
12+
paths:{test: "."},
13+
testVarName: "$$root$$"
14+
},
15+
globalContext: {
16+
numParents: 1
1317
},
1418
loader: path.join(__dirname, "../../../js/dojo/dojo.js")
1519
})
@@ -21,10 +25,14 @@ module.exports = [contextdir, contextdir+'/'].map(context => {
2125
plugins: [
2226
new DojoWebpackPlugin({
2327
loaderConfig: {
24-
paths:{test: "."}
28+
paths:{test: "."},
29+
testVarName: "$$root$$"
2530
},
2631
loader: path.join(__dirname, "../../../js/dojo/dojo.js"),
27-
globalContext: context
32+
globalContext: {
33+
context: context,
34+
numParents: 1
35+
}
2836
})
2937
]
3038
};
@@ -34,10 +42,15 @@ module.exports = [contextdir, contextdir+'/'].map(context => {
3442
plugins: [
3543
new DojoWebpackPlugin({
3644
loaderConfig: {
37-
paths:{test: "."}
45+
paths:{test: "."},
46+
testVarName: "%%root%%"
3847
},
3948
loader: path.join(__dirname, "../../../js/dojo/dojo.js"),
40-
globalContext: context
49+
globalContext: {
50+
context: context,
51+
numParents: 1,
52+
varName: "%%root%%"
53+
}
4154
})
4255
]
4356
};

0 commit comments

Comments
 (0)