From f6f08d43d443c398ddb2aa0333317133f0f35f24 Mon Sep 17 00:00:00 2001 From: Stephen Mathieson Date: Tue, 31 Jul 2018 10:00:00 -0400 Subject: [PATCH] feat(imports): add `doT` template engine (#1024) 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 regardless of environment (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 | 74 +++++++++++++++++-- .../full/umd/umd-module-exports.js | 6 ++ 3 files changed, 79 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..277934c09e 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,45 @@ 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. + // + // 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["${libraryGlobal}"]; + + ${sourceCode} + + // Preserve a reference to the library and remove it from + // the global scope. + var lib = global["${libraryGlobal}"]; + delete global["${libraryGlobal}"]; + + // Reset a previous global when applicable. + if (__old_global__) { + global["${libraryGlobal}"] = __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 +123,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..3de63c54ef 100644 --- a/test/integration/full/umd/umd-module-exports.js +++ b/test/integration/full/umd/umd-module-exports.js @@ -9,4 +9,10 @@ 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'); + assert.equal(doT.name, 'doT'); + }); });