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} from '../../utils'; 36import { 37 preprocessExtend, 38 preprocessNewExtend, 39 validateUISyntax, 40 propertyCollection, 41 linkCollection, 42 resetComponentCollection, 43 componentCollection 44} from '../../validate_ui_syntax'; 45import { 46 processUISyntax, 47 resetLog, 48 transformLog 49} from '../../process_ui_syntax'; 50import { 51 projectConfig, 52 abilityPagesFullPath, 53 globalProgram 54} from '../../../main'; 55import { 56 appComponentCollection, 57 compilerOptions as etsCheckerCompilerOptions, 58 resolveModuleNames, 59 resolveTypeReferenceDirectives 60} from '../../ets_checker'; 61import { 62 CUSTOM_BUILDER_METHOD, 63 GLOBAL_CUSTOM_BUILDER_METHOD, 64 INNER_CUSTOM_BUILDER_METHOD 65} from '../../component_map'; 66 67const filter:any = createFilter(/(?<!\.d)\.(ets|ts)$/); 68 69let shouldDisableCache: boolean = false; 70const disableCacheOptions = { 71 bundleName: 'default', 72 entryModuleName: 'default', 73 runtimeOS: 'default', 74 resourceTableHash: 'default', 75 etsLoaderVersion: 'default' 76}; 77 78export function etsTransform() { 79 const incrementalFileInHar: Map<string, string> = new Map(); 80 return { 81 name: 'etsTransform', 82 transform: transform, 83 buildStart() { 84 judgeCacheShouldDisabled.call(this); 85 if (process.env.compileMode === 'moduleJson') { 86 storedFileInfo.addGlobalCacheInfo(this.cache.get('resourceListCacheInfo'), 87 this.cache.get('resourceToFileCacheInfo')); 88 if (this.cache.get('lastResourcesArr')) { 89 storedFileInfo.lastResourcesSet = new Set([...this.cache.get('lastResourcesArr')]); 90 } 91 if (process.env.rawFileResource) { 92 resourcesRawfile(process.env.rawFileResource, storedFileInfo.resourcesArr); 93 this.share.rawfilechanged = differenceResourcesRawfile(storedFileInfo.lastResourcesSet, storedFileInfo.resourcesArr); 94 } 95 } 96 }, 97 load(id: string) { 98 let fileCacheInfo: fileInfo; 99 if (this.cache.get('fileCacheInfo')) { 100 fileCacheInfo = this.cache.get('fileCacheInfo')[path.resolve(id)]; 101 } 102 // Exclude Component Preview page 103 if (projectConfig.isPreview && !projectConfig.checkEntry && id.match(/(?<!\.d)\.(ets)$/)) { 104 abilityPagesFullPath.push(path.resolve(id).toLowerCase()); 105 storedFileInfo.judgeShouldHaveEntryFiles(abilityPagesFullPath); 106 } 107 storedFileInfo.addFileCacheInfo(path.resolve(id), fileCacheInfo); 108 storedFileInfo.setCurrentArkTsFile(); 109 }, 110 shouldInvalidCache(options) { 111 const fileName: string = path.resolve(options.id); 112 let shouldDisable: boolean = shouldDisableCache || disableNonEntryFileCache(fileName); 113 if (process.env.compileMode === 'moduleJson') { 114 shouldDisable = shouldDisable || storedFileInfo.shouldInvalidFiles.has(fileName) || this.share.rawfilechanged; 115 } 116 if (!shouldDisable) { 117 storedFileInfo.collectCachedFiles(fileName); 118 } 119 return shouldDisable; 120 }, 121 moduleParsed(moduleInfo) { 122 if (projectConfig.compileHar) { 123 if (moduleInfo.id && !moduleInfo.id.match(new RegExp(projectConfig.packageDir)) && 124 !moduleInfo.id.startsWith('\x00') && 125 path.resolve(moduleInfo.id).startsWith(projectConfig.moduleRootPath + path.sep)) { 126 const filePath: string = moduleInfo.id; 127 const jsCacheFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath, 128 process.env.cachePath, projectConfig); 129 const jsBuildFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath, 130 projectConfig.buildPath, projectConfig, true); 131 if (filePath.match(/\.e?ts$/)) { 132 incrementalFileInHar.set(jsCacheFilePath.replace(/\.ets$/, '.d.ets').replace(/\.ts$/, '.d.ts'), 133 jsBuildFilePath.replace(/\.ets$/, '.d.ets').replace(/\.ts$/, '.d.ts')); 134 incrementalFileInHar.set(jsCacheFilePath.replace(/\.e?ts$/, '.js'), jsBuildFilePath.replace(/\.e?ts$/, '.js')); 135 } else { 136 incrementalFileInHar.set(jsCacheFilePath, jsBuildFilePath); 137 } 138 } 139 } 140 }, 141 afterBuildEnd() { 142 if (projectConfig.compileHar) { 143 incrementalFileInHar.forEach((jsBuildFilePath, jsCacheFilePath) => { 144 if (fs.existsSync(jsCacheFilePath)) { 145 const sourceCode: string = fs.readFileSync(jsCacheFilePath, 'utf-8'); 146 writeFileSync(jsBuildFilePath, sourceCode); 147 } 148 }); 149 } 150 if (process.env.watchMode !== 'true' && !projectConfig.xtsMode) { 151 writeCollectionFile(projectConfig.cachePath, appComponentCollection, 152 this.share.allComponents, 'component_collection.json', this.share.allFiles); 153 } 154 shouldDisableCache = false; 155 this.cache.set('disableCacheOptions', disableCacheOptions); 156 this.cache.set('lastResourcesArr', [...storedFileInfo.resourcesArr]); 157 storedFileInfo.clearCollectedInfo(this.cache); 158 } 159 }; 160} 161 162// If a ArkTS file don't have @Entry decorator but it is entry file this time 163function disableNonEntryFileCache(filePath: string): boolean { 164 return storedFileInfo.buildStart && filePath.match(/(?<!\.d)\.(ets)$/) && 165 !storedFileInfo.wholeFileInfo[filePath].hasEntry && 166 storedFileInfo.shouldHaveEntry.includes(filePath); 167} 168 169function judgeCacheShouldDisabled(): void { 170 for (const key in disableCacheOptions) { 171 if (this.cache.get('disableCacheOptions') && this.share && 172 this.share.projectConfig && this.share.projectConfig[key] && 173 this.cache.get('disableCacheOptions')[key] !== this.share.projectConfig[key]) { 174 if (key === 'resourceTableHash' && process.env.compileMode === 'moduleJson') { 175 storedFileInfo.resourceTableChanged = true; 176 } else if (!shouldDisableCache) { 177 shouldDisableCache = true; 178 } 179 } 180 if (this.share && this.share.projectConfig && this.share.projectConfig[key]) { 181 disableCacheOptions[key] = this.share.projectConfig[key]; 182 } 183 storedFileInfo.judgeShouldHaveEntryFiles(abilityPagesFullPath); 184 } 185} 186 187interface EmitResult { 188 outputText: string, 189 sourceMapText: string, 190} 191 192const compilerHost: ts.CompilerHost = ts.createCompilerHost(etsCheckerCompilerOptions); 193compilerHost.writeFile = () => {}; 194compilerHost.resolveModuleNames = resolveModuleNames; 195compilerHost.getCurrentDirectory = () => process.cwd(); 196compilerHost.getDefaultLibFileName = options => ts.getDefaultLibFilePath(options); 197compilerHost.resolveTypeReferenceDirectives = resolveTypeReferenceDirectives; 198 199async function transform(code: string, id: string) { 200 if (!filter(id)) { 201 return null; 202 } 203 204 storedFileInfo.collectTransformedFiles(path.resolve(id)); 205 206 const logger = this.share.getLogger('etsTransform'); 207 208 if (projectConfig.compileMode !== "esmodule") { 209 const compilerOptions = ts.readConfigFile( 210 path.resolve(__dirname, '../../../tsconfig.json'), ts.sys.readFile).config.compilerOptions; 211 compilerOptions['moduleResolution'] = 'nodenext'; 212 compilerOptions['module'] = 'es2020'; 213 const newContent: string = jsBundlePreProcess(code, id, this.getModuleInfo(id).isEntry, logger); 214 const result: ts.TranspileOutput = ts.transpileModule(newContent, { 215 compilerOptions: compilerOptions, 216 fileName: id, 217 transformers: { before: [ processUISyntax(null) ] } 218 }); 219 220 resetCollection(); 221 if (transformLog && transformLog.errors.length) { 222 emitLogInfo(logger, getTransformLog(transformLog), true, id); 223 resetLog(); 224 } 225 226 return { 227 code: result.outputText, 228 map: result.sourceMapText ? JSON.parse(result.sourceMapText) : new MagicString(code).generateMap() 229 }; 230 } 231 232 let tsProgram: ts.Program = globalProgram.program; 233 let targetSourceFile: ts.SourceFile | undefined = tsProgram.getSourceFile(id); 234 235 // createProgram from the file which does not have corresponding ast from ets-checker's program 236 // by those following cases: 237 // 1. .ets/.ts imported by .js file with tsc's `allowJS` option is false. 238 // 2. .ets/.ts imported by .js file with same name '.d.ts' file which is prior to .js by tsc default resolving 239 if (!targetSourceFile) { 240 tsProgram = ts.createProgram([id], etsCheckerCompilerOptions, compilerHost); 241 // init TypeChecker to run binding 242 globalProgram.checker = tsProgram.getTypeChecker(); 243 targetSourceFile = tsProgram.getSourceFile(id)!; 244 storedFileInfo.reUseProgram = false; 245 } else { 246 if (!storedFileInfo.reUseProgram) { 247 globalProgram.checker = globalProgram.program.getTypeChecker(); 248 } 249 storedFileInfo.reUseProgram = true; 250 } 251 252 targetSourceFile.fileName = id; 253 254 validateEts(code, id, this.getModuleInfo(id).isEntry, logger, targetSourceFile); 255 256 const emitResult: EmitResult = { outputText: '', sourceMapText: '' }; 257 const writeFile: ts.WriteFileCallback = (fileName: string, data: string) => { 258 if (/.map$/.test(fileName)) { 259 emitResult.sourceMapText = data; 260 } else { 261 emitResult.outputText = data; 262 } 263 } 264 265 // close `noEmit` to make invoking emit() effective. 266 tsProgram.getCompilerOptions().noEmit = false; 267 // use `try finally` to restore `noEmit` when error thrown by `processUISyntax` in preview mode 268 try { 269 tsProgram.emit(targetSourceFile, writeFile, undefined, undefined, { before: [ processUISyntax(null) ] }); 270 } finally { 271 // restore `noEmit` to prevent tsc's watchService emitting automatically. 272 tsProgram.getCompilerOptions().noEmit = true; 273 } 274 275 resetCollection(); 276 if (transformLog && transformLog.errors.length) { 277 emitLogInfo(logger, getTransformLog(transformLog), true, id); 278 resetLog(); 279 } 280 281 return { 282 code: emitResult.outputText, 283 // Use magicString to generate sourceMap because of Typescript do not emit sourceMap in some cases 284 map: emitResult.sourceMapText ? JSON.parse(emitResult.sourceMapText) : new MagicString(code).generateMap() 285 }; 286} 287 288function validateEts(code: string, id: string, isEntry: boolean, logger: any, sourceFile: ts.SourceFile) { 289 if (/\.ets$/.test(id)) { 290 clearCollection(); 291 const fileQuery: string = isEntry && !abilityPagesFullPath.includes(path.resolve(id).toLowerCase()) ? '?entry' : ''; 292 const log: LogInfo[] = validateUISyntax(code, code, id, fileQuery, sourceFile); 293 if (log.length) { 294 emitLogInfo(logger, log, true, id); 295 } 296 } 297} 298 299function jsBundlePreProcess(code: string, id: string, isEntry: boolean, logger: any): string { 300 if (/\.ets$/.test(id)) { 301 clearCollection(); 302 let content = preprocessExtend(code); 303 content = preprocessNewExtend(content); 304 const fileQuery: string = isEntry && !abilityPagesFullPath.includes(path.resolve(id).toLowerCase()) ? '?entry' : ''; 305 const log: LogInfo[] = validateUISyntax(code, content, id, fileQuery); 306 if (log.length) { 307 emitLogInfo(logger, log, true, id); 308 } 309 return content; 310 } 311 return code; 312} 313 314function clearCollection(): void { 315 componentCollection.customComponents.clear(); 316 CUSTOM_BUILDER_METHOD.clear(); 317 GLOBAL_CUSTOM_BUILDER_METHOD.clear(); 318 INNER_CUSTOM_BUILDER_METHOD.clear(); 319 storedFileInfo.getCurrentArkTsFile().compFromDETS.clear(); 320} 321 322function resetCollection() { 323 componentInfo.id = 0; 324 propertyCollection.clear(); 325 linkCollection.clear(); 326 resetComponentCollection(); 327} 328