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