UNPKG

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