• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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