• 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  // The following cases do not support lazy-import.
70  // case1: import '...'
71  // case2: import type { t } from '...' or import type t from '...'
72  // case3: import lazy { x } from '...'
73  if (!importClause || importClause.isTypeOnly || importClause.isLazy) {
74    return node;
75  }
76  // case4: import * as ns from '...'
77  // case5: import y, * as ns from '...'
78  if (importClause.namedBindings && ts.isNamespaceImport(importClause.namedBindings)) {
79    return node;
80  }
81  const namedBindings: ts.NamedImportBindings = importClause.namedBindings;
82  let newImportClause: ts.ImportClause;
83  // The following cases support lazy-import.
84  // case1: import { x } from '...' --> import lazy { x } from '...'
85  // case2: import y, { x } from '...' --> import lazy y, { x } from '...'
86  if (namedBindings && ts.isNamedImports(namedBindings)) {
87    // The resolver is used to determine whether type symbols need to be processed.
88    // Only TS/ETS files have type symbols.
89    if (resolver) {
90      // eliminate the type symbol
91      // eg: import { type t, x } from '...' --> import { x } from '...'
92      const newNameBindings: ts.ImportSpecifier[] = eliminateTypeSymbol(namedBindings, resolver);
93      newImportClause = ts.factory.createImportClause(false, importClause.name,
94        ts.factory.createNamedImports(newNameBindings));
95    } else {
96      newImportClause = importClause;
97    }
98  } else if (!namedBindings && importClause.name) {
99    // case3: import y from '...' --> import lazy y from '...'
100    newImportClause = importClause;
101  }
102  // @ts-ignore
103  newImportClause.isLazy = true;
104  const modifiers: readonly ts.Modifier[] | undefined = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
105  return ts.factory.updateImportDeclaration(node, modifiers, newImportClause, node.moduleSpecifier, node.assertClause);
106}
107
108function eliminateTypeSymbol(namedBindings: ts.NamedImportBindings, resolver: Object): ts.ImportSpecifier[] {
109  const newNameBindings: ts.ImportSpecifier[] = [];
110  namedBindings.elements.forEach(item => {
111    const element = item as ts.ImportSpecifier;
112    if (!element.isTypeOnly && resolver.isReferencedAliasDeclaration(element)) {
113      // import { x } from './y' --> propertyName is undefined
114      // import { x as a } from './y' --> propertyName is x
115      newNameBindings.push(
116        ts.factory.createImportSpecifier(
117          false,
118          element.propertyName ? ts.factory.createIdentifier(element.propertyName.text) : undefined,
119          ts.factory.createIdentifier(element.name.text)
120        )
121      );
122    }
123  });
124  return newNameBindings;
125}
126
127export function resetReExportCheckLog(): void {
128  reExportCheckLog.cleanUp();
129}
130
131export function lazyImportReExportCheck(node: ts.SourceFile, reExportCheckMode: string): void {
132  if (reExportCheckMode === reExportNoCheckMode) {
133    return;
134  }
135  reExportCheckLog.sourceFile = node;
136  const lazyImportSymbols: Set<string> = new Set();
137  const exportSymbols: Map<string, ts.Statement[]> = new Map();
138  const result: Map<string, ts.Statement[]> = new Map();
139  node.statements.forEach(stmt => {
140    collectLazyImportSymbols(stmt, lazyImportSymbols, exportSymbols, result);
141    collectLazyReExportSymbols(stmt, lazyImportSymbols, exportSymbols, result);
142  });
143  for (const [key, statements] of result.entries()) {
144    for (const statement of statements) {
145      collectReExportErrors(statement, key, reExportCheckMode);
146    }
147  }
148}
149
150function collectLazyImportSymbols(stmt: ts.Statement, lazyImportSymbols: Set<string>,
151  exportSymbols: Map<string, ts.Statement[]>, result: Map<string, ts.Statement[]>): void {
152  if (ts.isImportDeclaration(stmt) && stmt.importClause && stmt.importClause.isLazy) {
153    // For import lazy x from './y', collect 'x'
154    const importClauseName = stmt.importClause.name;
155    if (importClauseName) {
156      lazyImportSymbols.add(importClauseName.text);
157      result.set(importClauseName.text, exportSymbols.get(importClauseName.text) ?? []);
158    }
159    // For import lazy { x } from './y', collect 'x'
160    const importNamedBindings: ts.NamedImportBindings = stmt.importClause.namedBindings;
161    if (importNamedBindings && ts.isNamedImports(importNamedBindings) && importNamedBindings.elements.length !== 0) {
162      importNamedBindings.elements.forEach((element: ts.ImportSpecifier) => {
163        const nameText = element.name.text;
164        lazyImportSymbols.add(nameText);
165        result.set(nameText, exportSymbols.get(nameText) ?? []);
166      });
167    }
168  }
169}
170
171function collectLazyReExportSymbols(stmt: ts.Statement, lazyImportSymbols: Set<string>,
172  exportSymbols: Map<string, ts.Statement[]>, result: Map<string, ts.Statement[]>): void {
173  // export default x
174  if (ts.isExportAssignment(stmt) && ts.isIdentifier(stmt.expression)) {
175    const nameText: string = stmt.expression.text;
176    const targetMap = lazyImportSymbols.has(nameText) ? result : exportSymbols;
177    if (!targetMap.get(nameText)) {
178      targetMap.set(nameText, []);
179    }
180    targetMap.get(nameText).push(stmt);
181  }
182  // export { x }
183  if (ts.isExportDeclaration(stmt) && !stmt.moduleSpecifier &&
184    ts.isNamedExports(stmt.exportClause) && stmt.exportClause.elements.length !== 0) {
185    stmt.exportClause.elements.forEach((element: ts.ExportSpecifier) => {
186      // For example, in 'export { foo as bar }', exportName is 'bar', localName is 'foo'
187      const exportName: string = element.name.text;
188      const localName: string = element.propertyName ? element.propertyName.text : exportName;
189      const targetMap = lazyImportSymbols.has(localName) ? result : exportSymbols;
190      if (!targetMap.get(localName)) {
191        targetMap.set(localName, []);
192      }
193      targetMap.get(localName).push(stmt);
194    });
195  }
196}
197
198function collectReExportErrors(node: ts.Node, elementText: string, reExportCheckMode: string): void {
199  let pos: number;
200  try {
201    pos = node.getStart();
202  } catch {
203    pos = 0;
204  }
205  let type: LogType = LogType.WARN;
206  if (reExportCheckMode === reExportStrictMode) {
207    type = LogType.ERROR;
208  }
209  // reExportCheckMode explanation:
210  // - 'noCheck': NoCheck mode. The functionality to block re-exported lazy-import is disabled.
211  // - 'strict': Strict mode. It intercepts errors and treats them as critical (LogType.ERROR).
212  // - 'compatible': Compatible mode. It logs warnings (LogType.WARN) but does not intercept or block them.
213  const errInfo: LogData = LogDataFactory.newInstance(
214    ErrorCode.ETS2BUNDLE_EXTERNAL_LAZY_IMPORT_RE_EXPORT_ERROR,
215    ArkTSErrorDescription,
216    `'${elementText}' of lazy-import is re-export`,
217    '',
218    ['Please make sure the namedBindings of lazy-import are not be re-exported.',
219      'Please check whether the autoLazyImport switch is opened.']
220  );
221  reExportCheckLog.errors.push({
222    type: type,
223    message: errInfo.toString(),
224    pos: pos
225  });
226}