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}