• 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  CacheFile,
36  startTimeStatisticsLocation,
37  stopTimeStatisticsLocation,
38  CompilationTimeStatistics,
39  genLoaderOutPathOfHar,
40  harFilesRecord,
41  resetUtils,
42  getResolveModules
43} from '../../utils';
44import {
45  preprocessExtend,
46  preprocessNewExtend,
47  validateUISyntax,
48  propertyCollection,
49  linkCollection,
50  resetComponentCollection,
51  componentCollection,
52  resetValidateUiSyntax
53} from '../../validate_ui_syntax';
54import {
55  processUISyntax,
56  resetLog,
57  transformLog,
58  resetProcessUiSyntax
59} from '../../process_ui_syntax';
60import {
61  projectConfig,
62  abilityPagesFullPath,
63  globalProgram,
64  resetMain,
65  globalModulePaths
66} from '../../../main';
67import {
68  appComponentCollection,
69  compilerOptions as etsCheckerCompilerOptions,
70  resolveModuleNames,
71  resolveTypeReferenceDirectives,
72  resetEtsCheck,
73  collectAllFiles,
74  allSourceFilePaths
75} from '../../ets_checker';
76import {
77  CUSTOM_BUILDER_METHOD,
78  GLOBAL_CUSTOM_BUILDER_METHOD,
79  INNER_CUSTOM_BUILDER_METHOD,
80  resetComponentMap
81} from '../../component_map';
82import {
83  kitTransformLog,
84  processKitImport
85} from '../../process_kit_import';
86import { resetProcessComponentMember } from '../../process_component_member';
87import { mangleFilePath, resetObfuscation } from '../ark_compiler/common/ob_config_resolver';
88
89const filter:any = createFilter(/(?<!\.d)\.(ets|ts)$/);
90
91let shouldDisableCache: boolean = false;
92let shouldEnableDebugLine: boolean = false;
93let disableCacheOptions = {
94  bundleName: 'default',
95  entryModuleName: 'default',
96  runtimeOS: 'default',
97  resourceTableHash: 'default',
98  etsLoaderVersion: 'default'
99};
100
101export function etsTransform() {
102  const allFilesInHar: Map<string, string> = new Map();
103  let cacheFile: CacheFile;
104  if (projectConfig.useArkoala) {
105    // Dynamic loading to avoid resolving arkoala-only dependencies
106    const arkoalaSdkRoot: string = findArkoalaRoot();
107    const pluginPackagePath: string = path.join(arkoalaSdkRoot, '@arkoala', 'rollup-plugin-ets-arkoala');
108    const pluginOptions: Object = {
109      arkoalaSdkRoot,
110      projectConfig,
111      globalModulePaths,
112      getResolveModules,
113    };
114    return require(pluginPackagePath).makeArkoalaPlugin(pluginOptions);
115  }
116  return {
117    name: 'etsTransform',
118    transform: transform,
119    buildStart() {
120      const compilationTime: CompilationTimeStatistics = new CompilationTimeStatistics(this.share, 'etsTransform', 'buildStart');
121      startTimeStatisticsLocation(compilationTime ? compilationTime.etsTransformBuildStartTime : undefined);
122      judgeCacheShouldDisabled.call(this);
123      if (process.env.compileMode === 'moduleJson') {
124        cacheFile = this.cache.get('transformCacheFiles');
125        storedFileInfo.addGlobalCacheInfo(this.cache.get('resourceListCacheInfo'),
126          this.cache.get('resourceToFileCacheInfo'));
127        if (this.cache.get('lastResourcesArr')) {
128          storedFileInfo.lastResourcesSet = new Set([...this.cache.get('lastResourcesArr')]);
129        }
130        if (process.env.rawFileResource) {
131          resourcesRawfile(process.env.rawFileResource, storedFileInfo.resourcesArr);
132          this.share.rawfilechanged = differenceResourcesRawfile(storedFileInfo.lastResourcesSet, storedFileInfo.resourcesArr);
133        }
134      }
135      if (this.cache.get('enableDebugLine') !== projectConfig.enableDebugLine) {
136        shouldEnableDebugLine = true;
137      }
138      stopTimeStatisticsLocation(compilationTime ? compilationTime.etsTransformBuildStartTime : undefined);
139    },
140    load(id: string) {
141      let fileCacheInfo: fileInfo;
142      const compilationTime: CompilationTimeStatistics = new CompilationTimeStatistics(this.share, 'etsTransform', 'load');
143      startTimeStatisticsLocation(compilationTime ? compilationTime.etsTransformLoadTime : undefined);
144      if (this.cache.get('fileCacheInfo')) {
145        fileCacheInfo = this.cache.get('fileCacheInfo')[path.resolve(id)];
146      }
147      // Exclude Component Preview page
148      if (projectConfig.isPreview && !projectConfig.checkEntry && id.match(/(?<!\.d)\.(ets)$/)) {
149        abilityPagesFullPath.push(path.resolve(id).toLowerCase());
150        storedFileInfo.judgeShouldHaveEntryFiles(abilityPagesFullPath);
151      }
152      storedFileInfo.addFileCacheInfo(path.resolve(id), fileCacheInfo);
153      storedFileInfo.setCurrentArkTsFile();
154      stopTimeStatisticsLocation(compilationTime ? compilationTime.etsTransformLoadTime : undefined);
155    },
156    shouldInvalidCache(options) {
157      const fileName: string = path.resolve(options.id);
158      let shouldDisable: boolean = shouldDisableCache || disableNonEntryFileCache(fileName) || shouldEnableDebugLine;
159      if (process.env.compileMode === 'moduleJson') {
160        shouldDisable = shouldDisable || storedFileInfo.shouldInvalidFiles.has(fileName) || this.share.rawfilechanged;
161        if (cacheFile && cacheFile[fileName] && cacheFile[fileName].children.length) {
162          for (let child of cacheFile[fileName].children) {
163            const newTimeMs: number = fs.existsSync(child.fileName) ? fs.statSync(child.fileName).mtimeMs : -1;
164            if (newTimeMs !== child.mtimeMs) {
165              shouldDisable = true;
166              break;
167            }
168          }
169        }
170      }
171      if (!shouldDisable) {
172        storedFileInfo.collectCachedFiles(fileName);
173      }
174      return shouldDisable;
175    },
176    afterBuildEnd() {
177      // Copy the cache files in the compileArkTS directory to the loader_out directory
178      if (projectConfig.compileHar) {
179        for (let moduleInfoId of allSourceFilePaths) {
180          if (moduleInfoId && !moduleInfoId.match(new RegExp(projectConfig.packageDir)) &&
181            !moduleInfoId.startsWith('\x00') &&
182            path.resolve(moduleInfoId).startsWith(projectConfig.moduleRootPath + path.sep)) {
183            let filePath: string = moduleInfoId;
184            if (this.share.arkProjectConfig?.obfuscationMergedObConfig?.options?.enableFileNameObfuscation) {
185              filePath = mangleFilePath(filePath);
186            }
187
188            const jsCacheFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath,
189              process.env.cachePath, projectConfig);
190            const jsBuildFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath,
191              projectConfig.buildPath, projectConfig, true);
192            if (filePath.match(/\.e?ts$/)) {
193              setIncrementalFileInHar(jsCacheFilePath, jsBuildFilePath, allFilesInHar);
194            } else {
195              allFilesInHar.set(jsCacheFilePath, jsBuildFilePath);
196            }
197          }
198        }
199
200        allFilesInHar.forEach((jsBuildFilePath, jsCacheFilePath) => {
201          // if the ts or ets file code only contain interface, it doesn't have js file.
202          if (fs.existsSync(jsCacheFilePath)) {
203            const sourceCode: string = fs.readFileSync(jsCacheFilePath, 'utf-8');
204            writeFileSync(jsBuildFilePath, sourceCode);
205          }
206        });
207      }
208      if (process.env.watchMode !== 'true' && !projectConfig.xtsMode) {
209        let widgetPath: string;
210        if (projectConfig.widgetCompile) {
211          widgetPath = path.resolve(projectConfig.aceModuleBuild, 'widget');
212        }
213        writeCollectionFile(projectConfig.cachePath, appComponentCollection,
214          this.share.allComponents, 'component_collection.json', this.share.allFiles, widgetPath);
215      }
216      shouldDisableCache = false;
217      this.cache.set('disableCacheOptions', disableCacheOptions);
218      this.cache.set('lastResourcesArr', [...storedFileInfo.resourcesArr]);
219      if (projectConfig.enableDebugLine) {
220        this.cache.set('enableDebugLine', true);
221      } else {
222        this.cache.set('enableDebugLine', false);
223      }
224      storedFileInfo.clearCollectedInfo(this.cache);
225      this.cache.set('transformCacheFiles', storedFileInfo.transformCacheFiles);
226    },
227    cleanUp(): void {
228      resetMain();
229      resetComponentMap();
230      resetEtsCheck();
231      resetEtsTransform();
232      resetProcessComponentMember();
233      resetProcessUiSyntax();
234      resetUtils();
235      resetValidateUiSyntax();
236      resetObfuscation();
237    }
238  };
239}
240
241// If a ArkTS file don't have @Entry decorator but it is entry file this time
242function disableNonEntryFileCache(filePath: string): boolean {
243  return storedFileInfo.buildStart && filePath.match(/(?<!\.d)\.(ets)$/) &&
244    !storedFileInfo.wholeFileInfo[filePath].hasEntry &&
245    storedFileInfo.shouldHaveEntry.includes(filePath);
246}
247
248function judgeCacheShouldDisabled(): void {
249  for (const key in disableCacheOptions) {
250    if (this.cache.get('disableCacheOptions') && this.share &&
251      this.share.projectConfig && this.share.projectConfig[key] &&
252      this.cache.get('disableCacheOptions')[key] !== this.share.projectConfig[key]) {
253      if (key === 'resourceTableHash' && process.env.compileMode === 'moduleJson') {
254        storedFileInfo.resourceTableChanged = true;
255      } else if (!shouldDisableCache) {
256        shouldDisableCache = true;
257      }
258    }
259    if (this.share && this.share.projectConfig && this.share.projectConfig[key]) {
260      disableCacheOptions[key] = this.share.projectConfig[key];
261    }
262    storedFileInfo.judgeShouldHaveEntryFiles(abilityPagesFullPath);
263  }
264}
265
266interface EmitResult {
267  outputText: string,
268  sourceMapText: string,
269}
270
271const compilerHost: ts.CompilerHost = ts.createCompilerHost(etsCheckerCompilerOptions);
272compilerHost.writeFile = () => {};
273compilerHost.resolveModuleNames = resolveModuleNames;
274compilerHost.getCurrentDirectory = () => process.cwd();
275compilerHost.getDefaultLibFileName = options => ts.getDefaultLibFilePath(options);
276compilerHost.resolveTypeReferenceDirectives = resolveTypeReferenceDirectives;
277
278async function transform(code: string, id: string) {
279  const compilationTime: CompilationTimeStatistics = new CompilationTimeStatistics(this.share, 'etsTransform', 'transform');
280  if (!filter(id)) {
281    return null;
282  }
283
284  storedFileInfo.collectTransformedFiles(path.resolve(id));
285
286  const logger = this.share.getLogger('etsTransform');
287
288  if (projectConfig.compileMode !== "esmodule") {
289    const compilerOptions = ts.readConfigFile(
290      path.resolve(__dirname, '../../../tsconfig.json'), ts.sys.readFile).config.compilerOptions;
291    compilerOptions['moduleResolution'] = 'nodenext';
292    compilerOptions['module'] = 'es2020';
293    const newContent: string = jsBundlePreProcess(code, id, this.getModuleInfo(id).isEntry, logger);
294    const result: ts.TranspileOutput = ts.transpileModule(newContent, {
295      compilerOptions: compilerOptions,
296      fileName: id,
297      transformers: { before: [ processUISyntax(null) ] }
298    });
299
300    resetCollection();
301    if (transformLog && transformLog.errors.length && !projectConfig.ignoreWarning) {
302      emitLogInfo(logger, getTransformLog(transformLog), true, id);
303      resetLog();
304    }
305
306    return {
307      code: result.outputText,
308      map: result.sourceMapText ? JSON.parse(result.sourceMapText) : new MagicString(code).generateMap()
309    };
310  }
311
312  let tsProgram: ts.Program = globalProgram.program;
313  let targetSourceFile: ts.SourceFile | undefined = tsProgram.getSourceFile(id);
314
315  // createProgram from the file which does not have corresponding ast from ets-checker's program
316  // by those following cases:
317  // 1. .ets/.ts imported by .js file with tsc's `allowJS` option is false.
318  // 2. .ets/.ts imported by .js file with same name '.d.ts' file which is prior to .js by tsc default resolving
319  if (!targetSourceFile) {
320    startTimeStatisticsLocation(compilationTime ? compilationTime.noSourceFileRebuildProgramTime : undefined);
321    if (storedFileInfo.isFirstBuild && storedFileInfo.changeFiles) {
322      storedFileInfo.newTsProgram = ts.createProgram(storedFileInfo.changeFiles, etsCheckerCompilerOptions, compilerHost);
323      storedFileInfo.isFirstBuild = false;
324    }
325    if (storedFileInfo.newTsProgram && storedFileInfo.newTsProgram.getSourceFile(id)) {
326      tsProgram = storedFileInfo.newTsProgram;
327    } else {
328      tsProgram = ts.createProgram([id], etsCheckerCompilerOptions, compilerHost);
329    }
330    stopTimeStatisticsLocation(compilationTime ? compilationTime.noSourceFileRebuildProgramTime : undefined);
331    // init TypeChecker to run binding
332    globalProgram.checker = tsProgram.getTypeChecker();
333    targetSourceFile = tsProgram.getSourceFile(id)!;
334    storedFileInfo.reUseProgram = false;
335    collectAllFiles(tsProgram);
336  } else {
337    if (!storedFileInfo.reUseProgram) {
338      globalProgram.checker = globalProgram.program.getTypeChecker();
339    }
340    storedFileInfo.reUseProgram = true;
341  }
342
343  targetSourceFile.fileName = id;
344  startTimeStatisticsLocation(compilationTime ? compilationTime.validateEtsTime : undefined);
345  validateEts(code, id, this.getModuleInfo(id).isEntry, logger, targetSourceFile);
346  stopTimeStatisticsLocation(compilationTime ? compilationTime.validateEtsTime : undefined);
347  const emitResult: EmitResult = { outputText: '', sourceMapText: '' };
348  const writeFile: ts.WriteFileCallback = (fileName: string, data: string) => {
349    if (/.map$/.test(fileName)) {
350      emitResult.sourceMapText = data;
351    } else {
352      emitResult.outputText = data;
353    }
354  }
355
356  // close `noEmit` to make invoking emit() effective.
357  tsProgram.getCompilerOptions().noEmit = false;
358  // use `try finally` to restore `noEmit` when error thrown by `processUISyntax` in preview mode
359  try {
360    startTimeStatisticsLocation(compilationTime ? compilationTime.tsProgramEmitTime : undefined);
361    tsProgram.emit(targetSourceFile, writeFile, undefined, undefined,
362      {
363        before: [
364          processUISyntax(null, false, compilationTime),
365          processKitImport()
366        ]
367      }
368    );
369    stopTimeStatisticsLocation(compilationTime ? compilationTime.tsProgramEmitTime : undefined);
370  } finally {
371    // restore `noEmit` to prevent tsc's watchService emitting automatically.
372    tsProgram.getCompilerOptions().noEmit = true;
373  }
374
375  resetCollection();
376  if (((transformLog && transformLog.errors.length) || (kitTransformLog && kitTransformLog.errors.length)) &&
377    !projectConfig.ignoreWarning) {
378    emitLogInfo(logger, [...getTransformLog(kitTransformLog), ...getTransformLog(transformLog)], true, id);
379    resetLog();
380  }
381
382  return {
383    code: emitResult.outputText,
384    // Use magicString to generate sourceMap because of Typescript do not emit sourceMap in some cases
385    map: emitResult.sourceMapText ? JSON.parse(emitResult.sourceMapText) : new MagicString(code).generateMap()
386  };
387}
388
389function validateEts(code: string, id: string, isEntry: boolean, logger: any, sourceFile: ts.SourceFile) {
390  if (/\.ets$/.test(id)) {
391    clearCollection();
392    const fileQuery: string = isEntry && !abilityPagesFullPath.includes(path.resolve(id).toLowerCase()) ? '?entry' : '';
393    const log: LogInfo[] = validateUISyntax(code, code, id, fileQuery, sourceFile);
394    if (log.length && !projectConfig.ignoreWarning) {
395      emitLogInfo(logger, log, true, id);
396    }
397  }
398}
399
400function jsBundlePreProcess(code: string, id: string, isEntry: boolean, logger: any): string {
401  if (/\.ets$/.test(id)) {
402    clearCollection();
403    let content = preprocessExtend(code);
404    content = preprocessNewExtend(content);
405    const fileQuery: string = isEntry && !abilityPagesFullPath.includes(path.resolve(id).toLowerCase()) ? '?entry' : '';
406    const log: LogInfo[] = validateUISyntax(code, content, id, fileQuery);
407    if (log.length && !projectConfig.ignoreWarning) {
408      emitLogInfo(logger, log, true, id);
409    }
410    return content;
411  }
412  return code;
413}
414
415function clearCollection(): void {
416  componentCollection.customComponents.clear();
417  CUSTOM_BUILDER_METHOD.clear();
418  GLOBAL_CUSTOM_BUILDER_METHOD.clear();
419  INNER_CUSTOM_BUILDER_METHOD.clear();
420  storedFileInfo.getCurrentArkTsFile().compFromDETS.clear();
421}
422
423function resetCollection() {
424  componentInfo.id = 0;
425  propertyCollection.clear();
426  linkCollection.clear();
427  resetComponentCollection();
428}
429
430function resetEtsTransform(): void {
431  shouldEnableDebugLine = false;
432  projectConfig.ignoreWarning = false;
433  projectConfig.widgetCompile = false;
434  disableCacheOptions = {
435    bundleName: 'default',
436    entryModuleName: 'default',
437    runtimeOS: 'default',
438    resourceTableHash: 'default',
439    etsLoaderVersion: 'default'
440  };
441}
442
443function findArkoalaRoot(): string {
444  let arkoalaSdkRoot: string;
445  if (process.env.ARKOALA_SDK_ROOT) {
446    arkoalaSdkRoot = process.env.ARKOALA_SDK_ROOT;
447    if (!isDir(arkoalaSdkRoot)) {
448      throw new Error('Arkoala SDK not found in ' + arkoalaSdkRoot);
449    }
450  } else {
451    const arkoalaPossiblePaths: string[] = globalModulePaths.map(dir => path.join(dir, '../../arkoala'));
452    arkoalaSdkRoot = arkoalaPossiblePaths.find(possibleRootDir => isDir(possibleRootDir)) ?? '';
453    if (!arkoalaSdkRoot) {
454      throw new Error('Arkoala SDK not found in ' + arkoalaPossiblePaths.join(';'));
455    }
456  }
457
458  return arkoalaSdkRoot;
459}
460
461function isDir(filePath: string): boolean {
462  try {
463    let stat: fs.Stats = fs.statSync(filePath);
464    return stat.isDirectory();
465  } catch (e) {
466    return false;
467  }
468}
469
470function setIncrementalFileInHar(jsCacheFilePath: string, jsBuildFilePath: string, allFilesInHar: Map<string, string>): void {
471  if (jsCacheFilePath.match(/\.d.e?ts$/)) {
472    allFilesInHar.set(jsCacheFilePath, jsBuildFilePath);
473    return;
474  }
475  allFilesInHar.set(jsCacheFilePath.replace(/\.ets$/, '.d.ets').replace(/\.ts$/, '.d.ts'),
476    jsBuildFilePath.replace(/\.ets$/, '.d.ets').replace(/\.ts$/, '.d.ts'));
477  allFilesInHar.set(jsCacheFilePath.replace(/\.e?ts$/, '.js'), jsBuildFilePath.replace(/\.e?ts$/, '.js'));
478}