• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use rollupObject file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import * as ts from 'typescript';
17import fs from 'fs';
18import path from 'path';
19import MagicString from 'magic-string';
20import {
21  GEN_ABC_PLUGIN_NAME,
22  PACKAGES
23} from '../common/ark_define';
24import {
25  getOhmUrlByFilepath,
26  getOhmUrlByHarName,
27  getOhmUrlBySystemApiOrLibRequest,
28  mangleDeclarationFileName,
29} from '../../../ark_utils';
30import { writeFileSyncByNode } from '../../../process_module_files';
31import {
32  isDebug,
33  isJsonSourceFile,
34  isJsSourceFile,
35  updateSourceMap,
36  writeFileContentToTempDir
37} from '../utils';
38import { toUnixPath } from '../../../utils';
39import {
40  createAndStartEvent,
41  stopEvent
42} from '../../../ark_utils';
43import { newSourceMaps } from '../transform';
44import { writeObfuscationNameCache } from '../common/ob_config_resolver';
45import { ORIGIN_EXTENTION } from '../process_mock';
46import {
47  ESMODULE,
48  TRANSFORMED_MOCK_CONFIG,
49  USER_DEFINE_MOCK_CONFIG
50} from '../../../pre_define';
51import { readProjectAndLibsSource } from '../common/process_ark_config';
52import { allSourceFilePaths, collectAllFiles } from '../../../ets_checker';
53import { projectConfig } from '../../../../main';
54const ROLLUP_IMPORT_NODE: string = 'ImportDeclaration';
55const ROLLUP_EXPORTNAME_NODE: string = 'ExportNamedDeclaration';
56const ROLLUP_EXPORTALL_NODE: string = 'ExportAllDeclaration';
57const ROLLUP_DYNAMICIMPORT_NODE: string = 'ImportExpression';
58const ROLLUP_LITERAL_NODE: string = 'Literal';
59
60export class ModuleSourceFile {
61  private static sourceFiles: ModuleSourceFile[] = [];
62  private moduleId: string;
63  private source: string | ts.SourceFile;
64  private isSourceNode: boolean = false;
65  private static projectConfig: Object;
66  private static logger: Object;
67  private static mockConfigInfo: Object = {};
68  private static mockFiles: string[] = [];
69  private static newMockConfigInfo: Object = {};
70  private static needProcessMock: boolean = false;
71
72  constructor(moduleId: string, source: string | ts.SourceFile) {
73    this.moduleId = moduleId;
74    this.source = source;
75    if (typeof this.source !== 'string') {
76      this.isSourceNode = true;
77    }
78  }
79
80  static setProcessMock(rollupObject: Object): void {
81    // only processing mock-config.json5 in preview or OhosTest mode
82    if (!(rollupObject.share.projectConfig.isPreview || rollupObject.share.projectConfig.isOhosTest)) {
83      ModuleSourceFile.needProcessMock = false;
84      return;
85    }
86
87    // mockParams is essential, and etsSourceRootPath && mockConfigPath need to be defined in mockParams
88    // mockParams = {
89    //   "decorator": "name of mock decorator",
90    //   "packageName": "name of mock package",
91    //   "etsSourceRootPath": "path of ets source root",
92    //   "mockConfigPath": "path of mock configuration file"
93    // }
94    ModuleSourceFile.needProcessMock = (rollupObject.share.projectConfig.mockParams &&
95                                        rollupObject.share.projectConfig.mockParams.etsSourceRootPath &&
96                                        rollupObject.share.projectConfig.mockParams.mockConfigPath) ? true : false;
97  }
98
99  static collectMockConfigInfo(rollupObject: Object): void {
100    ModuleSourceFile.mockConfigInfo = require('json5').parse(
101      fs.readFileSync(rollupObject.share.projectConfig.mockParams.mockConfigPath, 'utf-8'));
102    for (let mockedTarget in ModuleSourceFile.mockConfigInfo) {
103      if (ModuleSourceFile.mockConfigInfo[mockedTarget].source) {
104        ModuleSourceFile.mockFiles.push(ModuleSourceFile.mockConfigInfo[mockedTarget].source);
105      }
106    }
107  }
108
109  static addNewMockConfig(key: string, src: string): void {
110    if (ModuleSourceFile.newMockConfigInfo.hasOwnProperty(key)) {
111      return;
112    }
113
114    ModuleSourceFile.newMockConfigInfo[key] = {'source': src};
115  }
116
117  static generateNewMockInfoByOrignMockConfig(originKey: string, transKey: string, rollupObject: Object): void {
118    if (!ModuleSourceFile.mockConfigInfo.hasOwnProperty(originKey)) {
119      return;
120    }
121
122    let mockFile: string = ModuleSourceFile.mockConfigInfo[originKey].source;
123    let mockFilePath: string = `${toUnixPath(rollupObject.share.projectConfig.modulePath)}/${mockFile}`;
124    let mockFileOhmUrl: string = getOhmUrlByFilepath(mockFilePath,
125                                                     ModuleSourceFile.projectConfig,
126                                                     ModuleSourceFile.logger,
127                                                     rollupObject.share.projectConfig.entryModuleName);
128    mockFileOhmUrl = mockFileOhmUrl.startsWith(PACKAGES) ? `@package:${mockFileOhmUrl}` : `@bundle:${mockFileOhmUrl}`;
129    // record mock target mapping for incremental compilation
130    ModuleSourceFile.addNewMockConfig(transKey, mockFileOhmUrl);
131  }
132
133  static isMockFile(file: string, rollupObject: Object): boolean {
134    if (!ModuleSourceFile.needProcessMock) {
135      return false;
136    }
137
138    for (let mockFile of ModuleSourceFile.mockFiles) {
139      let absoluteMockFilePath: string = `${toUnixPath(rollupObject.share.projectConfig.modulePath)}/${mockFile}`;
140      if (toUnixPath(absoluteMockFilePath) === toUnixPath(file)) {
141        return true;
142      }
143    }
144
145    return false;
146  }
147
148  static generateMockConfigFile(rollupObject: Object): void {
149    let transformedMockConfigCache: string =
150      path.resolve(rollupObject.share.projectConfig.cachePath, `./${TRANSFORMED_MOCK_CONFIG}`);
151    let transformedMockConfig: string =
152      path.resolve(rollupObject.share.projectConfig.aceModuleJsonPath, `../${TRANSFORMED_MOCK_CONFIG}`);
153    let userDefinedMockConfigCache: string =
154      path.resolve(rollupObject.share.projectConfig.cachePath, `./${USER_DEFINE_MOCK_CONFIG}`);
155    // full compilation
156    if (!fs.existsSync(transformedMockConfigCache) || !fs.existsSync(userDefinedMockConfigCache)) {
157      fs.writeFileSync(transformedMockConfig, JSON.stringify(ModuleSourceFile.newMockConfigInfo));
158      fs.copyFileSync(transformedMockConfig, transformedMockConfigCache);
159      fs.copyFileSync(rollupObject.share.projectConfig.mockParams.mockConfigPath, userDefinedMockConfigCache);
160      return;
161    }
162
163    // incremental compilation
164    const cachedMockConfigInfo: Object =
165      require('json5').parse(fs.readFileSync(userDefinedMockConfigCache, 'utf-8'));
166    // If mock-config.json5 is modified, incremental compilation will be disabled
167    if (JSON.stringify(ModuleSourceFile.mockConfigInfo) !== JSON.stringify(cachedMockConfigInfo)) {
168      fs.writeFileSync(transformedMockConfig, JSON.stringify(ModuleSourceFile.newMockConfigInfo));
169      fs.copyFileSync(transformedMockConfig, transformedMockConfigCache);
170      fs.copyFileSync(rollupObject.share.projectConfig.mockParams.mockConfigPath, userDefinedMockConfigCache);
171      return;
172    }
173    // if mock-config.json5 is not modified, use the cached mock config mapping file
174    fs.copyFileSync(transformedMockConfigCache, transformedMockConfig);
175  }
176
177  static removePotentialMockConfigCache(rollupObject: Object): void {
178    const transformedMockConfigCache: string =
179      path.resolve(rollupObject.share.projectConfig.cachePath, `./${TRANSFORMED_MOCK_CONFIG}`);
180    const userDefinedMockConfigCache: string =
181      path.resolve(rollupObject.share.projectConfig.cachePath, `./${USER_DEFINE_MOCK_CONFIG}`);
182    if (fs.existsSync(transformedMockConfigCache)) {
183      fs.rm(transformedMockConfigCache);
184    }
185
186    if (fs.existsSync(userDefinedMockConfigCache)) {
187      fs.rm(userDefinedMockConfigCache);
188    }
189  }
190
191  static newSourceFile(moduleId: string, source: string | ts.SourceFile) {
192    ModuleSourceFile.sourceFiles.push(new ModuleSourceFile(moduleId, source));
193  }
194
195  static getSourceFiles(): ModuleSourceFile[] {
196    return ModuleSourceFile.sourceFiles;
197  }
198
199  static async processModuleSourceFiles(rollupObject: Object, parentEvent: Object): Promise<void> {
200    this.initPluginEnv(rollupObject);
201
202    // collect mockConfigInfo
203    ModuleSourceFile.setProcessMock(rollupObject);
204    if (ModuleSourceFile.needProcessMock) {
205      ModuleSourceFile.collectMockConfigInfo(rollupObject);
206    } else {
207      ModuleSourceFile.removePotentialMockConfigCache(rollupObject);
208    }
209
210    collectAllFiles(undefined, rollupObject.getModuleIds());
211    readProjectAndLibsSource(allSourceFilePaths, ModuleSourceFile.projectConfig.obfuscationMergedObConfig,
212      ModuleSourceFile.projectConfig.arkObfuscator, ModuleSourceFile.projectConfig.compileHar);
213
214    // Sort the collection by file name to ensure binary consistency.
215    ModuleSourceFile.sortSourceFilesByModuleId();
216    for (const source of ModuleSourceFile.sourceFiles) {
217      if (!rollupObject.share.projectConfig.compileHar) {
218        // compileHar: compile closed source har of project, which convert .ets to .d.ts and js, doesn't transform module request.
219        const eventBuildModuleSourceFile = createAndStartEvent(parentEvent, 'build module source files');
220        await source.processModuleRequest(rollupObject, eventBuildModuleSourceFile);
221        stopEvent(eventBuildModuleSourceFile);
222      }
223      const eventWriteSourceFile = createAndStartEvent(parentEvent, 'write source file');
224      await source.writeSourceFile(eventWriteSourceFile);
225      stopEvent(eventWriteSourceFile);
226    }
227
228    if (rollupObject.share.arkProjectConfig.compileMode === ESMODULE) {
229      await mangleDeclarationFileName(ModuleSourceFile.logger, rollupObject.share.arkProjectConfig);
230    }
231
232    const eventObfuscatedCode = createAndStartEvent(parentEvent, 'write obfuscation name cache');
233    if ((ModuleSourceFile.projectConfig.arkObfuscator || ModuleSourceFile.projectConfig.terserConfig) &&
234      ModuleSourceFile.projectConfig.obfuscationOptions) {
235      writeObfuscationNameCache(ModuleSourceFile.projectConfig, ModuleSourceFile.projectConfig.obfuscationOptions.obfuscationCacheDir,
236        ModuleSourceFile.projectConfig.obfuscationMergedObConfig.options?.printNameCache);
237    }
238    stopEvent(eventObfuscatedCode);
239
240    const eventGenerateMockConfigFile = createAndStartEvent(parentEvent, 'generate mock config file');
241    if (ModuleSourceFile.needProcessMock) {
242      ModuleSourceFile.generateMockConfigFile(rollupObject);
243    }
244    stopEvent(eventGenerateMockConfigFile);
245
246    ModuleSourceFile.sourceFiles = [];
247  }
248
249  getModuleId(): string {
250    return this.moduleId;
251  }
252
253  private async writeSourceFile(parentEvent: Object): Promise<void> {
254    if (this.isSourceNode && !isJsSourceFile(this.moduleId)) {
255      await writeFileSyncByNode(<ts.SourceFile>this.source, ModuleSourceFile.projectConfig, parentEvent, ModuleSourceFile.logger);
256    } else {
257      await writeFileContentToTempDir(this.moduleId, <string>this.source, ModuleSourceFile.projectConfig, ModuleSourceFile.logger, parentEvent);
258    }
259  }
260
261  private getOhmUrl(rollupObject: Object, moduleRequest: string, filePath: string | undefined): string | undefined {
262    let systemOrLibOhmUrl: string | undefined = getOhmUrlBySystemApiOrLibRequest(moduleRequest);
263    if (systemOrLibOhmUrl != undefined) {
264      if (ModuleSourceFile.needProcessMock) {
265        ModuleSourceFile.generateNewMockInfoByOrignMockConfig(moduleRequest, systemOrLibOhmUrl, rollupObject);
266      }
267      return systemOrLibOhmUrl;
268    }
269    const harOhmUrl: string | undefined = getOhmUrlByHarName(moduleRequest, ModuleSourceFile.projectConfig);
270    if (harOhmUrl !== undefined) {
271      if (ModuleSourceFile.needProcessMock) {
272        ModuleSourceFile.generateNewMockInfoByOrignMockConfig(moduleRequest, harOhmUrl, rollupObject);
273      }
274      return harOhmUrl;
275    }
276    if (filePath) {
277      const targetModuleInfo: Object = rollupObject.getModuleInfo(filePath);
278      const namespace: string = targetModuleInfo['meta']['moduleName'];
279      const ohmUrl: string =
280        getOhmUrlByFilepath(filePath, ModuleSourceFile.projectConfig, ModuleSourceFile.logger, namespace);
281      let res: string = ohmUrl.startsWith(PACKAGES) ? `@package:${ohmUrl}` : `@bundle:${ohmUrl}`;
282      if (ModuleSourceFile.needProcessMock) {
283        // processing cases of har or lib mock targets
284        ModuleSourceFile.generateNewMockInfoByOrignMockConfig(moduleRequest, res, rollupObject);
285        // processing cases of user-defined mock targets
286        let mockedTarget: string = toUnixPath(filePath).
287            replace(toUnixPath(rollupObject.share.projectConfig.modulePath), '').
288            replace(`/${rollupObject.share.projectConfig.mockParams.etsSourceRootPath}/`, '');
289        ModuleSourceFile.generateNewMockInfoByOrignMockConfig(mockedTarget, res, rollupObject);
290      }
291      return res;
292    }
293    return undefined;
294  }
295
296  private processJsModuleRequest(rollupObject: Object): void {
297    const moduleInfo: Object = rollupObject.getModuleInfo(this.moduleId);
298    const importMap: Object = moduleInfo.importedIdMaps;
299    const REG_DEPENDENCY: RegExp = /(?:import|from)(?:\s*)['"]([^'"]+)['"]|(?:import)(?:\s*)\(['"]([^'"]+)['"]\)/g;
300    this.source = (<string>this.source).replace(REG_DEPENDENCY, (item, staticModuleRequest, dynamicModuleRequest) => {
301      const moduleRequest: string = staticModuleRequest || dynamicModuleRequest;
302      const ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]);
303      if (ohmUrl !== undefined) {
304        item = item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => {
305          return quotation + ohmUrl + quotation;
306        });
307      }
308      return item;
309    });
310    this.processJsResourceRequest();
311  }
312
313  private processJsResourceRequest(): void {
314    this.source = (this.source as string)
315      .replace(/\b__harDefaultBundleName__\b/gi, projectConfig.bundleName)
316      .replace(/\b__harDefaultModuleName__\b/gi, projectConfig.moduleName);
317  }
318
319  private async processTransformedJsModuleRequest(rollupObject: Object): Promise<void> {
320    const moduleInfo: Object = rollupObject.getModuleInfo(this.moduleId);
321    const importMap: Object = moduleInfo.importedIdMaps;
322    const code: MagicString = new MagicString(<string>this.source);
323    // The data collected by moduleNodeMap represents the node dataset of related types.
324    // The data is processed based on the AST collected during the transform stage.
325    const moduleNodeMap: Map<string, any> =
326      moduleInfo.getNodeByType(ROLLUP_IMPORT_NODE, ROLLUP_EXPORTNAME_NODE, ROLLUP_EXPORTALL_NODE,
327        ROLLUP_DYNAMICIMPORT_NODE);
328
329    let hasDynamicImport: boolean = false;
330    if (rollupObject.share.projectConfig.needCoverageInsert && moduleInfo.ast.program) {
331      // In coverage instrumentation scenario,
332      // ast from rollup because the data of ast and moduleNodeMap are inconsistent.
333      moduleInfo.ast.program.body.forEach((node) => {
334        if (!hasDynamicImport && node.type === ROLLUP_DYNAMICIMPORT_NODE) {
335          hasDynamicImport = true;
336        }
337        if ((node.type === ROLLUP_IMPORT_NODE || node.type === ROLLUP_EXPORTNAME_NODE ||
338        node.type === ROLLUP_EXPORTALL_NODE) && node.source) {
339          const ohmUrl: string | undefined =
340            this.getOhmUrl(rollupObject, node.source.value, importMap[node.source.value]);
341          if (ohmUrl !== undefined) {
342            code.update(node.source.start, node.source.end, `'${ohmUrl}'`);
343          }
344        }
345      });
346    } else {
347      for (let nodeSet of moduleNodeMap.values()) {
348        nodeSet.forEach(node => {
349          if (!hasDynamicImport && node.type === ROLLUP_DYNAMICIMPORT_NODE) {
350            hasDynamicImport = true;
351          }
352          if (node.source) {
353            if (node.source.type === ROLLUP_LITERAL_NODE) {
354              const ohmUrl: string | undefined =
355                this.getOhmUrl(rollupObject, node.source.value, importMap[node.source.value]);
356              if (ohmUrl !== undefined) {
357                code.update(node.source.start, node.source.end, `'${ohmUrl}'`);
358              }
359            }
360          }
361        });
362      }
363    }
364
365    if (hasDynamicImport) {
366      // update sourceMap
367      const relativeSourceFilePath: string =
368        toUnixPath(this.moduleId.replace(ModuleSourceFile.projectConfig.projectRootPath + path.sep, ''));
369      const updatedMap: Object = code.generateMap({
370        source: relativeSourceFilePath,
371        file: `${path.basename(this.moduleId)}`,
372        includeContent: false,
373        hires: true
374      });
375      newSourceMaps[relativeSourceFilePath] = await updateSourceMap(newSourceMaps[relativeSourceFilePath], updatedMap);
376    }
377
378    this.source = code.toString();
379  }
380
381  private processTransformedTsModuleRequest(rollupObject: Object): void {
382    const moduleInfo: Object = rollupObject.getModuleInfo(this.moduleId);
383    const importMap: Object = moduleInfo.importedIdMaps;
384    let isMockFile: boolean = ModuleSourceFile.isMockFile(this.moduleId, rollupObject);
385
386    const moduleNodeTransformer: ts.TransformerFactory<ts.SourceFile> = context => {
387      const visitor: ts.Visitor = node => {
388        node = ts.visitEachChild(node, visitor, context);
389        // staticImport node
390        if (ts.isImportDeclaration(node) || (ts.isExportDeclaration(node) && node.moduleSpecifier)) {
391          // moduleSpecifier.getText() returns string carrying on quotation marks which the importMap's key does not,
392          // so we need to remove the quotation marks from moduleRequest.
393          const moduleRequest: string = (node.moduleSpecifier! as ts.StringLiteral).text.replace(/'|"/g, '');
394          let ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]);
395          if (ohmUrl !== undefined) {
396            // the import module are added with ".origin" at the end of the ohm url in every mock file.
397            const realOhmUrl: string = isMockFile ? `${ohmUrl}${ORIGIN_EXTENTION}` : ohmUrl;
398            if (isMockFile) {
399              ModuleSourceFile.addNewMockConfig(realOhmUrl, ohmUrl);
400            }
401            const modifiers: readonly ts.Modifier[] = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
402            if (ts.isImportDeclaration(node)) {
403              return ts.factory.createImportDeclaration(modifiers,
404                node.importClause, ts.factory.createStringLiteral(realOhmUrl));
405            } else {
406              return ts.factory.createExportDeclaration(modifiers,
407                node.isTypeOnly, node.exportClause, ts.factory.createStringLiteral(realOhmUrl));
408            }
409          }
410        }
411        // dynamicImport node
412        if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
413          const moduleRequest: string = node.arguments[0].getText().replace(/'|"/g, '');
414          const ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]);
415          if (ohmUrl !== undefined) {
416            const args: ts.Expression[] = [...node.arguments];
417            args[0] = ts.factory.createStringLiteral(ohmUrl);
418            return ts.factory.createCallExpression(node.expression, node.typeArguments, args);
419          }
420        }
421        return node;
422      };
423      return node => ts.visitNode(node, visitor);
424    };
425
426    const result: ts.TransformationResult<ts.SourceFile> =
427      ts.transform(<ts.SourceFile>this.source!, [moduleNodeTransformer]);
428
429    this.source = result.transformed[0];
430  }
431
432  // Replace each module request in source file to a unique representation which is called 'ohmUrl'.
433  // This 'ohmUrl' will be the same as the record name for each file, to make sure runtime can find the corresponding
434  // record based on each module request.
435  async processModuleRequest(rollupObject: Object, parentEvent: Object): Promise<void> {
436    if (isJsonSourceFile(this.moduleId)) {
437      return;
438    }
439    if (isJsSourceFile(this.moduleId)) {
440      const eventProcessJsModuleRequest = createAndStartEvent(parentEvent, 'process Js module request');
441      this.processJsModuleRequest(rollupObject);
442      stopEvent(eventProcessJsModuleRequest);
443      return;
444    }
445
446
447    // Only when files were transformed to ts, the corresponding ModuleSourceFile were initialized with sourceFile node,
448    // if files were transformed to js, ModuleSourceFile were initialized with srouce string.
449    if (this.isSourceNode) {
450      const eventProcessTransformedTsModuleRequest = createAndStartEvent(parentEvent, 'process transformed Ts module request');
451      this.processTransformedTsModuleRequest(rollupObject);
452      stopEvent(eventProcessTransformedTsModuleRequest);
453    } else {
454      const eventProcessTransformedJsModuleRequest = createAndStartEvent(parentEvent, 'process transformed Js module request');
455      await this.processTransformedJsModuleRequest(rollupObject);
456      stopEvent(eventProcessTransformedJsModuleRequest);
457    }
458  }
459
460  private static initPluginEnv(rollupObject: Object): void {
461    this.projectConfig = Object.assign(rollupObject.share.arkProjectConfig, rollupObject.share.projectConfig);
462    this.logger = rollupObject.share.getLogger(GEN_ABC_PLUGIN_NAME);
463  }
464
465  public static sortSourceFilesByModuleId(): void {
466    ModuleSourceFile.sourceFiles.sort((a, b) => a.moduleId.localeCompare(b.moduleId));
467  }
468
469  public static cleanUpObjects(): void {
470    ModuleSourceFile.sourceFiles = [];
471    ModuleSourceFile.projectConfig = undefined;
472    ModuleSourceFile.logger = undefined;
473    ModuleSourceFile.mockConfigInfo = {};
474    ModuleSourceFile.mockFiles = [];
475    ModuleSourceFile.newMockConfigInfo = {};
476    ModuleSourceFile.needProcessMock = false;
477  }
478}
479