UNPKG

12.6 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/* jshint node: true */
13'use strict';
14
15const fs = require('fs');
16const path = require('path');
17const sha = require('sha1');
18const cheerio = require('cheerio');
19const Filter = require('broccoli-filter');
20const esprima = require('esprima');
21const eswalk = require('esprima-walk');
22const requirejs = require('requirejs');
23const _ = require('lodash');
24const beautify_js = require('js-beautify');
25const beautify_html = require('js-beautify').html;
26var SilentError = require('silent-error');
27
28// The root of the project
29let root;
30
31// The finger printing base urls
32var fingerprintBaseUrl = '';
33
34// For contiinuous build, we need to cache a series of properties
35var indexHtmlCache = {
36 app: {
37 modulesAsString: '',
38 startScript: '',
39 startFileName: ''
40 },
41 test: {
42 modulesAsString: '',
43 startScript: '',
44 startFileName: ''
45 }
46};
47
48// Template used to manufacture the start script
49const startTemplate = _.template(fs.readFileSync(path.join(__dirname, 'start-template.txt'), 'utf8'));
50
51// Identifiers and Literals to replace in the code to avoid conflict with amd loader
52const identifiers = {
53 'require': 'eriuqer',
54 'define': 'enifed'
55};
56
57const literals = {
58 'require': '\'eriuqer\'',
59 '(require)': '\'(eriuqer)\''
60};
61
62module.exports = {
63
64 name: 'ember-cli-amd',
65
66 amdModules: new Set(),
67
68 included: function(app) {
69 // Note: this functionis only called once even if using ember build --watch or ember serve
70
71 // This is the entry point for this addon. We will collect the amd definitions from the ember-cli-build.js and
72 // we will build the list off the amd modules usedby the application.
73 root = app.project.root;
74
75 // This addon relies on an 'amd' options in the ember-cli-build.js file
76 if (!app.options.amd) {
77 return new SilentError('ember-cli-amd: No amd options specified in the ember-cli-build.js file.');
78 }
79
80 // Merge the default options
81 app.options.amd = _.merge({ packages: [], excludePaths: [] }, app.options.amd);
82
83 // Determine the type of loader.
84 if (!app.options.amd.loader) {
85 throw new Error('ember-cli-amd: You must specify a loader option the amd options in ember-cli-build.js.');
86 }
87
88 // The finger printing base url
89 if (this.app.options.fingerprint && this.app.options.fingerprint.enabled && this.app.options.fingerprint.prepend) {
90 fingerprintBaseUrl = this.app.options.fingerprint.prepend;
91 }
92 },
93
94 postprocessTree: function(type, tree) {
95 // Note: this function will be called once during the continuous builds. However, the tree returned will be directly manipulated.
96 // It means that the de-requireing will be going on.
97 if (!this.app.options.amd) {
98 return tree;
99 }
100
101 if (type !== 'all') {
102 return tree;
103 }
104
105 // Use the RequireFilter class to replace in the code that conflict with AMD loader
106 return new RequireFilter(tree, {
107 amdPackages: this.app.options.amd.packages,
108 amdModules: this.amdModules,
109 excludePaths: this.app.options.amd.excludePaths
110 });
111 },
112
113 indexBuilder: function(config) {
114 // If the current index html is not the same as teh one we built, it means
115 // that another extension must have forced to regenerate the index html or
116 // this is the first time this extension is running
117 var indexPath = path.join(config.directory, config.indexFile);
118
119 var indexHtml;
120 try {
121 indexHtml = fs.readFileSync(indexPath, 'utf8');
122 } catch (e) {
123 // no index file, we are done.
124 return null;
125 }
126
127 // Check if we have to continue
128 // - If the index already contains the AMD loader
129 // - if the list of modules is still the same
130 const cheerioQuery = cheerio.load(indexHtml);
131 const amdScriptElements = cheerioQuery('script[data-amd]')
132 if (amdScriptElements.length === 1 && config.indexHtmlCache.modulesAsString === config.moduleInfos.names) {
133 return config.indexHtmlCache;
134 }
135
136 // Get the collection of scripts
137 // Scripts that have a 'src' will be loaded by AMD
138 // Scripts that have a body will be assembled into a post loading file and loaded at the end of the AMD loading process
139 var scriptElements = cheerioQuery('body > script');
140 var scriptsToLoad = [];
141 var scriptsToPostExecute = [];
142 scriptElements.each(function() {
143 if (cheerioQuery(this).attr('src')) {
144 scriptsToLoad.push(`"${cheerioQuery(this).attr('src')}"`)
145 } else {
146 scriptsToPostExecute.push(cheerioQuery(this).html());
147 }
148 });
149
150 // Remove the script tags
151 scriptElements.remove();
152
153 // If we have scripts that have to be executed after the AMD load, then serialize them into a file
154 // afterLoading.js and add this file to the list of AMD modules.
155 if (scriptsToPostExecute.length > 0) {
156 var afterLoadingScript = replaceRequireAndDefine(scriptsToPostExecute.join('\n\n'));
157 fs.writeFileSync(path.join(config.directory, 'afterLoading.js'), beautify_js(afterLoadingScript, {
158 indent_size: 2
159 }));
160 scriptsToLoad.push('"/afterLoading.js"');
161 }
162
163 // We have to rebuild this index file.
164 config.indexHtmlCache.modulesAsString = config.moduleInfos.names;
165
166 // Add the amd config
167 var amdScripts = '';
168 if (this.app.options.amd.configPath) {
169 amdScripts += '<script>' + config.amdConfigScript + '</script>';
170 } else if (this.app.options.amd.configScript) {
171 amdScripts += '<script>' + this.app.options.amd.configScript + '</script>';
172 }
173
174 // Add the loader
175 var loaderSrc = this.app.options.amd.loader;
176 amdScripts += `<script src="${loaderSrc}" data-amd="true"></script>`;
177
178 // Add the start scripts
179 var startScript = startTemplate(_.assign(config.moduleInfos, {
180 scripts: scriptsToLoad.join(',')
181 }));
182
183 if (this.app.options.amd.inline) {
184 // Inline the start script
185 amdScripts += '<script>' + startScript + '</script>';
186 } else {
187 // fingerprint the start script if necessary
188 var startFileName = 'assets/' + config.startSrc;
189 if (this.app.options.fingerprint && this.app.options.fingerprint.enabled) {
190 var startSha = sha(startScript);
191 startFileName += '-' + startSha;
192 }
193 startFileName += '.js';
194
195 // Save the file name and the script. We will save the file later.
196 // The start script file needs to be saved each time the app is rebuilt in continuous build
197 config.indexHtmlCache.startFileName = startFileName;
198 config.indexHtmlCache.startScript = startScript;
199
200 // All what we need to do for now is add the script tag
201 amdScripts += '<script src="' + fingerprintBaseUrl + startFileName + '"></script>';
202 }
203
204 // Add the scripts to the body
205 cheerioQuery('body').prepend(amdScripts);
206
207 // Beautify the index.html
208 var html = beautify_html(cheerioQuery.html(), {
209 indent_size: 2
210 });
211
212 // Rewrite the index file
213 fs.writeFileSync(indexPath, html);
214
215 return config.indexHtmlCache;
216 },
217
218 buildModuleInfos: function() {
219
220 // Build different arrays representing the modules for the injection in the start script
221 const objs = [];
222 const names = [];
223 const adoptables = [];
224 let index = 0;
225 this.amdModules.forEach(function(amdModule) {
226 objs.push(`mod${index}`);
227 names.push(`'${amdModule}'`);
228 adoptables.push(`{name:'${amdModule}',obj:mod${index}}`);
229 index++;
230 });
231
232 return {
233 names: names.join(','),
234 objects: objs.join(','),
235 adoptables: adoptables.join(','),
236 vendor: path.parse(this.app.options.outputPaths.vendor.js).name
237 };
238 }
239};
240
241//
242// Class for replacing in the generated code the AMD protected keyword 'require' and 'define'.
243// We are replacing these keywords by non conflicting words.
244// It uses the broccoli filter to go thru the different files (as string).
245function RequireFilter(inputTree, options) {
246 if (!(this instanceof RequireFilter)) {
247 return new RequireFilter(inputTree, options);
248 }
249
250 Filter.call(this, inputTree, options); // this._super()
251
252 options = options || {};
253
254 this.inputTree = inputTree;
255 this.files = options.files || [];
256 this.description = options.description;
257 this.amdPackages = options.amdPackages || [];
258 this.amdModules = options.amdModules;
259 this.excludePaths = options.excludePaths;
260}
261
262RequireFilter.prototype = Object.create(Filter.prototype);
263RequireFilter.prototype.constructor = RequireFilter;
264
265RequireFilter.prototype.extensions = ['js'];
266RequireFilter.prototype.targetExtension = 'js';
267
268RequireFilter.prototype.getDestFilePath = function(relativePath) {
269 relativePath = Filter.prototype.getDestFilePath.call(this, relativePath);
270 if (!relativePath) {
271 return relativePath;
272 }
273 for (var i = 0, len = this.excludePaths.length; i < len; i++) {
274 if (relativePath.indexOf(this.excludePaths[i]) === 0) {
275 return null;
276 }
277 }
278 return relativePath;
279}
280RequireFilter.prototype.processString = function(code, relativePath) {
281 return replaceRequireAndDefine(code, this.amdPackages, this.amdModules);
282};
283
284function write(arr, str, offset) {
285 for (var i = 0, l = str.length; i < l; i++) {
286 arr[offset + i] = str[i];
287 }
288}
289
290// Use Esprima to parse the code and eswalk to walk thru the code
291// Replace require and define by non-conflicting verbs
292function replaceRequireAndDefine(code, amdPackages, amdModules) {
293 // Parse the code as an AST
294 const ast = esprima.parseScript(code, {
295 range: true
296 });
297
298 // Split the code into an array for easier substitutions
299 const buffer = code.split('');
300
301 // Walk thru the tree, find and replace our targets
302 eswalk(ast, function(node) {
303 if (!node) {
304 return;
305 }
306
307 switch (node.type) {
308 case 'CallExpression':
309
310 if (!amdPackages || !amdModules) {
311 // If not provided then we don't need to track them
312 break;
313 }
314
315 // Collect the AMD modules
316 // Looking for something like define(<name>, [<module1>, <module2>, ...], <function>)
317 // This is the way ember defines a module
318 if (node.callee.name === 'define') {
319
320 if (node.arguments.length < 2 || node.arguments[1].type !== 'ArrayExpression' || !node.arguments[1].elements) {
321 return;
322 }
323
324 node.arguments[1].elements.forEach(function(element) {
325 if (element.type !== 'Literal') {
326 return;
327 }
328
329 const isAMD = amdPackages.some(function(amdPackage) {
330 if (typeof element.value !== 'string') {
331 return false;
332 }
333 return element.value.indexOf(amdPackage + '/') === 0 || element.value === amdPackage;
334 });
335
336 if (!isAMD) {
337 return;
338 }
339
340 amdModules.add(element.value);
341
342 });
343
344 return;
345 }
346
347 // Dealing with ember-auto-import eval
348 if (node.callee.name === 'eval') {
349 const evalCode = node.arguments[0].value;
350 const evalCodeAfter = replaceRequireAndDefine(evalCode, amdPackages, amdModules);
351 if (evalCode !== evalCodeAfter) {
352 write(buffer, "eval(\"" + evalCodeAfter.replace(/\n/g, "\\n") + "\");", node.range[0]);
353 }
354 }
355
356 return;
357
358 case 'Identifier':
359 {
360 // We are dealing with code, make sure the node.name is not inherited from object
361 if (!identifiers.hasOwnProperty(node.name)) {
362 return;
363 }
364
365 const identifier = identifiers[node.name];
366 if (!identifier) {
367 return;
368 }
369
370 write(buffer, identifier, node.range[0]);
371 }
372 return;
373
374 case 'Literal':
375 {
376 // We are dealing with code, make sure the node.name is not inherited from object
377 if (!literals.hasOwnProperty(node.value)) {
378 return;
379 }
380
381 const literal = literals[node.value];
382 if (!literal) {
383 return;
384 }
385
386 write(buffer, literal, node.range[0]);
387 }
388 return;
389 }
390 });
391
392 // Return the new code
393 return buffer.join('');
394}
\No newline at end of file