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 this 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 { minify, MinifyOutput } from 'terser'; 19 20import { OH_MODULES } from './fast_build/ark_compiler/common/ark_define'; 21import { 22 PACKAGES, 23 TEMPORARY, 24 ZERO, 25 ONE, 26 EXTNAME_JS, 27 EXTNAME_TS, 28 EXTNAME_MJS, 29 EXTNAME_CJS, 30 EXTNAME_ABC, 31 EXTNAME_ETS, 32 EXTNAME_TS_MAP, 33 EXTNAME_JS_MAP, 34 ESMODULE, 35 FAIL, 36 TS2ABC, 37 ES2ABC, 38 EXTNAME_PROTO_BIN, 39 NATIVE_MODULE, 40} from './pre_define'; 41import { 42 isMac, 43 isWindows, 44 isPackageModulesFile, 45 genTemporaryPath, 46 getExtensionIfUnfullySpecifiedFilepath, 47 mkdirsSync, 48 toUnixPath, 49 validateFilePathLength 50} from './utils'; 51import { 52 projectConfig 53} from '../main'; 54 55const red: string = '\u001b[31m'; 56const reset: string = '\u001b[39m'; 57 58export const SRC_MAIN: string = 'src/main'; 59 60export var newSourceMaps: Object = {}; 61export const packageCollection: Map<string, Array<string>> = new Map(); 62 63export function getOhmUrlByFilepath(filePath: string, projectConfig: any, logger: any, namespace?: string): string { 64 // remove '\x00' from the rollup virtual commonjs file's filePath 65 if (filePath.startsWith('\x00')) { 66 filePath = filePath.replace('\x00', ''); 67 } 68 let unixFilePath: string = toUnixPath(filePath); 69 unixFilePath = unixFilePath.substring(0, filePath.lastIndexOf('.')); // remove extension 70 const REG_PROJECT_SRC: RegExp = /(\S+)\/src\/(?:main|ohosTest)\/(ets|js)\/(\S+)/; 71 72 const packageInfo: string[] = getPackageInfo(projectConfig.aceModuleJsonPath); 73 const bundleName: string = packageInfo[0]; 74 const moduleName: string = packageInfo[1]; 75 const moduleRootPath: string = toUnixPath(projectConfig.modulePathMap[moduleName]); 76 const projectRootPath: string = toUnixPath(projectConfig.projectRootPath); 77 // case1: /entry/src/main/ets/xxx/yyy ---> @bundle:<bundleName>/entry/ets/xxx/yyy 78 // case2: /entry/src/ohosTest/ets/xxx/yyy ---> @bundle:<bundleName>/entry_test@entry/ets/xxx/yyy 79 // case3: /node_modules/xxx/yyy ---> @package:pkg_modules/xxx/yyy 80 // case4: /entry/node_modules/xxx/yyy ---> @package:pkg_modules@entry/xxx/yyy 81 // case5: /library/node_modules/xxx/yyy ---> @package:pkg_modules@library/xxx/yyy 82 // case6: /library/index.ts ---> @budnle:<bundleName>/library/index 83 const projectFilePath: string = unixFilePath.replace(projectRootPath, ''); 84 const packageDir: string = projectConfig.packageDir; 85 const result: RegExpMatchArray | null = projectFilePath.match(REG_PROJECT_SRC); 86 if (result && result[1].indexOf(packageDir) === -1) { 87 if (namespace && moduleName !== namespace) { 88 return `${bundleName}/${moduleName}@${namespace}/${result[2]}/${result[3]}`; 89 } 90 return `${bundleName}/${moduleName}/${result[2]}/${result[3]}`; 91 } 92 93 if (projectFilePath.indexOf(packageDir) !== -1) { 94 if (process.env.compileTool === 'rollup') { 95 const tryProjectPkg: string = toUnixPath(path.join(projectRootPath, packageDir)); 96 if (unixFilePath.indexOf(tryProjectPkg) !== -1) { 97 return unixFilePath.replace(tryProjectPkg, `${packageDir}`).replace(new RegExp(packageDir, 'g'), PACKAGES); 98 } 99 // iterate the modulePathMap to find the moudleName which contains the pkg_module's file 100 for (const moduleName in projectConfig.modulePathMap) { 101 const modulePath: string = projectConfig.modulePathMap[moduleName]; 102 const tryModulePkg: string = toUnixPath(path.resolve(modulePath, packageDir)); 103 if (unixFilePath.indexOf(tryModulePkg) !== -1) { 104 return unixFilePath.replace(tryModulePkg, `${packageDir}@${moduleName}`).replace( 105 new RegExp(packageDir, 'g'), PACKAGES); 106 } 107 } 108 109 logger.error(red, `ArkTS:ERROR Failed to get an resolved OhmUrl by filepath "${filePath}"`, reset); 110 return filePath; 111 } 112 113 // webpack with old implematation 114 const tryProjectPkg: string = toUnixPath(path.join(projectRootPath, packageDir)); 115 if (unixFilePath.indexOf(tryProjectPkg) !== -1) { 116 return unixFilePath.replace(tryProjectPkg, `${packageDir}/${ONE}`).replace(new RegExp(packageDir, 'g'), PACKAGES); 117 } 118 119 const tryModulePkg: string = toUnixPath(path.join(moduleRootPath, packageDir)); 120 if (unixFilePath.indexOf(tryModulePkg) !== -1) { 121 return unixFilePath.replace(tryModulePkg, `${packageDir}/${ZERO}`).replace(new RegExp(packageDir, 'g'), PACKAGES); 122 } 123 } 124 125 for (const key in projectConfig.modulePathMap) { 126 const moduleRootPath: string = toUnixPath(projectConfig.modulePathMap[key]); 127 if (unixFilePath.indexOf(moduleRootPath + '/') !== -1) { 128 const relativeModulePath: string = unixFilePath.replace(moduleRootPath + '/', ''); 129 if (namespace && moduleName !== namespace) { 130 return `${bundleName}/${moduleName}@${namespace}/${relativeModulePath}`; 131 } 132 return `${bundleName}/${moduleName}/${relativeModulePath}`; 133 } 134 } 135 136 logger.error(red, `ArkTS:ERROR Failed to get an resolved OhmUrl by filepath "${filePath}"`, reset); 137 return filePath; 138} 139 140export function getOhmUrlBySystemApiOrLibRequest(moduleRequest: string) : string 141{ 142 const REG_SYSTEM_MODULE: RegExp = /@(system|ohos)\.(\S+)/; 143 const REG_LIB_SO: RegExp = /lib(\S+)\.so/; 144 145 if (REG_SYSTEM_MODULE.test(moduleRequest.trim())) { 146 return moduleRequest.replace(REG_SYSTEM_MODULE, (_, moduleType, systemKey) => { 147 const systemModule: string = `${moduleType}.${systemKey}`; 148 if (NATIVE_MODULE.has(systemModule)) { 149 return `@native:${systemModule}`; 150 } else { 151 return `@ohos:${systemKey}`; 152 }; 153 }); 154 } 155 if (REG_LIB_SO.test(moduleRequest.trim())) { 156 return moduleRequest.replace(REG_LIB_SO, (_, libsoKey) => { 157 return `@app:${projectConfig.bundleName}/${projectConfig.moduleName}/${libsoKey}`; 158 }); 159 } 160 161 return undefined; 162} 163 164export function genSourceMapFileName(temporaryFile: string): string { 165 let abcFile: string = temporaryFile; 166 if (temporaryFile.endsWith(EXTNAME_TS)) { 167 abcFile = temporaryFile.replace(/\.ts$/, EXTNAME_TS_MAP); 168 } else { 169 abcFile = temporaryFile.replace(/\.js$/, EXTNAME_JS_MAP); 170 } 171 return abcFile; 172} 173 174export function getBuildModeInLowerCase(projectConfig: any): string { 175 return (process.env.compileTool === 'rollup' ? projectConfig.buildMode : projectConfig.buildArkMode).toLowerCase(); 176} 177 178export function writeFileSyncByString(sourcePath: string, sourceCode: string, projectConfig: any, logger: any): void { 179 const filePath: string = genTemporaryPath(sourcePath, projectConfig.projectPath, process.env.cachePath, projectConfig); 180 if (filePath.length === 0) { 181 return; 182 } 183 mkdirsSync(path.dirname(filePath)); 184 if (/\.js$/.test(sourcePath)) { 185 sourceCode = transformModuleSpecifier(sourcePath, sourceCode, projectConfig); 186 if (projectConfig.buildArkMode === 'debug') { 187 fs.writeFileSync(filePath, sourceCode); 188 return; 189 } 190 writeMinimizedSourceCode(sourceCode, filePath, logger); 191 } 192 if (/\.json$/.test(sourcePath)) { 193 fs.writeFileSync(filePath, sourceCode); 194 } 195} 196 197export function transformModuleSpecifier(sourcePath: string, sourceCode: string, projectConfig: any): string { 198 // replace relative moduleSpecifier with ohmURl 199 const REG_RELATIVE_DEPENDENCY: RegExp = /(?:import|from)(?:\s*)['"]((?:\.\/|\.\.\/)[^'"]+|(?:\.\/?|\.\.\/?))['"]/g; 200 const REG_HAR_DEPENDENCY: RegExp = /(?:import|from)(?:\s*)['"]([^\.\/][^'"]+)['"]/g; 201 // replace requireNapi and requireNativeModule with import 202 const REG_REQUIRE_NATIVE_MODULE: RegExp = /var (\S+) = globalThis.requireNativeModule\(['"](\S+)['"]\);/g; 203 const REG_REQUIRE_NAPI_APP: RegExp = /var (\S+) = globalThis.requireNapi\(['"](\S+)['"], true, ['"](\S+)['"]\);/g; 204 const REG_REQUIRE_NAPI_OHOS: RegExp = /var (\S+) = globalThis.requireNapi\(['"](\S+)['"]\);/g; 205 206 return sourceCode.replace(REG_HAR_DEPENDENCY, (item, moduleRequest) => { 207 return replaceHarDependency(item, moduleRequest, projectConfig); 208 }).replace(REG_RELATIVE_DEPENDENCY, (item, moduleRequest) => { 209 return replaceRelativeDependency(item, moduleRequest, toUnixPath(sourcePath), projectConfig); 210 }).replace(REG_REQUIRE_NATIVE_MODULE, (_, moduleRequest, moduleName) => { 211 return `import ${moduleRequest} from '@native:${moduleName}';`; 212 }).replace(REG_REQUIRE_NAPI_APP, (_, moduleRequest, soName, moudlePath) => { 213 return `import ${moduleRequest} from '@app:${moudlePath}/${soName}';`; 214 }).replace(REG_REQUIRE_NAPI_OHOS, (_, moduleRequest, moduleName) => { 215 return `import ${moduleRequest} from '@ohos:${moduleName}';`; 216 }); 217} 218 219export function getOhmUrlByHarName(moduleRequest: string, projectConfig: any): string | undefined { 220 if (projectConfig.harNameOhmMap) { 221 // case1: "@ohos/lib" ---> "@bundle:bundleName/lib/ets/index" 222 if (projectConfig.harNameOhmMap.hasOwnProperty(moduleRequest)) { 223 return projectConfig.harNameOhmMap[moduleRequest]; 224 } 225 // case2: "@ohos/lib/src/main/ets/pages/page1" ---> "@bundle:bundleName/lib/ets/pages/page1" 226 for (const harName in projectConfig.harNameOhmMap) { 227 if (moduleRequest.startsWith(harName + '/')) { 228 const idx: number = projectConfig.harNameOhmMap[harName].split('/', 2).join('/').length; 229 const harOhmName: string = projectConfig.harNameOhmMap[harName].substring(0, idx); 230 if (moduleRequest.indexOf(harName + '/' + SRC_MAIN) === 0) { 231 return moduleRequest.replace(harName + '/' + SRC_MAIN, harOhmName); 232 } else { 233 return moduleRequest.replace(harName, harOhmName); 234 } 235 } 236 } 237 } 238 return undefined; 239} 240 241function replaceHarDependency(item:string, moduleRequest: string, projectConfig: any): string { 242 const harOhmUrl: string | undefined = getOhmUrlByHarName(moduleRequest, projectConfig); 243 if (harOhmUrl !== undefined) { 244 return item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => { 245 return quotation + harOhmUrl + quotation; 246 }); 247 } 248 return item; 249} 250 251function locateActualFilePathWithModuleRequest(absolutePath: string): string { 252 if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isDirectory()) { 253 return absolutePath 254 } 255 256 const filePath: string = absolutePath + getExtensionIfUnfullySpecifiedFilepath(absolutePath); 257 if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { 258 return absolutePath; 259 } 260 261 return path.join(absolutePath, 'index'); 262} 263 264function replaceRelativeDependency(item:string, moduleRequest: string, sourcePath: string, projectConfig: any): string { 265 if (sourcePath && projectConfig.compileMode === ESMODULE) { 266 // remove file extension from moduleRequest 267 const SUFFIX_REG: RegExp = /\.(?:[cm]?js|[e]?ts|json)$/; 268 moduleRequest = moduleRequest.replace(SUFFIX_REG, ''); 269 270 // normalize the moduleRequest 271 item = item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => { 272 let normalizedModuleRequest: string = toUnixPath(path.normalize(moduleRequest)); 273 if (moduleRequest.startsWith("./")) { 274 normalizedModuleRequest = "./" + normalizedModuleRequest; 275 } 276 return quotation + normalizedModuleRequest + quotation; 277 }); 278 279 const filePath: string = 280 locateActualFilePathWithModuleRequest(path.resolve(path.dirname(sourcePath), moduleRequest)); 281 const result: RegExpMatchArray | null = 282 filePath.match(/(\S+)(\/|\\)src(\/|\\)(?:main|ohosTest)(\/|\\)(ets|js)(\/|\\)(\S+)/); 283 if (result && projectConfig.aceModuleJsonPath) { 284 const npmModuleIdx: number = result[1].search(/(\/|\\)node_modules(\/|\\)/); 285 const projectRootPath: string = projectConfig.projectRootPath; 286 if (npmModuleIdx === -1 || npmModuleIdx === projectRootPath.search(/(\/|\\)node_modules(\/|\\)/)) { 287 const packageInfo: string[] = getPackageInfo(projectConfig.aceModuleJsonPath); 288 const bundleName: string = packageInfo[0]; 289 const moduleName: string = packageInfo[1]; 290 moduleRequest = `@bundle:${bundleName}/${moduleName}/${result[5]}/${toUnixPath(result[7])}`; 291 item = item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => { 292 return quotation + moduleRequest + quotation; 293 }); 294 } 295 } 296 } 297 return item; 298} 299 300export async function writeMinimizedSourceCode(content: string, filePath: string, logger: any, 301 isHar: boolean = false, relativeSourceFilePath: string = '', rollupNewSourceMaps: any = {}): Promise<void> { 302 let result: MinifyOutput; 303 try { 304 const minifyOptions = { 305 compress: { 306 join_vars: false, 307 sequences: 0, 308 directives: false 309 } 310 }; 311 if (!isHar) { 312 minifyOptions['format'] = { 313 semicolons: false, 314 beautify: true, 315 indent_level: 2 316 }; 317 if (process.env.compileTool === 'rollup' && relativeSourceFilePath.length > 0) { 318 minifyOptions['sourceMap'] = { 319 content: rollupNewSourceMaps[relativeSourceFilePath], 320 asObject: true 321 }; 322 } 323 } 324 result = await minify(content, minifyOptions); 325 } catch { 326 logger.error(red, `ArkTS:ERROR Failed to source code obfuscation.`, reset); 327 process.exit(FAIL); 328 } 329 if (process.env.compileTool === 'rollup' && result.map) { 330 result.map.sourcesContent && delete result.map.sourcesContent; 331 result.map.sources = [relativeSourceFilePath]; 332 rollupNewSourceMaps[relativeSourceFilePath] = result.map; 333 } 334 fs.writeFileSync(filePath, result.code); 335} 336 337export function genBuildPath(filePath: string, projectPath: string, buildPath: string, projectConfig: any): string { 338 filePath = toUnixPath(filePath); 339 if (filePath.endsWith(EXTNAME_MJS)) { 340 filePath = filePath.replace(/\.mjs$/, EXTNAME_JS); 341 } 342 if (filePath.endsWith(EXTNAME_CJS)) { 343 filePath = filePath.replace(/\.cjs$/, EXTNAME_JS); 344 } 345 projectPath = toUnixPath(projectPath); 346 347 if (isPackageModulesFile(filePath, projectConfig)) { 348 const packageDir: string = projectConfig.packageDir; 349 const fakePkgModulesPath: string = toUnixPath(path.join(projectConfig.projectRootPath, packageDir)); 350 let output: string = ''; 351 if (filePath.indexOf(fakePkgModulesPath) === -1) { 352 const hapPath: string = toUnixPath(projectConfig.projectRootPath); 353 const tempFilePath: string = filePath.replace(hapPath, ''); 354 const sufStr: string = tempFilePath.substring(tempFilePath.indexOf(packageDir) + packageDir.length + 1); 355 output = path.join(projectConfig.nodeModulesPath, ZERO, sufStr); 356 } else { 357 output = filePath.replace(fakePkgModulesPath, path.join(projectConfig.nodeModulesPath, ONE)); 358 } 359 return output; 360 } 361 362 if (filePath.indexOf(projectPath) !== -1) { 363 const sufStr: string = filePath.replace(projectPath, ''); 364 const output: string = path.join(buildPath, sufStr); 365 return output; 366 } 367 368 return ''; 369} 370 371export function getPackageInfo(configFile: string): Array<string> { 372 if (packageCollection.has(configFile)) { 373 return packageCollection.get(configFile); 374 } 375 const data: any = JSON.parse(fs.readFileSync(configFile).toString()); 376 const bundleName: string = data.app.bundleName; 377 const moduleName: string = data.module.name; 378 packageCollection.set(configFile, [bundleName, moduleName]); 379 return [bundleName, moduleName]; 380} 381 382export function generateSourceFilesToTemporary(sourcePath: string, sourceContent: string, sourceMap: any, 383 projectConfig: any, logger: any): void { 384 let jsFilePath: string = genTemporaryPath(sourcePath, projectConfig.projectPath, process.env.cachePath, projectConfig); 385 if (jsFilePath.length === 0) { 386 return; 387 } 388 if (jsFilePath.endsWith(EXTNAME_ETS)) { 389 jsFilePath = jsFilePath.replace(/\.ets$/, EXTNAME_JS); 390 } else { 391 jsFilePath = jsFilePath.replace(/\.ts$/, EXTNAME_JS); 392 } 393 let sourceMapFile: string = genSourceMapFileName(jsFilePath); 394 if (sourceMapFile.length > 0 && projectConfig.buildArkMode === 'debug') { 395 let source = toUnixPath(sourcePath).replace(toUnixPath(projectConfig.projectRootPath) + '/', ''); 396 // adjust sourceMap info 397 sourceMap.sources = [source]; 398 sourceMap.file = path.basename(sourceMap.file); 399 delete sourceMap.sourcesContent; 400 newSourceMaps[source] = sourceMap; 401 } 402 sourceContent = transformModuleSpecifier(sourcePath, sourceContent, projectConfig); 403 404 mkdirsSync(path.dirname(jsFilePath)); 405 if (projectConfig.buildArkMode === 'debug') { 406 fs.writeFileSync(jsFilePath, sourceContent); 407 return; 408 } 409 410 writeMinimizedSourceCode(sourceContent, jsFilePath, logger); 411} 412 413export function genAbcFileName(temporaryFile: string): string { 414 let abcFile: string = temporaryFile; 415 if (temporaryFile.endsWith(EXTNAME_TS)) { 416 abcFile = temporaryFile.replace(/\.ts$/, EXTNAME_ABC); 417 } else { 418 abcFile = temporaryFile.replace(/\.js$/, EXTNAME_ABC); 419 } 420 return abcFile; 421} 422 423export function isOhModules(projectConfig: any): boolean { 424 return projectConfig.packageDir === OH_MODULES; 425} 426 427export function isEs2Abc(projectConfig: any): boolean { 428 return projectConfig.pandaMode === ES2ABC || projectConfig.pandaMode === "undefined" || 429 projectConfig.pandaMode === undefined; 430} 431 432export function isTs2Abc(projectConfig: any): boolean { 433 return projectConfig.pandaMode === TS2ABC; 434} 435 436export function genProtoFileName(temporaryFile: string): string { 437 return temporaryFile.replace(/\.(?:[tj]s|json)$/, EXTNAME_PROTO_BIN); 438} 439 440export function genMergeProtoFileName(temporaryFile: string): string { 441 let protoTempPathArr: string[] = temporaryFile.split(TEMPORARY); 442 const sufStr: string = protoTempPathArr[protoTempPathArr.length - 1]; 443 let protoBuildPath: string = path.join(process.env.cachePath, "protos", sufStr); 444 445 return protoBuildPath; 446} 447 448export function removeDuplicateInfo(moduleInfos: Array<any>): Array<any> { 449 const tempModuleInfos: any[] = Array<any>(); 450 moduleInfos.forEach((item) => { 451 let check: boolean = tempModuleInfos.every((newItem) => { 452 return item.tempFilePath !== newItem.tempFilePath; 453 }); 454 if (check) { 455 tempModuleInfos.push(item); 456 } 457 }); 458 moduleInfos = tempModuleInfos; 459 460 return moduleInfos; 461} 462 463export function buildCachePath(tailName: string, projectConfig: any, logger: any): string { 464 let pathName: string = process.env.cachePath !== undefined ? 465 path.join(projectConfig.cachePath, tailName) : path.join(projectConfig.aceModuleBuild, tailName); 466 validateFilePathLength(pathName, logger); 467 return pathName; 468} 469 470export function getArkBuildDir(arkDir: string): string { 471 if (isWindows()) { 472 return path.join(arkDir, 'build-win'); 473 } else if (isMac()) { 474 return path.join(arkDir, 'build-mac'); 475 } else { 476 return path.join(arkDir, 'build'); 477 } 478} 479 480export function getBuildBinDir(arkDir: string): string { 481 return path.join(getArkBuildDir(arkDir), 'bin'); 482} 483