• 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  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