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 path from 'path'; 18import MagicString from 'magic-string'; 19import { 20 GEN_ABC_PLUGIN_NAME, 21 PACKAGES 22} from '../common/ark_define'; 23import { 24 getOhmUrlByFilepath, 25 getOhmUrlByHarName, 26 getOhmUrlBySystemApiOrLibRequest 27} from '../../../ark_utils'; 28import { writeFileSyncByNode } from '../../../process_module_files'; 29import { 30 isDebug, 31 isJsonSourceFile, 32 isJsSourceFile, 33 updateSourceMap, 34 writeFileContentToTempDir 35} from '../utils'; 36import { toUnixPath } from '../../../utils'; 37import { newSourceMaps } from '../transform'; 38 39import { getArkguardNameCache, writeObfuscationNameCache } from '../common/ob_config_resolver'; 40const ROLLUP_IMPORT_NODE: string = 'ImportDeclaration'; 41const ROLLUP_EXPORTNAME_NODE: string = 'ExportNamedDeclaration'; 42const ROLLUP_EXPORTALL_NODE: string = 'ExportAllDeclaration'; 43const ROLLUP_DYNAMICIMPORT_NODE: string = 'ImportExpression'; 44const ROLLUP_LITERAL_NODE: string = 'Literal'; 45 46export class ModuleSourceFile { 47 private static sourceFiles: ModuleSourceFile[] = []; 48 private moduleId: string; 49 private source: string | ts.SourceFile; 50 private isSourceNode: boolean = false; 51 private static projectConfig: any; 52 private static logger: any; 53 54 constructor(moduleId: string, source: string | ts.SourceFile) { 55 this.moduleId = moduleId; 56 this.source = source; 57 if (typeof this.source !== 'string') { 58 this.isSourceNode = true; 59 } 60 } 61 62 static newSourceFile(moduleId: string, source: string | ts.SourceFile) { 63 ModuleSourceFile.sourceFiles.push(new ModuleSourceFile(moduleId, source)); 64 } 65 66 static getSourceFiles(): ModuleSourceFile[] { 67 return ModuleSourceFile.sourceFiles; 68 } 69 70 static async processModuleSourceFiles(rollupObject: any) { 71 this.initPluginEnv(rollupObject); 72 for (const source of ModuleSourceFile.sourceFiles) { 73 if (!rollupObject.share.projectConfig.compileHar) { 74 // compileHar: compile closed source har of project, which convert .ets to .d.ts and js, doesn't transform module request. 75 await source.processModuleRequest(rollupObject); 76 } 77 await source.writeSourceFile(); 78 } 79 80 if ((ModuleSourceFile.projectConfig.arkObfuscator || ModuleSourceFile.projectConfig.terserConfig) && 81 ModuleSourceFile.projectConfig.obfuscationOptions) { 82 writeObfuscationNameCache(ModuleSourceFile.projectConfig, ModuleSourceFile.projectConfig.obfuscationOptions.obfuscationCacheDir, 83 ModuleSourceFile.projectConfig.obfuscationMergedObConfig.options?.printNameCache); 84 } 85 86 ModuleSourceFile.sourceFiles = []; 87 } 88 89 getModuleId(): string { 90 return this.moduleId; 91 } 92 93 private async writeSourceFile() { 94 if (this.isSourceNode && !isJsSourceFile(this.moduleId)) { 95 await writeFileSyncByNode(<ts.SourceFile>this.source, ModuleSourceFile.projectConfig, ModuleSourceFile.logger); 96 } else { 97 await writeFileContentToTempDir(this.moduleId, <string>this.source, ModuleSourceFile.projectConfig, ModuleSourceFile.logger); 98 } 99 } 100 101 private getOhmUrl(rollupObject: any, moduleRequest: string, filePath: string | undefined): string | undefined { 102 let systemOrLibOhmUrl: string | undefined = getOhmUrlBySystemApiOrLibRequest(moduleRequest); 103 if (systemOrLibOhmUrl != undefined) { 104 return systemOrLibOhmUrl; 105 } 106 const harOhmUrl: string | undefined = getOhmUrlByHarName(moduleRequest, ModuleSourceFile.projectConfig); 107 if (harOhmUrl !== undefined) { 108 return harOhmUrl; 109 } 110 if (filePath) { 111 const targetModuleInfo: any = rollupObject.getModuleInfo(filePath); 112 const namespace: string = targetModuleInfo['meta']['moduleName']; 113 const ohmUrl: string = 114 getOhmUrlByFilepath(filePath, ModuleSourceFile.projectConfig, ModuleSourceFile.logger, namespace); 115 return ohmUrl.startsWith(PACKAGES) ? `@package:${ohmUrl}` : `@bundle:${ohmUrl}`; 116 } 117 return undefined; 118 } 119 120 private processJsModuleRequest(rollupObject: any) { 121 const moduleInfo: any = rollupObject.getModuleInfo(this.moduleId); 122 const importMap: any = moduleInfo.importedIdMaps; 123 const REG_DEPENDENCY: RegExp = /(?:import|from)(?:\s*)['"]([^'"]+)['"]|(?:import)(?:\s*)\(['"]([^'"]+)['"]\)/g; 124 this.source = (<string>this.source).replace(REG_DEPENDENCY, (item, staticModuleRequest, dynamicModuleRequest) => { 125 const moduleRequest: string = staticModuleRequest || dynamicModuleRequest; 126 const ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]); 127 if (ohmUrl !== undefined) { 128 item = item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => { 129 return quotation + ohmUrl + quotation; 130 }); 131 } 132 return item; 133 }); 134 } 135 136 private async processTransformedJsModuleRequest(rollupObject: any) { 137 const moduleInfo: any = rollupObject.getModuleInfo(this.moduleId); 138 const importMap: any = moduleInfo.importedIdMaps; 139 const code: MagicString = new MagicString(<string>this.source); 140 const moduleNodeMap: Map<string, any> = 141 moduleInfo.getNodeByType(ROLLUP_IMPORT_NODE, ROLLUP_EXPORTNAME_NODE, ROLLUP_EXPORTALL_NODE, 142 ROLLUP_DYNAMICIMPORT_NODE); 143 144 let hasDynamicImport: boolean = false; 145 for (let nodeSet of moduleNodeMap.values()) { 146 nodeSet.forEach(node => { 147 if (!hasDynamicImport && node.type === ROLLUP_DYNAMICIMPORT_NODE) { 148 hasDynamicImport = true; 149 } 150 if (node.source) { 151 if (node.source.type === ROLLUP_LITERAL_NODE) { 152 const ohmUrl: string | undefined = 153 this.getOhmUrl(rollupObject, node.source.value, importMap[node.source.value]); 154 if (ohmUrl !== undefined) { 155 code.update(node.source.start, node.source.end, `'${ohmUrl}'`); 156 } 157 } else { 158 const errorMsg: string = `ArkTS:ERROR ArkTS:ERROR File: ${this.moduleId}\n` 159 +`DynamicImport only accept stringLiteral as argument currently.\n`; 160 ModuleSourceFile.logger.error('\u001b[31m' + errorMsg); 161 } 162 } 163 }); 164 } 165 166 if (hasDynamicImport) { 167 // update sourceMap 168 const relativeSourceFilePath: string = 169 toUnixPath(this.moduleId.replace(ModuleSourceFile.projectConfig.projectRootPath + path.sep, '')); 170 const updatedMap: any = code.generateMap({ 171 source: relativeSourceFilePath, 172 file: `${path.basename(this.moduleId)}`, 173 includeContent: false, 174 hires: true 175 }); 176 newSourceMaps[relativeSourceFilePath] = await updateSourceMap(newSourceMaps[relativeSourceFilePath], updatedMap); 177 } 178 179 this.source = code.toString(); 180 } 181 182 private processTransformedTsModuleRequest(rollupObject: any) { 183 const moduleInfo: any = rollupObject.getModuleInfo(this.moduleId); 184 const importMap: any = moduleInfo.importedIdMaps; 185 186 const moduleNodeTransformer: ts.TransformerFactory<ts.SourceFile> = context => { 187 const visitor: ts.Visitor = node => { 188 node = ts.visitEachChild(node, visitor, context); 189 // staticImport node 190 if (ts.isImportDeclaration(node) || (ts.isExportDeclaration(node) && node.moduleSpecifier)) { 191 // moduleSpecifier.getText() returns string carrying on quotation marks which the importMap's key does not, 192 // so we need to remove the quotation marks from moduleRequest. 193 const moduleRequest: string = node.moduleSpecifier.getText().replace(/'|"/g, ''); 194 const ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]); 195 if (ohmUrl !== undefined) { 196 if (ts.isImportDeclaration(node)) { 197 return ts.factory.createImportDeclaration(node.decorators, node.modifiers, 198 node.importClause, ts.factory.createStringLiteral(ohmUrl)); 199 } else { 200 return ts.factory.createExportDeclaration(node.decorators, node.modifiers, 201 node.isTypeOnly, node.exportClause, ts.factory.createStringLiteral(ohmUrl)); 202 } 203 } 204 } 205 // dynamicImport node 206 if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) { 207 if (!ts.isStringLiteral(node.arguments[0])) { 208 const { line, character }: ts.LineAndCharacter = 209 ts.getLineAndCharacterOfPosition(<ts.SourceFile>this.source!, node.arguments[0].pos); 210 const errorMsg: string = `ArkTS:ERROR ArkTS:ERROR File: ${this.moduleId}:${line + 1}:${character + 1}\n` 211 +`DynamicImport only accept stringLiteral as argument currently.\n`; 212 ModuleSourceFile.logger.error('\u001b[31m' + errorMsg); 213 return node; 214 } 215 const moduleRequest: string = node.arguments[0].getText().replace(/'|"/g, ''); 216 const ohmUrl: string | undefined = this.getOhmUrl(rollupObject, moduleRequest, importMap[moduleRequest]); 217 if (ohmUrl !== undefined) { 218 const args: ts.Expression[] = [...node.arguments]; 219 args[0] = ts.factory.createStringLiteral(ohmUrl); 220 return ts.factory.createCallExpression(node.expression, node.typeArguments, args); 221 } 222 } 223 return node; 224 }; 225 return node => ts.visitNode(node, visitor); 226 }; 227 228 const result: ts.TransformationResult<ts.SourceFile> = 229 ts.transform(<ts.SourceFile>this.source!, [moduleNodeTransformer]); 230 231 this.source = result.transformed[0]; 232 } 233 234 // Replace each module request in source file to a unique representation which is called 'ohmUrl'. 235 // This 'ohmUrl' will be the same as the record name for each file, to make sure runtime can find the corresponding 236 // record based on each module request. 237 async processModuleRequest(rollupObject: any) { 238 if (isJsonSourceFile(this.moduleId)) { 239 return; 240 } 241 if (isJsSourceFile(this.moduleId)) { 242 this.processJsModuleRequest(rollupObject); 243 return; 244 } 245 246 // Only when files were transformed to ts, the corresponding ModuleSourceFile were initialized with sourceFile node, 247 // if files were transformed to js, ModuleSourceFile were initialized with srouce string. 248 this.isSourceNode ? this.processTransformedTsModuleRequest(rollupObject) : 249 await this.processTransformedJsModuleRequest(rollupObject); 250 } 251 252 private static initPluginEnv(rollupObject: any) { 253 this.projectConfig = Object.assign(rollupObject.share.arkProjectConfig, rollupObject.share.projectConfig); 254 this.logger = rollupObject.share.getLogger(GEN_ABC_PLUGIN_NAME); 255 } 256} 257