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}