• 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 path from 'path';
18import MagicString from 'magic-string';
19import {
20  GEN_ABC_PLUGIN_NAME,
21  PACKAGES
22} from '../common/ark_define';
23import {
24  getOhmUrlByFilepath,
25  getOhmUrlByHarName,
26  getOhmUrlBySystemApiOrLibRequest
27} from '../../../ark_utils';
28import { writeFileSyncByNode } from '../../../process_module_files';
29import {
30  isDebug,
31  isJsonSourceFile,
32  isJsSourceFile,
33  updateSourceMap,
34  writeFileContentToTempDir
35} from '../utils';
36import { toUnixPath } from '../../../utils';
37import { newSourceMaps } from '../transform';
38
39import { getArkguardNameCache, writeObfuscationNameCache } from '../common/ob_config_resolver';
40const ROLLUP_IMPORT_NODE: string = 'ImportDeclaration';
41const ROLLUP_EXPORTNAME_NODE: string = 'ExportNamedDeclaration';
42const ROLLUP_EXPORTALL_NODE: string = 'ExportAllDeclaration';
43const ROLLUP_DYNAMICIMPORT_NODE: string = 'ImportExpression';
44const ROLLUP_LITERAL_NODE: string = 'Literal';
45
46export class ModuleSourceFile {
47  private static sourceFiles: ModuleSourceFile[] = [];
48  private moduleId: string;
49  private source: string | ts.SourceFile;
50  private isSourceNode: boolean = false;
51  private static projectConfig: any;
52  private static logger: any;
53
54  constructor(moduleId: string, source: string | ts.SourceFile) {
55    this.moduleId = moduleId;
56    this.source = source;
57    if (typeof this.source !== 'string') {
58      this.isSourceNode = true;
59    }
60  }
61
62  static newSourceFile(moduleId: string, source: string | ts.SourceFile) {
63    ModuleSourceFile.sourceFiles.push(new ModuleSourceFile(moduleId, source));
64  }
65
66  static getSourceFiles(): ModuleSourceFile[] {
67    return ModuleSourceFile.sourceFiles;
68  }
69
70  static async processModuleSourceFiles(rollupObject: any) {
71    this.initPluginEnv(rollupObject);
72    for (const source of ModuleSourceFile.sourceFiles) {
73      if (!rollupObject.share.projectConfig.compileHar) {
74        // compileHar: compile closed source har of project, which convert .ets to .d.ts and js, doesn't transform module request.
75        await source.processModuleRequest(rollupObject);
76      }
77      await source.writeSourceFile();
78    }
79
80    if ((ModuleSourceFile.projectConfig.arkObfuscator || ModuleSourceFile.projectConfig.terserConfig) &&
81      ModuleSourceFile.projectConfig.obfuscationOptions) {
82      writeObfuscationNameCache(ModuleSourceFile.projectConfig, ModuleSourceFile.projectConfig.obfuscationOptions.obfuscationCacheDir,
83        ModuleSourceFile.projectConfig.obfuscationMergedObConfig.options?.printNameCache);
84    }
85
86    ModuleSourceFile.sourceFiles = [];
87  }
88
89  getModuleId(): string {
90    return this.moduleId;
91  }
92
93  private async writeSourceFile() {
94    if (this.isSourceNode && !isJsSourceFile(this.moduleId)) {
95      await writeFileSyncByNode(<ts.SourceFile>this.source, ModuleSourceFile.projectConfig, ModuleSourceFile.logger);
96    } else {
97      await writeFileContentToTempDir(this.moduleId, <string>this.source, ModuleSourceFile.projectConfig, ModuleSourceFile.logger);
98    }
99  }
100
101  private getOhmUrl(rollupObject: any, moduleRequest: string, filePath: string | undefined): string | undefined {
102    let systemOrLibOhmUrl: string | undefined = getOhmUrlBySystemApiOrLibRequest(moduleRequest);
103    if (systemOrLibOhmUrl != undefined) {
104      return systemOrLibOhmUrl;
105    }
106    const harOhmUrl: string | undefined = getOhmUrlByHarName(moduleRequest, ModuleSourceFile.projectConfig);
107    if (harOhmUrl !== undefined) {
108      return harOhmUrl;
109    }
110    if (filePath) {
111      const targetModuleInfo: any = rollupObject.getModuleInfo(filePath);
112      const namespace: string = targetModuleInfo['meta']['moduleName'];
113      const ohmUrl: string =
114        getOhmUrlByFilepath(filePath, ModuleSourceFile.projectConfig, ModuleSourceFile.logger, namespace);
115      return ohmUrl.startsWith(PACKAGES) ? `@package:${ohmUrl}` : `@bundle:${ohmUrl}`;
116    }
117    return undefined;
118  }
119
120  private processJsModuleRequest(rollupObject: any) {
121    const moduleInfo: any = rollupObject.getModuleInfo(this.moduleId);
122    const importMap: any = moduleInfo.importedIdMaps;
123    const REG_DEPENDENCY: RegExp = /(?:import|from)(?:\s*)['"]([^'"]+)['"]|(?:import)(?:\s*)\(['"]([^'"]+)['"]\)/g;
124    this.source = (<string>this.source).replace(REG_DEPENDENCY, (item, staticModuleRequest, dynamicModuleRequest) => {
125      const moduleRequest: string = staticModuleRequest || dynamicModuleRequest;
126      const ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]);
127      if (ohmUrl !== undefined) {
128        item = item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => {
129          return quotation + ohmUrl + quotation;
130        });
131      }
132      return item;
133    });
134  }
135
136  private async processTransformedJsModuleRequest(rollupObject: any) {
137    const moduleInfo: any = rollupObject.getModuleInfo(this.moduleId);
138    const importMap: any = moduleInfo.importedIdMaps;
139    const code: MagicString = new MagicString(<string>this.source);
140    const moduleNodeMap: Map<string, any> =
141      moduleInfo.getNodeByType(ROLLUP_IMPORT_NODE, ROLLUP_EXPORTNAME_NODE, ROLLUP_EXPORTALL_NODE,
142        ROLLUP_DYNAMICIMPORT_NODE);
143
144    let hasDynamicImport: boolean = false;
145    for (let nodeSet of moduleNodeMap.values()) {
146      nodeSet.forEach(node => {
147        if (!hasDynamicImport && node.type === ROLLUP_DYNAMICIMPORT_NODE) {
148          hasDynamicImport = true;
149        }
150        if (node.source) {
151          if (node.source.type === ROLLUP_LITERAL_NODE) {
152            const ohmUrl: string | undefined =
153              this.getOhmUrl(rollupObject, node.source.value, importMap[node.source.value]);
154            if (ohmUrl !== undefined) {
155              code.update(node.source.start, node.source.end, `'${ohmUrl}'`);
156            }
157          } else {
158            const errorMsg: string = `ArkTS:ERROR ArkTS:ERROR File: ${this.moduleId}\n`
159              +`DynamicImport only accept stringLiteral as argument currently.\n`;
160            ModuleSourceFile.logger.error('\u001b[31m' + errorMsg);
161          }
162        }
163      });
164    }
165
166    if (hasDynamicImport) {
167      // update sourceMap
168      const relativeSourceFilePath: string =
169        toUnixPath(this.moduleId.replace(ModuleSourceFile.projectConfig.projectRootPath + path.sep, ''));
170      const updatedMap: any = code.generateMap({
171        source: relativeSourceFilePath,
172        file: `${path.basename(this.moduleId)}`,
173        includeContent: false,
174        hires: true
175      });
176      newSourceMaps[relativeSourceFilePath] = await updateSourceMap(newSourceMaps[relativeSourceFilePath], updatedMap);
177    }
178
179    this.source = code.toString();
180  }
181
182  private processTransformedTsModuleRequest(rollupObject: any) {
183    const moduleInfo: any = rollupObject.getModuleInfo(this.moduleId);
184    const importMap: any = moduleInfo.importedIdMaps;
185
186    const moduleNodeTransformer: ts.TransformerFactory<ts.SourceFile> = context => {
187      const visitor: ts.Visitor = node => {
188        node = ts.visitEachChild(node, visitor, context);
189        // staticImport node
190        if (ts.isImportDeclaration(node) || (ts.isExportDeclaration(node) && node.moduleSpecifier)) {
191          // moduleSpecifier.getText() returns string carrying on quotation marks which the importMap's key does not,
192          // so we need to remove the quotation marks from moduleRequest.
193          const moduleRequest: string = node.moduleSpecifier.getText().replace(/'|"/g, '');
194          const ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]);
195          if (ohmUrl !== undefined) {
196            if (ts.isImportDeclaration(node)) {
197              return ts.factory.createImportDeclaration(node.decorators, node.modifiers,
198                node.importClause, ts.factory.createStringLiteral(ohmUrl));
199            } else {
200              return ts.factory.createExportDeclaration(node.decorators, node.modifiers,
201                node.isTypeOnly, node.exportClause, ts.factory.createStringLiteral(ohmUrl));
202            }
203          }
204        }
205        // dynamicImport node
206        if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
207          if (!ts.isStringLiteral(node.arguments[0])) {
208            const { line, character }: ts.LineAndCharacter =
209              ts.getLineAndCharacterOfPosition(<ts.SourceFile>this.source!, node.arguments[0].pos);
210            const errorMsg: string = `ArkTS:ERROR ArkTS:ERROR File: ${this.moduleId}:${line + 1}:${character + 1}\n`
211              +`DynamicImport only accept stringLiteral as argument currently.\n`;
212            ModuleSourceFile.logger.error('\u001b[31m' + errorMsg);
213            return node;
214          }
215          const moduleRequest: string = node.arguments[0].getText().replace(/'|"/g, '');
216          const ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]);
217          if (ohmUrl !== undefined) {
218            const args: ts.Expression[] = [...node.arguments];
219            args[0] = ts.factory.createStringLiteral(ohmUrl);
220            return ts.factory.createCallExpression(node.expression, node.typeArguments, args);
221          }
222        }
223        return node;
224      };
225      return node => ts.visitNode(node, visitor);
226    };
227
228    const result: ts.TransformationResult<ts.SourceFile> =
229      ts.transform(<ts.SourceFile>this.source!, [moduleNodeTransformer]);
230
231    this.source = result.transformed[0];
232  }
233
234  // Replace each module request in source file to a unique representation which is called 'ohmUrl'.
235  // This 'ohmUrl' will be the same as the record name for each file, to make sure runtime can find the corresponding
236  // record based on each module request.
237  async processModuleRequest(rollupObject: any) {
238    if (isJsonSourceFile(this.moduleId)) {
239      return;
240    }
241    if (isJsSourceFile(this.moduleId)) {
242      this.processJsModuleRequest(rollupObject);
243      return;
244    }
245
246    // Only when files were transformed to ts, the corresponding ModuleSourceFile were initialized with sourceFile node,
247    // if files were transformed to js, ModuleSourceFile were initialized with srouce string.
248    this.isSourceNode ? this.processTransformedTsModuleRequest(rollupObject) :
249      await this.processTransformedJsModuleRequest(rollupObject);
250  }
251
252  private static initPluginEnv(rollupObject: any) {
253    this.projectConfig = Object.assign(rollupObject.share.arkProjectConfig, rollupObject.share.projectConfig);
254    this.logger = rollupObject.share.getLogger(GEN_ABC_PLUGIN_NAME);
255  }
256}
257