1/* 2 * Copyright (c) 2023 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 * as ts from 'typescript'; 17import fs from 'fs'; 18import path from 'path'; 19import MagicString from 'magic-string'; 20import { 21 GEN_ABC_PLUGIN_NAME, 22 PACKAGES 23} from '../common/ark_define'; 24import { 25 getOhmUrlByFilepath, 26 getOhmUrlByHarName, 27 getOhmUrlBySystemApiOrLibRequest, 28 mangleDeclarationFileName, 29} from '../../../ark_utils'; 30import { writeFileSyncByNode } from '../../../process_module_files'; 31import { 32 isDebug, 33 isJsonSourceFile, 34 isJsSourceFile, 35 updateSourceMap, 36 writeFileContentToTempDir 37} from '../utils'; 38import { toUnixPath } from '../../../utils'; 39import { 40 createAndStartEvent, 41 stopEvent 42} from '../../../ark_utils'; 43import { newSourceMaps } from '../transform'; 44import { writeObfuscationNameCache } from '../common/ob_config_resolver'; 45import { ORIGIN_EXTENTION } from '../process_mock'; 46import { 47 ESMODULE, 48 TRANSFORMED_MOCK_CONFIG, 49 USER_DEFINE_MOCK_CONFIG 50} from '../../../pre_define'; 51import { readProjectAndLibsSource } from '../common/process_ark_config'; 52import { allSourceFilePaths, collectAllFiles } from '../../../ets_checker'; 53import { projectConfig } from '../../../../main'; 54const ROLLUP_IMPORT_NODE: string = 'ImportDeclaration'; 55const ROLLUP_EXPORTNAME_NODE: string = 'ExportNamedDeclaration'; 56const ROLLUP_EXPORTALL_NODE: string = 'ExportAllDeclaration'; 57const ROLLUP_DYNAMICIMPORT_NODE: string = 'ImportExpression'; 58const ROLLUP_LITERAL_NODE: string = 'Literal'; 59 60export class ModuleSourceFile { 61 private static sourceFiles: ModuleSourceFile[] = []; 62 private moduleId: string; 63 private source: string | ts.SourceFile; 64 private isSourceNode: boolean = false; 65 private static projectConfig: Object; 66 private static logger: Object; 67 private static mockConfigInfo: Object = {}; 68 private static mockFiles: string[] = []; 69 private static newMockConfigInfo: Object = {}; 70 private static needProcessMock: boolean = false; 71 72 constructor(moduleId: string, source: string | ts.SourceFile) { 73 this.moduleId = moduleId; 74 this.source = source; 75 if (typeof this.source !== 'string') { 76 this.isSourceNode = true; 77 } 78 } 79 80 static setProcessMock(rollupObject: Object): void { 81 // only processing mock-config.json5 in preview or OhosTest mode 82 if (!(rollupObject.share.projectConfig.isPreview || rollupObject.share.projectConfig.isOhosTest)) { 83 ModuleSourceFile.needProcessMock = false; 84 return; 85 } 86 87 // mockParams is essential, and etsSourceRootPath && mockConfigPath need to be defined in mockParams 88 // mockParams = { 89 // "decorator": "name of mock decorator", 90 // "packageName": "name of mock package", 91 // "etsSourceRootPath": "path of ets source root", 92 // "mockConfigPath": "path of mock configuration file" 93 // } 94 ModuleSourceFile.needProcessMock = (rollupObject.share.projectConfig.mockParams && 95 rollupObject.share.projectConfig.mockParams.etsSourceRootPath && 96 rollupObject.share.projectConfig.mockParams.mockConfigPath) ? true : false; 97 } 98 99 static collectMockConfigInfo(rollupObject: Object): void { 100 ModuleSourceFile.mockConfigInfo = require('json5').parse( 101 fs.readFileSync(rollupObject.share.projectConfig.mockParams.mockConfigPath, 'utf-8')); 102 for (let mockedTarget in ModuleSourceFile.mockConfigInfo) { 103 if (ModuleSourceFile.mockConfigInfo[mockedTarget].source) { 104 ModuleSourceFile.mockFiles.push(ModuleSourceFile.mockConfigInfo[mockedTarget].source); 105 } 106 } 107 } 108 109 static addNewMockConfig(key: string, src: string): void { 110 if (ModuleSourceFile.newMockConfigInfo.hasOwnProperty(key)) { 111 return; 112 } 113 114 ModuleSourceFile.newMockConfigInfo[key] = {'source': src}; 115 } 116 117 static generateNewMockInfoByOrignMockConfig(originKey: string, transKey: string, rollupObject: Object): void { 118 if (!ModuleSourceFile.mockConfigInfo.hasOwnProperty(originKey)) { 119 return; 120 } 121 122 let mockFile: string = ModuleSourceFile.mockConfigInfo[originKey].source; 123 let mockFilePath: string = `${toUnixPath(rollupObject.share.projectConfig.modulePath)}/${mockFile}`; 124 let mockFileOhmUrl: string = getOhmUrlByFilepath(mockFilePath, 125 ModuleSourceFile.projectConfig, 126 ModuleSourceFile.logger, 127 rollupObject.share.projectConfig.entryModuleName); 128 mockFileOhmUrl = mockFileOhmUrl.startsWith(PACKAGES) ? `@package:${mockFileOhmUrl}` : `@bundle:${mockFileOhmUrl}`; 129 // record mock target mapping for incremental compilation 130 ModuleSourceFile.addNewMockConfig(transKey, mockFileOhmUrl); 131 } 132 133 static isMockFile(file: string, rollupObject: Object): boolean { 134 if (!ModuleSourceFile.needProcessMock) { 135 return false; 136 } 137 138 for (let mockFile of ModuleSourceFile.mockFiles) { 139 let absoluteMockFilePath: string = `${toUnixPath(rollupObject.share.projectConfig.modulePath)}/${mockFile}`; 140 if (toUnixPath(absoluteMockFilePath) === toUnixPath(file)) { 141 return true; 142 } 143 } 144 145 return false; 146 } 147 148 static generateMockConfigFile(rollupObject: Object): void { 149 let transformedMockConfigCache: string = 150 path.resolve(rollupObject.share.projectConfig.cachePath, `./${TRANSFORMED_MOCK_CONFIG}`); 151 let transformedMockConfig: string = 152 path.resolve(rollupObject.share.projectConfig.aceModuleJsonPath, `../${TRANSFORMED_MOCK_CONFIG}`); 153 let userDefinedMockConfigCache: string = 154 path.resolve(rollupObject.share.projectConfig.cachePath, `./${USER_DEFINE_MOCK_CONFIG}`); 155 // full compilation 156 if (!fs.existsSync(transformedMockConfigCache) || !fs.existsSync(userDefinedMockConfigCache)) { 157 fs.writeFileSync(transformedMockConfig, JSON.stringify(ModuleSourceFile.newMockConfigInfo)); 158 fs.copyFileSync(transformedMockConfig, transformedMockConfigCache); 159 fs.copyFileSync(rollupObject.share.projectConfig.mockParams.mockConfigPath, userDefinedMockConfigCache); 160 return; 161 } 162 163 // incremental compilation 164 const cachedMockConfigInfo: Object = 165 require('json5').parse(fs.readFileSync(userDefinedMockConfigCache, 'utf-8')); 166 // If mock-config.json5 is modified, incremental compilation will be disabled 167 if (JSON.stringify(ModuleSourceFile.mockConfigInfo) !== JSON.stringify(cachedMockConfigInfo)) { 168 fs.writeFileSync(transformedMockConfig, JSON.stringify(ModuleSourceFile.newMockConfigInfo)); 169 fs.copyFileSync(transformedMockConfig, transformedMockConfigCache); 170 fs.copyFileSync(rollupObject.share.projectConfig.mockParams.mockConfigPath, userDefinedMockConfigCache); 171 return; 172 } 173 // if mock-config.json5 is not modified, use the cached mock config mapping file 174 fs.copyFileSync(transformedMockConfigCache, transformedMockConfig); 175 } 176 177 static removePotentialMockConfigCache(rollupObject: Object): void { 178 const transformedMockConfigCache: string = 179 path.resolve(rollupObject.share.projectConfig.cachePath, `./${TRANSFORMED_MOCK_CONFIG}`); 180 const userDefinedMockConfigCache: string = 181 path.resolve(rollupObject.share.projectConfig.cachePath, `./${USER_DEFINE_MOCK_CONFIG}`); 182 if (fs.existsSync(transformedMockConfigCache)) { 183 fs.rm(transformedMockConfigCache); 184 } 185 186 if (fs.existsSync(userDefinedMockConfigCache)) { 187 fs.rm(userDefinedMockConfigCache); 188 } 189 } 190 191 static newSourceFile(moduleId: string, source: string | ts.SourceFile) { 192 ModuleSourceFile.sourceFiles.push(new ModuleSourceFile(moduleId, source)); 193 } 194 195 static getSourceFiles(): ModuleSourceFile[] { 196 return ModuleSourceFile.sourceFiles; 197 } 198 199 static async processModuleSourceFiles(rollupObject: Object, parentEvent: Object): Promise<void> { 200 this.initPluginEnv(rollupObject); 201 202 // collect mockConfigInfo 203 ModuleSourceFile.setProcessMock(rollupObject); 204 if (ModuleSourceFile.needProcessMock) { 205 ModuleSourceFile.collectMockConfigInfo(rollupObject); 206 } else { 207 ModuleSourceFile.removePotentialMockConfigCache(rollupObject); 208 } 209 210 collectAllFiles(undefined, rollupObject.getModuleIds()); 211 readProjectAndLibsSource(allSourceFilePaths, ModuleSourceFile.projectConfig.obfuscationMergedObConfig, 212 ModuleSourceFile.projectConfig.arkObfuscator, ModuleSourceFile.projectConfig.compileHar); 213 214 // Sort the collection by file name to ensure binary consistency. 215 ModuleSourceFile.sortSourceFilesByModuleId(); 216 for (const source of ModuleSourceFile.sourceFiles) { 217 if (!rollupObject.share.projectConfig.compileHar) { 218 // compileHar: compile closed source har of project, which convert .ets to .d.ts and js, doesn't transform module request. 219 const eventBuildModuleSourceFile = createAndStartEvent(parentEvent, 'build module source files'); 220 await source.processModuleRequest(rollupObject, eventBuildModuleSourceFile); 221 stopEvent(eventBuildModuleSourceFile); 222 } 223 const eventWriteSourceFile = createAndStartEvent(parentEvent, 'write source file'); 224 await source.writeSourceFile(eventWriteSourceFile); 225 stopEvent(eventWriteSourceFile); 226 } 227 228 if (rollupObject.share.arkProjectConfig.compileMode === ESMODULE) { 229 await mangleDeclarationFileName(ModuleSourceFile.logger, rollupObject.share.arkProjectConfig); 230 } 231 232 const eventObfuscatedCode = createAndStartEvent(parentEvent, 'write obfuscation name cache'); 233 if ((ModuleSourceFile.projectConfig.arkObfuscator || ModuleSourceFile.projectConfig.terserConfig) && 234 ModuleSourceFile.projectConfig.obfuscationOptions) { 235 writeObfuscationNameCache(ModuleSourceFile.projectConfig, ModuleSourceFile.projectConfig.obfuscationOptions.obfuscationCacheDir, 236 ModuleSourceFile.projectConfig.obfuscationMergedObConfig.options?.printNameCache); 237 } 238 stopEvent(eventObfuscatedCode); 239 240 const eventGenerateMockConfigFile = createAndStartEvent(parentEvent, 'generate mock config file'); 241 if (ModuleSourceFile.needProcessMock) { 242 ModuleSourceFile.generateMockConfigFile(rollupObject); 243 } 244 stopEvent(eventGenerateMockConfigFile); 245 246 ModuleSourceFile.sourceFiles = []; 247 } 248 249 getModuleId(): string { 250 return this.moduleId; 251 } 252 253 private async writeSourceFile(parentEvent: Object): Promise<void> { 254 if (this.isSourceNode && !isJsSourceFile(this.moduleId)) { 255 await writeFileSyncByNode(<ts.SourceFile>this.source, ModuleSourceFile.projectConfig, parentEvent, ModuleSourceFile.logger); 256 } else { 257 await writeFileContentToTempDir(this.moduleId, <string>this.source, ModuleSourceFile.projectConfig, ModuleSourceFile.logger, parentEvent); 258 } 259 } 260 261 private getOhmUrl(rollupObject: Object, moduleRequest: string, filePath: string | undefined): string | undefined { 262 let systemOrLibOhmUrl: string | undefined = getOhmUrlBySystemApiOrLibRequest(moduleRequest); 263 if (systemOrLibOhmUrl != undefined) { 264 if (ModuleSourceFile.needProcessMock) { 265 ModuleSourceFile.generateNewMockInfoByOrignMockConfig(moduleRequest, systemOrLibOhmUrl, rollupObject); 266 } 267 return systemOrLibOhmUrl; 268 } 269 const harOhmUrl: string | undefined = getOhmUrlByHarName(moduleRequest, ModuleSourceFile.projectConfig); 270 if (harOhmUrl !== undefined) { 271 if (ModuleSourceFile.needProcessMock) { 272 ModuleSourceFile.generateNewMockInfoByOrignMockConfig(moduleRequest, harOhmUrl, rollupObject); 273 } 274 return harOhmUrl; 275 } 276 if (filePath) { 277 const targetModuleInfo: Object = rollupObject.getModuleInfo(filePath); 278 const namespace: string = targetModuleInfo['meta']['moduleName']; 279 const ohmUrl: string = 280 getOhmUrlByFilepath(filePath, ModuleSourceFile.projectConfig, ModuleSourceFile.logger, namespace); 281 let res: string = ohmUrl.startsWith(PACKAGES) ? `@package:${ohmUrl}` : `@bundle:${ohmUrl}`; 282 if (ModuleSourceFile.needProcessMock) { 283 // processing cases of har or lib mock targets 284 ModuleSourceFile.generateNewMockInfoByOrignMockConfig(moduleRequest, res, rollupObject); 285 // processing cases of user-defined mock targets 286 let mockedTarget: string = toUnixPath(filePath). 287 replace(toUnixPath(rollupObject.share.projectConfig.modulePath), ''). 288 replace(`/${rollupObject.share.projectConfig.mockParams.etsSourceRootPath}/`, ''); 289 ModuleSourceFile.generateNewMockInfoByOrignMockConfig(mockedTarget, res, rollupObject); 290 } 291 return res; 292 } 293 return undefined; 294 } 295 296 private processJsModuleRequest(rollupObject: Object): void { 297 const moduleInfo: Object = rollupObject.getModuleInfo(this.moduleId); 298 const importMap: Object = moduleInfo.importedIdMaps; 299 const REG_DEPENDENCY: RegExp = /(?:import|from)(?:\s*)['"]([^'"]+)['"]|(?:import)(?:\s*)\(['"]([^'"]+)['"]\)/g; 300 this.source = (<string>this.source).replace(REG_DEPENDENCY, (item, staticModuleRequest, dynamicModuleRequest) => { 301 const moduleRequest: string = staticModuleRequest || dynamicModuleRequest; 302 const ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]); 303 if (ohmUrl !== undefined) { 304 item = item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => { 305 return quotation + ohmUrl + quotation; 306 }); 307 } 308 return item; 309 }); 310 this.processJsResourceRequest(); 311 } 312 313 private processJsResourceRequest(): void { 314 this.source = (this.source as string) 315 .replace(/\b__harDefaultBundleName__\b/gi, projectConfig.bundleName) 316 .replace(/\b__harDefaultModuleName__\b/gi, projectConfig.moduleName); 317 } 318 319 private async processTransformedJsModuleRequest(rollupObject: Object): Promise<void> { 320 const moduleInfo: Object = rollupObject.getModuleInfo(this.moduleId); 321 const importMap: Object = moduleInfo.importedIdMaps; 322 const code: MagicString = new MagicString(<string>this.source); 323 // The data collected by moduleNodeMap represents the node dataset of related types. 324 // The data is processed based on the AST collected during the transform stage. 325 const moduleNodeMap: Map<string, any> = 326 moduleInfo.getNodeByType(ROLLUP_IMPORT_NODE, ROLLUP_EXPORTNAME_NODE, ROLLUP_EXPORTALL_NODE, 327 ROLLUP_DYNAMICIMPORT_NODE); 328 329 let hasDynamicImport: boolean = false; 330 if (rollupObject.share.projectConfig.needCoverageInsert && moduleInfo.ast.program) { 331 // In coverage instrumentation scenario, 332 // ast from rollup because the data of ast and moduleNodeMap are inconsistent. 333 moduleInfo.ast.program.body.forEach((node) => { 334 if (!hasDynamicImport && node.type === ROLLUP_DYNAMICIMPORT_NODE) { 335 hasDynamicImport = true; 336 } 337 if ((node.type === ROLLUP_IMPORT_NODE || node.type === ROLLUP_EXPORTNAME_NODE || 338 node.type === ROLLUP_EXPORTALL_NODE) && node.source) { 339 const ohmUrl: string | undefined = 340 this.getOhmUrl(rollupObject, node.source.value, importMap[node.source.value]); 341 if (ohmUrl !== undefined) { 342 code.update(node.source.start, node.source.end, `'${ohmUrl}'`); 343 } 344 } 345 }); 346 } else { 347 for (let nodeSet of moduleNodeMap.values()) { 348 nodeSet.forEach(node => { 349 if (!hasDynamicImport && node.type === ROLLUP_DYNAMICIMPORT_NODE) { 350 hasDynamicImport = true; 351 } 352 if (node.source) { 353 if (node.source.type === ROLLUP_LITERAL_NODE) { 354 const ohmUrl: string | undefined = 355 this.getOhmUrl(rollupObject, node.source.value, importMap[node.source.value]); 356 if (ohmUrl !== undefined) { 357 code.update(node.source.start, node.source.end, `'${ohmUrl}'`); 358 } 359 } 360 } 361 }); 362 } 363 } 364 365 if (hasDynamicImport) { 366 // update sourceMap 367 const relativeSourceFilePath: string = 368 toUnixPath(this.moduleId.replace(ModuleSourceFile.projectConfig.projectRootPath + path.sep, '')); 369 const updatedMap: Object = code.generateMap({ 370 source: relativeSourceFilePath, 371 file: `${path.basename(this.moduleId)}`, 372 includeContent: false, 373 hires: true 374 }); 375 newSourceMaps[relativeSourceFilePath] = await updateSourceMap(newSourceMaps[relativeSourceFilePath], updatedMap); 376 } 377 378 this.source = code.toString(); 379 } 380 381 private processTransformedTsModuleRequest(rollupObject: Object): void { 382 const moduleInfo: Object = rollupObject.getModuleInfo(this.moduleId); 383 const importMap: Object = moduleInfo.importedIdMaps; 384 let isMockFile: boolean = ModuleSourceFile.isMockFile(this.moduleId, rollupObject); 385 386 const moduleNodeTransformer: ts.TransformerFactory<ts.SourceFile> = context => { 387 const visitor: ts.Visitor = node => { 388 node = ts.visitEachChild(node, visitor, context); 389 // staticImport node 390 if (ts.isImportDeclaration(node) || (ts.isExportDeclaration(node) && node.moduleSpecifier)) { 391 // moduleSpecifier.getText() returns string carrying on quotation marks which the importMap's key does not, 392 // so we need to remove the quotation marks from moduleRequest. 393 const moduleRequest: string = (node.moduleSpecifier! as ts.StringLiteral).text.replace(/'|"/g, ''); 394 let ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]); 395 if (ohmUrl !== undefined) { 396 // the import module are added with ".origin" at the end of the ohm url in every mock file. 397 const realOhmUrl: string = isMockFile ? `${ohmUrl}${ORIGIN_EXTENTION}` : ohmUrl; 398 if (isMockFile) { 399 ModuleSourceFile.addNewMockConfig(realOhmUrl, ohmUrl); 400 } 401 const modifiers: readonly ts.Modifier[] = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined; 402 if (ts.isImportDeclaration(node)) { 403 return ts.factory.createImportDeclaration(modifiers, 404 node.importClause, ts.factory.createStringLiteral(realOhmUrl)); 405 } else { 406 return ts.factory.createExportDeclaration(modifiers, 407 node.isTypeOnly, node.exportClause, ts.factory.createStringLiteral(realOhmUrl)); 408 } 409 } 410 } 411 // dynamicImport node 412 if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) { 413 const moduleRequest: string = node.arguments[0].getText().replace(/'|"/g, ''); 414 const ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]); 415 if (ohmUrl !== undefined) { 416 const args: ts.Expression[] = [...node.arguments]; 417 args[0] = ts.factory.createStringLiteral(ohmUrl); 418 return ts.factory.createCallExpression(node.expression, node.typeArguments, args); 419 } 420 } 421 return node; 422 }; 423 return node => ts.visitNode(node, visitor); 424 }; 425 426 const result: ts.TransformationResult<ts.SourceFile> = 427 ts.transform(<ts.SourceFile>this.source!, [moduleNodeTransformer]); 428 429 this.source = result.transformed[0]; 430 } 431 432 // Replace each module request in source file to a unique representation which is called 'ohmUrl'. 433 // This 'ohmUrl' will be the same as the record name for each file, to make sure runtime can find the corresponding 434 // record based on each module request. 435 async processModuleRequest(rollupObject: Object, parentEvent: Object): Promise<void> { 436 if (isJsonSourceFile(this.moduleId)) { 437 return; 438 } 439 if (isJsSourceFile(this.moduleId)) { 440 const eventProcessJsModuleRequest = createAndStartEvent(parentEvent, 'process Js module request'); 441 this.processJsModuleRequest(rollupObject); 442 stopEvent(eventProcessJsModuleRequest); 443 return; 444 } 445 446 447 // Only when files were transformed to ts, the corresponding ModuleSourceFile were initialized with sourceFile node, 448 // if files were transformed to js, ModuleSourceFile were initialized with srouce string. 449 if (this.isSourceNode) { 450 const eventProcessTransformedTsModuleRequest = createAndStartEvent(parentEvent, 'process transformed Ts module request'); 451 this.processTransformedTsModuleRequest(rollupObject); 452 stopEvent(eventProcessTransformedTsModuleRequest); 453 } else { 454 const eventProcessTransformedJsModuleRequest = createAndStartEvent(parentEvent, 'process transformed Js module request'); 455 await this.processTransformedJsModuleRequest(rollupObject); 456 stopEvent(eventProcessTransformedJsModuleRequest); 457 } 458 } 459 460 private static initPluginEnv(rollupObject: Object): void { 461 this.projectConfig = Object.assign(rollupObject.share.arkProjectConfig, rollupObject.share.projectConfig); 462 this.logger = rollupObject.share.getLogger(GEN_ABC_PLUGIN_NAME); 463 } 464 465 public static sortSourceFilesByModuleId(): void { 466 ModuleSourceFile.sourceFiles.sort((a, b) => a.moduleId.localeCompare(b.moduleId)); 467 } 468 469 public static cleanUpObjects(): void { 470 ModuleSourceFile.sourceFiles = []; 471 ModuleSourceFile.projectConfig = undefined; 472 ModuleSourceFile.logger = undefined; 473 ModuleSourceFile.mockConfigInfo = {}; 474 ModuleSourceFile.mockFiles = []; 475 ModuleSourceFile.newMockConfigInfo = {}; 476 ModuleSourceFile.needProcessMock = false; 477 } 478} 479