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