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