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