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 ts from 'typescript'; 17import path from 'path'; 18import fs from 'fs'; 19import { createFilter } from '@rollup/pluginutils'; 20import MagicString from 'magic-string'; 21 22import { 23 LogInfo, 24 componentInfo, 25 emitLogInfo, 26 getTransformLog, 27 genTemporaryPath, 28 writeFileSync, 29 getAllComponentsOrModules, 30 writeCollectionFile, 31 storedFileInfo, 32 fileInfo, 33 resourcesRawfile, 34 differenceResourcesRawfile, 35 CacheFile, 36 startTimeStatisticsLocation, 37 stopTimeStatisticsLocation, 38 CompilationTimeStatistics, 39 genLoaderOutPathOfHar, 40 harFilesRecord, 41 resetUtils, 42 getResolveModules 43} from '../../utils'; 44import { 45 preprocessExtend, 46 preprocessNewExtend, 47 validateUISyntax, 48 propertyCollection, 49 linkCollection, 50 resetComponentCollection, 51 componentCollection, 52 resetValidateUiSyntax 53} from '../../validate_ui_syntax'; 54import { 55 processUISyntax, 56 resetLog, 57 transformLog, 58 resetProcessUiSyntax 59} from '../../process_ui_syntax'; 60import { 61 projectConfig, 62 abilityPagesFullPath, 63 globalProgram, 64 resetMain, 65 globalModulePaths 66} from '../../../main'; 67import { 68 appComponentCollection, 69 compilerOptions as etsCheckerCompilerOptions, 70 resolveModuleNames, 71 resolveTypeReferenceDirectives, 72 resetEtsCheck, 73 collectAllFiles, 74 allSourceFilePaths 75} from '../../ets_checker'; 76import { 77 CUSTOM_BUILDER_METHOD, 78 GLOBAL_CUSTOM_BUILDER_METHOD, 79 INNER_CUSTOM_BUILDER_METHOD, 80 resetComponentMap 81} from '../../component_map'; 82import { 83 kitTransformLog, 84 processKitImport 85} from '../../process_kit_import'; 86import { resetProcessComponentMember } from '../../process_component_member'; 87import { mangleFilePath, resetObfuscation } from '../ark_compiler/common/ob_config_resolver'; 88 89const filter:any = createFilter(/(?<!\.d)\.(ets|ts)$/); 90 91let shouldDisableCache: boolean = false; 92let shouldEnableDebugLine: boolean = false; 93let disableCacheOptions = { 94 bundleName: 'default', 95 entryModuleName: 'default', 96 runtimeOS: 'default', 97 resourceTableHash: 'default', 98 etsLoaderVersion: 'default' 99}; 100 101export function etsTransform() { 102 const allFilesInHar: Map<string, string> = new Map(); 103 let cacheFile: CacheFile; 104 if (projectConfig.useArkoala) { 105 // Dynamic loading to avoid resolving arkoala-only dependencies 106 const arkoalaSdkRoot: string = findArkoalaRoot(); 107 const pluginPackagePath: string = path.join(arkoalaSdkRoot, '@arkoala', 'rollup-plugin-ets-arkoala'); 108 const pluginOptions: Object = { 109 arkoalaSdkRoot, 110 projectConfig, 111 globalModulePaths, 112 getResolveModules, 113 }; 114 return require(pluginPackagePath).makeArkoalaPlugin(pluginOptions); 115 } 116 return { 117 name: 'etsTransform', 118 transform: transform, 119 buildStart() { 120 const compilationTime: CompilationTimeStatistics = new CompilationTimeStatistics(this.share, 'etsTransform', 'buildStart'); 121 startTimeStatisticsLocation(compilationTime ? compilationTime.etsTransformBuildStartTime : undefined); 122 judgeCacheShouldDisabled.call(this); 123 if (process.env.compileMode === 'moduleJson') { 124 cacheFile = this.cache.get('transformCacheFiles'); 125 storedFileInfo.addGlobalCacheInfo(this.cache.get('resourceListCacheInfo'), 126 this.cache.get('resourceToFileCacheInfo')); 127 if (this.cache.get('lastResourcesArr')) { 128 storedFileInfo.lastResourcesSet = new Set([...this.cache.get('lastResourcesArr')]); 129 } 130 if (process.env.rawFileResource) { 131 resourcesRawfile(process.env.rawFileResource, storedFileInfo.resourcesArr); 132 this.share.rawfilechanged = differenceResourcesRawfile(storedFileInfo.lastResourcesSet, storedFileInfo.resourcesArr); 133 } 134 } 135 if (this.cache.get('enableDebugLine') !== projectConfig.enableDebugLine) { 136 shouldEnableDebugLine = true; 137 } 138 stopTimeStatisticsLocation(compilationTime ? compilationTime.etsTransformBuildStartTime : undefined); 139 }, 140 load(id: string) { 141 let fileCacheInfo: fileInfo; 142 const compilationTime: CompilationTimeStatistics = new CompilationTimeStatistics(this.share, 'etsTransform', 'load'); 143 startTimeStatisticsLocation(compilationTime ? compilationTime.etsTransformLoadTime : undefined); 144 if (this.cache.get('fileCacheInfo')) { 145 fileCacheInfo = this.cache.get('fileCacheInfo')[path.resolve(id)]; 146 } 147 // Exclude Component Preview page 148 if (projectConfig.isPreview && !projectConfig.checkEntry && id.match(/(?<!\.d)\.(ets)$/)) { 149 abilityPagesFullPath.push(path.resolve(id).toLowerCase()); 150 storedFileInfo.judgeShouldHaveEntryFiles(abilityPagesFullPath); 151 } 152 storedFileInfo.addFileCacheInfo(path.resolve(id), fileCacheInfo); 153 storedFileInfo.setCurrentArkTsFile(); 154 stopTimeStatisticsLocation(compilationTime ? compilationTime.etsTransformLoadTime : undefined); 155 }, 156 shouldInvalidCache(options) { 157 const fileName: string = path.resolve(options.id); 158 let shouldDisable: boolean = shouldDisableCache || disableNonEntryFileCache(fileName) || shouldEnableDebugLine; 159 if (process.env.compileMode === 'moduleJson') { 160 shouldDisable = shouldDisable || storedFileInfo.shouldInvalidFiles.has(fileName) || this.share.rawfilechanged; 161 if (cacheFile && cacheFile[fileName] && cacheFile[fileName].children.length) { 162 for (let child of cacheFile[fileName].children) { 163 const newTimeMs: number = fs.existsSync(child.fileName) ? fs.statSync(child.fileName).mtimeMs : -1; 164 if (newTimeMs !== child.mtimeMs) { 165 shouldDisable = true; 166 break; 167 } 168 } 169 } 170 } 171 if (!shouldDisable) { 172 storedFileInfo.collectCachedFiles(fileName); 173 } 174 return shouldDisable; 175 }, 176 afterBuildEnd() { 177 // Copy the cache files in the compileArkTS directory to the loader_out directory 178 if (projectConfig.compileHar) { 179 for (let moduleInfoId of allSourceFilePaths) { 180 if (moduleInfoId && !moduleInfoId.match(new RegExp(projectConfig.packageDir)) && 181 !moduleInfoId.startsWith('\x00') && 182 path.resolve(moduleInfoId).startsWith(projectConfig.moduleRootPath + path.sep)) { 183 let filePath: string = moduleInfoId; 184 if (this.share.arkProjectConfig?.obfuscationMergedObConfig?.options?.enableFileNameObfuscation) { 185 filePath = mangleFilePath(filePath); 186 } 187 188 const jsCacheFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath, 189 process.env.cachePath, projectConfig); 190 const jsBuildFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath, 191 projectConfig.buildPath, projectConfig, true); 192 if (filePath.match(/\.e?ts$/)) { 193 setIncrementalFileInHar(jsCacheFilePath, jsBuildFilePath, allFilesInHar); 194 } else { 195 allFilesInHar.set(jsCacheFilePath, jsBuildFilePath); 196 } 197 } 198 } 199 200 allFilesInHar.forEach((jsBuildFilePath, jsCacheFilePath) => { 201 // if the ts or ets file code only contain interface, it doesn't have js file. 202 if (fs.existsSync(jsCacheFilePath)) { 203 const sourceCode: string = fs.readFileSync(jsCacheFilePath, 'utf-8'); 204 writeFileSync(jsBuildFilePath, sourceCode); 205 } 206 }); 207 } 208 if (process.env.watchMode !== 'true' && !projectConfig.xtsMode) { 209 let widgetPath: string; 210 if (projectConfig.widgetCompile) { 211 widgetPath = path.resolve(projectConfig.aceModuleBuild, 'widget'); 212 } 213 writeCollectionFile(projectConfig.cachePath, appComponentCollection, 214 this.share.allComponents, 'component_collection.json', this.share.allFiles, widgetPath); 215 } 216 shouldDisableCache = false; 217 this.cache.set('disableCacheOptions', disableCacheOptions); 218 this.cache.set('lastResourcesArr', [...storedFileInfo.resourcesArr]); 219 if (projectConfig.enableDebugLine) { 220 this.cache.set('enableDebugLine', true); 221 } else { 222 this.cache.set('enableDebugLine', false); 223 } 224 storedFileInfo.clearCollectedInfo(this.cache); 225 this.cache.set('transformCacheFiles', storedFileInfo.transformCacheFiles); 226 }, 227 cleanUp(): void { 228 resetMain(); 229 resetComponentMap(); 230 resetEtsCheck(); 231 resetEtsTransform(); 232 resetProcessComponentMember(); 233 resetProcessUiSyntax(); 234 resetUtils(); 235 resetValidateUiSyntax(); 236 resetObfuscation(); 237 } 238 }; 239} 240 241// If a ArkTS file don't have @Entry decorator but it is entry file this time 242function disableNonEntryFileCache(filePath: string): boolean { 243 return storedFileInfo.buildStart && filePath.match(/(?<!\.d)\.(ets)$/) && 244 !storedFileInfo.wholeFileInfo[filePath].hasEntry && 245 storedFileInfo.shouldHaveEntry.includes(filePath); 246} 247 248function judgeCacheShouldDisabled(): void { 249 for (const key in disableCacheOptions) { 250 if (this.cache.get('disableCacheOptions') && this.share && 251 this.share.projectConfig && this.share.projectConfig[key] && 252 this.cache.get('disableCacheOptions')[key] !== this.share.projectConfig[key]) { 253 if (key === 'resourceTableHash' && process.env.compileMode === 'moduleJson') { 254 storedFileInfo.resourceTableChanged = true; 255 } else if (!shouldDisableCache) { 256 shouldDisableCache = true; 257 } 258 } 259 if (this.share && this.share.projectConfig && this.share.projectConfig[key]) { 260 disableCacheOptions[key] = this.share.projectConfig[key]; 261 } 262 storedFileInfo.judgeShouldHaveEntryFiles(abilityPagesFullPath); 263 } 264} 265 266interface EmitResult { 267 outputText: string, 268 sourceMapText: string, 269} 270 271const compilerHost: ts.CompilerHost = ts.createCompilerHost(etsCheckerCompilerOptions); 272compilerHost.writeFile = () => {}; 273compilerHost.resolveModuleNames = resolveModuleNames; 274compilerHost.getCurrentDirectory = () => process.cwd(); 275compilerHost.getDefaultLibFileName = options => ts.getDefaultLibFilePath(options); 276compilerHost.resolveTypeReferenceDirectives = resolveTypeReferenceDirectives; 277 278async function transform(code: string, id: string) { 279 const compilationTime: CompilationTimeStatistics = new CompilationTimeStatistics(this.share, 'etsTransform', 'transform'); 280 if (!filter(id)) { 281 return null; 282 } 283 284 storedFileInfo.collectTransformedFiles(path.resolve(id)); 285 286 const logger = this.share.getLogger('etsTransform'); 287 288 if (projectConfig.compileMode !== "esmodule") { 289 const compilerOptions = ts.readConfigFile( 290 path.resolve(__dirname, '../../../tsconfig.json'), ts.sys.readFile).config.compilerOptions; 291 compilerOptions['moduleResolution'] = 'nodenext'; 292 compilerOptions['module'] = 'es2020'; 293 const newContent: string = jsBundlePreProcess(code, id, this.getModuleInfo(id).isEntry, logger); 294 const result: ts.TranspileOutput = ts.transpileModule(newContent, { 295 compilerOptions: compilerOptions, 296 fileName: id, 297 transformers: { before: [ processUISyntax(null) ] } 298 }); 299 300 resetCollection(); 301 if (transformLog && transformLog.errors.length && !projectConfig.ignoreWarning) { 302 emitLogInfo(logger, getTransformLog(transformLog), true, id); 303 resetLog(); 304 } 305 306 return { 307 code: result.outputText, 308 map: result.sourceMapText ? JSON.parse(result.sourceMapText) : new MagicString(code).generateMap() 309 }; 310 } 311 312 let tsProgram: ts.Program = globalProgram.program; 313 let targetSourceFile: ts.SourceFile | undefined = tsProgram.getSourceFile(id); 314 315 // createProgram from the file which does not have corresponding ast from ets-checker's program 316 // by those following cases: 317 // 1. .ets/.ts imported by .js file with tsc's `allowJS` option is false. 318 // 2. .ets/.ts imported by .js file with same name '.d.ts' file which is prior to .js by tsc default resolving 319 if (!targetSourceFile) { 320 startTimeStatisticsLocation(compilationTime ? compilationTime.noSourceFileRebuildProgramTime : undefined); 321 if (storedFileInfo.isFirstBuild && storedFileInfo.changeFiles) { 322 storedFileInfo.newTsProgram = ts.createProgram(storedFileInfo.changeFiles, etsCheckerCompilerOptions, compilerHost); 323 storedFileInfo.isFirstBuild = false; 324 } 325 if (storedFileInfo.newTsProgram && storedFileInfo.newTsProgram.getSourceFile(id)) { 326 tsProgram = storedFileInfo.newTsProgram; 327 } else { 328 tsProgram = ts.createProgram([id], etsCheckerCompilerOptions, compilerHost); 329 } 330 stopTimeStatisticsLocation(compilationTime ? compilationTime.noSourceFileRebuildProgramTime : undefined); 331 // init TypeChecker to run binding 332 globalProgram.checker = tsProgram.getTypeChecker(); 333 targetSourceFile = tsProgram.getSourceFile(id)!; 334 storedFileInfo.reUseProgram = false; 335 collectAllFiles(tsProgram); 336 } else { 337 if (!storedFileInfo.reUseProgram) { 338 globalProgram.checker = globalProgram.program.getTypeChecker(); 339 } 340 storedFileInfo.reUseProgram = true; 341 } 342 343 targetSourceFile.fileName = id; 344 startTimeStatisticsLocation(compilationTime ? compilationTime.validateEtsTime : undefined); 345 validateEts(code, id, this.getModuleInfo(id).isEntry, logger, targetSourceFile); 346 stopTimeStatisticsLocation(compilationTime ? compilationTime.validateEtsTime : undefined); 347 const emitResult: EmitResult = { outputText: '', sourceMapText: '' }; 348 const writeFile: ts.WriteFileCallback = (fileName: string, data: string) => { 349 if (/.map$/.test(fileName)) { 350 emitResult.sourceMapText = data; 351 } else { 352 emitResult.outputText = data; 353 } 354 } 355 356 // close `noEmit` to make invoking emit() effective. 357 tsProgram.getCompilerOptions().noEmit = false; 358 // use `try finally` to restore `noEmit` when error thrown by `processUISyntax` in preview mode 359 try { 360 startTimeStatisticsLocation(compilationTime ? compilationTime.tsProgramEmitTime : undefined); 361 tsProgram.emit(targetSourceFile, writeFile, undefined, undefined, 362 { 363 before: [ 364 processUISyntax(null, false, compilationTime), 365 processKitImport() 366 ] 367 } 368 ); 369 stopTimeStatisticsLocation(compilationTime ? compilationTime.tsProgramEmitTime : undefined); 370 } finally { 371 // restore `noEmit` to prevent tsc's watchService emitting automatically. 372 tsProgram.getCompilerOptions().noEmit = true; 373 } 374 375 resetCollection(); 376 if (((transformLog && transformLog.errors.length) || (kitTransformLog && kitTransformLog.errors.length)) && 377 !projectConfig.ignoreWarning) { 378 emitLogInfo(logger, [...getTransformLog(kitTransformLog), ...getTransformLog(transformLog)], true, id); 379 resetLog(); 380 } 381 382 return { 383 code: emitResult.outputText, 384 // Use magicString to generate sourceMap because of Typescript do not emit sourceMap in some cases 385 map: emitResult.sourceMapText ? JSON.parse(emitResult.sourceMapText) : new MagicString(code).generateMap() 386 }; 387} 388 389function validateEts(code: string, id: string, isEntry: boolean, logger: any, sourceFile: ts.SourceFile) { 390 if (/\.ets$/.test(id)) { 391 clearCollection(); 392 const fileQuery: string = isEntry && !abilityPagesFullPath.includes(path.resolve(id).toLowerCase()) ? '?entry' : ''; 393 const log: LogInfo[] = validateUISyntax(code, code, id, fileQuery, sourceFile); 394 if (log.length && !projectConfig.ignoreWarning) { 395 emitLogInfo(logger, log, true, id); 396 } 397 } 398} 399 400function jsBundlePreProcess(code: string, id: string, isEntry: boolean, logger: any): string { 401 if (/\.ets$/.test(id)) { 402 clearCollection(); 403 let content = preprocessExtend(code); 404 content = preprocessNewExtend(content); 405 const fileQuery: string = isEntry && !abilityPagesFullPath.includes(path.resolve(id).toLowerCase()) ? '?entry' : ''; 406 const log: LogInfo[] = validateUISyntax(code, content, id, fileQuery); 407 if (log.length && !projectConfig.ignoreWarning) { 408 emitLogInfo(logger, log, true, id); 409 } 410 return content; 411 } 412 return code; 413} 414 415function clearCollection(): void { 416 componentCollection.customComponents.clear(); 417 CUSTOM_BUILDER_METHOD.clear(); 418 GLOBAL_CUSTOM_BUILDER_METHOD.clear(); 419 INNER_CUSTOM_BUILDER_METHOD.clear(); 420 storedFileInfo.getCurrentArkTsFile().compFromDETS.clear(); 421} 422 423function resetCollection() { 424 componentInfo.id = 0; 425 propertyCollection.clear(); 426 linkCollection.clear(); 427 resetComponentCollection(); 428} 429 430function resetEtsTransform(): void { 431 shouldEnableDebugLine = false; 432 projectConfig.ignoreWarning = false; 433 projectConfig.widgetCompile = false; 434 disableCacheOptions = { 435 bundleName: 'default', 436 entryModuleName: 'default', 437 runtimeOS: 'default', 438 resourceTableHash: 'default', 439 etsLoaderVersion: 'default' 440 }; 441} 442 443function findArkoalaRoot(): string { 444 let arkoalaSdkRoot: string; 445 if (process.env.ARKOALA_SDK_ROOT) { 446 arkoalaSdkRoot = process.env.ARKOALA_SDK_ROOT; 447 if (!isDir(arkoalaSdkRoot)) { 448 throw new Error('Arkoala SDK not found in ' + arkoalaSdkRoot); 449 } 450 } else { 451 const arkoalaPossiblePaths: string[] = globalModulePaths.map(dir => path.join(dir, '../../arkoala')); 452 arkoalaSdkRoot = arkoalaPossiblePaths.find(possibleRootDir => isDir(possibleRootDir)) ?? ''; 453 if (!arkoalaSdkRoot) { 454 throw new Error('Arkoala SDK not found in ' + arkoalaPossiblePaths.join(';')); 455 } 456 } 457 458 return arkoalaSdkRoot; 459} 460 461function isDir(filePath: string): boolean { 462 try { 463 let stat: fs.Stats = fs.statSync(filePath); 464 return stat.isDirectory(); 465 } catch (e) { 466 return false; 467 } 468} 469 470function setIncrementalFileInHar(jsCacheFilePath: string, jsBuildFilePath: string, allFilesInHar: Map<string, string>): void { 471 if (jsCacheFilePath.match(/\.d.e?ts$/)) { 472 allFilesInHar.set(jsCacheFilePath, jsBuildFilePath); 473 return; 474 } 475 allFilesInHar.set(jsCacheFilePath.replace(/\.ets$/, '.d.ets').replace(/\.ts$/, '.d.ts'), 476 jsBuildFilePath.replace(/\.ets$/, '.d.ets').replace(/\.ts$/, '.d.ts')); 477 allFilesInHar.set(jsCacheFilePath.replace(/\.e?ts$/, '.js'), jsBuildFilePath.replace(/\.e?ts$/, '.js')); 478}