* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as ts from 'typescript';
import {
IFileLog,
LogType
} from './utils';
import {
LogData,
LogDataFactory
} from './fast_build/ark_compiler/logger';
import {
ArkTSErrorDescription,
ErrorCode
} from './fast_build/ark_compiler/error_code';
import creatAstNodeUtils from './create_ast_node_utils';
import { CompileEvent, createAndStartEvent, stopEvent } from './performance';
export const reExportCheckLog: IFileLog = new creatAstNodeUtils.FileLog();
export const reExportNoCheckMode: string = 'noCheck';
const reExportStrictMode: string = 'strict';
export interface LazyImportOptions {
autoLazyImport: boolean;
reExportCheckMode: string;
autoLazyFilter: Object;
}
export function processJsCodeLazyImport(id: string, code: string,
autoLazyImport: boolean, reExportCheckMode: string, metaInfo: Object, autoLazyFilter: Object, parentEvent?: CompileEvent): string {
const eventProcessJsCodeLazyImport: CompileEvent = createAndStartEvent(parentEvent, 'process Js code lazy import');
let sourceNode: ts.SourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.ES2021, true, ts.ScriptKind.JS);
if (autoLazyImport) {
sourceNode = transformLazyImport(metaInfo, sourceNode, autoLazyFilter, undefined, eventProcessJsCodeLazyImport);
}
lazyImportReExportCheck(sourceNode, reExportCheckMode, eventProcessJsCodeLazyImport);
code = autoLazyImport ? ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }).printFile(sourceNode) : code;
stopEvent(eventProcessJsCodeLazyImport);
return code;
}
export function transformLazyImport(metaInfo: Object, sourceNode: ts.SourceFile,
autoLazyFilter: Object, resolver?: Object, parentEvent?: CompileEvent): ts.SourceFile {
const eventTransformLazyImport: CompileEvent = createAndStartEvent(parentEvent, 'transform lazy import');
if (isNotAutoLazyImport(metaInfo, autoLazyFilter)) {
return sourceNode;
}
const moduleNodeTransformer: ts.TransformerFactory<ts.SourceFile> = context => {
const visitor: ts.Visitor = node => {
if (ts.isImportDeclaration(node)) {
return updateImportDecl(node, resolver);
}
return node;
};
return node => ts.visitEachChild(node, visitor, context);
};
const result: ts.SourceFile =
ts.transform(sourceNode, [moduleNodeTransformer]).transformed[0];
stopEvent(eventTransformLazyImport);
return result;
}
function isNotAutoLazyImport(metaInfo: Object, autoLazyFilter: Object): boolean {
if (!autoLazyFilter || Object.keys(autoLazyFilter).length === 0) {
return false;
}
const pkgName: string = metaInfo?.pkgName;
const includeList: string[] = autoLazyFilter?.include;
const excludeList: string[] = autoLazyFilter?.exclude;
if (pkgName && includeList && includeList.length > 0) {
return !includeList.includes(pkgName);
}
if (pkgName && excludeList && excludeList.length > 0) {
return excludeList.includes(pkgName);
}
return false;
}
function updateImportDecl(node: ts.ImportDeclaration, resolver: Object): ts.ImportDeclaration | ts.ImportDeclaration[] {
const importClause: ts.ImportClause | undefined = node.importClause;
const moduleRequest: string = (node.moduleSpecifier! as ts.StringLiteral).text.replace(/'|"/g, '');
if (!importClause || importClause.isTypeOnly || importClause.isLazy) {
return node;
}
if (importClause.namedBindings && ts.isNamespaceImport(importClause.namedBindings)) {
return node;
}
if (moduleRequest.endsWith('.json')) {
return node;
}
const modifiers: readonly ts.Modifier[] | undefined =
ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
const namedBindings: ts.NamedImportBindings = importClause.namedBindings;
let newImportClause: ts.ImportClause;
if (namedBindings && ts.isNamedImports(namedBindings)) {
if (resolver) {
const { valueBindings, typeBindings } = splitImportBindings(namedBindings, resolver);
const typeImportDeclaration: ts.ImportDeclaration | undefined = typeBindings.length > 0 ?
ts.factory.updateImportDeclaration(
node,
node.modifiers,
ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports(typeBindings)),
node.moduleSpecifier,
node.assertClause
) : undefined;
if (valueBindings.length === 0 && !importClause.name) {
return typeImportDeclaration ?? node;
}
const valueImportClause: ts.ImportClause = ts.factory.updateImportClause(
importClause,
false,
importClause.name,
valueBindings.length > 0 ? ts.factory.updateNamedImports(namedBindings, valueBindings) : undefined
);
valueImportClause.isLazy = true;
const lazyImportDeclaration: ts.ImportDeclaration = ts.factory.updateImportDeclaration(
node,
modifiers,
valueImportClause,
node.moduleSpecifier,
node.assertClause
);
return typeImportDeclaration ? [lazyImportDeclaration, typeImportDeclaration] : lazyImportDeclaration;
} else {
newImportClause = importClause;
}
} else if (!namedBindings && importClause.name) {
newImportClause = importClause;
}
newImportClause.isLazy = true;
return ts.factory.updateImportDeclaration(node, modifiers, newImportClause, node.moduleSpecifier, node.assertClause);
}
function splitImportBindings(namedBindings: ts.NamedImportBindings, resolver: Object): {
valueBindings: ts.ImportSpecifier[];
typeBindings: ts.ImportSpecifier[];
} {
const valueBindings: ts.ImportSpecifier[] = [];
const typeBindings: ts.ImportSpecifier[] = [];
namedBindings.elements.forEach(item => {
const element = item as ts.ImportSpecifier;
let targetBindings: ts.ImportSpecifier[];
if (element.isTypeOnly) {
targetBindings = typeBindings;
} else if (resolver.isReferencedAliasDeclaration(element)) {
targetBindings = valueBindings;
} else {
return;
}
targetBindings.push(ts.factory.updateImportSpecifier(
element,
targetBindings === typeBindings,
element.propertyName,
element.name
));
});
return { valueBindings, typeBindings };
}
export function resetReExportCheckLog(): void {
reExportCheckLog.cleanUp();
}
export function lazyImportReExportCheck(node: ts.SourceFile, reExportCheckMode: string, parentEvent?: CompileEvent): void {
if (reExportCheckMode === reExportNoCheckMode) {
return;
}
const eventLazyImportReExportCheck: CompileEvent = createAndStartEvent(parentEvent, 'lazy import re export check');
reExportCheckLog.sourceFile = node;
const lazyImportSymbols: Set<string> = new Set();
const exportSymbols: Map<string, ts.Statement[]> = new Map();
const result: Map<string, ts.Statement[]> = new Map();
node.statements.forEach(stmt => {
collectLazyImportSymbols(stmt, lazyImportSymbols, exportSymbols, result);
collectLazyReExportSymbols(stmt, lazyImportSymbols, exportSymbols, result);
});
for (const [key, statements] of result.entries()) {
for (const statement of statements) {
collectReExportErrors(statement, key, reExportCheckMode);
}
}
stopEvent(eventLazyImportReExportCheck);
}
function collectLazyImportSymbols(stmt: ts.Statement, lazyImportSymbols: Set<string>,
exportSymbols: Map<string, ts.Statement[]>, result: Map<string, ts.Statement[]>): void {
if (ts.isImportDeclaration(stmt) && stmt.importClause && stmt.importClause.isLazy) {
const importClauseName = stmt.importClause.name;
if (importClauseName) {
lazyImportSymbols.add(importClauseName.text);
result.set(importClauseName.text, exportSymbols.get(importClauseName.text) ?? []);
}
const importNamedBindings: ts.NamedImportBindings = stmt.importClause.namedBindings;
if (importNamedBindings && ts.isNamedImports(importNamedBindings) && importNamedBindings.elements.length !== 0) {
importNamedBindings.elements.forEach((element: ts.ImportSpecifier) => {
const nameText = element.name.text;
lazyImportSymbols.add(nameText);
result.set(nameText, exportSymbols.get(nameText) ?? []);
});
}
}
}
function collectLazyReExportSymbols(stmt: ts.Statement, lazyImportSymbols: Set<string>,
exportSymbols: Map<string, ts.Statement[]>, result: Map<string, ts.Statement[]>): void {
if (ts.isExportAssignment(stmt) && ts.isIdentifier(stmt.expression)) {
const nameText: string = stmt.expression.text;
const targetMap = lazyImportSymbols.has(nameText) ? result : exportSymbols;
if (!targetMap.get(nameText)) {
targetMap.set(nameText, []);
}
targetMap.get(nameText).push(stmt);
}
if (ts.isExportDeclaration(stmt) && !stmt.moduleSpecifier &&
ts.isNamedExports(stmt.exportClause) && stmt.exportClause.elements.length !== 0) {
stmt.exportClause.elements.forEach((element: ts.ExportSpecifier) => {
const exportName: string = element.name.text;
const localName: string = element.propertyName ? element.propertyName.text : exportName;
const targetMap = lazyImportSymbols.has(localName) ? result : exportSymbols;
if (!targetMap.get(localName)) {
targetMap.set(localName, []);
}
targetMap.get(localName).push(stmt);
});
}
}
function collectReExportErrors(node: ts.Node, elementText: string, reExportCheckMode: string): void {
let pos: number;
try {
pos = node.getStart();
} catch {
pos = 0;
}
let type: LogType = LogType.WARN;
if (reExportCheckMode === reExportStrictMode) {
type = LogType.ERROR;
}
const errInfo: LogData = LogDataFactory.newInstance(
ErrorCode.ETS2BUNDLE_EXTERNAL_LAZY_IMPORT_RE_EXPORT_ERROR,
ArkTSErrorDescription,
`'${elementText}' of lazy-import is re-export`,
'',
['Please make sure the namedBindings of lazy-import are not be re-exported.',
'Please check whether the autoLazyImport switch is opened.']
);
reExportCheckLog.errors.push({
type: type,
message: errInfo.toString(),
pos: pos
});
}