diff --git a/lib/convert-to-amd-filter.js b/lib/convert-to-amd-filter.js index da31aaf..54b2328 100644 --- a/lib/convert-to-amd-filter.js +++ b/lib/convert-to-amd-filter.js @@ -10,20 +10,22 @@ // limitations under the License. /* eslint-env node */ -'use strict'; +"use strict"; -const fs = require('fs'); -const path = require('path'); -const Filter = require('broccoli-filter'); -const cheerio = require('cheerio'); -const beautify_js = require('js-beautify'); -const beautify_html = require('js-beautify').html; -const _ = require('lodash'); +const fs = require("fs"); +const path = require("path"); +const Filter = require("broccoli-filter"); +const cheerio = require("cheerio"); +const beautify_js = require("js-beautify"); +const beautify_html = require("js-beautify").html; +const _ = require("lodash"); -const replaceRequireAndDefine = require('./replace-require-and-define'); +const replaceRequireAndDefine = require("./replace-require-and-define"); -const amdLoadingTemplate = _.template(fs.readFileSync(path.join(__dirname, 'amd-loading.txt'), 'utf8')); -const indexFiles = ['index.html', 'tests/index.html']; +const amdLoadingTemplate = _.template( + fs.readFileSync(path.join(__dirname, "amd-loading.txt"), "utf8") +); +const indexFiles = ["index.html", "tests/index.html"]; // Class for replacing, in the generated code, the AMD protected keyword 'require' and 'define'. // We are replacing these keywords by non conflicting words. @@ -32,14 +34,16 @@ module.exports = class ConvertToAMD extends Filter { constructor(inputTree, options = {}) { super(inputTree, {}); - this.extensions = ['js', 'html']; + this.extensions = ["js", "html"]; // Options for the process this.loader = options.amdOptions.loader; this.amdPackages = options.amdOptions.packages || []; this.excludePaths = options.amdOptions.excludePaths; - this.loadingFilePath = (options.amdOptions.loadingFilePath || 'assets').replace(/\/$/, ""); - this.rootURL = options.rootURL || ''; + this.loadingFilePath = ( + options.amdOptions.loadingFilePath || "assets" + ).replace(/\/$/, ""); + this.rootURL = options.rootURL || ""; this.inline = !!options.amdOptions.inline; // Because the filter is call for partial rebuild during 'ember serve', we need to @@ -52,16 +56,17 @@ module.exports = class ConvertToAMD extends Filter { // - tests/index.html // We need to keep things separated as they don't load the same script set. this.indexHtmlCaches = { - 'index.html': { + "index.html": { scriptsToLoad: [], - loadingScript: this.loadingFilePath + '/amd-loading.js', - afterLoadingScript: this.loadingFilePath + '/after-amd-loading.js' + loadingScript: this.loadingFilePath + "/amd-loading.js", + afterLoadingScript: this.loadingFilePath + "/after-amd-loading.js", }, - 'tests/index.html': { + "tests/index.html": { scriptsToLoad: [], - loadingScript: this.loadingFilePath + '/amd-loading-tests.js', - afterLoadingScript: this.loadingFilePath + '/after-amd-loading-tests.js' - } + loadingScript: this.loadingFilePath + "/amd-loading-tests.js", + afterLoadingScript: + this.loadingFilePath + "/after-amd-loading-tests.js", + }, }; } @@ -71,7 +76,7 @@ module.exports = class ConvertToAMD extends Filter { return null; } - if (relativePath.indexOf('index.html') >= 0) { + if (relativePath.indexOf("index.html") >= 0) { return relativePath; } @@ -81,7 +86,7 @@ module.exports = class ConvertToAMD extends Filter { } } - if (relativePath.indexOf('.js') >= 0) { + if (relativePath.indexOf(".js") >= 0) { return relativePath; } @@ -89,7 +94,7 @@ module.exports = class ConvertToAMD extends Filter { } processString(code, relativePath) { - if (relativePath.indexOf('.js') >= 0) { + if (relativePath.indexOf(".js") >= 0) { return this._processJsFile(code, relativePath); } @@ -97,18 +102,17 @@ module.exports = class ConvertToAMD extends Filter { } _processIndexFile(code, relativePath) { - const cheerioQuery = cheerio.load(code); // Get the collection of scripts // Scripts that have a 'src' will be loaded by AMD // Scripts that have a body will be assembled into a post loading file and loaded at the end of the AMD loading process - const scriptElements = cheerioQuery('body > script'); + const scriptElements = cheerioQuery("body > script"); const scriptsToLoad = []; const inlineScripts = []; scriptElements.each(function () { - if (cheerioQuery(this).attr('src')) { - scriptsToLoad.push(`"${cheerioQuery(this).attr('src')}"`); + if (cheerioQuery(this).attr("src")) { + scriptsToLoad.push(`"${cheerioQuery(this).attr("src")}"`); } else { inlineScripts.push(cheerioQuery(this).html()); } @@ -119,11 +123,16 @@ module.exports = class ConvertToAMD extends Filter { // If we have inline scripts, we will save them into a script file and load it as part of the amd loading this.indexHtmlCaches[relativePath].afterLoadingCode = undefined; if (inlineScripts.length > 0) { - this.indexHtmlCaches[relativePath].afterLoadingCode = beautify_js(replaceRequireAndDefine(inlineScripts.join('\n\n')), { - indent_size: 2, - max_preserve_newlines: 1 - }); - scriptsToLoad.push(`"${this.rootURL}${this.indexHtmlCaches[relativePath].afterLoadingScript}"`); + this.indexHtmlCaches[relativePath].afterLoadingCode = beautify_js( + replaceRequireAndDefine(inlineScripts.join("\n\n")), + { + indent_size: 2, + max_preserve_newlines: 1, + } + ); + scriptsToLoad.push( + `"${this.rootURL}${this.indexHtmlCaches[relativePath].afterLoadingScript}"` + ); } // Replace the original ember scripts by the amd ones @@ -132,14 +141,18 @@ module.exports = class ConvertToAMD extends Filter { // Beautify the index.html return beautify_html(cheerioQuery.html(), { indent_size: 2, - max_preserve_newlines: 0 + max_preserve_newlines: 0, }); } _processJsFile(code, relativePath) { - const externalAmdModulesForFile = new Set(); - const modifiedSource = replaceRequireAndDefine(code, this.amdPackages, externalAmdModulesForFile); + const modifiedSource = replaceRequireAndDefine( + code, + this.amdPackages, + externalAmdModulesForFile, + relativePath.includes("vendor.js") + ); // Bookkeeping of what has changed for this file compared to previous builds if (externalAmdModulesForFile.size === 0) { @@ -154,7 +167,6 @@ module.exports = class ConvertToAMD extends Filter { } _buildModuleInfos() { - // Build different arrays representing the modules for the injection in the start script const objs = []; const names = []; @@ -168,14 +180,13 @@ module.exports = class ConvertToAMD extends Filter { }); return { - names: names.join(','), - objects: objs.join(','), - adoptables: adoptables.join(',') + names: names.join(","), + objects: objs.join(","), + adoptables: adoptables.join(","), }; } async build() { - // Clear before each build since the filter is kept by ember-cli during 'ember serve' // and being reused without going thru postProcessTree. If we don't clean we may get // previous modules. @@ -184,16 +195,15 @@ module.exports = class ConvertToAMD extends Filter { const result = await super.build(); // Re-assemble the external AMD modules set with the updated cache - this.externalAmdModulesCache.forEach(externalAmdModules => { - externalAmdModules.forEach(externalAmdModule => { + this.externalAmdModulesCache.forEach((externalAmdModules) => { + externalAmdModules.forEach((externalAmdModule) => { this.externalAmdModules.add(externalAmdModule); }); }); // Write the various script files we need const moduleInfos = this._buildModuleInfos(); - indexFiles.forEach(indexFile => { - + indexFiles.forEach((indexFile) => { const indexPath = path.join(this.outputPath, indexFile); if (!fs.existsSync(indexPath)) { // When building for production, tests/index.html will not exist, so we can skip its loading scripts @@ -202,39 +212,58 @@ module.exports = class ConvertToAMD extends Filter { // We add scripts to each index.html file to kick off the loading of amd modules. const cheerioQuery = cheerio.load(fs.readFileSync(indexPath)); - const amdScripts = [ `` ]; - const scripts = this.indexHtmlCaches[indexFile].scriptsToLoad.join(','); - const loadingScript = beautify_js(amdLoadingTemplate(_.assign(moduleInfos, { scripts })), { - indent_size: 2, - max_preserve_newlines: 1 - }); + const amdScripts = [ + ``, + ]; + const scripts = this.indexHtmlCaches[indexFile].scriptsToLoad.join(","); + const loadingScript = beautify_js( + amdLoadingTemplate(_.assign(moduleInfos, { scripts })), + { + indent_size: 2, + max_preserve_newlines: 1, + } + ); if (this.inline) { // Inline the amd-loading script directly in index.html amdScripts.push(``); } else { // Add a script tag to index.html to load the amd-loading script, and write the script to the output directory - amdScripts.push(``); - fs.writeFileSync(path.join(this.outputPath, this.indexHtmlCaches[indexFile].loadingScript), loadingScript); + amdScripts.push( + `` + ); + fs.writeFileSync( + path.join( + this.outputPath, + this.indexHtmlCaches[indexFile].loadingScript + ), + loadingScript + ); } // After loading script if (this.indexHtmlCaches[indexFile].afterLoadingCode) { - fs.writeFileSync(path.join(this.outputPath, this.indexHtmlCaches[indexFile].afterLoadingScript), this.indexHtmlCaches[indexFile].afterLoadingCode); + fs.writeFileSync( + path.join( + this.outputPath, + this.indexHtmlCaches[indexFile].afterLoadingScript + ), + this.indexHtmlCaches[indexFile].afterLoadingCode + ); } // Remove from body previous script tags (case for ember serve). - cheerioQuery('body > script').remove(); - + cheerioQuery("body > script").remove(); + // Write the new script tags - cheerioQuery('body').prepend(amdScripts.join('\n')); + cheerioQuery("body").prepend(amdScripts.join("\n")); const html = beautify_html(cheerioQuery.html(), { indentSize: 2, - max_preserve_newlines: 0 + max_preserve_newlines: 0, }); fs.writeFileSync(indexPath, html); }); return result; } -} +}; diff --git a/lib/replace-require-and-define.js b/lib/replace-require-and-define.js index 03fd46e..a089a82 100644 --- a/lib/replace-require-and-define.js +++ b/lib/replace-require-and-define.js @@ -10,30 +10,31 @@ // limitations under the License. /* eslint-env node */ -'use strict'; +"use strict"; -const { - parse -} = require('@babel/parser'); -const traverse = require('@babel/traverse').default; -const t = require('@babel/types'); -const generator = require('@babel/generator').default; +const { parse } = require("@babel/parser"); +const traverse = require("@babel/traverse").default; +const t = require("@babel/types"); +const generator = require("@babel/generator").default; // Replace indentifier const IdentifierMap = { - 'require': 'eriuqer', - 'define': 'enifed' + require: "eriuqer", + define: "enifed", }; const Identifiers = Object.keys(IdentifierMap); - // Use babel to parse/traverse/generate code. // - replace define and require with enifed and eriuqer // - if required, collect the external AMD modules referenced -module.exports = function replaceRequireAndDefine(code, amdPackages, externalAmdModules) { - +module.exports = function replaceRequireAndDefine( + code, + amdPackages, + externalAmdModules, + handleGlobalObj = false +) { const ast = parse(code); traverse(ast, { @@ -42,36 +43,39 @@ module.exports = function replaceRequireAndDefine(code, amdPackages, externalAmd // This will take care of the loader.js code where define and require are define globally // The cool thing is that babel will rename all references as well // Rename at the end so we don't overlap with the CallExpression visitor - Identifiers.forEach(identifier => path.scope.rename(identifier, IdentifierMap[identifier])); - } + Identifiers.forEach((identifier) => + path.scope.rename(identifier, IdentifierMap[identifier]) + ); + }, }, CallExpression(path) { - // Looking for: // - call for the global define function. If it matches then rename it and collect the external AMD references // - call for the global require function. It it matches then rename it // - eval calls: We need to process the eval code itself (used by auto-import) - const { - node - } = path; + const { node } = path; // Collect external AMD references // Looking for define('foo', ['a', 'b'], function(a, b) {}) - if (amdPackages && + if ( + amdPackages && externalAmdModules && t.isIdentifier(node.callee, { - name: 'define' + name: "define", }) && node.arguments.length >= 2 && - t.isArrayExpression(node.arguments[1])) { - + t.isArrayExpression(node.arguments[1]) + ) { node.arguments[1].elements.forEach(function (element) { if (!t.isStringLiteral(element)) { return; } const isExternalAmd = amdPackages.some(function (amdPackage) { - return element.value.indexOf(amdPackage + '/') === 0 || element.value === amdPackage; + return ( + element.value.indexOf(amdPackage + "/") === 0 || + element.value === amdPackage + ); }); if (!isExternalAmd) { @@ -79,41 +83,85 @@ module.exports = function replaceRequireAndDefine(code, amdPackages, externalAmd } externalAmdModules.add(element.value); - }); } // auto-import injects eval expression. We need to process them as individual code - if (t.isIdentifier(node.callee, { - name: 'eval' - }) && t.isStringLiteral(node.arguments[0])) { - node.arguments[0].value = replaceRequireAndDefine(node.arguments[0].value, amdPackages, externalAmdModules); + if ( + t.isIdentifier(node.callee, { + name: "eval", + }) && + t.isStringLiteral(node.arguments[0]) + ) { + node.arguments[0].value = replaceRequireAndDefine( + node.arguments[0].value, + amdPackages, + externalAmdModules + ); } }, + MemberExpression(path) { + // Deal with ember loader changes + // Specifically look for globalObj.define or globalObj.require + // This is fragile but ember is such a mess by re-defining define and require! + if (!handleGlobalObj) { + return; + } + + if ( + !t.isIdentifier(path.node.object) || + path.node.object.name !== "globalObj" + ) { + return; + } + + if ( + !t.isIdentifier(path.node.property) || + !Identifiers.includes(path.node.property.name) + ) { + return; + } + + // Rename + path.node.property.name = IdentifierMap[path.node.property.name]; + }, Identifier(path) { // Only interested by our identifiers that have no bindings in the path scope - if (!Identifiers.includes(path.node.name) || path.scope.hasBinding(path.node.name)) { + if ( + !Identifiers.includes(path.node.name) || + path.scope.hasBinding(path.node.name) + ) { return; } // Avoid: foo.define/require - if (t.isMemberExpression(path.container) && path.container.property === path.node) { + if ( + t.isMemberExpression(path.container) && + path.container.property === path.node + ) { return; } // Avoid class properties/methods - if ((t.isClassMethod(path.container) || t.isClassProperty(path.container)) && path.container.key === path.node) { + if ( + (t.isClassMethod(path.container) || + t.isClassProperty(path.container)) && + path.container.key === path.node + ) { return; } // Rename path.node.name = IdentifierMap[path.node.name]; - } - + }, }); - return generator(ast, { - retainLines: true, - retainFunctionParens: true - }, code).code; -} + return generator( + ast, + { + retainLines: true, + retainFunctionParens: true, + }, + code + ).code; +};