1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | const babylon = require('babylon');
|
21 | const traverse = require('babel-traverse').default;
|
22 | const path = require('path');
|
23 | const minimatch = require('minimatch');
|
24 | const Util = require('./utils');
|
25 | const fs = require('fs');
|
26 | const assetPathUtil = require('./assetPathUtils');
|
27 |
|
28 | const MODULE_SPLITER = '\n';
|
29 |
|
30 | import type { Config } from '../flow/types';
|
31 |
|
32 | export 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 |
|
44 | type CustomEntry = {
|
45 | moduleId: number,
|
46 | name: string,
|
47 | moduleSet: Set<number>
|
48 | };
|
49 |
|
50 | type CodeRange = {
|
51 | start : number,
|
52 | end : number
|
53 | };
|
54 |
|
55 | type 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 |
|
65 | type SubBundle = {
|
66 | name: string,
|
67 | codes : Array<string>,
|
68 | assetRenames: Array<AssetRename>
|
69 | }
|
70 |
|
71 | type AssetRename = {
|
72 | originPath: string,
|
73 | relativePath: string,
|
74 | newPath: string
|
75 | };
|
76 |
|
77 | class 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 = [];
|
96 | this._moduleCalls = [];
|
97 |
|
98 | this._base = new Set();
|
99 | this._customEntries = [];
|
100 | this._bundles = [];
|
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)) {
|
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 |
|
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 |
|
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 |
|
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 |
|
229 | customEntry.forEach(entry => {
|
230 | this._genCustomEntryModules(entry.name, entry.id);
|
231 | });
|
232 | }
|
233 |
|
234 |
|
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 |
|
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 |
|
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 |
|
521 | module.exports = Parser;
|