Skip to content

Commit

Permalink
Allow multiple compiled-data chunks at once
Browse files Browse the repository at this point in the history
Localized compiled data modules all share the same moduleIds derived from the set of development locale compiled data modules. One newly loaded locale replaces the current one.

We now create a set of compiled data modules per locale, enabled webpack to allocate them distincts Ids.

Multiple locales can now coexist, allowing dynamic switching and multiple locales on the same page.

Two caveats:
1. A set of data modules have to be referenced. We picked the developement locale’s -> Its compiled-data file should always be loaded.
2. The last loaded compiled-data file sets the default locale.

Signed-off-by: Frédéric Miserey <frederic@none.net>

Closes #65
  • Loading branch information
Frédéric Miserey authored and rxaviers committed Aug 14, 2017
1 parent c84e60e commit 8f39cc6
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 45 deletions.
10 changes: 5 additions & 5 deletions GlobalizeCompilerHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ class GlobalizeCompilerHelper {
return this.extractsMap[request];
}

createCompiledDataModule(request) {
const filepath = this.getModuleFilepath(request);
createCompiledDataModule(request, locale) {
const filepath = this.getModuleFilepath(request, locale);
this.modules[filepath] = true;

fs.writeFileSync(filepath, this.compile(this.developmentLocale, request));
fs.writeFileSync(filepath, this.compile(locale, request));

return filepath;
}

getModuleFilepath(request) {
getModuleFilepath(request, locale) {
// Always append .js to the file path to cater for non-JS files (e.g. .coffee).
return path.join(this.tmpdir, request.replace(/.*!/, "").replace(/[\/\\?" :\.]/g, "-") + ".js");
return path.join(this.tmpdir, request.replace(/.*!/, "").replace(/[\/\\?" :\.]/g, "-") + "-" + locale + ".js");
}

compile(locale, request) {
Expand Down
84 changes: 45 additions & 39 deletions ProductionModePlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,34 +69,41 @@ class ProductionModePlugin {
if(param.isString() && param.string === "globalize" && this.moduleFilter(request) &&
!(globalizeCompilerHelper.isCompiledDataModule(request))) {

// Statically extract Globalize formatters and parsers from the request
// file only. Then, create a custom precompiled formatters/parsers module
// that will be called instead of Globalize, which in turn requires
// Globalize, set the default locale and then exports the Globalize
// object.
const compiledDataFilepath = globalizeCompilerHelper.createCompiledDataModule(request);
// Extract Globalize formatters and parsers for all the locales. Webpack
// allocates distinct moduleIds per locale, enabling multiple locales to
// be used at the same time.
this.supportedLocales.forEach((locale) => {
// Statically extract Globalize formatters and parsers from the request
// file only. Then, create a custom precompiled formatters/parsers module
// that will be called instead of Globalize, which in turn requires
// Globalize, set the default locale and then exports the Globalize
// object.
const compiledDataFilepath = globalizeCompilerHelper.createCompiledDataModule(request, locale);

// Skip the AMD part of the custom precompiled formatters/parsers UMD
// wrapper.
//
// Note: We're hacking an already created SkipAMDPlugin instance instead
// of using a regular code like the below in order to take advantage of
// its position in the plugins list. Otherwise, it'd be too late to plugin
// and AMD would no longer be skipped at this point.
//
// compiler.apply(new SkipAMDPlugin(new RegExp(compiledDataFilepath));
//
// 1: Removes the leading and the trailing `/` from the regexp string.
globalizeSkipAMDPlugin.requestRegExp = new RegExp([
globalizeSkipAMDPlugin.requestRegExp.toString().slice(1, -1)/* 1 */,
util.escapeRegex(compiledDataFilepath)
].join("|"));
// Skip the AMD part of the custom precompiled formatters/parsers UMD
// wrapper.
//
// Note: We're hacking an already created SkipAMDPlugin instance instead
// of using a regular code like the below in order to take advantage of
// its position in the plugins list. Otherwise, it'd be too late to plugin
// and AMD would no longer be skipped at this point.
//
// compiler.apply(new SkipAMDPlugin(new RegExp(compiledDataFilepath));
//
// 1: Removes the leading and the trailing `/` from the regexp string.
globalizeSkipAMDPlugin.requestRegExp = new RegExp([
globalizeSkipAMDPlugin.requestRegExp.toString().slice(1, -1)/* 1 */,
util.escapeRegex(compiledDataFilepath)
].join("|"));

// Replace require("globalize") with require(<custom precompiled module>).
const dep = new CommonJsRequireDependency(compiledDataFilepath, param.range);
dep.loc = expr.loc;
dep.optional = !!parser.scope.inTry;
parser.state.current.addDependency(dep);
// Add localized Globalize formatters and parsers as dependencies
// Replace require("globalize") with require(<custom precompiled module of
// developmentLocale>).
const dep = new CommonJsRequireDependency(compiledDataFilepath, locale == this.developmentLocale ? param.range : null);
dep.loc = expr.loc;
dep.optional = !!parser.scope.inTry;
parser.state.current.addDependency(dep);
});

return true;
}
Expand All @@ -112,11 +119,6 @@ class ProductionModePlugin {

// Place the Globalize compiled data modules into the globalize-compiled-data
// chunks.
//
// Note that, at this point, all compiled data have been compiled for
// developmentLocale. All globalize-compiled-data chunks will equally include all
// precompiled modules for the developmentLocale instead of their respective
// locales. This will get fixed in the subsquent step.
let allModules;
compiler.plugin("this-compilation", (compilation) => {
compilation.plugin("optimize-modules", (modules) => {
Expand All @@ -127,7 +129,10 @@ class ProductionModePlugin {
compiler.plugin("this-compilation", (compilation) => {
compilation.plugin("after-optimize-chunks", (chunks) => {
let hasAnyModuleBeenIncluded;
const compiledDataChunks = chunks.filter((chunk) => /globalize-compiled-data/.test(chunk.name));
const compiledDataChunks = new Map(
chunks.filter((chunk) => /globalize-compiled-data/.test(chunk.name)).
map(chunk => [chunk.name.replace("globalize-compiled-data-", ""), chunk] )
);

allModules.forEach((module) => {
let chunkRemoved, chunk;
Expand All @@ -140,16 +145,17 @@ class ProductionModePlugin {
throw new Error("Failed to remove chunk " + chunk.id + " for module " + module.request);
}
}
compiledDataChunks.forEach((compiledDataChunk) => {
compiledDataChunk.addModule(module);
module.addChunk(compiledDataChunk);
});
for (let [locale, chunk] of compiledDataChunks.entries()) {
if (module.request.endsWith(locale + ".js")) {
chunk.addModule(module);
module.addChunk(chunk);
}
}
}
});
compiledDataChunks.forEach((chunk) => {
const locale = chunk.name.replace("globalize-compiled-data-", "");
for (let [locale, chunk] of compiledDataChunks.entries()) {
chunk.filenameTemplate = output.replace("[locale]", locale);
});
}
if(!hasAnyModuleBeenIncluded) {
console.warn("No Globalize compiled data module found");
}
Expand Down
13 changes: 12 additions & 1 deletion test/ProductionModePlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const path = require("path");
const rimraf = require("rimraf");
const webpack = require("webpack");

const supportedLocales = ["en", "es"];

const mkOutputPath = (testName, file) => path.join(__dirname, "../_test-output", testName, file || "");

const mkWebpackConfig = (options) => ({
Expand All @@ -24,7 +26,7 @@ const mkWebpackConfig = (options) => ({
{
production: true,
developmentLocale: "en",
supportedLocales: ["en", "es"],
supportedLocales: supportedLocales,
messages: path.join(__dirname, "fixtures/translations/[locale].json"),
output: "[locale].js"
},
Expand Down Expand Up @@ -120,6 +122,15 @@ function commonTests(testName, webpackConfig, outputPath) {
expect(enChunkLastLine).to.contain(compiledDataModuleStats.id);
});

it("should have as many globalize-runtime-data modules as supported locales", () => {
const statsJson = compileStats.toJson();
const dataModulesCount = statsJson.modules.reduce((total, module) => {
return module.name.startsWith("./.tmp-globalize-webpack/") ? total+1 : total;
}, 0);

expect(dataModulesCount).to.equal(supportedLocales.length);
});

it("should include formatDate", () => {
const result = Globalize.formatDate(new Date(2017, 3, 15), {datetime: "medium"});
// Note, the reason for the loose match below is due to ignore the local time zone differences.
Expand Down

0 comments on commit 8f39cc6

Please sign in to comment.