UNPKG

8.52 kBJavaScriptView Raw
1// Copyright 2015 Esri
2// Licensed under The MIT License(MIT);
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5// http://opensource.org/licenses/MIT
6// Unless required by applicable law or agreed to in writing, software
7// distributed under the License is distributed on an "AS IS" BASIS,
8// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9// See the License for the specific language governing permissions and
10// limitations under the License.
11
12/* eslint-env node */
13'use strict';
14
15const fs = require('fs');
16const path = require('path');
17const Filter = require('broccoli-filter');
18const cheerio = require('cheerio');
19const beautify_js = require('js-beautify');
20const beautify_html = require('js-beautify').html;
21const _ = require('lodash');
22
23const replaceRequireAndDefine = require('./replace-require-and-define');
24
25const amdLoadingTemplate = _.template(fs.readFileSync(path.join(__dirname, 'amd-loading.txt'), 'utf8'));
26const indexFiles = ['index.html', 'tests/index.html'];
27
28// Class for replacing, in the generated code, the AMD protected keyword 'require' and 'define'.
29// We are replacing these keywords by non conflicting words.
30// It uses the broccoli filter to go thru the different files (as string).
31module.exports = class ConvertToAMD extends Filter {
32 constructor(inputTree, options = {}) {
33 super(inputTree, {});
34
35 this.extensions = ['js', 'html'];
36
37 // Options for the process
38 this.loader = options.amdOptions.loader;
39 this.amdPackages = options.amdOptions.packages || [];
40 this.excludePaths = options.amdOptions.excludePaths;
41 this.loadingFilePath = (options.amdOptions.loadingFilePath || 'assets').replace(/\/$/, "");
42 this.rootURL = options.rootURL || '';
43 this.inline = !!options.amdOptions.inline;
44
45 // Because the filter is call for partial rebuild during 'ember serve', we need to
46 // know what was added/removed for a partial build
47 this.externalAmdModules = new Set();
48 this.externalAmdModulesCache = new Map();
49
50 // There are two index files that should be converted:
51 // - index.html
52 // - tests/index.html
53 // We need to keep things separated as they don't load the same script set.
54 this.indexHtmlCaches = {
55 'index.html': {
56 scriptsToLoad: [],
57 loadingScript: this.loadingFilePath + '/amd-loading.js',
58 afterLoadingScript: this.loadingFilePath + '/after-amd-loading.js'
59 },
60 'tests/index.html': {
61 scriptsToLoad: [],
62 loadingScript: this.loadingFilePath + '/amd-loading-tests.js',
63 afterLoadingScript: this.loadingFilePath + '/after-amd-loading-tests.js'
64 }
65 };
66 }
67
68 getDestFilePath(relativePath) {
69 relativePath = super.getDestFilePath(relativePath);
70 if (!relativePath) {
71 return null;
72 }
73
74 if (relativePath.indexOf('index.html') >= 0) {
75 return relativePath;
76 }
77
78 for (let i = 0, len = this.excludePaths.length; i < len; i++) {
79 if (relativePath.indexOf(this.excludePaths[i]) === 0) {
80 return null;
81 }
82 }
83
84 if (relativePath.indexOf('.js') >= 0) {
85 return relativePath;
86 }
87
88 return null;
89 }
90
91 processString(code, relativePath) {
92 if (relativePath.indexOf('.js') >= 0) {
93 return this._processJsFile(code, relativePath);
94 }
95
96 return this._processIndexFile(code, relativePath);
97 }
98
99 _processIndexFile(code, relativePath) {
100
101 const cheerioQuery = cheerio.load(code);
102
103 // Get the collection of scripts
104 // Scripts that have a 'src' will be loaded by AMD
105 // Scripts that have a body will be assembled into a post loading file and loaded at the end of the AMD loading process
106 const scriptElements = cheerioQuery('body > script');
107 const scriptsToLoad = [];
108 const inlineScripts = [];
109 scriptElements.each(function () {
110 if (cheerioQuery(this).attr('src')) {
111 scriptsToLoad.push(`"${cheerioQuery(this).attr('src')}"`);
112 } else {
113 inlineScripts.push(cheerioQuery(this).html());
114 }
115 });
116
117 this.indexHtmlCaches[relativePath].scriptsToLoad = scriptsToLoad;
118
119 // If we have inline scripts, we will save them into a script file and load it as part of the amd loading
120 this.indexHtmlCaches[relativePath].afterLoadingCode = undefined;
121 if (inlineScripts.length > 0) {
122 this.indexHtmlCaches[relativePath].afterLoadingCode = beautify_js(replaceRequireAndDefine(inlineScripts.join('\n\n')), {
123 indent_size: 2,
124 max_preserve_newlines: 1
125 });
126 scriptsToLoad.push(`"${this.rootURL}${this.indexHtmlCaches[relativePath].afterLoadingScript}"`);
127 }
128
129 // Replace the original ember scripts by the amd ones
130 scriptElements.remove();
131
132 // Beautify the index.html
133 return beautify_html(cheerioQuery.html(), {
134 indent_size: 2,
135 max_preserve_newlines: 0
136 });
137 }
138
139 _processJsFile(code, relativePath) {
140
141 const externalAmdModulesForFile = new Set();
142 const modifiedSource = replaceRequireAndDefine(code, this.amdPackages, externalAmdModulesForFile);
143
144 // Bookkeeping of what has changed for this file compared to previous builds
145 if (externalAmdModulesForFile.size === 0) {
146 // No more AMD references
147 this.externalAmdModulesCache.delete(relativePath);
148 } else {
149 // Replace with the new set
150 this.externalAmdModulesCache.set(relativePath, externalAmdModulesForFile);
151 }
152
153 return modifiedSource;
154 }
155
156 _buildModuleInfos() {
157
158 // Build different arrays representing the modules for the injection in the start script
159 const objs = [];
160 const names = [];
161 const adoptables = [];
162 let index = 0;
163 this.externalAmdModules.forEach((externalAmdModule) => {
164 objs.push(`mod${index}`);
165 names.push(`'${externalAmdModule}'`);
166 adoptables.push(`{name:'${externalAmdModule}',obj:mod${index}}`);
167 index++;
168 });
169
170 return {
171 names: names.join(','),
172 objects: objs.join(','),
173 adoptables: adoptables.join(',')
174 };
175 }
176
177 async build() {
178
179 // Clear before each build since the filter is kept by ember-cli during 'ember serve'
180 // and being reused without going thru postProcessTree. If we don't clean we may get
181 // previous modules.
182 this.externalAmdModules.clear();
183
184 const result = await super.build();
185
186 // Re-assemble the external AMD modules set with the updated cache
187 this.externalAmdModulesCache.forEach(externalAmdModules => {
188 externalAmdModules.forEach(externalAmdModule => {
189 this.externalAmdModules.add(externalAmdModule);
190 });
191 });
192
193 // Write the various script files we need
194 const moduleInfos = this._buildModuleInfos();
195 indexFiles.forEach(indexFile => {
196
197 const indexPath = path.join(this.outputPath, indexFile);
198 if (!fs.existsSync(indexPath)) {
199 // When building for production, tests/index.html will not exist, so we can skip its loading scripts
200 return;
201 }
202
203 // We add scripts to each index.html file to kick off the loading of amd modules.
204 const cheerioQuery = cheerio.load(fs.readFileSync(indexPath));
205 const amdScripts = [ `<script src="${this.loader}" data-amd=true></script>` ];
206 const scripts = this.indexHtmlCaches[indexFile].scriptsToLoad.join(',');
207 const loadingScript = beautify_js(amdLoadingTemplate(_.assign(moduleInfos, { scripts })), {
208 indent_size: 2,
209 max_preserve_newlines: 1
210 });
211
212 if (this.inline) {
213 // Inline the amd-loading script directly in index.html
214 amdScripts.push(`<script>${loadingScript}</script>`);
215 } else {
216 // Add a script tag to index.html to load the amd-loading script, and write the script to the output directory
217 amdScripts.push(`<script src="${this.rootURL}${this.indexHtmlCaches[indexFile].loadingScript}" data-amd-loading=true></script>`);
218 fs.writeFileSync(path.join(this.outputPath, this.indexHtmlCaches[indexFile].loadingScript), loadingScript);
219 }
220
221 // After loading script
222 if (this.indexHtmlCaches[indexFile].afterLoadingCode) {
223 fs.writeFileSync(path.join(this.outputPath, this.indexHtmlCaches[indexFile].afterLoadingScript), this.indexHtmlCaches[indexFile].afterLoadingCode);
224 }
225
226 cheerioQuery('body').prepend(amdScripts.join('\n'));
227 const html = beautify_html(cheerioQuery.html(), {
228 indentSize: 2,
229 max_preserve_newlines: 0
230 });
231 fs.writeFileSync(indexPath, html);
232 });
233
234 return result;
235 }
236}