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