• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2024 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 path from 'path';
17import fs from 'fs';
18import {
19  createAndStartEvent,
20  stopEvent
21} from '../../ark_utils';
22import {
23  EXTNAME_ETS,
24  EXTNAME_JS,
25  EXTNAME_TS,
26  EXTNAME_MJS,
27  EXTNAME_CJS,
28  GEN_ABC_PLUGIN_NAME,
29  SOURCEMAPS,
30  SOURCEMAPS_JSON,
31  yellow,
32  reset
33} from "./common/ark_define";
34import {
35  changeFileExtension,
36  isCommonJsPluginVirtualFile,
37  isCurrentProjectFiles,
38  isDebug,
39  shouldETSOrTSFileTransformToJS
40} from "./utils";
41import {
42  toUnixPath,
43  isPackageModulesFile,
44  getProjectRootPath
45} from "../../utils";
46import {
47  handleObfuscatedFilePath,
48  mangleFilePath,
49  enableObfuscateFileName
50} from './common/ob_config_resolver';
51import { MemoryMonitor } from '../meomry_monitor/rollup-plugin-memory-monitor';
52import { MemoryDefine } from '../meomry_monitor/memory_define';
53import {
54  ArkTSInternalErrorDescription,
55  ErrorCode
56} from './error_code';
57import {
58  CommonLogger,
59  LogData,
60  LogDataFactory
61} from './logger';
62
63export class SourceMapGenerator {
64  private static instance: SourceMapGenerator | undefined = undefined;
65  private static rollupObject: Object | undefined;
66
67  private projectConfig: Object;
68  private sourceMapPath: string;
69  private cacheSourceMapPath: string;
70  private triggerAsync: Object;
71  private triggerEndSignal: Object;
72  private sourceMaps: Object = {};
73  private isNewSourceMap: boolean = true;
74  private keyCache: Map<string, string> = new Map();
75  private logger: CommonLogger;
76
77  public sourceMapKeyMappingForObf: Map<string, string> = new Map();
78
79  constructor(rollupObject: Object) {
80    this.projectConfig = Object.assign(rollupObject.share.arkProjectConfig, rollupObject.share.projectConfig);
81    this.sourceMapPath = this.getSourceMapSavePath();
82    this.cacheSourceMapPath = path.join(this.projectConfig.cachePath, SOURCEMAPS_JSON);
83    this.triggerAsync = rollupObject.async;
84    this.triggerEndSignal = rollupObject.signal;
85    this.logger = CommonLogger.getInstance(rollupObject);
86  }
87
88  static init(rollupObject: Object): void {
89    SourceMapGenerator.rollupObject = rollupObject;
90    SourceMapGenerator.instance = new SourceMapGenerator(SourceMapGenerator.rollupObject);
91
92    // adapt compatibility with hvigor
93    if (!SourceMapGenerator.instance.projectConfig.entryPackageName ||
94      !SourceMapGenerator.instance.projectConfig.entryModuleVersion) {
95        SourceMapGenerator.instance.isNewSourceMap = false;
96    }
97  }
98
99  static getInstance(): SourceMapGenerator {
100    if (!SourceMapGenerator.instance) {
101      SourceMapGenerator.instance = new SourceMapGenerator(SourceMapGenerator.rollupObject);
102    }
103    return SourceMapGenerator.instance;
104  }
105
106  //In window plateform, if receive path join by '/', should transform '/' to '\'
107  private getAdaptedModuleId(moduleId: string): string {
108    return moduleId.replace(/\//g, path.sep);
109  }
110
111  private getPkgInfoByModuleId(moduleId: string, shouldObfuscateFileName: boolean = false): Object {
112    moduleId = this.getAdaptedModuleId(moduleId);
113
114    const moduleInfo: Object = SourceMapGenerator.rollupObject.getModuleInfo(moduleId);
115    if (!moduleInfo) {
116      const errInfo: LogData = LogDataFactory.newInstance(
117        ErrorCode.ETS2BUNDLE_INTERNAL_GET_MODULE_INFO_FAILED,
118        ArkTSInternalErrorDescription,
119        `Failed to get ModuleInfo, moduleId: ${moduleId}`
120      );
121      this.logger.printErrorAndExit(errInfo);
122    }
123    const metaInfo: Object = moduleInfo['meta'];
124    if (!metaInfo) {
125      const errInfo: LogData = LogDataFactory.newInstance(
126        ErrorCode.ETS2BUNDLE_INTERNAL_UNABLE_TO_GET_MODULE_INFO_META,
127        ArkTSInternalErrorDescription,
128        `Failed to get ModuleInfo properties 'meta', moduleId: ${moduleId}`
129      );
130      this.logger.printErrorAndExit(errInfo);
131    }
132    const pkgPath = metaInfo['pkgPath'];
133    if (!pkgPath) {
134      const errInfo: LogData = LogDataFactory.newInstance(
135        ErrorCode.ETS2BUNDLE_INTERNAL_UNABLE_TO_GET_MODULE_INFO_META_PKG_PATH,
136        ArkTSInternalErrorDescription,
137        `Failed to get ModuleInfo properties 'meta.pkgPath', moduleId: ${moduleId}`
138      );
139      this.logger.printErrorAndExit(errInfo);
140    }
141
142    const dependencyPkgInfo = metaInfo['dependencyPkgInfo'];
143    let middlePath = this.getIntermediateModuleId(moduleId, metaInfo).replace(pkgPath + path.sep, '');
144    if (shouldObfuscateFileName) {
145      middlePath = mangleFilePath(middlePath);
146    }
147    return {
148      entry: {
149        name: this.projectConfig.entryPackageName,
150        version: this.projectConfig.entryModuleVersion
151      },
152      dependency: dependencyPkgInfo ? {
153        name: dependencyPkgInfo['pkgName'],
154        version: dependencyPkgInfo['pkgVersion']
155      } : undefined,
156      modulePath: toUnixPath(middlePath)
157    };
158  }
159
160  public setNewSoureMaps(isNewSourceMap: boolean): void {
161    this.isNewSourceMap = isNewSourceMap;
162  }
163
164  public isNewSourceMaps(): boolean {
165    return this.isNewSourceMap;
166  }
167
168  //generate sourcemap key, notice: moduleId is absolute path
169  public genKey(moduleId: string, shouldObfuscateFileName: boolean = false): string {
170    moduleId = this.getAdaptedModuleId(moduleId);
171
172    let key: string = this.keyCache.get(moduleId);
173    if (key && !shouldObfuscateFileName) {
174      return key;
175    }
176    const pkgInfo = this.getPkgInfoByModuleId(moduleId, shouldObfuscateFileName);
177    if (pkgInfo.dependency) {
178      key = `${pkgInfo.entry.name}|${pkgInfo.dependency.name}|${pkgInfo.dependency.version}|${pkgInfo.modulePath}`;
179    } else {
180      key = `${pkgInfo.entry.name}|${pkgInfo.entry.name}|${pkgInfo.entry.version}|${pkgInfo.modulePath}`;
181    }
182    if (key && !shouldObfuscateFileName) {
183      this.keyCache.set(moduleId, key);
184    }
185    return key;
186  }
187
188  private getSourceMapSavePath(): string {
189    if (this.projectConfig.compileHar && this.projectConfig.sourceMapDir && !this.projectConfig.byteCodeHar) {
190      return path.join(this.projectConfig.sourceMapDir, SOURCEMAPS);
191    }
192    return isDebug(this.projectConfig) ? path.join(this.projectConfig.aceModuleBuild, SOURCEMAPS) :
193      path.join(this.projectConfig.cachePath, SOURCEMAPS);
194  }
195
196  public buildModuleSourceMapInfo(parentEvent: Object): void {
197    if (this.projectConfig.widgetCompile) {
198      return;
199    }
200
201    const eventUpdateCachedSourceMaps = createAndStartEvent(parentEvent, 'update cached source maps');
202    // If hap/hsp depends on bytecode har under debug mode, the source map of bytecode har need to be merged with
203    // source map of hap/hsp.
204    if (isDebug(this.projectConfig) && !this.projectConfig.byteCodeHar && !!this.projectConfig.byteCodeHarInfo) {
205      Object.keys(this.projectConfig.byteCodeHarInfo).forEach((packageName) => {
206        const sourceMapsPath = this.projectConfig.byteCodeHarInfo[packageName].sourceMapsPath;
207        if (!sourceMapsPath && !!this.logger && !!this.logger.warn) {
208          this.logger.warn(yellow, `ArkTS:WARN Property 'sourceMapsPath' not found in '${packageName}'.`, reset);
209        }
210        if (!!sourceMapsPath) {
211          const bytecodeHarSourceMap = JSON.parse(fs.readFileSync(toUnixPath(sourceMapsPath)).toString());
212          Object.assign(this.sourceMaps, bytecodeHarSourceMap);
213        }
214      });
215    }
216    const updateSourceRecordInfo = MemoryMonitor.recordStage(MemoryDefine.UPDATE_SOURCE_MAPS);
217    const cacheSourceMapObject: Object = this.updateCachedSourceMaps();
218    MemoryMonitor.stopRecordStage(updateSourceRecordInfo);
219    stopEvent(eventUpdateCachedSourceMaps);
220
221    this.triggerAsync(() => {
222      const eventWriteFile = createAndStartEvent(parentEvent, 'write source map (async)', true);
223      fs.writeFile(this.sourceMapPath, JSON.stringify(cacheSourceMapObject, null, 2), 'utf-8', (err) => {
224        if (err) {
225          const errInfo: LogData = LogDataFactory.newInstance(
226            ErrorCode.ETS2BUNDLE_INTERNAL_WRITE_SOURCE_MAP_FAILED,
227            ArkTSInternalErrorDescription,
228            `Failed to write sourceMaps. ${err.message}`,
229            this.sourceMapPath
230          );
231          this.logger.printErrorAndExit(errInfo);
232        }
233        fs.copyFileSync(this.sourceMapPath, this.cacheSourceMapPath);
234        stopEvent(eventWriteFile, true);
235        this.triggerEndSignal();
236      });
237    });
238  }
239
240  //update cache sourcemap object
241  public updateCachedSourceMaps(): Object {
242    if (!this.isNewSourceMap) {
243      this.modifySourceMapKeyToCachePath(this.sourceMaps);
244    }
245
246    let cacheSourceMapObject: Object;
247
248    if (!fs.existsSync(this.cacheSourceMapPath)) {
249      cacheSourceMapObject = this.sourceMaps;
250    } else {
251      cacheSourceMapObject = JSON.parse(fs.readFileSync(this.cacheSourceMapPath).toString());
252
253      // remove unused source files's sourceMap
254      let unusedFiles = [];
255      let compileFileList: Set<string> = new Set();
256      for (let moduleId of SourceMapGenerator.rollupObject.getModuleIds()) {
257        // exclude .dts|.d.ets file
258        if (isCommonJsPluginVirtualFile(moduleId) || !isCurrentProjectFiles(moduleId, this.projectConfig)) {
259          continue;
260        }
261
262        if (this.isNewSourceMap) {
263          const isPackageModules = isPackageModulesFile(moduleId, this.projectConfig);
264          if (enableObfuscateFileName(isPackageModules, this.projectConfig)){
265            compileFileList.add(this.genKey(moduleId, true));
266          } else {
267            compileFileList.add(this.genKey(moduleId));
268          }
269          continue;
270        }
271
272        // adapt compatibilty with hvigor
273        const projectRootPath = getProjectRootPath(moduleId, this.projectConfig, this.projectConfig?.rootPathSet);
274        let cacheModuleId = this.getIntermediateModuleId(toUnixPath(moduleId))
275          .replace(toUnixPath(projectRootPath), toUnixPath(this.projectConfig.cachePath));
276
277        const isPackageModules = isPackageModulesFile(moduleId, this.projectConfig);
278        if (enableObfuscateFileName(isPackageModules, this.projectConfig)) {
279          compileFileList.add(mangleFilePath(cacheModuleId));
280        } else {
281          compileFileList.add(cacheModuleId);
282        }
283      }
284
285      Object.keys(cacheSourceMapObject).forEach(key => {
286        let newkeyOrOldCachePath = key;
287        if (!this.isNewSourceMap) {
288          newkeyOrOldCachePath = toUnixPath(path.join(this.projectConfig.projectRootPath, key));
289        }
290        if (!compileFileList.has(newkeyOrOldCachePath)) {
291          unusedFiles.push(key);
292        }
293      });
294      unusedFiles.forEach(file => {
295        delete cacheSourceMapObject[file];
296      })
297
298      // update sourceMap
299      Object.keys(this.sourceMaps).forEach(key => {
300        cacheSourceMapObject[key] = this.sourceMaps[key];
301      });
302    }
303    // update the key for filename obfuscation
304    for (let [key, newKey] of this.sourceMapKeyMappingForObf) {
305      this.updateSourceMapKeyWithObf(cacheSourceMapObject, key, newKey);
306    }
307    return cacheSourceMapObject;
308  }
309
310  public getSourceMaps(): Object {
311    return this.sourceMaps;
312  }
313
314  public getSourceMap(moduleId: string): Object {
315    return this.getSpecifySourceMap(this.sourceMaps, moduleId);
316  }
317
318  //get specify sourcemap, allow receive param sourcemap
319  public getSpecifySourceMap(specifySourceMap: Object, moduleId: string): Object {
320    const key = this.isNewSourceMap ? this.genKey(moduleId) : moduleId;
321    if (specifySourceMap && specifySourceMap[key]) {
322      return specifySourceMap[key];
323    }
324    return undefined;
325  }
326
327  public updateSourceMap(moduleId: string, map: Object) {
328    if (!this.sourceMaps) {
329      this.sourceMaps = {};
330    }
331    this.updateSpecifySourceMap(this.sourceMaps, moduleId, map);
332  }
333
334  //update specify sourcemap, allow receive param sourcemap
335  public updateSpecifySourceMap(specifySourceMap: Object, moduleId: string, sourceMap: Object) {
336    const key = this.isNewSourceMap ? this.genKey(moduleId) : moduleId;
337    specifySourceMap[key] = sourceMap;
338  }
339
340  public fillSourceMapPackageInfo(moduleId: string, sourcemap: Object) {
341    if (!this.isNewSourceMap) {
342      return;
343    }
344
345    const pkgInfo = this.getPkgInfoByModuleId(moduleId);
346    sourcemap['entry-package-info'] = `${pkgInfo.entry.name}|${pkgInfo.entry.version}`;
347    if (pkgInfo.dependency) {
348      sourcemap['package-info'] = `${pkgInfo.dependency.name}|${pkgInfo.dependency.version}`;
349    }
350  }
351
352  private getIntermediateModuleId(moduleId: string, metaInfo?: Object): string {
353    let extName: string = "";
354    switch (path.extname(moduleId)) {
355      case EXTNAME_ETS: {
356        extName = shouldETSOrTSFileTransformToJS(moduleId, this.projectConfig, metaInfo) ? EXTNAME_JS : EXTNAME_TS;
357        break;
358      }
359      case EXTNAME_TS: {
360        extName = shouldETSOrTSFileTransformToJS(moduleId, this.projectConfig, metaInfo) ? EXTNAME_JS : '';
361        break;
362      }
363      case EXTNAME_JS:
364      case EXTNAME_MJS:
365      case EXTNAME_CJS: {
366        extName = (moduleId.endsWith(EXTNAME_MJS) || moduleId.endsWith(EXTNAME_CJS)) ? EXTNAME_JS : '';
367        break;
368      }
369      default:
370        break;
371    }
372    if (extName.length !== 0) {
373      return changeFileExtension(moduleId, extName);
374    }
375    return moduleId;
376  }
377
378  public setSourceMapPath(path: string): void {
379    this.sourceMapPath = path;
380  }
381
382  public modifySourceMapKeyToCachePath(sourceMap: object): void {
383    const projectConfig: object = this.projectConfig;
384
385    // modify source map keys to keep IDE tools right
386    const relativeCachePath: string = toUnixPath(projectConfig.cachePath.replace(
387      projectConfig.projectRootPath + path.sep, ''));
388    Object.keys(sourceMap).forEach(key => {
389      let newKey: string = relativeCachePath + '/' + key;
390      if (!newKey.endsWith(EXTNAME_JS)) {
391        const moduleId: string = this.projectConfig.projectRootPath + path.sep + key;
392        const extName: string = shouldETSOrTSFileTransformToJS(moduleId, this.projectConfig) ? EXTNAME_JS : EXTNAME_TS;
393        newKey = changeFileExtension(newKey, extName);
394      }
395      const isOhModules = key.startsWith('oh_modules');
396      newKey = handleObfuscatedFilePath(newKey, isOhModules, this.projectConfig);
397      sourceMap[newKey] = sourceMap[key];
398      delete sourceMap[key];
399    });
400  }
401
402  public static cleanSourceMapObject(): void {
403    if (this.instance) {
404      this.instance.keyCache.clear();
405      this.instance.sourceMaps = undefined;
406      this.instance = undefined;
407    }
408    if (this.rollupObject) {
409      this.rollupObject = undefined;
410    }
411  }
412
413  private updateSourceMapKeyWithObf(specifySourceMap: Object, key: string, newKey: string): void {
414    if (!specifySourceMap.hasOwnProperty(key) || key === newKey) {
415      return;
416    }
417    specifySourceMap[newKey] = specifySourceMap[key];
418    delete specifySourceMap[key];
419  }
420
421  public saveKeyMappingForObfFileName(originalFilePath: string): void {
422    this.sourceMapKeyMappingForObf.set(this.genKey(originalFilePath), this.genKey(originalFilePath, true));
423  }
424
425  //use by UT
426  static initInstance(rollupObject: Object): SourceMapGenerator {
427    if (!SourceMapGenerator.instance) {
428      SourceMapGenerator.init(rollupObject);
429    }
430    return SourceMapGenerator.getInstance();
431  }
432}