From bbcf13a88ffd3e4542180a74285d164f5a07d210 Mon Sep 17 00:00:00 2001 From: Stephen Mathieson Date: Mon, 30 Jul 2018 13:02:26 -0400 Subject: [PATCH 1/3] feat(imports): add `doT` template engine This patch adds the [`doT`](https://npmjs.org/dot) template engine to `axe.imports`. This is required for a runtime localization (a WIP feature). A lot of hackery was required to get this working, since `doT` doesn't expose a UMD wrapper that our build system supports. Rather than attempting to get our existing tooling to "unwrap" `doT`'s custom UMD, I've decided to save a lot of time/effort and just wrap it in an IIFE. There are likely 1,000,000s of variations of UMD in the wild and it's unreasonable for us to attempt to support them all. The added IIFE method works in cases where the code functions exactly the same regarless of enviroment (this does not include libraries like Axios). IMHO the only reasonable solution to this problem is to use a proper bundler for building `axe-core`. --- Gruntfile.js | 7 +- build/tasks/generate-imports.js | 71 +++++++++++++++++-- .../full/umd/umd-module-exports.js | 11 +++ 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index eaecf21406..813b3e0cea 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -149,7 +149,12 @@ module.exports = function(grunt) { 'generate-imports': { // list of external dependencies, which needs to be added to axe.imports object data: { - axios: './node_modules/axios/dist/axios.js' + axios: './node_modules/axios/dist/axios.js', + doT: { + file: './node_modules/dot/doT.js', + umd: false, + global: 'doT' + } } }, 'aria-supported': { diff --git a/build/tasks/generate-imports.js b/build/tasks/generate-imports.js index 7978cae94d..adeba64c02 100644 --- a/build/tasks/generate-imports.js +++ b/build/tasks/generate-imports.js @@ -1,5 +1,6 @@ /*eslint-env node */ const UglifyJS = require('uglify-js'); +const assert = require('assert'); module.exports = grunt => { grunt.registerMultiTask( @@ -57,11 +58,18 @@ module.exports = grunt => { /** * Process a given library to unwrapped UMD module if exists, and return the factory * @param {string} libName name of the library - * @param {string} sourceUrl path to the distributable of the library + * @param {string} sourceCode source code for the library + * @param {Object} [options] Optional options + * @property {Boolean} umd Does the library contain a UMD wrapper + * @property {String} global The library's global (`window.myLibrary`) */ - const processImport = (libName, sourceUrl) => { - const sourceCode = grunt.file.read(sourceUrl); - if (hasUmdWrapper(sourceCode)) { + const processImport = (libName, sourceCode, options) => { + const hasUMD = options ? options.umd : hasUmdWrapper(sourceCode); + const global = options && options.global; + + if (hasUMD) { + // If the library has a "standard" UMD wrapper, we'll remove it + // and expose the library directly. unwrappedCode(sourceCode, (err, factory) => { if (err) { // running uglifyjs transform in a try block, this is to catch any errors from the transform. @@ -69,6 +77,42 @@ module.exports = grunt => { } writeLibrary(libName, factory); }); + } else if (global) { + // We wrap the library's source code in an IFFE which voids + // existing globals (module, define, process, etc.) forces and + // forces it to export a global. + // + // This method should only be used for "universal" code that + // follows the same code paths for all environments (Node, + // browser, etc). If there are different paths for different + // envs, the UMD method should be used instead. + const wrappedLibrary = ` + (function (module, exports, define, require, process) { + // Get a reference to the "true" global scope. This works in + // ES5's "strict mode", browsers, node.js and other environments. + var global = Function('return this')(); + + // If there was a global prior to our script, make sure we + // "save" it (think "$.noConflict()"). + var __old_global__ = global["${global}"]; + + ${sourceCode} + + // Preserve a reference to the library and remove it from + // the global scope. + var lib = global["${global}"]; + delete global["${global}"]; + + // Reset a previous global when applicable. + if (__old_global__) { + global["${global}"] = __old_global__; + } + + // Return the library to populate "axe.imports". + return lib; + })(); + `; + writeLibrary(libName, wrappedLibrary); } else { // assumption is that the library returns an IIFE writeLibrary(libName, sourceCode); @@ -76,9 +120,22 @@ module.exports = grunt => { }; // Iterate through each library to import and process the code - Object.keys(this.data).forEach(key => { - processImport(key, this.data[key]); - }); + for (const name in this.data) { + const val = this.data[name]; + if (typeof val === 'string') { + // Provided a path to a file with no options + const sourceCode = grunt.file.read(val); + processImport(name, sourceCode); + } else if (typeof val === 'object') { + // Provided an object with options + const { file, umd, global } = val; + assert(file, 'File required'); + const sourceCode = grunt.file.read(file); + processImport(name, sourceCode, { umd, global }); + } else { + grunt.fail.warn(`Unsupported generate-import: "${name}"`); + } + } } ); }; diff --git a/test/integration/full/umd/umd-module-exports.js b/test/integration/full/umd/umd-module-exports.js index 07f7ca90f1..0722d5637e 100644 --- a/test/integration/full/umd/umd-module-exports.js +++ b/test/integration/full/umd/umd-module-exports.js @@ -9,4 +9,15 @@ describe('UMD module.export', function() { it('should ensure axe source includes axios', function() { assert.isTrue(axe.source.includes(axe.imports.axios.toString())); }); + + it('should include doT', function() { + var doT = axe.imports.doT; + assert(doT, 'doT is registered on axe.imports'); + + var compileSrc = doT.compile.toString(); + var templateSrc = doT.template.toString(); + + assert(axe.source.includes(compileSrc), 'source includes doT.compile()'); + assert(axe.source.includes(templateSrc), 'source includes doT.template()'); + }); }); From 27edc009244e4ad00571b708ca621a396b004f11 Mon Sep 17 00:00:00 2001 From: Stephen Mathieson Date: Mon, 30 Jul 2018 14:00:13 -0400 Subject: [PATCH 2/3] test: simplify and fix doT assertions --- test/integration/full/umd/umd-module-exports.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/integration/full/umd/umd-module-exports.js b/test/integration/full/umd/umd-module-exports.js index 0722d5637e..3de63c54ef 100644 --- a/test/integration/full/umd/umd-module-exports.js +++ b/test/integration/full/umd/umd-module-exports.js @@ -13,11 +13,6 @@ describe('UMD module.export', function() { it('should include doT', function() { var doT = axe.imports.doT; assert(doT, 'doT is registered on axe.imports'); - - var compileSrc = doT.compile.toString(); - var templateSrc = doT.template.toString(); - - assert(axe.source.includes(compileSrc), 'source includes doT.compile()'); - assert(axe.source.includes(templateSrc), 'source includes doT.template()'); + assert.equal(doT.name, 'doT'); }); }); From 046d270557107cfd7d204040610d91741815672a Mon Sep 17 00:00:00 2001 From: Stephen Mathieson Date: Tue, 31 Jul 2018 09:46:01 -0400 Subject: [PATCH 3/3] build: make refactor var name (s/global/libraryGlobal/) --- build/tasks/generate-imports.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/build/tasks/generate-imports.js b/build/tasks/generate-imports.js index adeba64c02..277934c09e 100644 --- a/build/tasks/generate-imports.js +++ b/build/tasks/generate-imports.js @@ -78,6 +78,9 @@ module.exports = grunt => { writeLibrary(libName, factory); }); } else if (global) { + // The global variable exposed by the library. This is not necessarily the same as "libName". + const libraryGlobal = global; + // We wrap the library's source code in an IFFE which voids // existing globals (module, define, process, etc.) forces and // forces it to export a global. @@ -94,18 +97,18 @@ module.exports = grunt => { // If there was a global prior to our script, make sure we // "save" it (think "$.noConflict()"). - var __old_global__ = global["${global}"]; + var __old_global__ = global["${libraryGlobal}"]; ${sourceCode} // Preserve a reference to the library and remove it from // the global scope. - var lib = global["${global}"]; - delete global["${global}"]; + var lib = global["${libraryGlobal}"]; + delete global["${libraryGlobal}"]; // Reset a previous global when applicable. if (__old_global__) { - global["${global}"] = __old_global__; + global["${libraryGlobal}"] = __old_global__; } // Return the library to populate "axe.imports".