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}