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}