UNPKG

7.68 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(app, inputTree, options = {}) {
33 super(inputTree, options);
34
35 this.extensions = ['js', 'html'];
36
37 // Options for the process
38 this.loader = app.options.amd.loader;
39 this.amdPackages = app.options.amd.packages || [];
40 this.excludePaths = app.options.amd.excludePaths;
41 this.loadingFilePath = (app.options.amd.loadingFilePath || 'assets').replace(/\/$/, "");
42 this.rootURL = app.project.config(app.env).rootURL;
43
44 // Because the filter is call for partial rebuild during 'ember serve', we need to
45 // know what was added/removed for a partial build
46 this.externalAmdModules = new Set();
47 this.externalAmdModulesCache = new Map();
48
49 // There are two index files that should be converted:
50 // - index.html
51 // - tests/index.html
52 // We need to keep things separated as they don't load the same script set.
53 this.indexHtmlCaches = {
54 'index.html': {
55 scriptsToLoad: [],
56 loadingScript: this.rootURL + this.loadingFilePath + '/amd-loading.js',
57 afterLoadingScript: this.rootURL + this.loadingFilePath + '/after-amd-loading.js'
58 },
59 'tests/index.html': {
60 scriptsToLoad: [],
61 loadingScript: this.rootURL + this.loadingFilePath + '/amd-loading-tests.js',
62 afterLoadingScript: this.rootURL + this.loadingFilePath + '/after-amd-loading-tests.js'
63 }
64 };
65 }
66
67 getDestFilePath(relativePath) {
68 relativePath = super.getDestFilePath(relativePath);
69 if (!relativePath) {
70 return null;
71 }
72
73 if (relativePath.indexOf('index.html') >= 0) {
74 return relativePath;
75 }
76
77 for (let i = 0, len = this.excludePaths.length; i < len; i++) {
78 if (relativePath.indexOf(this.excludePaths[i]) === 0) {
79 return null;
80 }
81 }
82
83 if (relativePath.indexOf('.js') >= 0) {
84 return relativePath;
85 }
86
87 return null;
88 }
89
90 processString(code, relativePath) {
91 if (relativePath.indexOf('.js') >= 0) {
92 return this._processJsFile(code, relativePath);
93 }
94
95 return this._processIndexFile(code, relativePath);
96 }
97
98 _processIndexFile(code, relativePath) {
99
100 const cheerioQuery = cheerio.load(code);
101
102 // Get the collection of scripts
103 // Scripts that have a 'src' will be loaded by AMD
104 // Scripts that have a body will be assembled into a post loading file and loaded at the end of the AMD loading process
105 const scriptElements = cheerioQuery('body > script');
106 const scriptsToLoad = [];
107 const inlineScripts = [];
108 scriptElements.each(function () {
109 if (cheerioQuery(this).attr('src')) {
110 scriptsToLoad.push(`"${cheerioQuery(this).attr('src')}"`);
111 } else {
112 inlineScripts.push(cheerioQuery(this).html());
113 }
114 });
115
116 this.indexHtmlCaches[relativePath].scriptsToLoad = scriptsToLoad;
117
118 // If we have inline scripts, we will save them into a script file and load it as part of the amd loading
119 this.indexHtmlCaches[relativePath].afterLoadingCode = undefined;
120 if (inlineScripts.length > 0) {
121 this.indexHtmlCaches[relativePath].afterLoadingCode = beautify_js(replaceRequireAndDefine(inlineScripts.join('\n\n')), {
122 indent_size: 2,
123 max_preserve_newlines: 1
124 });
125 scriptsToLoad.push(`"${this.indexHtmlCaches[relativePath].afterLoadingScript}"`);
126 }
127
128 // Replace the original ember scripts by the amd ones
129 scriptElements.remove();
130 const amdScripts = [
131 `<script src="${this.loader}" data-amd=true></script>`,
132 `<script src="${this.indexHtmlCaches[relativePath].loadingScript}" data-amd-loading=true></script>`
133 ];
134 cheerioQuery('body').prepend(amdScripts.join('\n'));
135
136 // Beautify the index.html
137 return beautify_html(cheerioQuery.html(), {
138 indent_size: 2,
139 max_preserve_newlines: 0
140 });
141 }
142
143 _processJsFile(code, relativePath) {
144
145 const externalAmdModulesForFile = new Set();
146 const modifiedSource = replaceRequireAndDefine(code, this.amdPackages, externalAmdModulesForFile);
147
148 // Bookkeeping of what has changed for this file compared to previous builds
149 if (externalAmdModulesForFile.size === 0) {
150 // No more AMD references
151 this.externalAmdModulesCache.delete(relativePath);
152 } else {
153 // Replace with the new set
154 this.externalAmdModulesCache.set(relativePath, externalAmdModulesForFile);
155 }
156
157 return modifiedSource;
158 }
159
160 _buildModuleInfos() {
161
162 // Build different arrays representing the modules for the injection in the start script
163 const objs = [];
164 const names = [];
165 const adoptables = [];
166 let index = 0;
167 this.externalAmdModules.forEach((externalAmdModule) => {
168 objs.push(`mod${index}`);
169 names.push(`'${externalAmdModule}'`);
170 adoptables.push(`{name:'${externalAmdModule}',obj:mod${index}}`);
171 index++;
172 });
173
174 return {
175 names: names.join(','),
176 objects: objs.join(','),
177 adoptables: adoptables.join(',')
178 };
179 }
180
181 async build() {
182
183 // Clear before each build since the filter is kept by ember-cli during 'ember serve'
184 // and being reused without going thru postProcessTree. If we don't clean we may get
185 // previous modules.
186 this.externalAmdModules.clear();
187
188 const result = await super.build();
189
190 // Re-assemble the external AMD modules set with the updated cache
191 this.externalAmdModulesCache.forEach(externalAmdModules => {
192 externalAmdModules.forEach(externalAmdModule => {
193 this.externalAmdModules.add(externalAmdModule);
194 });
195 });
196
197 // Write the various sript files we need
198 const moduleInfos = this._buildModuleInfos();
199 indexFiles.forEach(indexFile => {
200
201 // The loading script
202 const scripts = this.indexHtmlCaches[indexFile].scriptsToLoad.join(',');
203 const loadingScript = amdLoadingTemplate(_.assign(moduleInfos, { scripts }));
204 fs.writeFileSync(path.join(this.outputPath, this.indexHtmlCaches[indexFile].loadingScript), beautify_js(loadingScript, {
205 indent_size: 2,
206 max_preserve_newlines: 1
207 }));
208
209 // After loading script
210 if (this.indexHtmlCaches[indexFile].afterLoadingCode) {
211 fs.writeFileSync(path.join(this.outputPath, this.indexHtmlCaches[indexFile].afterLoadingScript), this.indexHtmlCaches[indexFile].afterLoadingCode);
212 }
213 });
214
215 return result;
216 }
217}
\No newline at end of file