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 path.resolve(moduleInfo.id).startsWith(projectConfig.moduleRootPath + path.sep)) { 104 const filePath: string = moduleInfo.id; 105 const jsCacheFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath, 106 process.env.cachePath, projectConfig); 107 const jsBuildFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath, 108 projectConfig.buildPath, projectConfig, true); 109 if (filePath.match(/\.e?ts$/)) { 110 incrementalFileInHar.set(jsCacheFilePath.replace(/\.ets$/, '.d.ets').replace(/\.ts$/, '.d.ts'), 111 jsBuildFilePath.replace(/\.ets$/, '.d.ets').replace(/\.ts$/, '.d.ts')); 112 incrementalFileInHar.set(jsCacheFilePath.replace(/\.e?ts$/, '.js'), jsBuildFilePath.replace(/\.e?ts$/, '.js')); 113 } else { 114 incrementalFileInHar.set(jsCacheFilePath, jsBuildFilePath); 115 } 116 } 117 } 118 }, 119 afterBuildEnd() { 120 if (projectConfig.compileHar) { 121 incrementalFileInHar.forEach((jsBuildFilePath, jsCacheFilePath) => { 122 if (fs.existsSync(jsCacheFilePath)) { 123 const sourceCode: string = fs.readFileSync(jsCacheFilePath, 'utf-8'); 124 writeFileSync(jsBuildFilePath, sourceCode); 125 } 126 }); 127 } 128 shouldDisableCache = false; 129 this.cache.set('disableCacheOptions', disableCacheOptions); 130 storedFileInfo.buildStart = false; 131 storedFileInfo.saveCacheFileInfo(this.cache); 132 } 133 }; 134} 135 136// If a ArkTS file don't have @Entry decorator but it is entry file this time 137function disableNonEntryFileCache(filePath: string): boolean { 138 return storedFileInfo.buildStart && filePath.match(/(?<!\.d)\.(ets)$/) && 139 !storedFileInfo.wholeFileInfo[filePath].hasEntry && 140 storedFileInfo.shouldHaveEntry.includes(filePath); 141} 142 143function judgeCacheShouldDisabled(): void { 144 for (const key in disableCacheOptions) { 145 if (!shouldDisableCache && this.cache.get('disableCacheOptions') && this.share && 146 this.share.projectConfig && this.share.projectConfig[key] && 147 this.cache.get('disableCacheOptions')[key] !== this.share.projectConfig[key]) { 148 shouldDisableCache = true; 149 } 150 if (this.share && this.share.projectConfig && this.share.projectConfig[key]) { 151 disableCacheOptions[key] = this.share.projectConfig[key]; 152 } 153 storedFileInfo.judgeShouldHaveEntryFiles(abilityPagesFullPath); 154 } 155} 156 157interface EmitResult { 158 outputText: string, 159 sourceMapText: string, 160} 161 162const compilerHost: ts.CompilerHost = ts.createCompilerHost(etsCheckerCompilerOptions); 163compilerHost.writeFile = () => {}; 164compilerHost.resolveModuleNames = resolveModuleNames; 165compilerHost.getCurrentDirectory = () => process.cwd(); 166compilerHost.getDefaultLibFileName = options => ts.getDefaultLibFilePath(options); 167 168async function transform(code: string, id: string) { 169 if (!filter(id)) { 170 return null; 171 } 172 173 storedFileInfo.collectTransformedFiles(path.resolve(id)); 174 175 const logger = this.share.getLogger('etsTransform'); 176 177 if (projectConfig.compileMode !== "esmodule") { 178 const compilerOptions = ts.readConfigFile( 179 path.resolve(__dirname, '../../../tsconfig.json'), ts.sys.readFile).config.compilerOptions; 180 compilerOptions['moduleResolution'] = 'nodenext'; 181 compilerOptions['module'] = 'es2020' 182 const newContent: string = jsBundlePreProcess(code, id, this.getModuleInfo(id).isEntry, logger); 183 const result: ts.TranspileOutput = ts.transpileModule(newContent, { 184 compilerOptions: compilerOptions, 185 fileName: id, 186 transformers: { before: [ processUISyntax(null) ] } 187 }); 188 189 resetCollection(); 190 if (transformLog && transformLog.errors.length) { 191 emitLogInfo(logger, getTransformLog(transformLog), true, id); 192 resetLog(); 193 } 194 195 return { 196 code: result.outputText, 197 map: result.sourceMapText ? JSON.parse(result.sourceMapText) : new MagicString(code).generateMap() 198 }; 199 } 200 201 if (process.env.watchMode === 'true' && process.env.triggerTsWatch === 'true') { 202 // need to wait the tsc watch end signal to continue emitting in watch mode 203 await tsWatchEndPromise; 204 } 205 206 let tsProgram: ts.Program = process.env.watchMode !== 'true' ? 207 globalProgram.program : globalProgram.watchProgram.getCurrentProgram().getProgram(); 208 let targetSourceFile: ts.SourceFile | undefined = tsProgram.getSourceFile(id); 209 210 // createProgram from the file which does not have corresponding ast from ets-checker's program 211 if (!targetSourceFile) { 212 tsProgram = ts.createProgram([id], etsCheckerCompilerOptions, compilerHost); 213 targetSourceFile = tsProgram.getSourceFile(id)!; 214 } 215 216 validateEts(code, id, this.getModuleInfo(id).isEntry, logger); 217 218 const emitResult: EmitResult = { outputText: '', sourceMapText: '' }; 219 const writeFile: ts.WriteFileCallback = (fileName: string, data: string) => { 220 if (/.map$/.test(fileName)) { 221 emitResult.sourceMapText = data; 222 } else { 223 emitResult.outputText = data; 224 } 225 } 226 227 try { 228 // close `noEmit` to make invoking emit() effective. 229 tsProgram.getCompilerOptions().noEmit = false; 230 tsProgram.emit(targetSourceFile, writeFile, undefined, undefined, { before: [ processUISyntax(null) ] }); 231 } finally { 232 // restore `noEmit` to prevent tsc's watchService emitting automatically. 233 tsProgram.getCompilerOptions().noEmit = true; 234 } 235 236 resetCollection(); 237 if (transformLog && transformLog.errors.length) { 238 emitLogInfo(logger, getTransformLog(transformLog), true, id); 239 resetLog(); 240 } 241 242 return { 243 code: emitResult.outputText, 244 // Use magicString to generate sourceMap because of Typescript do not emit sourceMap in watchMode 245 map: emitResult.sourceMapText ? JSON.parse(emitResult.sourceMapText) : new MagicString(code).generateMap() 246 }; 247} 248 249function validateEts(code: string, id: string, isEntry: boolean, logger: any) { 250 if (/\.ets$/.test(id)) { 251 clearCollection(); 252 const fileQuery: string = isEntry && !abilityPagesFullPath.includes(path.resolve(id).toLowerCase()) ? '?entry' : ''; 253 const log: LogInfo[] = validateUISyntax(code, code, id, fileQuery); 254 if (log.length) { 255 emitLogInfo(logger, log, true, id); 256 } 257 } 258} 259 260function jsBundlePreProcess(code: string, id: string, isEntry: boolean, logger: any): string { 261 if (/\.ets$/.test(id)) { 262 clearCollection(); 263 let content = preprocessExtend(code); 264 content = preprocessNewExtend(content); 265 const fileQuery: string = isEntry && !abilityPagesFullPath.includes(path.resolve(id).toLowerCase()) ? '?entry' : ''; 266 const log: LogInfo[] = validateUISyntax(code, content, id, fileQuery); 267 if (log.length) { 268 emitLogInfo(logger, log, true, id); 269 } 270 return content; 271 } 272 return code; 273} 274 275function clearCollection(): void { 276 componentCollection.customComponents.clear(); 277 CUSTOM_BUILDER_METHOD.clear(); 278 GLOBAL_CUSTOM_BUILDER_METHOD.clear(); 279 INNER_CUSTOM_BUILDER_METHOD.clear(); 280} 281 282function resetCollection() { 283 componentInfo.id = 0; 284 propertyCollection.clear(); 285 linkCollection.clear(); 286 resetComponentCollection(); 287} 288