diff --git a/.eslintrc b/.eslintrc index e8b48ad..1399c2f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,8 +18,10 @@ "indent": ["warn", 2], "no-console": "off", "no-trailing-spaces": "warn", + "no-var": "error", "quotes": ["warn", "double"], "semi": "warn", - "sort-requires/sort-requires": "warn" + "sort-requires/sort-requires": "warn", + "strict": ["error", "global"] } } diff --git a/DevelopmentModePlugin.js b/DevelopmentModePlugin.js index 83d2fa7..fbcee2c 100644 --- a/DevelopmentModePlugin.js +++ b/DevelopmentModePlugin.js @@ -1,77 +1,66 @@ -var CommonJsRequireDependency = require("webpack/lib/dependencies/CommonJsRequireDependency"); -var fs = require("fs"); -var NormalModuleFactory = require("webpack/lib/NormalModuleFactory"); -var path = require("path"); -var SkipAMDPlugin = require("skip-amd-webpack-plugin"); -var util = require("./util"); +"use strict"; + +const CommonJsRequireDependency = require("webpack/lib/dependencies/CommonJsRequireDependency"); +const fs = require("fs"); +const path = require("path"); +const SkipAMDPlugin = require("skip-amd-webpack-plugin"); +const util = require("./util"); /** * Development Mode: * - Automatically loads CLDR data (i.e., injects `Globalize.load()`). * - Automatically define default locale (i.e., injects `Globalize.locale()`). */ -function DevelopmentModePlugin(attributes) { - var i18nDataTemplate, messages; - var cldr = attributes.cldr || util.cldr; - var tmpdir = util.tmpdir(); - - messages = attributes.messages && util.readMessages(attributes.messages, attributes.developmentLocale); +class DevelopmentModePlugin { + constructor(attributes) { + let i18nDataTemplate, messages; + const cldr = attributes.cldr || util.cldr; + const tmpdir = util.tmpdir(); - i18nDataTemplate = [ - "var Globalize = require(\"globalize\");", - "", - "Globalize.load(" + JSON.stringify(cldr(attributes.developmentLocale)) + ");", - messages ? "Globalize.loadMessages(" + JSON.stringify(messages) + ");": "", - "Globalize.locale(" + JSON.stringify(attributes.developmentLocale) + ");", - "", - "module.exports = Globalize;" - ].join("\n"); - - this.i18nData = path.join(tmpdir, "dev-i18n-data.js"); - this.moduleFilter = util.moduleFilterFn(attributes.moduleFilter); - fs.writeFileSync(this.i18nData, i18nDataTemplate); -} + messages = attributes.messages && util.readMessages(attributes.messages, attributes.developmentLocale); -DevelopmentModePlugin.prototype.apply = function(compiler) { - var i18nData = this.i18nData; - var moduleFilter = this.moduleFilter; + i18nDataTemplate = [ + "var Globalize = require(\"globalize\");", + "", + `Globalize.load(${JSON.stringify(cldr(attributes.developmentLocale))});`, + messages ? `Globalize.loadMessages(${JSON.stringify(messages)});` : "", + `Globalize.locale(${JSON.stringify(attributes.developmentLocale)});`, + "", + "module.exports = Globalize;" + ].join("\n"); - // Skip AMD part of Globalize Runtime UMD wrapper. - compiler.apply(new SkipAMDPlugin(/(^|[\/\\])globalize($|[\/\\])/)); + this.i18nData = path.join(tmpdir, "dev-i18n-data.js"); + this.moduleFilter = util.moduleFilterFn(attributes.moduleFilter); + fs.writeFileSync(this.i18nData, i18nDataTemplate); + } - // "Intercepts" all `require("globalize")` by transforming them into a - // `require` to our custom generated template, which in turn requires - // Globalize, loads CLDR, set the default locale and then exports the - // Globalize object. - var bindParser = function(parser) { - parser.plugin("call require:commonjs:item", function(expr, param) { - var request = this.state.current.request; + apply(compiler) { + // Skip AMD part of Globalize Runtime UMD wrapper. + compiler.apply(new SkipAMDPlugin(/(^|[\/\\])globalize($|[\/\\])/)); - if(param.isString() && param.string === "globalize" && moduleFilter(request) && - !(new RegExp(util.escapeRegex(i18nData))).test(request)) { - var dep; + // "Intercepts" all `require("globalize")` by transforming them into a + // `require` to our custom generated template, which in turn requires + // Globalize, loads CLDR, set the default locale and then exports the + // Globalize object. + compiler.plugin("compilation", (compilation, params) => { + params.normalModuleFactory.plugin("parser", (parser) => { + parser.plugin("call require:commonjs:item", (expr, param) => { + const request = parser.state.current.request; - dep = new CommonJsRequireDependency(i18nData, param.range); - dep.loc = expr.loc; - dep.optional = !!this.scope.inTry; - this.state.current.addDependency(dep); + if(param.isString() && param.string === "globalize" && this.moduleFilter(request) && + !(new RegExp(util.escapeRegex(this.i18nData))).test(request)) { - return true; - } - }); - }; + const dep = new CommonJsRequireDependency(this.i18nData, param.range); + dep.loc = expr.loc; + dep.optional = !!parser.scope.inTry; + parser.state.current.addDependency(dep); - // Hack to support webpack 1.x and 2.x. - // webpack 2.x - if (NormalModuleFactory.prototype.createParser) { - compiler.plugin("compilation", function(compilation, params) { - params.normalModuleFactory.plugin("parser", bindParser); + return true; + } + }); + }); }); - - // webpack 1.x - } else { - bindParser(compiler.parser); } -}; +} module.exports = DevelopmentModePlugin; diff --git a/GlobalizeCompilerHelper.js b/GlobalizeCompilerHelper.js index 703057b..b0980a0 100644 --- a/GlobalizeCompilerHelper.js +++ b/GlobalizeCompilerHelper.js @@ -1,87 +1,89 @@ -var fs = require("fs"); -var globalizeCompiler = require("globalize-compiler"); -var path = require("path"); - -function GlobalizeCompilerHelper(attributes) { - this.asts = {}; - this.extracts = []; - this.extractsMap = {}; - this.modules = {}; - - this.cldr = attributes.cldr; - this.developmentLocale = attributes.developmentLocale; - this.messages = attributes.messages || {}; - this.tmpdir = attributes.tmpdir; - this.webpackCompiler = attributes.webpackCompiler; -} +"use strict"; + +const fs = require("fs"); +const globalizeCompiler = require("globalize-compiler"); +const path = require("path"); + +class GlobalizeCompilerHelper { + constructor(attributes) { + this.asts = {}; + this.extracts = []; + this.extractsMap = {}; + this.modules = {}; + + this.cldr = attributes.cldr; + this.developmentLocale = attributes.developmentLocale; + this.messages = attributes.messages || {}; + this.tmpdir = attributes.tmpdir; + this.webpackCompiler = attributes.webpackCompiler; + } -GlobalizeCompilerHelper.prototype.setAst = function(request, ast) { - this.asts[request] = ast; -}; - -GlobalizeCompilerHelper.prototype.getExtract = function(request) { - var ast, extract; - if(!this.extractsMap[request]) { - ast = this.asts[request]; - extract = globalizeCompiler.extract(ast); - this.extractsMap[request] = extract; - this.extracts.push(extract); + setAst(request, ast) { + this.asts[request] = ast; } - return this.extractsMap[request]; -}; -GlobalizeCompilerHelper.prototype.createCompiledDataModule = function(request) { - var filepath = this.getModuleFilepath(request); - this.modules[filepath] = true; + getExtract(request) { + let ast, extract; + if(!this.extractsMap[request]) { + ast = this.asts[request]; + extract = globalizeCompiler.extract(ast); + this.extractsMap[request] = extract; + this.extracts.push(extract); + } + return this.extractsMap[request]; + } - fs.writeFileSync(filepath, this.compile(this.developmentLocale, request)); + createCompiledDataModule(request) { + const filepath = this.getModuleFilepath(request); + this.modules[filepath] = true; - return filepath; -}; + fs.writeFileSync(filepath, this.compile(this.developmentLocale, request)); -GlobalizeCompilerHelper.prototype.getModuleFilepath = function(request) { - // 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 filepath; + } -GlobalizeCompilerHelper.prototype.compile = function(locale, request) { - var content; - var messages = this.messages; + getModuleFilepath(request) { + // 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"); + } - var attributes = { - cldr: this.cldr, - defaultLocale: locale, - extracts: request ? this.getExtract(request) : this.extracts - }; + compile(locale, request) { + let content; - if (messages[locale]) { - attributes.messages = messages[locale]; - } + const attributes = { + cldr: this.cldr, + defaultLocale: locale, + extracts: request ? this.getExtract(request) : this.extracts + }; - this.webpackCompiler.applyPlugins("globalize-before-compile-extracts", locale, attributes, request); - - try { - content = globalizeCompiler.compileExtracts(attributes); - } catch(e) { - // The only case to throw is when it's missing formatters/parsers for the - // whole chunk, i.e., when `request` isn't present; or when error is - // something else obviously. If a particular file misses formatters/parsers, - // it can be safely ignored (i.e., by using a stub content), because in the - // end generating the content for the whole chunk will ultimately verify - // whether or not formatters/parsers has been used. - if (!/No formatters or parsers has been provided/.test(e.message) || !request) { - throw e; + if (this.messages[locale]) { + attributes.messages = this.messages[locale]; } - content = "module.exports = {};"; - } - // Inject set defaultLocale. - return content.replace(/(return Globalize;)/, "Globalize.locale(\"" + locale + "\"); $1"); -}; + this.webpackCompiler.applyPlugins("globalize-before-compile-extracts", locale, attributes, request); + + try { + content = globalizeCompiler.compileExtracts(attributes); + } catch(e) { + // The only case to throw is when it's missing formatters/parsers for the + // whole chunk, i.e., when `request` isn't present; or when error is + // something else obviously. If a particular file misses formatters/parsers, + // it can be safely ignored (i.e., by using a stub content), because in the + // end generating the content for the whole chunk will ultimately verify + // whether or not formatters/parsers has been used. + if (!/No formatters or parsers has been provided/.test(e.message) || !request) { + throw e; + } + content = "module.exports = {};"; + } -GlobalizeCompilerHelper.prototype.isCompiledDataModule = function(request) { - return request && this.modules[request.replace(/.*!/, "")]; -}; + // Inject set defaultLocale. + return content.replace(/(return Globalize;)/, "Globalize.locale(\"" + locale + "\"); $1"); + } + isCompiledDataModule(request) { + return request && this.modules[request.replace(/.*!/, "")]; + } +} module.exports = GlobalizeCompilerHelper; diff --git a/ProductionModePlugin.js b/ProductionModePlugin.js index 749812a..9675009 100644 --- a/ProductionModePlugin.js +++ b/ProductionModePlugin.js @@ -1,10 +1,12 @@ -var CommonJsRequireDependency = require("webpack/lib/dependencies/CommonJsRequireDependency"); -var GlobalizeCompilerHelper = require("./GlobalizeCompilerHelper"); -var MultiEntryPlugin = require("webpack/lib/MultiEntryPlugin"); -var NormalModuleReplacementPlugin = require("webpack/lib/NormalModuleReplacementPlugin"); -var RawModule = require("webpack/lib/RawModule"); -var SkipAMDPlugin = require("skip-amd-webpack-plugin"); -var util = require("./util"); +"use strict"; + +const CommonJsRequireDependency = require("webpack/lib/dependencies/CommonJsRequireDependency"); +const GlobalizeCompilerHelper = require("./GlobalizeCompilerHelper"); +const MultiEntryPlugin = require("webpack/lib/MultiEntryPlugin"); +const NormalModuleReplacementPlugin = require("webpack/lib/NormalModuleReplacementPlugin"); +const RawModule = require("webpack/lib/RawModule"); +const SkipAMDPlugin = require("skip-amd-webpack-plugin"); +const util = require("./util"); /** * Production Mode: @@ -12,310 +14,294 @@ var util = require("./util"); * - Statically extracts formatters and parsers from user code and pre-compile * them into globalize-compiled-data chunks. */ -function ProductionModePlugin(attributes) { - this.cldr = attributes.cldr || util.cldr; - this.developmentLocale = attributes.developmentLocale; - this.messages = attributes.messages && attributes.supportedLocales.reduce(function(sum, locale) { - sum[locale] = util.readMessages(attributes.messages, locale) || {}; - return sum; - }, {}); - this.moduleFilter = util.moduleFilterFn(attributes.moduleFilter); - this.supportedLocales = attributes.supportedLocales; - this.output = attributes.output; - this.tmpdir = util.tmpdir(); -} +class ProductionModePlugin { + constructor(attributes) { + this.cldr = attributes.cldr || util.cldr; + this.developmentLocale = attributes.developmentLocale; + this.messages = attributes.messages && attributes.supportedLocales.reduce((sum, locale) => { + sum[locale] = util.readMessages(attributes.messages, locale) || {}; + return sum; + }, {}); + this.moduleFilter = util.moduleFilterFn(attributes.moduleFilter); + this.supportedLocales = attributes.supportedLocales; + this.output = attributes.output; + this.tmpdir = util.tmpdir(); + } -ProductionModePlugin.prototype.apply = function(compiler) { - var globalizeSkipAMDPlugin; - var cldr = this.cldr; - var developmentLocale = this.developmentLocale; - var moduleFilter = this.moduleFilter; - var messages = this.messages; - var supportedLocales = this.supportedLocales; - var output = this.output || "i18n-[locale].js"; + apply(compiler) { + let globalizeSkipAMDPlugin; + const output = this.output || "i18n-[locale].js"; + const globalizeCompilerHelper = new GlobalizeCompilerHelper({ + cldr: this.cldr, + developmentLocale: this.developmentLocale, + messages: this.messages, + tmpdir: this.tmpdir, + webpackCompiler: compiler + }); - var globalizeCompilerHelper = new GlobalizeCompilerHelper({ - cldr: cldr, - developmentLocale: developmentLocale, - messages: messages, - tmpdir: this.tmpdir, - webpackCompiler: compiler - }); + compiler.apply( + // Skip AMD part of Globalize Runtime UMD wrapper. + globalizeSkipAMDPlugin = new SkipAMDPlugin(/(^|[\/\\])globalize($|[\/\\])/), - compiler.apply( - // Skip AMD part of Globalize Runtime UMD wrapper. - globalizeSkipAMDPlugin = new SkipAMDPlugin(/(^|[\/\\])globalize($|[\/\\])/), + // Replaces `require("globalize")` with `require("globalize/dist/globalize-runtime")`. + new NormalModuleReplacementPlugin(/(^|[\/\\])globalize$/, "globalize/dist/globalize-runtime"), - // Replaces `require("globalize")` with `require("globalize/dist/globalize-runtime")`. - new NormalModuleReplacementPlugin(/(^|[\/\\])globalize$/, "globalize/dist/globalize-runtime"), + // Skip AMD part of Globalize Runtime UMD wrapper. + new SkipAMDPlugin(/(^|[\/\\])globalize-runtime($|[\/\\])/) + ); - // Skip AMD part of Globalize Runtime UMD wrapper. - new SkipAMDPlugin(/(^|[\/\\])globalize-runtime($|[\/\\])/) - ); + const bindParser = (parser) => { - var bindParser = function(parser) { + // Map each AST and its request filepath. + parser.plugin("program", (ast) => { + globalizeCompilerHelper.setAst(parser.state.current.request, ast); + }); - // Map each AST and its request filepath. - parser.plugin("program", function(ast) { - globalizeCompilerHelper.setAst(this.state.current.request, ast); - }); + // "Intercepts" all `require("globalize")` by transforming them into a + // `require` to our custom precompiled formatters/parsers, which in turn + // requires Globalize, set the default locale and then exports the + // Globalize object. + parser.plugin("call require:commonjs:item", (expr, param) => { + const request = parser.state.current.request; + if(param.isString() && param.string === "globalize" && this.moduleFilter(request) && + !(globalizeCompilerHelper.isCompiledDataModule(request))) { - // "Intercepts" all `require("globalize")` by transforming them into a - // `require` to our custom precompiled formatters/parsers, which in turn - // requires Globalize, set the default locale and then exports the - // Globalize object. - parser.plugin("call require:commonjs:item", function(expr, param) { - var request = this.state.current.request; - if(param.isString() && param.string === "globalize" && moduleFilter(request) && - !(globalizeCompilerHelper.isCompiledDataModule(request))) { - var dep; + // 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); - // 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. - var compiledDataFilepath = globalizeCompilerHelper.createCompiledDataModule(request); + // 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(). + const dep = new CommonJsRequireDependency(compiledDataFilepath, param.range); + dep.loc = expr.loc; + dep.optional = !!parser.scope.inTry; + parser.state.current.addDependency(dep); - // Replace require("globalize") with require(). - dep = new CommonJsRequireDependency(compiledDataFilepath, param.range); - dep.loc = expr.loc; - dep.optional = !!this.scope.inTry; - this.state.current.addDependency(dep); + return true; + } + }); + }; - return true; - } + // Create globalize-compiled-data chunks for the supportedLocales. + compiler.plugin("entry-option", (context) => { + this.supportedLocales.forEach((locale) => { + compiler.apply(new MultiEntryPlugin(context, [], "globalize-compiled-data-" + locale )); + }); }); - }; - // Create globalize-compiled-data chunks for the supportedLocales. - compiler.plugin("entry-option", function(context) { - supportedLocales.forEach(function(locale) { - compiler.apply(new MultiEntryPlugin(context, [], "globalize-compiled-data-" + locale )); + // 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) => { + allModules = modules; + }); }); - }); - // 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. - var allModules; - compiler.plugin("this-compilation", function(compilation) { - compilation.plugin("optimize-modules", function(modules) { - allModules = modules; - }); - }); - compiler.plugin("this-compilation", function(compilation) { - compilation.plugin("after-optimize-chunks", function(chunks) { - var hasAnyModuleBeenIncluded; - var compiledDataChunks = chunks.filter(function(chunk) { - return /globalize-compiled-data/.test(chunk.name); - }); - allModules.forEach(function(module) { - var chunkRemoved, chunk; - if (globalizeCompilerHelper.isCompiledDataModule(module.request)) { - hasAnyModuleBeenIncluded = true; - while (module.chunks.length) { - chunk = module.chunks[0]; - chunkRemoved = module.removeChunk(chunk); - if (!chunkRemoved) { - throw new Error("Failed to remove chunk " + chunk.id + " for module " + module.request); + compiler.plugin("this-compilation", (compilation) => { + compilation.plugin("after-optimize-chunks", (chunks) => { + let hasAnyModuleBeenIncluded; + const compiledDataChunks = chunks.filter((chunk) => /globalize-compiled-data/.test(chunk.name)); + + allModules.forEach((module) => { + let chunkRemoved, chunk; + if (globalizeCompilerHelper.isCompiledDataModule(module.request)) { + hasAnyModuleBeenIncluded = true; + while (module.chunks.length) { + chunk = module.chunks[0]; + chunkRemoved = module.removeChunk(chunk); + if (!chunkRemoved) { + throw new Error("Failed to remove chunk " + chunk.id + " for module " + module.request); + } } + compiledDataChunks.forEach((compiledDataChunk) => { + compiledDataChunk.addModule(module); + module.addChunk(compiledDataChunk); + }); } - compiledDataChunks.forEach(function(compiledDataChunk) { - compiledDataChunk.addModule(module); - module.addChunk(compiledDataChunk); - }); + }); + compiledDataChunks.forEach((chunk) => { + const locale = chunk.name.replace("globalize-compiled-data-", ""); + chunk.filenameTemplate = output.replace("[locale]", locale); + }); + if(!hasAnyModuleBeenIncluded) { + console.warn("No Globalize compiled data module found"); } }); - compiledDataChunks.forEach(function(chunk) { - var locale = chunk.name.replace("globalize-compiled-data-", ""); - chunk.filenameTemplate = output.replace("[locale]", locale); - }); - if(!hasAnyModuleBeenIncluded) { - console.warn("No Globalize compiled data module found"); - } - }); - // Have each globalize-compiled-data chunks include precompiled data for - // each supported locale. In each chunk, merge all the precompiled modules - // into a single one. Finally, allow the chunks to be loaded incrementally - // (not mutually exclusively). Details below. - // - // Up to this step, all globalize-compiled-data chunks include several - // precompiled modules, which have been mandatory to allow webpack to figure - // out the Globalize runtime dependencies. But for the final chunk we need - // something a little different: - // - // a) Instead of including several individual precompiled modules, it's - // better (i.e., reduced size due to less boilerplate and due to deduped - // formatters and parsers) having one single precompiled module for all - // these individual modules. - // - // b) globalize-compiled-data chunks shouldn't be mutually exclusive to each - // other, but users should be able to load two or more of these chunks - // and be able to switch from one locale to another dynamically during - // runtime. - // - // Some background: by having each individual precompiled module defining - // the formatters and parsers for its individual parents, what happens is - // that each parent will load the globalize precompiled data by its id - // with __webpack_require__(id). These ids are equally defined by the - // globalize-compiled-data chunks (each chunk including data for a - // certain locale). When one chunk is loaded, these ids get defined by - // webpack. When a second chunk is loaded, these ids would get - // overwritten. - // - // Therefore, instead of having each individual precompiled module - // defining the formatters and parsers for its individual parents, we - // actually simplify them by returning Globalize only. The precompiled - // content for the whole set of formatters and parsers are going to be - // included in the entry module of each of these chunks. - // So, we accomplish what we need: have the data loaded as soon as the - // chunk is loaded, which means it will be available when each - // individual parent code needs it. - compilation.plugin("after-optimize-module-ids", function() { - var globalizeModuleIds = []; - var globalizeModuleIdsMap = {}; + // Have each globalize-compiled-data chunks include precompiled data for + // each supported locale. In each chunk, merge all the precompiled modules + // into a single one. Finally, allow the chunks to be loaded incrementally + // (not mutually exclusively). Details below. + // + // Up to this step, all globalize-compiled-data chunks include several + // precompiled modules, which have been mandatory to allow webpack to figure + // out the Globalize runtime dependencies. But for the final chunk we need + // something a little different: + // + // a) Instead of including several individual precompiled modules, it's + // better (i.e., reduced size due to less boilerplate and due to deduped + // formatters and parsers) having one single precompiled module for all + // these individual modules. + // + // b) globalize-compiled-data chunks shouldn't be mutually exclusive to each + // other, but users should be able to load two or more of these chunks + // and be able to switch from one locale to another dynamically during + // runtime. + // + // Some background: by having each individual precompiled module defining + // the formatters and parsers for its individual parents, what happens is + // that each parent will load the globalize precompiled data by its id + // with __webpack_require__(id). These ids are equally defined by the + // globalize-compiled-data chunks (each chunk including data for a + // certain locale). When one chunk is loaded, these ids get defined by + // webpack. When a second chunk is loaded, these ids would get + // overwritten. + // + // Therefore, instead of having each individual precompiled module + // defining the formatters and parsers for its individual parents, we + // actually simplify them by returning Globalize only. The precompiled + // content for the whole set of formatters and parsers are going to be + // included in the entry module of each of these chunks. + // So, we accomplish what we need: have the data loaded as soon as the + // chunk is loaded, which means it will be available when each + // individual parent code needs it. + compilation.plugin("after-optimize-module-ids", () => { + const globalizeModuleIds = []; + const globalizeModuleIdsMap = {}; - this.chunks.forEach(function(chunk) { - chunk.modules.forEach(function(module) { - var aux; - var request = module.request; - if (request && util.isGlobalizeRuntimeModule(request)) { - // While request has the full pathname, aux has something like - // "globalize/dist/globalize-runtime/date". - aux = request.split(/[\/\\]/); - aux = aux.slice(aux.lastIndexOf("globalize")).join("/").replace(/\.js$/, ""); + compilation.chunks.forEach((chunk) => { + chunk.modules.forEach((module) => { + let aux; + const request = module.request; + if (request && util.isGlobalizeRuntimeModule(request)) { + // While request has the full pathname, aux has something like + // "globalize/dist/globalize-runtime/date". + aux = request.split(/[\/\\]/); + aux = aux.slice(aux.lastIndexOf("globalize")).join("/").replace(/\.js$/, ""); - // some plugins, like HashedModuleIdsPlugin, may change module ids - // into strings. - var moduleId = module.id; - if (typeof moduleId === "string") { - moduleId = JSON.stringify(moduleId); - } + // some plugins, like HashedModuleIdsPlugin, may change module ids + // into strings. + let moduleId = module.id; + if (typeof moduleId === "string") { + moduleId = JSON.stringify(moduleId); + } - globalizeModuleIds.push(moduleId); - globalizeModuleIdsMap[aux] = moduleId; - } + globalizeModuleIds.push(moduleId); + globalizeModuleIdsMap[aux] = moduleId; + } + }); }); - }); - // rewrite the modules in the localized chunks: - // - entry module will contain the compiled formatters and parsers - // - non-entry modules will be rewritten to export globalize - this.chunks - .filter(function(chunk) { - return /globalize-compiled-data/.test(chunk.name); - }) - .forEach(function(chunk) { - // remove dead entry module for these reasons - // - because the module has no dependencies, it won't be rendered - // with __webpack_require__, making it difficult to modify its - // source in a way that can import globalize - // - // - it was a placeholder MultiModule that held no content, created - // when we added a MultiEntryPlugin - // - // - the true entry module should be globalize-compiled-data - // module, which has been created as a NormalModule - chunk.removeModule(chunk.entryModule); - chunk.entryModule = chunk.modules.find(function(module) { - return module.context.endsWith(".tmp-globalize-webpack"); - }); + // rewrite the modules in the localized chunks: + // - entry module will contain the compiled formatters and parsers + // - non-entry modules will be rewritten to export globalize + compilation.chunks + .filter((chunk) => /globalize-compiled-data/.test(chunk.name)) + .forEach((chunk) => { + // remove dead entry module for these reasons + // - because the module has no dependencies, it won't be rendered + // with __webpack_require__, making it difficult to modify its + // source in a way that can import globalize + // + // - it was a placeholder MultiModule that held no content, created + // when we added a MultiEntryPlugin + // + // - the true entry module should be globalize-compiled-data + // module, which has been created as a NormalModule + chunk.removeModule(chunk.entryModule); + chunk.entryModule = chunk.modules.find((module) => module.context.endsWith(".tmp-globalize-webpack")); - var newModules = chunk.modules.map(function(module) { - var fnContent; - if (module === chunk.entryModule) { - // rewrite entry module to contain the globalize-compiled-data - var locale = chunk.name.replace("globalize-compiled-data-", ""); - fnContent = globalizeCompilerHelper.compile(locale) - .replace("typeof define === \"function\" && define.amd", "false") - .replace(/require\("([^)]+)"\)/g, function(garbage, moduleName) { - return "__webpack_require__(" + globalizeModuleIdsMap[moduleName] + ")"; - }); - } else { - // rewrite all other modules in this chunk as proxies for - // Globalize - fnContent = "module.exports = __webpack_require__(" + globalizeModuleIds[0] + ");"; - } + const newModules = chunk.modules.map((module) => { + let fnContent; + if (module === chunk.entryModule) { + // rewrite entry module to contain the globalize-compiled-data + const locale = chunk.name.replace("globalize-compiled-data-", ""); + fnContent = globalizeCompilerHelper.compile(locale) + .replace("typeof define === \"function\" && define.amd", "false") + .replace(/require\("([^)]+)"\)/g, (garbage, moduleName) => { + return `__webpack_require__(${globalizeModuleIdsMap[moduleName]})`; + }); + } else { + // rewrite all other modules in this chunk as proxies for + // Globalize + fnContent = `module.exports = __webpack_require__(${globalizeModuleIds[0]});`; + } - // The `module` object in scope here is in each locale chunk, and - // any modifications we make will be rendered into every locale - // chunk. Create a new module to contain the locale-specific source - // modifications we've made. - var newModule = new RawModule(fnContent); - newModule.context = module.context; - newModule.id = module.id; - newModule.dependencies = module.dependencies; - return newModule; - }); + // The `module` object in scope here is in each locale chunk, and + // any modifications we make will be rendered into every locale + // chunk. Create a new module to contain the locale-specific source + // modifications we've made. + const newModule = new RawModule(fnContent); + newModule.context = module.context; + newModule.id = module.id; + newModule.dependencies = module.dependencies; + return newModule; + }); - // remove old modules with modified clones - // chunk.removeModule doesn't always find the module to remove - // ¯\_(ツ)_/¯, so we have to be be a bit more thorough here. - chunk.modules.forEach(function(module) { - module.removeChunk(chunk); - }); - chunk.modules = []; + // remove old modules with modified clones + // chunk.removeModule doesn't always find the module to remove + // ¯\_(ツ)_/¯, so we have to be be a bit more thorough here. + chunk.modules.forEach((module) => module.removeChunk(chunk)); + chunk.modules = []; - // install the rewritten modules - newModules.forEach(function(module) { - chunk.addModule(module); + // install the rewritten modules + newModules.forEach((module) => chunk.addModule(module)); }); - }); - }); + }); - // Set the right chunks order. The globalize-compiled-data chunks must - // appear after globalize runtime modules, but before any app code. - compilation.plugin("optimize-chunk-order", function(chunks) { - var cachedChunkScore = {}; - function moduleScore(module) { - if (module.request && util.isGlobalizeRuntimeModule(module.request)) { - return 1; - } else if (module.request && globalizeCompilerHelper.isCompiledDataModule(module.request)) { - return 0; + // Set the right chunks order. The globalize-compiled-data chunks must + // appear after globalize runtime modules, but before any app code. + compilation.plugin("optimize-chunk-order", (chunks) => { + const cachedChunkScore = {}; + function moduleScore(module) { + if (module.request && util.isGlobalizeRuntimeModule(module.request)) { + return 1; + } else if (module.request && globalizeCompilerHelper.isCompiledDataModule(module.request)) { + return 0; + } + return -1; } - return -1; - } - function chunkScore(chunk) { - if (!cachedChunkScore[chunk.name]) { - cachedChunkScore[chunk.name] = chunk.modules.reduce(function(sum, module) { - var score = moduleScore(module); - return Math.max(sum, score); - }, -1); + function chunkScore(chunk) { + if (!cachedChunkScore[chunk.name]) { + cachedChunkScore[chunk.name] = chunk.modules.reduce((sum, module) => { + return Math.max(sum, moduleScore(module)); + }, -1); + } + return cachedChunkScore[chunk.name]; } - return cachedChunkScore[chunk.name]; - } - chunks.sort(function(a, b) { - return chunkScore(a) - chunkScore(b); + chunks.sort((a, b) => chunkScore(a) - chunkScore(b)); }); }); - }); - compiler.plugin("compilation", function(compilation, params) { - params.normalModuleFactory.plugin("parser", bindParser); - }); -}; + compiler.plugin("compilation", (compilation, params) => { + params.normalModuleFactory.plugin("parser", bindParser); + }); + } +} module.exports = ProductionModePlugin; diff --git a/index.js b/index.js index 5adcd1b..9f4d562 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,7 @@ -var DevelopmentModePlugin = require("./DevelopmentModePlugin"); -var ProductionModePlugin = require("./ProductionModePlugin"); +"use strict"; + +const DevelopmentModePlugin = require("./DevelopmentModePlugin"); +const ProductionModePlugin = require("./ProductionModePlugin"); /** * Development Mode: @@ -11,16 +13,18 @@ var ProductionModePlugin = require("./ProductionModePlugin"); * - Statically extracts formatters and parsers from user code and pre-compile * them into respective XXXX. */ -function GlobalizePlugin(attributes) { - this.attributes = attributes || {}; -} +class GlobalizePlugin { + constructor(attributes) { + this.attributes = attributes || {}; + } -GlobalizePlugin.prototype.apply = function(compiler) { - compiler.apply( - this.attributes.production ? - new ProductionModePlugin(this.attributes) : - new DevelopmentModePlugin(this.attributes) - ); -}; + apply(compiler) { + compiler.apply( + this.attributes.production ? + new ProductionModePlugin(this.attributes) : + new DevelopmentModePlugin(this.attributes) + ); + } +} module.exports = GlobalizePlugin; diff --git a/package.json b/package.json index 3458773..48a1314 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "globalize-webpack-plugin", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "description": "Globalize.js webpack plugin", "main": "index.js", "files": [ @@ -13,6 +13,9 @@ "pretest": "npm run lint", "lint": "eslint *js test" }, + "engines": { + "node": ">=4.3.0" + }, "repository": { "type": "git", "url": "git@github.com:rxaviers/globalize-webpack-plugin.git" @@ -35,7 +38,7 @@ "peerDependencies": { "cldr-data": ">=25", "globalize": "^1.1.0-rc.5 <1.3.0", - "webpack": "^2.5.1" + "webpack": "^2.2.0-rc" }, "devDependencies": { "chai": "^3.5.0", diff --git a/test/ProductionModePlugin.js b/test/ProductionModePlugin.js index db53771..047313e 100644 --- a/test/ProductionModePlugin.js +++ b/test/ProductionModePlugin.js @@ -1,102 +1,98 @@ -var expect = require("chai").expect; -var fs = require("fs"); -var GlobalizePlugin = require("../index"); -var mkdirp = require("mkdirp"); -var path = require("path"); -var rimraf = require("rimraf"); -var webpack = require("webpack"); - -var TEST_CASES = { +"use strict"; + +const expect = require("chai").expect; +const fs = require("fs"); +const GlobalizePlugin = require("../index"); +const mkdirp = require("mkdirp"); +const path = require("path"); +const rimraf = require("rimraf"); +const webpack = require("webpack"); + +const TEST_CASES = { default: [], named: [ new webpack.NamedModulesPlugin() ], hashed: [ new webpack.HashedModuleIdsPlugin() ] }; -function outputPath(key, file) { - return path.join(__dirname, "../_test-output", key, file || ""); -} - -function mkWebpackConfig(key) { - return { - entry: { - app: path.join(__dirname, "fixtures/app") - }, - output: { - path: outputPath(key), - filename: "app.js" - }, - plugins: TEST_CASES[key].concat([ - new GlobalizePlugin({ - production: true, - developmentLocale: "en", - supportedLocales: ["en", "es"], - messages: path.join(__dirname, "fixtures/translations/[locale].json"), - output: "[locale].js" - }), - new webpack.optimize.CommonsChunkPlugin({ - name: "vendor", - filename: "vendor.js", - minChunks: function(module) { - var nodeModules = path.resolve(__dirname, "../node_modules"); - return module.request && module.request.startsWith(nodeModules); - } - }), - new webpack.optimize.CommonsChunkPlugin({ - name: "runtime", - filename: "runtime.js", - minChunks: Infinity - }) - ]) - }; -} - -function promisefiedWebpack(config) { - return new Promise(function(resolve, reject) { - webpack(config, function(error, stats) { - if (error) { - return reject(error); +const outputPath = (key, file) => path.join(__dirname, "../_test-output", key, file || ""); + +const mkWebpackConfig = (key) => ({ + entry: { + app: path.join(__dirname, "fixtures/app") + }, + output: { + path: outputPath(key), + filename: "app.js" + }, + plugins: TEST_CASES[key].concat([ + new GlobalizePlugin({ + production: true, + developmentLocale: "en", + supportedLocales: ["en", "es"], + messages: path.join(__dirname, "fixtures/translations/[locale].json"), + output: "[locale].js" + }), + new webpack.optimize.CommonsChunkPlugin({ + name: "vendor", + filename: "vendor.js", + minChunks: (module) => { + const nodeModules = path.resolve(__dirname, "../node_modules"); + return module.request && module.request.startsWith(nodeModules); } - return resolve(stats); - }); + }), + new webpack.optimize.CommonsChunkPlugin({ + name: "runtime", + filename: "runtime.js", + minChunks: Infinity + }) + ]) +}); + +const promisefiedWebpack = (config) => new Promise((resolve, reject) => { + webpack(config, (error, stats) => { + if (error) { + return reject(error); + } + return resolve(stats); }); -} - -describe("Globalize Webpack Plugin", function() { - describe("Production Mode", function() { - Object.keys(TEST_CASES).forEach(function(key) { - describe(`when using ${key} module ids`, function() { - var webpackConfig = mkWebpackConfig(key); - var myOutputPath = outputPath(key); - var compileStats; - - before(function(done) { - rimraf(myOutputPath, function() { +}); + +describe("Globalize Webpack Plugin", () => { + describe("Production Mode", () => { + Object.keys(TEST_CASES).forEach((key) => { + describe(`when using ${key} module ids`, () => { + const webpackConfig = mkWebpackConfig(key); + const myOutputPath = outputPath(key); + let compileStats; + + before((done) => { + rimraf(myOutputPath, () => { mkdirp.sync(myOutputPath); promisefiedWebpack(webpackConfig) .catch(done) - .then(function (stats) { + .then((stats) => { compileStats = stats; done(); }); }); }); - it("should extract formatters and parsers from basic code", function() { - var outputFilepath = path.join(myOutputPath, "en.js"); - var outputFileExists = fs.existsSync(outputFilepath); + it("should extract formatters and parsers from basic code", () => { + const outputFilepath = path.join(myOutputPath, "en.js"); + const outputFileExists = fs.existsSync(outputFilepath); expect(outputFileExists).to.be.true; - var content = fs.readFileSync(outputFilepath).toString(); + const content = fs.readFileSync(outputFilepath).toString(); expect(content).to.be.a("string"); }); - describe("The compiled bundle", function() { - var Globalize; + describe("The compiled bundle", () => { + let Globalize; - before(function() { + before(() => { global.window = global; // Hack: Expose __webpack_require__. - var runtimeFilePath = outputPath(key, "runtime.js"); - var runtimeContent = fs.readFileSync(runtimeFilePath).toString(); + const runtimeFilePath = outputPath(key, "runtime.js"); + const runtimeContent = fs.readFileSync(runtimeFilePath).toString(); fs.writeFileSync(runtimeFilePath, runtimeContent.replace(/(function __webpack_require__\(moduleId\) {)/, "window.__webpack_require__ = $1")); // Hack2: Load compiled Globalize @@ -105,70 +101,70 @@ describe("Globalize Webpack Plugin", function() { require(outputPath(key, "en")); require(outputPath(key, "app")); - var globalizeModuleStats = compileStats.toJson().modules.find(function (module) { + const globalizeModuleStats = compileStats.toJson().modules.find((module) => { return module.name === "./~/globalize/dist/globalize-runtime.js"; }); Globalize = global.__webpack_require__(globalizeModuleStats.id); }); - after(function() { + after(() => { delete global.window; delete global.webpackJsonp; }); - it("should render locale chunk with correct entry module", function() { - var enFilePath = outputPath(key, "en.js"); - var enContent = fs.readFileSync(enFilePath).toString(); - var enChunkLastLine = enContent.split(/\n/).pop(); + it("should render locale chunk with correct entry module", () => { + const enFilePath = outputPath(key, "en.js"); + const enContent = fs.readFileSync(enFilePath).toString(); + const enChunkLastLine = enContent.split(/\n/).pop(); - var statsJson = compileStats.toJson(); - var compiledDataModuleStats = statsJson.modules.find(function (module) { + const statsJson = compileStats.toJson(); + const compiledDataModuleStats = statsJson.modules.find((module) => { return module.source && module.source.match(/Globalize.locale\("en"\); return Globalize;/); }); expect(enChunkLastLine).to.contain(compiledDataModuleStats.id); }); - it("should include formatDate", function() { - var result = Globalize.formatDate(new Date(2017, 3, 15), {datetime: "medium"}); + 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. expect(result).to.have.string("Apr"); expect(result).to.have.string("2017"); }); - it("should include formatNumber", function() { - var result = Globalize.formatNumber(Math.PI); + it("should include formatNumber", () => { + const result = Globalize.formatNumber(Math.PI); expect(result).to.equal("3.142"); }); - it("should include formatCurrency", function() { - var result = Globalize.formatCurrency(69900, "USD"); + it("should include formatCurrency", () => { + const result = Globalize.formatCurrency(69900, "USD"); expect(result).to.equal("$69,900.00"); }); - it("should include formatMessage", function() { - var result = Globalize.formatMessage("like", 0); + it("should include formatMessage", () => { + const result = Globalize.formatMessage("like", 0); expect(result).to.equal("Be the first to like this"); }); - it("should include formatRelativeTime", function() { - var result = Globalize.formatRelativeTime(1, "second"); + it("should include formatRelativeTime", () => { + const result = Globalize.formatRelativeTime(1, "second"); expect(result).to.equal("in 1 second"); }); - it("should include formatUnit", function() { - var result = Globalize.formatUnit(60, "mile/hour", {form: "short"}); + it("should include formatUnit", () => { + const result = Globalize.formatUnit(60, "mile/hour", {form: "short"}); expect(result).to.equal("60 mph"); }); - it("should include parseNumber", function() { - var result = Globalize.parseNumber("1,234.56"); + it("should include parseNumber", () => { + const result = Globalize.parseNumber("1,234.56"); expect(result).to.equal(1234.56); }); - it("should include parseDate", function() { - var result = Globalize.parseDate("1/2/1982"); + it("should include parseDate", () => { + const result = Globalize.parseDate("1/2/1982"); expect(result.getFullYear()).to.equal(1982); expect(result.getMonth()).to.equal(0); expect(result.getDate()).to.equal(2); diff --git a/test/fixtures/app.js b/test/fixtures/app.js index caea4a9..9da75ac 100644 --- a/test/fixtures/app.js +++ b/test/fixtures/app.js @@ -1,5 +1,7 @@ -var like; -var Globalize = require( "globalize" ); +"use strict"; + +const Globalize = require( "globalize" ); +let like; // Use Globalize to format dates. Globalize.formatDate( new Date(), { datetime: "medium" } ); diff --git a/test/index.js b/test/index.js index 8e5f903..d14df80 100644 --- a/test/index.js +++ b/test/index.js @@ -1,3 +1,5 @@ -var chai = require("chai"); -var chaiAsPromised = require("chai-as-promised"); +"use strict"; + +const chai = require("chai"); +const chaiAsPromised = require("chai-as-promised"); chai.use(chaiAsPromised); diff --git a/util.js b/util.js index 2df56b7..1318a5a 100644 --- a/util.js +++ b/util.js @@ -1,12 +1,14 @@ -var cldrData = require("cldr-data"); -var fs = require("fs"); -var path = require("path"); +"use strict"; -var mainFiles = ["ca-gregorian", "currencies", "dateFields", "numbers", "timeZoneNames", "units"]; +const cldrData = require("cldr-data"); +const fs = require("fs"); +const path = require("path"); -var isGlobalizeModule = function(filepath) { +const mainFiles = ["ca-gregorian", "currencies", "dateFields", "numbers", "timeZoneNames", "units"]; + +const isGlobalizeModule = (filepath) => { filepath = filepath.split( /[\/\\]/ ); - var i = filepath.lastIndexOf("globalize"); + const i = filepath.lastIndexOf("globalize"); // 1: path should contain "globalize", // 2: and it should appear either in the end (e.g., ../globalize) or right // before it (e.g., ../globalize/date). @@ -14,18 +16,18 @@ var isGlobalizeModule = function(filepath) { }; module.exports = { - cldr: function(locale) { - return cldrData.entireSupplemental().concat(mainFiles.map(function(mainFile) { + cldr: (locale) => { + return cldrData.entireSupplemental().concat(mainFiles.map((mainFile) => { return cldrData(path.join("main", locale, mainFile)); })); }, isGlobalizeModule: isGlobalizeModule, - isGlobalizeRuntimeModule: function(filepath) { + isGlobalizeRuntimeModule: (filepath) => { filepath = filepath.split( /[\/\\]/ ); - var i = filepath.lastIndexOf("globalize-runtime"); - var j = filepath.lastIndexOf("globalize-runtime.js"); + const i = filepath.lastIndexOf("globalize-runtime"); + const j = filepath.lastIndexOf("globalize-runtime.js"); // Either (1 and 2) or (3 and 4): // 1: path should contain "globalize-runtime", // 2: and it should appear right before it (e.g., ../globalize-runtime/date). @@ -35,19 +37,17 @@ module.exports = { (j !== -1 /* 3 */ && filepath.length - j === 1 /* 4 */); }, - moduleFilterFn: function(moduleFilter) { - return function(filepath) { - var globalizeModule = isGlobalizeModule(filepath); + moduleFilterFn: (moduleFilter) => (filepath) => { + const globalizeModule = isGlobalizeModule(filepath); - if (moduleFilter) { - return !(globalizeModule || moduleFilter(filepath)); - } else { - return !globalizeModule; - } - }; + if (moduleFilter) { + return !(globalizeModule || moduleFilter(filepath)); + } else { + return !globalizeModule; + } }, - readMessages: function(messagesFilepath, locale) { + readMessages: (messagesFilepath, locale) => { messagesFilepath = messagesFilepath.replace("[locale]", locale); if (!fs.existsSync(messagesFilepath) || !fs.statSync(messagesFilepath).isFile()) { console.warn("Unable to find messages file: `" + messagesFilepath + "`"); @@ -56,9 +56,8 @@ module.exports = { return JSON.parse(fs.readFileSync(messagesFilepath)); }, - tmpdir: function() { - var tmpdir = path.resolve("./.tmp-globalize-webpack"); - + tmpdir: () => { + const tmpdir = path.resolve("./.tmp-globalize-webpack"); if (!fs.existsSync(tmpdir)) { fs.mkdirSync(tmpdir); } else { @@ -70,7 +69,5 @@ module.exports = { return tmpdir; }, - escapeRegex: function(string) { - return string.replace(/(?=[\/\\^$*+?.()|{}[\]])/g, "\\"); - } + escapeRegex: (string) => string.replace(/(?=[\/\\^$*+?.()|{}[\]])/g, "\\") };