/* * Copyright (c) 2023-2025 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import path from 'path'; import fs from 'fs'; import type sourceMap from 'source-map'; import { minify, MinifyOutput } from 'terser'; import { endFilesEvent, endSingleFileEvent, EventList, MemoryUtils, nameCacheMap, performancePrinter, ProjectCollections, startFilesEvent, startSingleFileEvent, } from 'arkguard'; import type { HvigorErrorInfo } from 'arkguard'; import { OH_MODULES, SEPARATOR_AT, SEPARATOR_BITWISE_AND, SEPARATOR_SLASH } from './fast_build/ark_compiler/common/ark_define'; import { ARKTS_MODULE_NAME, PACKAGES, TEMPORARY, ZERO, ONE, EXTNAME_JS, EXTNAME_TS, EXTNAME_MJS, EXTNAME_CJS, EXTNAME_ABC, EXTNAME_ETS, EXTNAME_D_ETS, EXTNAME_TS_MAP, EXTNAME_JS_MAP, ESMODULE, FAIL, TS2ABC, ES2ABC, EXTNAME_PROTO_BIN, NATIVE_MODULE } from './pre_define'; import { isMac, isWindows, isPackageModulesFile, genTemporaryPath, getExtensionIfUnfullySpecifiedFilepath, mkdirsSync, toUnixPath, validateFilePathLength, harFilesRecord, getProjectRootPath } from './utils'; import type { GeneratedFileInHar } from './utils'; import { extendSdkConfigs, projectConfig, sdkConfigPrefix } from '../main'; import { getRelativeSourcePath, mangleFilePath, setNewNameCache, getNameCacheByPath, setUnobfuscationNames, writeObfuscatedFile } from './fast_build/ark_compiler/common/ob_config_resolver'; import { moduleRequestCallback } from './fast_build/system_api/api_check_utils'; import { SourceMapGenerator } from './fast_build/ark_compiler/generate_sourcemap'; import { sourceFileBelongProject } from './fast_build/ark_compiler/module/module_source_file'; import { MemoryMonitor } from './fast_build/meomry_monitor/rollup-plugin-memory-monitor'; import { MemoryDefine } from './fast_build/meomry_monitor/memory_define'; import { ArkTSInternalErrorDescription, ArkTSErrorDescription, ErrorCode } from './fast_build/ark_compiler/error_code'; import { LogData, LogDataFactory } from './fast_build/ark_compiler/logger'; import * as ts from 'typescript'; import { PreloadFileModules } from './fast_build/ark_compiler/module/module_preload_file_utils'; export const SRC_MAIN: string = 'src/main'; export let newSourceMaps: Object = {}; export const packageCollection: Map> = new Map(); // Splicing ohmurl or record name based on filePath and context information table. export function getNormalizedOhmUrlByFilepath(filePath: string, projectConfig: Object, logger: Object, pkgParams: Object, importerFile: string): string { const { pkgName, pkgPath, isRecordName } = pkgParams; const ohmurlInfo: Object = getPkgInfo(filePath, projectConfig, logger, pkgPath, pkgName, importerFile); if (!ohmurlInfo) { return filePath; } const { projectFilePath, pkgInfo } = ohmurlInfo; const recordName: string = `${pkgInfo.bundleName}&${pkgName}/${projectFilePath}&${pkgInfo.version}`; if (isRecordName) { // record name style: &/entry/ets/xxx/yyy& return recordName; } return `${pkgInfo.isSO ? 'Y' : 'N'}&${pkgInfo.moduleName}&${recordName}`; } export function getPkgInfo(filePath: string, projectConfig: Object, logger: Object, pkgPath: string, pkgName: string, importerFile?: string): Object { // rollup uses commonjs plugin to handle commonjs files, // the commonjs files are prefixed with '\x00' and need to be removed. if (filePath.startsWith('\x00')) { filePath = filePath.replace('\x00', ''); } let unixFilePath: string = toUnixPath(filePath); unixFilePath = unixFilePath.substring(0, filePath.lastIndexOf('.')); // remove extension // case1: /entry/src/main/ets/xxx/yyy // case2: /entry/src/ohosTest/ets/xxx/yyy // case3: /node_modules/xxx/yyy // case4: /entry/node_modules/xxx/yyy // case5: /library/node_modules/xxx/yyy // case6: /library/index.ts // ---> @normalized:N&&&/entry/ets/xxx/yyy& let pkgInfo = projectConfig.pkgContextInfo[pkgName]; if (!pkgInfo || !pkgPath) { const errInfo: LogData = LogDataFactory.newInstance( ErrorCode.ETS2BUNDLE_EXTERNAL_FAILED_TO_RESOLVE_OHM_URL, ArkTSErrorDescription, 'Failed to resolve OhmUrl. ' + `Failed to get a resolved OhmUrl for "${filePath}" imported by "${importerFile}".`, '', [`Check whether the "${pkgName}" module which ${filePath} belongs to is correctly configured.`, `Check if the corresponding file name "${filePath}" is correct(including case-sensitivity).`] ); logger.printError(errInfo); logger.returnErrorFileId(importerFile); return undefined; } const projectFilePath: string = unixFilePath.replace(toUnixPath(pkgPath) + '/', ''); return { projectFilePath, pkgInfo }; } export function getOhmUrlByFilepath(filePath: string, projectConfig: Object, logger: Object, namespace?: string, importerFile?: string): string { // remove '\x00' from the rollup virtual commonjs file's filePath if (filePath.startsWith('\x00')) { filePath = filePath.replace('\x00', ''); } let unixFilePath: string = toUnixPath(filePath); unixFilePath = unixFilePath.substring(0, filePath.lastIndexOf('.')); // remove extension const REG_PROJECT_SRC: RegExp = /(\S+)\/src\/(?:main|ohosTest)\/(ets|js|mock)\/(\S+)/; const packageInfo: string[] = getPackageInfo(projectConfig.aceModuleJsonPath); const bundleName: string = packageInfo[0]; const moduleName: string = packageInfo[1]; const moduleRootPath: string = toUnixPath(projectConfig.modulePathMap[moduleName]); const projectRootPath: string = toUnixPath(getProjectRootPath(filePath, projectConfig, projectConfig?.rootPathSet)); // case1: /entry/src/main/ets/xxx/yyy ---> @bundle:/entry/ets/xxx/yyy // case2: /entry/src/ohosTest/ets/xxx/yyy ---> @bundle:/entry_test@entry/ets/xxx/yyy // case3: /node_modules/xxx/yyy ---> @package:pkg_modules/xxx/yyy // case4: /entry/node_modules/xxx/yyy ---> @package:pkg_modules@entry/xxx/yyy // case5: /library/node_modules/xxx/yyy ---> @package:pkg_modules@library/xxx/yyy // case6: /library/index.ts ---> @bundle:/library/index const projectFilePath: string = unixFilePath.replace(projectRootPath, ''); const packageDir: string = projectConfig.packageDir; const result: RegExpMatchArray | null = projectFilePath.match(REG_PROJECT_SRC); if (result && result[1].indexOf(packageDir) === -1) { const relativePath = processSrcMain(result, projectFilePath); if (namespace && moduleName !== namespace) { return `${bundleName}/${moduleName}@${namespace}/${relativePath}`; } return `${bundleName}/${moduleName}/${relativePath}`; } const processParams: Object = { projectFilePath, unixFilePath, packageDir, projectRootPath, moduleRootPath, projectConfig, namespace, logger, importerFile, originalFilePath: filePath }; return processPackageDir(processParams); } function processSrcMain(result: RegExpMatchArray | null, projectFilePath: string): string { let langType: string = result[2]; let relativePath: string = result[3]; // case7: /entry/src/main/ets/xxx/src/main/js/yyy ---> @bundle:/entry/ets/xxx/src/main/js/yyy const REG_SRC_MAIN: RegExp = /src\/(?:main|ohosTest)\/(ets|js)\//; const srcMainIndex: number = result[1].search(REG_SRC_MAIN); if (srcMainIndex !== -1) { relativePath = projectFilePath.substring(srcMainIndex).replace(REG_SRC_MAIN, ''); langType = projectFilePath.replace(relativePath, '').match(REG_SRC_MAIN)[1]; } return `${langType}/${relativePath}`; } function processPackageDir(params: Object): string { const { projectFilePath, unixFilePath, packageDir, projectRootPath, moduleRootPath, projectConfig, namespace, logger, importerFile, originalFilePath } = params; if (projectFilePath.indexOf(packageDir) !== -1) { if (compileToolIsRollUp()) { const tryProjectPkg: string = toUnixPath(path.join(projectRootPath, packageDir)); if (unixFilePath.indexOf(tryProjectPkg) !== -1) { return unixFilePath.replace(tryProjectPkg, `${packageDir}`).replace(new RegExp(packageDir, 'g'), PACKAGES); } // iterate the modulePathMap to find the module which contains the pkg_module's file for (const moduleName in projectConfig.modulePathMap) { const modulePath: string = projectConfig.modulePathMap[moduleName]; const tryModulePkg: string = toUnixPath(path.resolve(modulePath, packageDir)); if (unixFilePath.indexOf(tryModulePkg) !== -1) { return unixFilePath.replace(tryModulePkg, `${packageDir}@${moduleName}`).replace(new RegExp(packageDir, 'g'), PACKAGES); } } const errInfo: LogData = LogDataFactory.newInstance( ErrorCode.ETS2BUNDLE_EXTERNAL_FAILED_TO_RESOLVE_OHM_URL, ArkTSErrorDescription, 'Failed to resolve OhmUrl. ' + `Failed to get a resolved OhmUrl for "${originalFilePath}" imported by "${importerFile}".`, '', [`Check whether the module which ${originalFilePath} belongs to is correctly configured.`, `Check if the corresponding file name "${originalFilePath}" is correct(including case-sensitivity).`] ); logger.printError(errInfo); logger.returnErrorFileId(importerFile); return originalFilePath; } // webpack with old implematation const tryProjectPkg: string = toUnixPath(path.join(projectRootPath, packageDir)); if (unixFilePath.indexOf(tryProjectPkg) !== -1) { return unixFilePath.replace(tryProjectPkg, `${packageDir}/${ONE}`).replace(new RegExp(packageDir, 'g'), PACKAGES); } const tryModulePkg: string = toUnixPath(path.join(moduleRootPath, packageDir)); if (unixFilePath.indexOf(tryModulePkg) !== -1) { return unixFilePath.replace(tryModulePkg, `${packageDir}/${ZERO}`).replace(new RegExp(packageDir, 'g'), PACKAGES); } } const packageInfo: string[] = getPackageInfo(projectConfig.aceModuleJsonPath); const bundleName: string = packageInfo[0]; const moduleName: string = packageInfo[1]; for (const key in projectConfig.modulePathMap) { const moduleRootPath: string = toUnixPath(projectConfig.modulePathMap[key]); if (unixFilePath.indexOf(moduleRootPath + '/') !== -1) { const relativeModulePath: string = unixFilePath.replace(moduleRootPath + '/', ''); if (namespace && moduleName !== namespace) { return `${bundleName}/${moduleName}@${namespace}/${relativeModulePath}`; } return `${bundleName}/${moduleName}/${relativeModulePath}`; } } const errInfo: LogData = LogDataFactory.newInstance( ErrorCode.ETS2BUNDLE_EXTERNAL_FAILED_TO_RESOLVE_OHM_URL, ArkTSErrorDescription, 'Failed to resolve OhmUrl. ' + `Failed to get a resolved OhmUrl for "${originalFilePath}" imported by "${importerFile}".`, '', [`Check whether the module which ${originalFilePath} belongs to is correctly configured.`, `Check if the corresponding file name "${originalFilePath}" is correct(including case-sensitivity).`] ); logger.printError(errInfo); logger.returnErrorFileId(importerFile); return originalFilePath; } export type OhmUrlParams = { moduleRequest: string; moduleId: string; config?: Object; logger?: Object; importerFile?: string; }; export function getOhmUrlBySystemApiOrLibRequest(params: OhmUrlParams, useNormalizedOHMUrl: boolean = false, needPreloadSo: boolean = false): string { // 'arkui-x' represents cross platform related APIs, processed as 'ohos' const REG_SYSTEM_MODULE: RegExp = new RegExp(`@(${sdkConfigPrefix})\\.(\\S+)`); const REG_LIB_SO: RegExp = /lib(\S+)\.so/; const moduleRequest = params.moduleRequest; if (REG_SYSTEM_MODULE.test(moduleRequest.trim())) { return moduleRequest.replace(REG_SYSTEM_MODULE, (_, moduleType, systemKey) => { let moduleRequestStr = ''; if (extendSdkConfigs) { moduleRequestStr = moduleRequestCallback(moduleRequest, _, moduleType, systemKey); } if (moduleRequestStr !== '') { needPreloadSo && PreloadFileModules.updatePreloadFileDataByItems( moduleRequest, moduleRequestStr, params.moduleId); return moduleRequestStr; } let resultOhmUrl: string = ''; const systemModule: string = `${moduleType}.${systemKey}`; if (NATIVE_MODULE.has(systemModule)) { resultOhmUrl = `@native:${systemModule}`; } else if (moduleType === ARKTS_MODULE_NAME) { // @arkts.xxx -> @ohos:arkts.xxx resultOhmUrl = `@ohos:${systemModule}`; } else { resultOhmUrl = `@ohos:${systemKey}`; } needPreloadSo && PreloadFileModules.updatePreloadFileDataByItems(moduleRequest, resultOhmUrl, params.moduleId); return resultOhmUrl; }); } if (REG_LIB_SO.test(moduleRequest.trim())) { if (useNormalizedOHMUrl) { const pkgInfo = params.config.pkgContextInfo[moduleRequest]; if (!pkgInfo) { const errInfo: LogData = LogDataFactory.newInstance( ErrorCode.ETS2BUNDLE_INTERNAL_UNABLE_TO_GET_PKG_CONTENT_INFO, ArkTSInternalErrorDescription, `Can not get pkgContextInfo of package '${moduleRequest}' ` + `which being imported by '${params.importerFile}'` ); params.logger?.printError(errInfo); return moduleRequest; } const isSo = pkgInfo.isSO ? 'Y' : 'N'; return `@normalized:${isSo}&${pkgInfo.moduleName}&${pkgInfo.bundleName}&${moduleRequest}&${pkgInfo.version}`; } return moduleRequest.replace(REG_LIB_SO, (_, libsoKey) => { return `@app:${projectConfig.bundleName}/${projectConfig.moduleName}/${libsoKey}`; }); } return undefined; } export function genSourceMapFileName(temporaryFile: string): string { let abcFile: string = temporaryFile; if (temporaryFile.endsWith(EXTNAME_TS)) { abcFile = temporaryFile.replace(/\.ts$/, EXTNAME_TS_MAP); } else { abcFile = temporaryFile.replace(/\.js$/, EXTNAME_JS_MAP); } return abcFile; } export function getBuildModeInLowerCase(projectConfig: Object): string { return (compileToolIsRollUp() ? projectConfig.buildMode : projectConfig.buildArkMode).toLowerCase(); } /** * This Api only used by webpack compiling process - js-loader * @param sourcePath The path in build cache dir * @param sourceCode The intermediate js source code */ export function writeFileSyncByString(sourcePath: string, sourceCode: string, projectConfig: Object, logger: Object): void { const filePath: string = genTemporaryPath(sourcePath, projectConfig.projectPath, process.env.cachePath, projectConfig, undefined); if (filePath.length === 0) { return; } mkdirsSync(path.dirname(filePath)); if (/\.js$/.test(sourcePath)) { sourceCode = transformModuleSpecifier(sourcePath, sourceCode, projectConfig); if (projectConfig.buildArkMode === 'debug') { fs.writeFileSync(filePath, sourceCode); return; } writeObfuscatedSourceCode({content: sourceCode, buildFilePath: filePath, relativeSourceFilePath: ''}, logger, projectConfig); } if (/\.json$/.test(sourcePath)) { fs.writeFileSync(filePath, sourceCode); } } export function transformModuleSpecifier(sourcePath: string, sourceCode: string, projectConfig: Object): string { // replace relative moduleSpecifier with ohmURl const REG_RELATIVE_DEPENDENCY: RegExp = /(?:import|from)(?:\s*)['"]((?:\.\/|\.\.\/)[^'"]+|(?:\.\/?|\.\.\/?))['"]/g; const REG_HAR_DEPENDENCY: RegExp = /(?:import|from)(?:\s*)['"]([^\.\/][^'"]+)['"]/g; // replace requireNapi and requireNativeModule with import const REG_REQUIRE_NATIVE_MODULE: RegExp = /var (\S+) = globalThis.requireNativeModule\(['"](\S+)['"]\);/g; const REG_REQUIRE_NAPI_APP: RegExp = /var (\S+) = globalThis.requireNapi\(['"](\S+)['"], true, ['"](\S+)['"]\);/g; const REG_REQUIRE_NAPI_OHOS: RegExp = /var (\S+) = globalThis.requireNapi\(['"](\S+)['"]\);/g; return sourceCode.replace(REG_HAR_DEPENDENCY, (item, moduleRequest) => { return replaceHarDependency(item, moduleRequest, projectConfig); }).replace(REG_RELATIVE_DEPENDENCY, (item, moduleRequest) => { return replaceRelativeDependency(item, moduleRequest, toUnixPath(sourcePath), projectConfig); }).replace(REG_REQUIRE_NATIVE_MODULE, (_, moduleRequest, moduleName) => { return `import ${moduleRequest} from '@native:${moduleName}';`; }).replace(REG_REQUIRE_NAPI_APP, (_, moduleRequest, soName, moudlePath) => { return `import ${moduleRequest} from '@app:${moudlePath}/${soName}';`; }).replace(REG_REQUIRE_NAPI_OHOS, (_, moduleRequest, moduleName) => { return `import ${moduleRequest} from '@ohos:${moduleName}';`; }); } function removeSuffix(filePath: string): string { const SUFFIX_REG = /\.(?:d\.)?(ets|ts|mjs|cjs|js)$/; return filePath.split(path.sep).join('/').replace(SUFFIX_REG, ''); } export function getNormalizedOhmUrlByModuleRequest(moduleInfoByModuleRequest: Object, projectConfig: Object, logger?: Object): string { const normalizedPath = moduleInfoByModuleRequest.normalizedPath; const pkgName = moduleInfoByModuleRequest.packageName; const pkgInfo: Object = projectConfig.pkgContextInfo[pkgName]; if (!normalizedPath || !pkgName || !pkgInfo) { const errInfo: LogData = LogDataFactory.newInstance( ErrorCode.ETS2BUNDLE_INTERNAL_PACKAGE_NOT_FOUND_IN_CONTEXT_INFO, ArkTSInternalErrorDescription, `Failed to find package '${pkgName}'. ` + `Failed to obtain package '${pkgName}' from the package context information.` ); logger?.printError(errInfo); return normalizedPath; } const isSo = pkgInfo.isSO ? 'Y' : 'N'; return `@normalized:${isSo}&${pkgInfo.moduleName}&${pkgInfo.bundleName}&${toUnixPath(normalizedPath)}&${pkgInfo.version}`; } export function getNormalizedOhmUrlByAliasName(aliasName: string, projectConfig: Object, logger?: Object, filePath?: string): string { let pkgName: string = aliasName; const aliasPkgNameMap: Map = projectConfig.dependencyAliasMap; if (aliasPkgNameMap.has(aliasName)) { pkgName = aliasPkgNameMap.get(aliasName); } const pkgInfo: Object = projectConfig.pkgContextInfo[pkgName]; if (!pkgInfo) { const errInfo: LogData = LogDataFactory.newInstance( ErrorCode.ETS2BUNDLE_INTERNAL_PACKAGE_NOT_FOUND_IN_CONTEXT_INFO, ArkTSInternalErrorDescription, `Failed to find package '${pkgName}'. ` + `Failed to obtain package '${pkgName}' from the package context information.` ); logger.printError(errInfo); } let normalizedPath: string = ''; if (!filePath) { if (!pkgInfo.entryPath) { const errInfo: LogData = LogDataFactory.newInstance( ErrorCode.ETS2BUNDLE_INTERNAL_PACKAGE_ENTRY_FILE_NOT_FOUND, ArkTSInternalErrorDescription, `Failed to find entry file of '${pkgName}'. ` + `Failed to obtain the entry file information of '${pkgName}' from the package context information.` ); logger.printError(errInfo); } normalizedPath = `${pkgName}/${toUnixPath(pkgInfo.entryPath)}`; normalizedPath = removeSuffix(normalizedPath); } else { const relativePath = toUnixPath(filePath).replace(aliasName, ''); normalizedPath = `${pkgName}${relativePath}`; } const isSo = pkgInfo.isSO ? 'Y' : 'N'; return `@normalized:${isSo}&${pkgInfo.moduleName}&${pkgInfo.bundleName}&${normalizedPath}&${pkgInfo.version}`; } export function getOhmUrlByByteCodeHar(moduleRequest: string, projectConfig: Object, rollupObject: Object, logger?: Object): string | undefined { if (projectConfig.useNormalizedOHMUrl && projectConfig.byteCodeHarInfo && Object.keys(projectConfig.byteCodeHarInfo).length > 0) { const moduleInfoByModuleRequest: Object = rollupObject.share?.importResolver?.(moduleRequest); if (moduleInfoByModuleRequest) { return getNormalizedOhmUrlByModuleRequest(moduleInfoByModuleRequest, projectConfig, logger); } let aliasName: string = getAliasNameFromPackageMap(projectConfig.byteCodeHarInfo, moduleRequest); if (aliasName) { return getNormalizedOhmUrlByAliasName(aliasName, projectConfig, logger); } for (const byteCodeHarName in projectConfig.byteCodeHarInfo) { if (moduleRequest.startsWith(byteCodeHarName + '/')) { return getNormalizedOhmUrlByAliasName(byteCodeHarName, projectConfig, logger, moduleRequest); } } } return undefined; } function getAliasNameFromPackageMap(externalPkgMap: Object, moduleRequest: string): string | undefined { // Matches strings that contain zero or more `/` or `\` characters const ONLY_SLASHES_REGEX: RegExp = /^\/*$|^\\*$/; const keys: string[] = Object.keys(externalPkgMap); for (const key of keys) { if (moduleRequest === key) { return key; } // In the following cases, the moduleRequest matches the aliasName field in the packageMap // case1: key = "hsp", moduleRequest = "hsp/" // case2: key = "hsp", moduleRequest = "hsp\\" if (moduleRequest.length > key.length && moduleRequest.startsWith(key)) { const remaining: string = moduleRequest.replace(key, ''); if (ONLY_SLASHES_REGEX.test(remaining)) { return key; } } } return undefined; } export function getOhmUrlByExternalPackage(moduleRequest: string, projectConfig: Object, rollupObject?: Object, logger?: Object, useNormalizedOHMUrl: boolean = false): string | undefined { // The externalPkgMap store the ohmurl with the alias of hsp package and the hars depended on bytecode har. let externalPkgMap: Object = Object.assign({}, projectConfig.hspNameOhmMap, projectConfig.harNameOhmMap); if (Object.keys(externalPkgMap).length !== 0) { const moduleInfoByModuleRequest: Object = rollupObject?.share?.importResolver?.(moduleRequest); if (useNormalizedOHMUrl && moduleInfoByModuleRequest) { return getNormalizedOhmUrlByModuleRequest(moduleInfoByModuleRequest, projectConfig, logger); } let aliasName: string = getAliasNameFromPackageMap(externalPkgMap, moduleRequest); if (aliasName) { if (useNormalizedOHMUrl) { return getNormalizedOhmUrlByAliasName(aliasName, projectConfig, logger); } // case1: "@ohos/lib" ---> "@bundle:bundleName/lib/ets/index" return externalPkgMap[moduleRequest]; } for (const externalPkgName in externalPkgMap) { if (moduleRequest.startsWith(externalPkgName + '/')) { if (useNormalizedOHMUrl) { return getNormalizedOhmUrlByAliasName(externalPkgName, projectConfig, logger, moduleRequest); } // case2: "@ohos/lib/src/main/ets/pages/page1" ---> "@bundle:bundleName/lib/ets/pages/page1" const idx: number = externalPkgMap[externalPkgName].split('/', 2).join('/').length; const ohmName: string = externalPkgMap[externalPkgName].substring(0, idx); if (moduleInfoByModuleRequest) { // when useNormalizedOHMUrl=false, and packageName combines with directory to import file, OhmUrl is supposed to end with "index" const relativePathByModuleRequest: string = toUnixPath(moduleInfoByModuleRequest.normalizedPath).split(SRC_MAIN)[1]; // "@bundle:bundleName/lib" + "/ets/pages/page1" return ohmName + relativePathByModuleRequest; } if (moduleRequest.indexOf(externalPkgName + '/' + SRC_MAIN) === 0) { return moduleRequest.replace(externalPkgName + '/' + SRC_MAIN, ohmName); } else { return moduleRequest.replace(externalPkgName, ohmName); } } } } return undefined; } function replaceHarDependency(item: string, moduleRequest: string, projectConfig: Object): string { const hspOhmUrl: string | undefined = getOhmUrlByExternalPackage(moduleRequest, projectConfig); if (hspOhmUrl !== undefined) { return item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => { return quotation + hspOhmUrl + quotation; }); } return item; } function locateActualFilePathWithModuleRequest(absolutePath: string): string { if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isDirectory()) { return absolutePath; } const filePath: string = absolutePath + getExtensionIfUnfullySpecifiedFilepath(absolutePath); if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { return absolutePath; } return path.join(absolutePath, 'index'); } function replaceRelativeDependency(item: string, moduleRequest: string, sourcePath: string, projectConfig: Object): string { if (sourcePath && projectConfig.compileMode === ESMODULE) { // remove file extension from moduleRequest const SUFFIX_REG: RegExp = /\.(?:[cm]?js|[e]?ts|json)$/; moduleRequest = moduleRequest.replace(SUFFIX_REG, ''); // normalize the moduleRequest item = item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => { let normalizedModuleRequest: string = toUnixPath(path.normalize(moduleRequest)); if (moduleRequest.startsWith('./')) { normalizedModuleRequest = './' + normalizedModuleRequest; } return quotation + normalizedModuleRequest + quotation; }); const filePath: string = locateActualFilePathWithModuleRequest(path.resolve(path.dirname(sourcePath), moduleRequest)); const result: RegExpMatchArray | null = filePath.match(/(\S+)(\/|\\)src(\/|\\)(?:main|ohosTest)(\/|\\)(ets|js)(\/|\\)(\S+)/); if (result && projectConfig.aceModuleJsonPath) { const npmModuleIdx: number = result[1].search(/(\/|\\)node_modules(\/|\\)/); const projectRootPath: string = projectConfig.projectRootPath; if (npmModuleIdx === -1 || npmModuleIdx === projectRootPath.search(/(\/|\\)node_modules(\/|\\)/)) { const packageInfo: string[] = getPackageInfo(projectConfig.aceModuleJsonPath); const bundleName: string = packageInfo[0]; const moduleName: string = packageInfo[1]; moduleRequest = `@bundle:${bundleName}/${moduleName}/${result[5]}/${toUnixPath(result[7])}`; item = item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => { return quotation + moduleRequest + quotation; }); } } } return item; } /** * Informantion of build files */ export interface ModuleInfo { content: string, /** * the path in build cache dir */ buildFilePath: string, /** * the `originSourceFilePath` relative to project root dir. */ relativeSourceFilePath: string, /** * the origin source file path will be set with rollup moduleId when obfuscate intermediate js source code, * whereas be set with tsc node.fileName when obfuscate intermediate ts source code. */ originSourceFilePath?: string, rollupModuleId?: string } export async function writeObfuscatedSourceCode(moduleInfo: ModuleInfo, logger: Function | Object, projectConfig: Object, rollupNewSourceMaps: Object = {}): Promise { /** * Obfuscation mode rules: * - When isArkguardEnabled=true (source code obfuscation): Obfuscate ALL files * - When isBytecodeObfEnabled=true (bytecode obfuscation): Only decl files will be obfuscated * - These two flags are mutually exclusive (only one can be true at a time) */ let shouldSkipObfuscation = false; if (projectConfig.isBytecodeObfEnabled) { shouldSkipObfuscation = !/\.d\.e?ts$/.test(moduleInfo.buildFilePath); } const isObfuscatorEnabled = projectConfig.arkObfuscator && (projectConfig.isArkguardEnabled || !shouldSkipObfuscation); if (compileToolIsRollUp() && isObfuscatorEnabled) { startFilesEvent(moduleInfo.buildFilePath); const recordInfo = MemoryMonitor.recordStage(MemoryDefine.WRITE_OBFUSCATED_SOURCE_CODE); const previousStageSourceMap: sourceMap.RawSourceMap | undefined = getPreviousStageSourceMap(moduleInfo, rollupNewSourceMaps); collectObfuscationFileContent(moduleInfo, projectConfig, previousStageSourceMap); // We should skip obfuscate here if we need reObfuscate all files, since they will all be obfuscated later in function 'reObfuscate'. if (!projectConfig.arkObfuscator.shouldReObfuscate) { MemoryUtils.tryGC(); await writeArkguardObfuscatedSourceCode(moduleInfo, logger as Function, projectConfig, previousStageSourceMap); MemoryUtils.tryGC(); } MemoryMonitor.stopRecordStage(recordInfo); endFilesEvent(moduleInfo.buildFilePath, undefined, true); return; } mkdirsSync(path.dirname(moduleInfo.buildFilePath)); if (!compileToolIsRollUp()) { await writeMinimizedSourceCode(moduleInfo.content, moduleInfo.buildFilePath, logger, projectConfig.compileHar); return; } if (moduleInfo.originSourceFilePath) { const originSourceFilePath = toUnixPath(moduleInfo.originSourceFilePath); let genFileInHar: GeneratedFileInHar = harFilesRecord.get(originSourceFilePath); if (!genFileInHar) { genFileInHar = { sourcePath: originSourceFilePath }; } if (!genFileInHar.sourceCachePath) { genFileInHar.sourceCachePath = toUnixPath(moduleInfo.buildFilePath); } harFilesRecord.set(originSourceFilePath, genFileInHar); } writeFileSyncCaseAware(moduleInfo.buildFilePath, moduleInfo.content); } export function getPreviousStageSourceMap(moduleInfo: ModuleInfo, rollupNewSourceMaps: Object = {}): sourceMap.RawSourceMap | undefined { const isDeclaration = (/\.d\.e?ts$/).test(moduleInfo.buildFilePath); const sourceMapGeneratorInstance = SourceMapGenerator.getInstance(); let previousStageSourceMap: sourceMap.RawSourceMap | undefined = undefined; if (moduleInfo.relativeSourceFilePath.length > 0 && !isDeclaration) { const selectedFilePath = sourceMapGeneratorInstance.isNewSourceMaps() ? moduleInfo.rollupModuleId! : moduleInfo.relativeSourceFilePath; previousStageSourceMap = sourceMapGeneratorInstance.getSpecifySourceMap(rollupNewSourceMaps, selectedFilePath) as sourceMap.RawSourceMap; } return previousStageSourceMap; } /** * collect current obfuscated input content, used for re-obfuscate. */ export function collectObfuscationFileContent(moduleInfo: ModuleInfo, projectConfig: Object, previousStageSourceMap: sourceMap.RawSourceMap | undefined): void { const arkObfuscator = projectConfig.arkObfuscator; const isDeclaration = (/\.d\.e?ts$/).test(moduleInfo.buildFilePath); const isOriginalDeclaration = (/\.d\.e?ts$/).test(moduleInfo.originSourceFilePath); // We only collect non-declaration files, unless it's a user-written declaration file if (arkObfuscator.filePathManager && (!isDeclaration || isOriginalDeclaration)) { const fileContent: ProjectCollections.FileContent = { moduleInfo: moduleInfo, previousStageSourceMap: previousStageSourceMap as ts.RawSourceMap, }; arkObfuscator.fileContentManager.updateFileContent(fileContent); } } /** * This Api only be used by rollup compiling process & only be * exported for unit test. */ export async function writeArkguardObfuscatedSourceCode(moduleInfo: ModuleInfo, printObfLogger: Function, projectConfig: Object, previousStageSourceMap: sourceMap.RawSourceMap | undefined): Promise { const arkObfuscator = projectConfig.arkObfuscator; const isDeclaration = (/\.d\.e?ts$/).test(moduleInfo.buildFilePath); const packageDir = projectConfig.packageDir; const projectRootPath = projectConfig.projectRootPath; const useNormalized = projectConfig.useNormalizedOHMUrl; const localPackageSet = projectConfig.localPackageSet; const useTsHar = projectConfig.useTsHar; const sourceMapGeneratorInstance = SourceMapGenerator.getInstance(); const historyNameCache: Map = getNameCacheByPath(moduleInfo, isDeclaration, projectRootPath); let mixedInfo: { content: string, sourceMap?: Object, nameCache?: Object, unobfuscationNameMap?: Map>}; let projectInfo: { packageDir: string, projectRootPath: string, localPackageSet: Set, useNormalized: boolean, useTsHar: boolean } = { packageDir, projectRootPath, localPackageSet, useNormalized, useTsHar }; let filePathObj = { buildFilePath: moduleInfo.buildFilePath, relativeFilePath: moduleInfo.relativeSourceFilePath }; try { startSingleFileEvent(EventList.OBFUSCATE, performancePrinter.timeSumPrinter, filePathObj.buildFilePath); mixedInfo = await arkObfuscator.obfuscate(moduleInfo.content, filePathObj, previousStageSourceMap, historyNameCache, moduleInfo.originSourceFilePath, projectInfo); endSingleFileEvent(EventList.OBFUSCATE, performancePrinter.timeSumPrinter); } catch (err) { const errorInfo: string = `ArkTS:INTERNAL ERROR: Failed to obfuscate file '${moduleInfo.relativeSourceFilePath}' with arkguard. ${err}`; const errorCodeInfo: HvigorErrorInfo = { code: '10810001', description: 'ArkTS compiler Error', cause: `ArkTS:INTERNAL ERROR: Failed to obfuscate file '${moduleInfo.relativeSourceFilePath}' with arkguard. ${err}`, position: moduleInfo.relativeSourceFilePath, solutions: [`Please modify the code based on the error information.`], }; printObfLogger(errorInfo, errorCodeInfo, 'error'); } if (mixedInfo.sourceMap && !isDeclaration) { const selectedFilePath = sourceMapGeneratorInstance.isNewSourceMaps() ? moduleInfo.rollupModuleId! : moduleInfo.relativeSourceFilePath; mixedInfo.sourceMap.sources = [moduleInfo.relativeSourceFilePath]; sourceMapGeneratorInstance.fillSourceMapPackageInfo(moduleInfo.rollupModuleId!, mixedInfo.sourceMap); sourceMapGeneratorInstance.updateSourceMap(selectedFilePath, mixedInfo.sourceMap); } setNewNameCache(mixedInfo.nameCache, isDeclaration, moduleInfo, projectConfig); setUnobfuscationNames(mixedInfo.unobfuscationNameMap, moduleInfo.relativeSourceFilePath, isDeclaration); const newFilePath: string = tryMangleFileName(moduleInfo.buildFilePath, projectConfig, moduleInfo.originSourceFilePath); if (newFilePath !== moduleInfo.buildFilePath && !isDeclaration) { sourceMapGeneratorInstance.saveKeyMappingForObfFileName(moduleInfo.rollupModuleId!); } writeObfuscatedFile(newFilePath, mixedInfo.content ?? ''); } /** * This function will be called when obfuscating sourceFile and declaration file. * Declaration file is not affected by bytecode obfuscation. * When bytecode obfuscation is enabel, the name of sourceFile will not be obfuscated. * Because harFilesRecord is collected in this function * This parameter is added to process sourceFile and return early only when bytecode obfuscation is turned on. * It does not affect the original call effect. */ export function tryMangleFileName(filePath: string, projectConfig: Object, originalFilePath: string, skipObf: boolean = false): string { originalFilePath = toUnixPath(originalFilePath); let isOhModule = isPackageModulesFile(originalFilePath, projectConfig); let genFileInHar: GeneratedFileInHar = harFilesRecord.get(originalFilePath); if (!genFileInHar) { genFileInHar = { sourcePath: originalFilePath }; harFilesRecord.set(originalFilePath, genFileInHar); } if (skipObf) { return filePath; } if (projectConfig.obfuscationMergedObConfig?.options?.enableFileNameObfuscation && !isOhModule) { const mangledFilePath: string = mangleFilePath(filePath); if ((/\.d\.e?ts$/).test(filePath)) { genFileInHar.obfuscatedDeclarationCachePath = mangledFilePath; } else { genFileInHar.obfuscatedSourceCachePath = mangledFilePath; } filePath = mangledFilePath; } else if (!(/\.d\.e?ts$/).test(filePath)) { genFileInHar.sourceCachePath = filePath; } return filePath; } export async function mangleDeclarationFileName(printObfLogger: Function, projectConfig: Object, sourceFileBelongProject: Map): Promise { for (const [sourcePath, genFilesInHar] of harFilesRecord) { if (genFilesInHar.originalDeclarationCachePath && genFilesInHar.originalDeclarationContent) { let filePath = genFilesInHar.originalDeclarationCachePath; let relativeSourceFilePath = getRelativeSourcePath(filePath, projectConfig.projectRootPath, sourceFileBelongProject.get(toUnixPath(filePath))); await writeObfuscatedSourceCode({ content: genFilesInHar.originalDeclarationContent, buildFilePath: genFilesInHar.originalDeclarationCachePath, relativeSourceFilePath: relativeSourceFilePath, originSourceFilePath: sourcePath }, printObfLogger, projectConfig, {}); } } } export async function writeMinimizedSourceCode(content: string, filePath: string, logger: Object, isHar: boolean = false): Promise { let result: MinifyOutput; try { const minifyOptions = { compress: { join_vars: false, sequences: 0, directives: false } }; if (!isHar) { minifyOptions.format = { semicolons: false, beautify: true, indent_level: 2 }; } result = await minify(content, minifyOptions); } catch { const errInfo: LogData = LogDataFactory.newInstance( ErrorCode.ETS2BUNDLE_INTERNAL_SOURCE_CODE_OBFUSCATION_FAILED, ArkTSInternalErrorDescription, `Failed to obfuscate source code for ${filePath}` ); logger.printError(errInfo); } fs.writeFileSync(filePath, result.code); } //Writes declaration files in debug mode export function writeDeclarationFiles(compileMode: string): void { if (compileToolIsRollUp() && compileMode === ESMODULE) { for (const genFilesInHar of harFilesRecord.values()) { if (genFilesInHar.originalDeclarationCachePath && genFilesInHar.originalDeclarationContent) { mkdirsSync(path.dirname(genFilesInHar.originalDeclarationCachePath)); writeFileSyncCaseAware(genFilesInHar.originalDeclarationCachePath, genFilesInHar.originalDeclarationContent); } } } } export function genBuildPath(filePath: string, projectPath: string, buildPath: string, projectConfig: Object): string { filePath = toUnixPath(filePath); if (filePath.endsWith(EXTNAME_MJS)) { filePath = filePath.replace(/\.mjs$/, EXTNAME_JS); } if (filePath.endsWith(EXTNAME_CJS)) { filePath = filePath.replace(/\.cjs$/, EXTNAME_JS); } projectPath = toUnixPath(projectPath); if (isPackageModulesFile(filePath, projectConfig)) { const packageDir: string = projectConfig.packageDir; const fakePkgModulesPath: string = toUnixPath(path.join(projectConfig.projectRootPath, packageDir)); let output: string = ''; if (filePath.indexOf(fakePkgModulesPath) === -1) { const hapPath: string = toUnixPath(projectConfig.projectRootPath); const tempFilePath: string = filePath.replace(hapPath, ''); const sufStr: string = tempFilePath.substring(tempFilePath.indexOf(packageDir) + packageDir.length + 1); output = path.join(projectConfig.nodeModulesPath, ZERO, sufStr); } else { output = filePath.replace(fakePkgModulesPath, path.join(projectConfig.nodeModulesPath, ONE)); } return output; } if (filePath.indexOf(projectPath) !== -1) { const sufStr: string = filePath.replace(projectPath, ''); const output: string = path.join(buildPath, sufStr); return output; } return ''; } export function getPackageInfo(configFile: string): Array { if (packageCollection.has(configFile)) { return packageCollection.get(configFile); } const data: Object = JSON.parse(fs.readFileSync(configFile).toString()); const bundleName: string = data.app.bundleName; const moduleName: string = data.module.name; packageCollection.set(configFile, [bundleName, moduleName]); return [bundleName, moduleName]; } /** * This Api only used by webpack compiling process - result_process * @param sourcePath The path in build cache dir * @param sourceContent The intermediate js source code */ export function generateSourceFilesToTemporary(sourcePath: string, sourceContent: string, sourceMap: Object, projectConfig: Object, logger: Object): void { let jsFilePath: string = genTemporaryPath(sourcePath, projectConfig.projectPath, process.env.cachePath, projectConfig, undefined); if (jsFilePath.length === 0) { return; } if (jsFilePath.endsWith(EXTNAME_ETS)) { jsFilePath = jsFilePath.replace(/\.ets$/, EXTNAME_JS); } else { jsFilePath = jsFilePath.replace(/\.ts$/, EXTNAME_JS); } let sourceMapFile: string = genSourceMapFileName(jsFilePath); if (sourceMapFile.length > 0 && projectConfig.buildArkMode === 'debug') { let source = toUnixPath(sourcePath).replace(toUnixPath(projectConfig.projectRootPath) + '/', ''); // adjust sourceMap info sourceMap.sources = [source]; sourceMap.file = path.basename(sourceMap.file); delete sourceMap.sourcesContent; newSourceMaps[source] = sourceMap; } sourceContent = transformModuleSpecifier(sourcePath, sourceContent, projectConfig); mkdirsSync(path.dirname(jsFilePath)); if (projectConfig.buildArkMode === 'debug') { fs.writeFileSync(jsFilePath, sourceContent); return; } writeObfuscatedSourceCode({content: sourceContent, buildFilePath: jsFilePath, relativeSourceFilePath: ''}, logger, projectConfig); } export function genAbcFileName(temporaryFile: string): string { let abcFile: string = temporaryFile; if (temporaryFile.endsWith(EXTNAME_TS)) { abcFile = temporaryFile.replace(/\.ts$/, EXTNAME_ABC); } else { abcFile = temporaryFile.replace(/\.js$/, EXTNAME_ABC); } return abcFile; } export function isOhModules(projectConfig: Object): boolean { return projectConfig.packageDir === OH_MODULES; } export function isEs2Abc(projectConfig: Object): boolean { return projectConfig.pandaMode === ES2ABC || projectConfig.pandaMode === 'undefined' || projectConfig.pandaMode === undefined; } export function isTs2Abc(projectConfig: Object): boolean { return projectConfig.pandaMode === TS2ABC; } export function genProtoFileName(temporaryFile: string): string { return temporaryFile.replace(/\.(?:[tj]s|json)$/, EXTNAME_PROTO_BIN); } export function genMergeProtoFileName(temporaryFile: string): string { let protoTempPathArr: string[] = temporaryFile.split(TEMPORARY); const sufStr: string = protoTempPathArr[protoTempPathArr.length - 1]; let protoBuildPath: string = path.join(process.env.cachePath, 'protos', sufStr); return protoBuildPath; } export function removeDuplicateInfo(moduleInfos: Array): Array { const tempModuleInfos: any[] = Array(); moduleInfos.forEach((item) => { let check: boolean = tempModuleInfos.every((newItem) => { return item.tempFilePath !== newItem.tempFilePath; }); if (check) { tempModuleInfos.push(item); } }); moduleInfos = tempModuleInfos; return moduleInfos; } export function buildCachePath(tailName: string, projectConfig: Object, logger: Object): string { let pathName: string = process.env.cachePath !== undefined ? path.join(projectConfig.cachePath, tailName) : path.join(projectConfig.aceModuleBuild, tailName); validateFilePathLength(pathName, logger); return pathName; } export function getArkBuildDir(arkDir: string): string { if (isWindows()) { return path.join(arkDir, 'build-win'); } else if (isMac()) { return path.join(arkDir, 'build-mac'); } else { return path.join(arkDir, 'build'); } } export function getBuildBinDir(arkDir: string): string { return path.join(getArkBuildDir(arkDir), 'bin'); } export function cleanUpUtilsObjects(): void { newSourceMaps = {}; nameCacheMap.clear(); packageCollection.clear(); } export function compileToolIsRollUp(): boolean { return process.env.compileTool === 'rollup'; } export function transformOhmurlToRecordName(ohmurl: string): string { // @normalized:N&&&/entry/ets/xxx/yyy& // ----> &/entry/ets/xxx/yyy& return ohmurl.split(SEPARATOR_BITWISE_AND).slice(2).join(SEPARATOR_BITWISE_AND); } export function transformOhmurlToPkgName(ohmurl: string): string { let normalizedPath: string = ohmurl.split(SEPARATOR_BITWISE_AND)[3]; let paths: Array = normalizedPath.split(SEPARATOR_SLASH); if (normalizedPath.startsWith(SEPARATOR_AT)) { // Spec: If the normalized import path starts with '@', the package name is before the second '/' in the normalized // import path, like: @aaa/bbb/ccc/ddd ---> package name is @aaa/bbb return paths.slice(0, 2).join(SEPARATOR_SLASH); } return paths[0]; } export function writeFileSyncCaseAware(filePath: fs.PathOrFileDescriptor, data: string | NodeJS.ArrayBufferView, options?: fs.WriteFileOptions): void { // If filePath is not a file descriptor number (i.e., it's a file path) and the file at that path already exists, // delete the existing file first to ensure the written file name matches the input. if (typeof filePath !== 'number' && fs.existsSync(filePath)) { fs.unlinkSync(filePath); } fs.writeFileSync(filePath, data, options); }