UNPKG

ember-cli-amd

Version:

Ember CLI Addon that can load AMD modules.

237 lines (195 loc) 8.54 kB
// Copyright 2015 Esri // Licensed under The MIT License(MIT); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://opensource.org/licenses/MIT // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /* eslint-env node */ '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 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']; // Class for replacing, in the generated code, the AMD protected keyword 'require' and 'define'. // We are replacing these keywords by non conflicting words. // It uses the broccoli filter to go thru the different files (as string). module.exports = class ConvertToAMD extends Filter { constructor(inputTree, options = {}) { super(inputTree, {}); 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.inline = !!options.amdOptions.inline; // Because the filter is call for partial rebuild during 'ember serve', we need to // know what was added/removed for a partial build this.externalAmdModules = new Set(); this.externalAmdModulesCache = new Map(); // There are two index files that should be converted: // - index.html // - tests/index.html // We need to keep things separated as they don't load the same script set. this.indexHtmlCaches = { 'index.html': { scriptsToLoad: [], loadingScript: this.rootURL + this.loadingFilePath + '/amd-loading.js', afterLoadingScript: this.rootURL + this.loadingFilePath + '/after-amd-loading.js' }, 'tests/index.html': { scriptsToLoad: [], loadingScript: this.rootURL + this.loadingFilePath + '/amd-loading-tests.js', afterLoadingScript: this.rootURL + this.loadingFilePath + '/after-amd-loading-tests.js' } }; } getDestFilePath(relativePath) { relativePath = super.getDestFilePath(relativePath); if (!relativePath) { return null; } if (relativePath.indexOf('index.html') >= 0) { return relativePath; } for (let i = 0, len = this.excludePaths.length; i < len; i++) { if (relativePath.indexOf(this.excludePaths[i]) === 0) { return null; } } if (relativePath.indexOf('.js') >= 0) { return relativePath; } return null; } processString(code, relativePath) { if (relativePath.indexOf('.js') >= 0) { return this._processJsFile(code, relativePath); } return this._processIndexFile(code, relativePath); } _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 scriptsToLoad = []; const inlineScripts = []; scriptElements.each(function () { if (cheerioQuery(this).attr('src')) { scriptsToLoad.push(`"${cheerioQuery(this).attr('src')}"`); } else { inlineScripts.push(cheerioQuery(this).html()); } }); this.indexHtmlCaches[relativePath].scriptsToLoad = scriptsToLoad; // 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.indexHtmlCaches[relativePath].afterLoadingScript}"`); } // Replace the original ember scripts by the amd ones scriptElements.remove(); // Beautify the index.html return beautify_html(cheerioQuery.html(), { indent_size: 2, max_preserve_newlines: 0 }); } _processJsFile(code, relativePath) { const externalAmdModulesForFile = new Set(); const modifiedSource = replaceRequireAndDefine(code, this.amdPackages, externalAmdModulesForFile); // Bookkeeping of what has changed for this file compared to previous builds if (externalAmdModulesForFile.size === 0) { // No more AMD references this.externalAmdModulesCache.delete(relativePath); } else { // Replace with the new set this.externalAmdModulesCache.set(relativePath, externalAmdModulesForFile); } return modifiedSource; } _buildModuleInfos() { // Build different arrays representing the modules for the injection in the start script const objs = []; const names = []; const adoptables = []; let index = 0; this.externalAmdModules.forEach((externalAmdModule) => { objs.push(`mod${index}`); names.push(`'${externalAmdModule}'`); adoptables.push(`{name:'${externalAmdModule}',obj:mod${index}}`); index++; }); return { 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. this.externalAmdModules.clear(); const result = await super.build(); // Re-assemble the external AMD modules set with the updated cache 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 => { 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 return; } // 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 = [ `<script src="${this.loader}" data-amd=true></script>` ]; 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(`<script>${loadingScript}</script>`); } else { // Add a script tag to index.html to load the amd-loading script, and write the script to the output directory amdScripts.push(`<script src="${this.indexHtmlCaches[indexFile].loadingScript}" data-amd-loading=true></script>`); 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); } cheerioQuery('body').prepend(amdScripts.join('\n')); const html = beautify_html(cheerioQuery.html(), { indentSize: 2, max_preserve_newlines: 0 }); fs.writeFileSync(indexPath, html); }); return result; } }