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