UNPKG

16.5 kBJavaScriptView Raw
1/**
2 * Copyright 2015-present Desmond Yao
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 * Created by desmond on 4/16/17.
17 * @flow
18 */
19
20const babylon = require('babylon');
21const traverse = require('babel-traverse').default;
22const path = require('path');
23const minimatch = require('minimatch');
24const Util = require('./utils');
25const fs = require('fs');
26const assetPathUtil = require('./assetPathUtils');
27
28const MODULE_SPLITER = '\n';
29
30import type { Config } from '../flow/types';
31
32export type Asset = {
33 moduleId: number,
34 httpServerLocation: string,
35 width: number,
36 height: number,
37 scales: Array<number>,
38 name: string,
39 type: string,
40 hash: string,
41 code: CodeRange
42};
43
44type CustomEntry = {
45 moduleId: number,
46 name: string,
47 moduleSet: Set<number>
48};
49
50type CodeRange = {
51 start : number,
52 end : number
53};
54
55type Module = {
56 id: number,
57 name: string,
58 dependencies: Array<string>,
59 code: CodeRange,
60 idCodeRange: CodeRange,
61 isAsset ?: boolean,
62 assetConfig ?: Asset
63};
64
65type SubBundle = {
66 name: string,
67 codes : Array<string>,
68 assetRenames: Array<AssetRename>
69}
70
71type AssetRename = {
72 originPath: string,
73 relativePath: string,
74 newPath: string
75};
76
77class Parser {
78 _codeBlob: string;
79 _config: Config;
80 _useCustomSplit: boolean;
81 _polyfills: Array<CodeRange>;
82 _moduleCalls: Array<CodeRange>;
83 _base: Set<number>;
84 _customEntries: Array<CustomEntry>;
85 _baseEntryIndexModule: number;
86 _bundles: Array<SubBundle>;
87 _modules: { [number] : Module };
88
89 constructor(codeBlob : string, config : Config) {
90 this._codeBlob = codeBlob;
91 this._config = config;
92 this._useCustomSplit = typeof config.customEntries !== 'undefined';
93 this._modules = {};
94
95 this._polyfills = []; // polyfill codes range, always append on start.
96 this._moduleCalls = []; // module call codes range, always append on end.
97
98 this._base = new Set(); // store module id of base modules
99 this._customEntries = [];
100 this._bundles = []; // store split codes
101 }
102
103 splitBundle() {
104 const outputDir = this._config.outputDir;
105 Util.ensureFolder(outputDir);
106 const bundleAST = babylon.parse(this._codeBlob, {
107 sourceType: 'script',
108 plugins: ['jsx', 'flow']
109 });
110 this._parseAST(bundleAST);
111 this._doSplit();
112
113 this._bundles.forEach(subBundle => {
114 console.log('====== Split ' + subBundle.name + ' ======');
115 const code = subBundle.codes.join(MODULE_SPLITER);
116 const subBundlePath = path.resolve(outputDir, subBundle.name);
117 Util.ensureFolder(subBundlePath);
118
119 const codePath = path.resolve(subBundlePath, 'index.bundle');
120 fs.writeFileSync(codePath, code);
121 console.log('[Code] Write code to ' + codePath);
122 if (subBundle.assetRenames) {
123 subBundle.assetRenames.forEach(item => {
124 const assetNewDir = path.dirname(item.newPath);
125 Util.mkdirsSync(assetNewDir);
126 console.log('[Resource] Move resource ' + item.originPath + ' to ' + item.newPath);
127 fs.createReadStream(item.originPath).pipe(fs.createWriteStream(item.newPath));
128 });
129 }
130 console.log('====== Split ' + subBundle.name + ' done! ======');
131 });
132 }
133
134 _parseAST(bundleAST : any) {
135 const program = bundleAST.program;
136 const body = program.body;
137 const customBase = [];
138 const customEntry = [];
139 let reactEntryModule = undefined;
140 let moduleCount = 0;
141 body.forEach(node => {
142 if (Util.isEmptyStmt(node)) {
143 return;
144 }
145
146 let {start, end} = node;
147
148 if (Util.isPolyfillCall(node, this._config.dev)) { // push polyfill codes to base.
149 this._polyfills.push({start, end});
150 } else if (Util.isModuleCall(node)) {
151 this._moduleCalls.push({start, end});
152 } else if (Util.isModuleDeclaration(node)) {
153 moduleCount++;
154 const args = node.expression.arguments;
155 const moduleId = parseInt(args[1].value);
156 const moduleName = args[3].value;
157 const module : Module = {
158 id: moduleId,
159 name: moduleName,
160 dependencies: this._getModuleDependency(args[0].body),
161 code: {start, end},
162 idCodeRange: {
163 start: args[1].start - node.start,
164 end: args[1].end - node.start
165 }
166 };
167
168 if (Util.isAssetModule(moduleName)) {
169 module.isAsset = true;
170 module.assetConfig = Object.assign({}, Util.getAssetConfig(node), { moduleId });
171 console.log('Get asset module ' + moduleName, module.assetConfig);
172 }
173
174 if (!reactEntryModule && Util.isReactNativeEntry(moduleName)) {
175 // get react native entry, then init base set.
176 reactEntryModule = moduleId;
177 }
178
179 if (this._isBaseEntryModule(module)) {
180 console.log('Get base entry module: ' + moduleName);
181 this._baseEntryIndexModule = moduleId;
182 } else if (this._isCustomBaseModule(module)) {
183 console.log('Get custom base ' + moduleName);
184 customBase.push(moduleId);
185 } else if (this._useCustomSplit) {
186 let entry = this._isCustomEntryModule(module);
187 if (!!entry) {
188 console.log('Get custom entry ' + moduleName);
189 customEntry.push({
190 id: moduleId,
191 name: entry.name
192 });
193 }
194 }
195
196 this._modules[moduleId] = module;
197 console.log('Module ' + moduleName + '(' + moduleId + ') dependency:' + JSON.stringify(module.dependencies));
198 } else {
199 console.log(require('util').inspect(node, false, null));
200 console.log('Cannot parse node!', this._codeBlob.substring(node.start, node.end));
201 }
202 });
203
204 // generate react-native based module firstly.
205 if (reactEntryModule) {
206 this._genBaseModules(reactEntryModule);
207 } else {
208 console.warn('Cannot find react-native entry module! You should require(\'react-native\') at some entry.');
209 }
210
211 // append custom base modules.
212 customBase.forEach(base => {
213 this._genBaseModules(base);
214 });
215
216 if (typeof this._baseEntryIndexModule !== 'undefined') {
217 let module = this._modules[this._baseEntryIndexModule];
218 let dependency = module.dependencies;
219 for (let i = dependency.length - 1; i >= 0; i--) {
220 if (!!customEntry.find(item => item.id === dependency[i])) {
221 dependency.splice(i, 1);
222 }
223 }
224 this._genBaseModules(this._baseEntryIndexModule);
225 }
226
227 if (!!customEntry) {
228 // after gen base module, generate custom entry sets.
229 customEntry.forEach(entry => {
230 this._genCustomEntryModules(entry.name, entry.id);
231 });
232 }
233
234 // console.log('Get polyfills', this._polyfills);
235 console.log('Total modules :' + moduleCount);
236 console.log('Base modules size: ' + this._base.size);
237 }
238
239 _genBaseModules(moduleId : number) {
240 this._base.add(moduleId);
241 const module = this._modules[moduleId];
242 const queue = module.dependencies;
243
244 if (!queue) {
245 return;
246 }
247 let added = 0;
248 while(queue.length > 0) {
249 const tmp = queue.shift();
250
251 if (this._base.has(tmp)) {
252 continue;
253 }
254
255 if (this._modules[tmp].dependencies &&
256 this._modules[tmp].dependencies.length > 0) {
257 this._modules[tmp].dependencies.forEach(dep => {
258 if (!this._base.has(dep)) {
259 queue.push(dep);
260 }
261 });
262 }
263 added++;
264 this._base.add(tmp);
265 }
266 console.log('Module ' + module.name + ' added to base (' + added + ' more dependency added too)');
267 }
268
269 _genCustomEntryModules(name : string, moduleId : string) {
270 const set = new Set();
271 set.add(moduleId);
272
273 const module = this._modules[moduleId];
274 const queue = module.dependencies;
275
276 if (!queue) {
277 return;
278 }
279 let added = 0;
280 while(queue.length > 0) {
281 const tmp = queue.shift();
282
283 if (set.has(tmp) || this._base.has(tmp)) {
284 continue;
285 }
286
287 const dependency = this._modules[tmp].dependencies;
288 if (dependency && dependency.length > 0) {
289 dependency.forEach(dep => {
290 if (!this._base.has(dep) && !set.has(dep)) {
291 queue.push(dep);
292 }
293 });
294 }
295 added++;
296 set.add(tmp);
297 }
298 this._customEntries.push({
299 moduleId,
300 name,
301 moduleSet: set
302 });
303 console.log('Module ' + module.name + ' added to bundle ' + name + '. (' + added + ' more dependency added too)');
304 }
305
306 _getModuleDependency(bodyNode: any) {
307 if (bodyNode.type === 'BlockStatement') {
308 let {start, end} = bodyNode;
309 return Util.getModuleDependency(this._codeBlob, start, end);
310 }
311 return [];
312 }
313
314 _isBaseEntryModule(module: Module) {
315 let baseIndex = this._config.baseEntry.index;
316 let indexGlob = path.join(this._config.packageName, baseIndex + '.tmp');
317 // base index entry.
318 return minimatch(module.name, indexGlob);
319 }
320
321 _isCustomEntryModule(module: Module) {
322 return this._config.customEntries.find(entry => {
323 const pathGlob = path.join(this._config.packageName, entry.index);
324 return minimatch(module.name, pathGlob);
325 });
326 }
327
328 _isCustomBaseModule(module: Module) {
329 if (this._config.baseEntry.includes && this._config.baseEntry.includes.length > 0) {
330 const includes = this._config.baseEntry.includes;
331 const match = includes.find(glob => {
332 const pathGlob = path.join(this._config.packageName, glob);
333 return minimatch(module.name, pathGlob);
334 });
335 return typeof match !== 'undefined';
336 }
337 return false;
338 }
339
340 _getAssetRenames(asset : Asset,
341 bundle : string) : Array<AssetRename> {
342 const assetRenames = [];
343 if (this._config.platform === 'android') {
344 console.log('Get asset renames', asset);
345 assetPathUtil.getAssetPathInDrawableFolder(asset).forEach(
346 (relativePath) => {
347 assetRenames.push({
348 originPath: path.resolve(this._config.bundleDir, relativePath),
349 relativePath: relativePath,
350 newPath: path.resolve(this._config.outputDir, bundle, relativePath)
351 });
352 }
353 )
354 } else {
355 console.log('Get ios asset renames', asset);
356 asset.scales.forEach(scale => {
357 const relativePath = this._getAssetDestPathIOS(asset, scale);
358 const originPath = path.resolve(this._config.bundleDir, relativePath);
359 if(Util.ensureFolder(originPath)) {
360 assetRenames.push({
361 originPath,
362 relativePath: relativePath,
363 newPath: path.resolve(this._config.outputDir, bundle, relativePath)
364 });
365 }
366 });
367 }
368
369 return assetRenames;
370 }
371
372 _getAssetDestPathIOS(asset, scale) {
373 const suffix = scale === 1 ? '' : '@' + scale + 'x';
374 const fileName = asset.name + suffix + '.' + asset.type;
375 return path.join(asset.httpServerLocation.substr(1), fileName);
376 }
377
378 _doSplit() {
379 this._splitBase();
380
381 if (this._useCustomSplit) {
382 this._customEntries.forEach(entry => {
383 this._splitCustomEntry(entry);
384 });
385 console.log('Use custom split');
386 } else {
387 this._splitNonBaseModules();
388 }
389 }
390
391 _splitBase() {
392 const bundleName = 'base';
393 const dev = this._config.dev;
394 let codes = [];
395 let assetRenames = [];
396 // append codes to base
397 this._polyfills.forEach((range, index) => {
398 let code = this._codeBlob.substring(range.start, range.end);
399 if (index === 1) {
400 let requireAST = babylon.parse(code);
401 let conditionNode;
402 traverse(requireAST, {
403 enter(path) {
404 if (Util.isRequirePolyfillCondition(path.node, dev)) {
405 conditionNode = path.node;
406 }
407 },
408 exit(path) { }
409 });
410 if (conditionNode) {
411 code = code.substring(0, conditionNode.start)
412 + code.substring(conditionNode.end);
413 }
414 }
415 codes.push(code);
416 });
417 this._base.forEach(moduleId => {
418 const module : Module = this._modules[moduleId];
419 let code = this._codeBlob.substring(module.code.start, module.code.end);
420 code = code.substring(0, module.idCodeRange.start) +
421 '\"' + module.name + '\"'
422 + code.substring(module.idCodeRange.end);
423 if (module.isAsset && !!module.assetConfig) {
424 assetRenames = this._getAssetRenames(module.assetConfig, bundleName);
425 code = this._addBundleToAsset(module, bundleName, code);
426 } else if (moduleId === this._baseEntryIndexModule) {
427 let dependencies = Util.getModuleDependencyCodeRange(code, 0, code.length);
428 for (let i = dependencies.length - 1; i >= 0; i--) {
429 if (this._customEntries.find(entry => parseInt(entry.moduleId) === parseInt(dependencies[i].module))) {
430 code = code.replace(dependencies[i].code, '');
431 }
432 }
433 }
434 code = Util.replaceModuleIdWithName(code, this._modules);
435 codes.push(code);
436 });
437 this._moduleCalls.forEach(moduleCall => {
438 let code = this._codeBlob.substring(moduleCall.start, moduleCall.end);
439 code = Util.replaceModuleIdWithName(code, this._modules);
440 codes.push(code);
441 });
442 this._bundles.push({
443 name: bundleName,
444 codes,
445 assetRenames
446 });
447 }
448
449 _splitCustomEntry(entry : CustomEntry) {
450 const bundleName = entry.name;
451 let codes = [];
452 let assetRenames = [];
453 entry.moduleSet.forEach(moduleId => {
454 const module : Module = this._modules[moduleId];
455 let code = this._codeBlob.substring(module.code.start, module.code.end);
456 code = code.substring(0, module.idCodeRange.start) +
457 '\"' + module.name + '\"'
458 + code.substring(module.idCodeRange.end);
459 if (module.isAsset && module.assetConfig) {
460 assetRenames = assetRenames.concat(this._getAssetRenames(module.assetConfig, bundleName));
461 code = this._addBundleToAsset(module, bundleName, code);
462 }
463 code = Util.replaceModuleIdWithName(code, this._modules);
464 codes.push(code);
465 });
466 let entryModuleName = this._modules[entry.moduleId].name;
467 codes.push('\nrequire(\"' + entryModuleName + '\");');
468 this._bundles.push({
469 name: bundleName,
470 codes,
471 assetRenames
472 });
473 }
474
475 _splitNonBaseModules() {
476 const bundleName = 'business';
477 let codes = [];
478 let assetRenames = [];
479 for (let moduleId in this._modules) {
480 let moduleIdInt = parseInt(moduleId);
481
482 if (this._modules.hasOwnProperty(moduleId) && !this._base.has(moduleIdInt)) {
483 const module : Module = this._modules[moduleIdInt];
484 let code = this._codeBlob.substring(module.code.start, module.code.end);
485 code = code.substring(0, module.idCodeRange.start) +
486 '\"' + module.name + '\"'
487 + code.substring(module.idCodeRange.end);
488 if (module.isAsset && module.assetConfig) {
489 assetRenames = this._getAssetRenames(module.assetConfig, bundleName);
490 code = this._addBundleToAsset(module, bundleName, code);
491 }
492 code = Util.replaceModuleIdWithName(code, this._modules)
493 codes.push(code);
494 }
495 }
496 this._bundles.push({
497 name: bundleName,
498 codes,
499 assetRenames
500 });
501 }
502
503 _addBundleToAsset(module : Module, bundleName : string, code : string) : string {
504 const asset : Asset = module.assetConfig;
505 let startInner = asset.code.start - module.code.start;
506 let endInner = asset.code.end - module.code.start;
507 return code.substring(0, startInner) + JSON.stringify({
508 httpServerLocation: asset.httpServerLocation,
509 width: asset.width,
510 height: asset.height,
511 scales: asset.scales,
512 hash: asset.hash,
513 name: asset.name,
514 type: asset.type,
515 bundle: bundleName
516 }) + code.substring(endInner);
517 }
518}
519
520
521module.exports = Parser;