• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2025 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 * as ts from 'typescript';
17
18import {
19  IFileLog,
20  LogType
21} from './utils';
22import {
23  LogData,
24  LogDataFactory
25} from './fast_build/ark_compiler/logger';
26import {
27  ArkTSErrorDescription,
28  ErrorCode
29} from './fast_build/ark_compiler/error_code';
30import creatAstNodeUtils from './create_ast_node_utils';
31
32export const reExportCheckLog: IFileLog = new creatAstNodeUtils.FileLog();
33export const reExportNoCheckMode: string = 'noCheck';
34const reExportStrictMode: string = 'strict';
35
36export interface LazyImportOptions {
37  autoLazyImport: boolean;
38  reExportCheckMode: string;
39}
40
41export function processJsCodeLazyImport(id: string, code: string,
42  autoLazyImport: boolean, reExportCheckMode: string): string {
43  let sourceNode: ts.SourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.ES2021, true, ts.ScriptKind.JS);
44  if (autoLazyImport) {
45    sourceNode = transformLazyImport(sourceNode);
46  }
47  lazyImportReExportCheck(sourceNode, reExportCheckMode);
48  return autoLazyImport ? ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }).printFile(sourceNode) : code;
49}
50
51export function transformLazyImport(sourceNode: ts.SourceFile, resolver?: Object): ts.SourceFile {
52  const moduleNodeTransformer: ts.TransformerFactory<ts.SourceFile> = context => {
53    const visitor: ts.Visitor = node => {
54      if (ts.isImportDeclaration(node)) {
55        return updateImportDecl(node, resolver);
56      }
57      return node;
58    };
59    return node => ts.visitEachChild(node, visitor, context);
60  };
61
62  const result: ts.TransformationResult<ts.SourceFile> =
63    ts.transform(sourceNode, [moduleNodeTransformer]);
64  return result.transformed[0];
65}
66
67function updateImportDecl(node: ts.ImportDeclaration, resolver: Object): ts.ImportDeclaration {
68  const importClause: ts.ImportClause | undefined = node.importClause;
69  const moduleRequest: string = (node.moduleSpecifier! as ts.StringLiteral).text.replace(/'|"/g, '');
70  // The following cases do not support lazy-import.
71  // case1: import '...'
72  // case2: import type { t } from '...' or import type t from '...'
73  // case3: import lazy { x } from '...'
74  if (!importClause || importClause.isTypeOnly || importClause.isLazy) {
75    return node;
76  }
77  // case4: import * as ns from '...'
78  // case5: import y, * as ns from '...'
79  if (importClause.namedBindings && ts.isNamespaceImport(importClause.namedBindings)) {
80    return node;
81  }
82  // case6: import ... from 'xxx.json'
83  if (moduleRequest.endsWith('.json')) {
84    return node;
85  }
86  const namedBindings: ts.NamedImportBindings = importClause.namedBindings;
87  let newImportClause: ts.ImportClause;
88  // The following cases support lazy-import.
89  // case1: import { x } from '...' --> import lazy { x } from '...'
90  // case2: import y, { x } from '...' --> import lazy y, { x } from '...'
91  if (namedBindings && ts.isNamedImports(namedBindings)) {
92    // The resolver is used to determine whether type symbols need to be processed.
93    // Only TS/ETS files have type symbols.
94    if (resolver) {
95      // eliminate the type symbol
96      // eg: import { type t, x } from '...' --> import { x } from '...'
97      const newNameBindings: ts.ImportSpecifier[] = eliminateTypeSymbol(namedBindings, resolver);
98      newImportClause = ts.factory.updateImportClause(importClause, false, importClause.name,
99        ts.factory.updateNamedImports(namedBindings, newNameBindings));
100    } else {
101      newImportClause = importClause;
102    }
103  } else if (!namedBindings && importClause.name) {
104    // case3: import y from '...' --> import lazy y from '...'
105    newImportClause = importClause;
106  }
107  // @ts-ignore
108  newImportClause.isLazy = true;
109  const modifiers: readonly ts.Modifier[] | undefined = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
110  return ts.factory.updateImportDeclaration(node, modifiers, newImportClause, node.moduleSpecifier, node.assertClause);
111}
112
113function eliminateTypeSymbol(namedBindings: ts.NamedImportBindings, resolver: Object): ts.ImportSpecifier[] {
114  const newNameBindings: ts.ImportSpecifier[] = [];
115  namedBindings.elements.forEach(item => {
116    const element = item as ts.ImportSpecifier;
117    if (!element.isTypeOnly && resolver.isReferencedAliasDeclaration(element)) {
118      // import { x } from './y' --> propertyName is undefined
119      // import { x as a } from './y' --> propertyName is x
120      newNameBindings.push(
121        ts.factory.updateImportSpecifier(
122          element,
123          false,
124          element.propertyName,
125          element.name
126        )
127      );
128    }
129  });
130  return newNameBindings;
131}
132
133export function resetReExportCheckLog(): void {
134  reExportCheckLog.cleanUp();
135}
136
137export function lazyImportReExportCheck(node: ts.SourceFile, reExportCheckMode: string): void {
138  if (reExportCheckMode === reExportNoCheckMode) {
139    return;
140  }
141  reExportCheckLog.sourceFile = node;
142  const lazyImportSymbols: Set<string> = new Set();
143  const exportSymbols: Map<string, ts.Statement[]> = new Map();
144  const result: Map<string, ts.Statement[]> = new Map();
145  node.statements.forEach(stmt => {
146    collectLazyImportSymbols(stmt, lazyImportSymbols, exportSymbols, result);
147    collectLazyReExportSymbols(stmt, lazyImportSymbols, exportSymbols, result);
148  });
149  for (const [key, statements] of result.entries()) {
150    for (const statement of statements) {
151      collectReExportErrors(statement, key, reExportCheckMode);
152    }
153  }
154}
155
156function collectLazyImportSymbols(stmt: ts.Statement, lazyImportSymbols: Set<string>,
157  exportSymbols: Map<string, ts.Statement[]>, result: Map<string, ts.Statement[]>): void {
158  if (ts.isImportDeclaration(stmt) && stmt.importClause && stmt.importClause.isLazy) {
159    // For import lazy x from './y', collect 'x'
160    const importClauseName = stmt.importClause.name;
161    if (importClauseName) {
162      lazyImportSymbols.add(importClauseName.text);
163      result.set(importClauseName.text, exportSymbols.get(importClauseName.text) ?? []);
164    }
165    // For import lazy { x } from './y', collect 'x'
166    const importNamedBindings: ts.NamedImportBindings = stmt.importClause.namedBindings;
167    if (importNamedBindings && ts.isNamedImports(importNamedBindings) && importNamedBindings.elements.length !== 0) {
168      importNamedBindings.elements.forEach((element: ts.ImportSpecifier) => {
169        const nameText = element.name.text;
170        lazyImportSymbols.add(nameText);
171        result.set(nameText, exportSymbols.get(nameText) ?? []);
172      });
173    }
174  }
175}
176
177function collectLazyReExportSymbols(stmt: ts.Statement, lazyImportSymbols: Set<string>,
178  exportSymbols: Map<string, ts.Statement[]>, result: Map<string, ts.Statement[]>): void {
179  // export default x
180  if (ts.isExportAssignment(stmt) && ts.isIdentifier(stmt.expression)) {
181    const nameText: string = stmt.expression.text;
182    const targetMap = lazyImportSymbols.has(nameText) ? result : exportSymbols;
183    if (!targetMap.get(nameText)) {
184      targetMap.set(nameText, []);
185    }
186    targetMap.get(nameText).push(stmt);
187  }
188  // export { x }
189  if (ts.isExportDeclaration(stmt) && !stmt.moduleSpecifier &&
190    ts.isNamedExports(stmt.exportClause) && stmt.exportClause.elements.length !== 0) {
191    stmt.exportClause.elements.forEach((element: ts.ExportSpecifier) => {
192      // For example, in 'export { foo as bar }', exportName is 'bar', localName is 'foo'
193      const exportName: string = element.name.text;
194      const localName: string = element.propertyName ? element.propertyName.text : exportName;
195      const targetMap = lazyImportSymbols.has(localName) ? result : exportSymbols;
196      if (!targetMap.get(localName)) {
197        targetMap.set(localName, []);
198      }
199      targetMap.get(localName).push(stmt);
200    });
201  }
202}
203
204function collectReExportErrors(node: ts.Node, elementText: string, reExportCheckMode: string): void {
205  let pos: number;
206  try {
207    pos = node.getStart();
208  } catch {
209    pos = 0;
210  }
211  let type: LogType = LogType.WARN;
212  if (reExportCheckMode === reExportStrictMode) {
213    type = LogType.ERROR;
214  }
215  // reExportCheckMode explanation:
216  // - 'noCheck': NoCheck mode. The functionality to block re-exported lazy-import is disabled.
217  // - 'strict': Strict mode. It intercepts errors and treats them as critical (LogType.ERROR).
218  // - 'compatible': Compatible mode. It logs warnings (LogType.WARN) but does not intercept or block them.
219  const errInfo: LogData = LogDataFactory.newInstance(
220    ErrorCode.ETS2BUNDLE_EXTERNAL_LAZY_IMPORT_RE_EXPORT_ERROR,
221    ArkTSErrorDescription,
222    `'${elementText}' of lazy-import is re-export`,
223    '',
224    ['Please make sure the namedBindings of lazy-import are not be re-exported.',
225      'Please check whether the autoLazyImport switch is opened.']
226  );
227  reExportCheckLog.errors.push({
228    type: type,
229    message: errInfo.toString(),
230    pos: pos
231  });
232}