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}