1/* 2 * Copyright (c) 2023 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 ts from 'typescript'; 17import fs from 'fs'; 18import path from 'path'; 19import { SourceMapGenerator } from 'source-map'; 20 21import { 22 validateUISyntax, 23 componentCollection, 24 ReplaceResult, 25 sourceReplace 26} from './validate_ui_syntax'; 27import { 28 LogType, 29 LogInfo, 30 mkDir, 31 emitLogInfo 32} from './utils'; 33import { 34 MODULE_ETS_PATH, 35 MODULE_VISUAL_PATH, 36 SUPERVISUAL, 37 SUPERVISUAL_SOURCEMAP_EXT 38} from './pre_define'; 39 40import { projectConfig } from '../main.js'; 41import { genETS } from '../codegen/codegen_ets.js'; 42import { concatenateEtsOptions, getExternalComponentPaths } from './external_component_map'; 43 44const visualMap: Map<number, number> = new Map(); 45const slotMap: Map<number, number> = new Map(); 46 47const red: string = '\u001b[31m'; 48const reset: string = '\u001b[39m'; 49 50let compilerOptions = ts.readConfigFile( 51 path.resolve(__dirname, '../tsconfig.json'), ts.sys.readFile).config.compilerOptions; 52const componentPaths: string[] | undefined = getExternalComponentPaths(); 53if (componentPaths) { 54 for (const componentPath of componentPaths) { 55 if (!fs.existsSync(componentPath)) { 56 continue; 57 } 58 const externalCompilerOptions: ts.CompilerOptions = ts.readConfigFile( 59 path.resolve(componentPath, 'externalconfig.json'), ts.sys.readFile 60 ).config.compilerOptions; 61 concatenateEtsOptions(compilerOptions, externalCompilerOptions); 62 } 63} 64compilerOptions.sourceMap = false; 65 66export function visualTransform(code: string, id: string, logger: any) { 67 const log: LogInfo[] = []; 68 const content: string | null = getParsedContent(code, path.normalize(id), log); 69 if (!content) { 70 return code; 71 } 72 if (log.length) { 73 emitLogInfo(logger, log, true, id); 74 } 75 generateSourceMapForNewAndOriEtsFile(path.normalize(id), code); 76 return content; 77} 78 79export function parseVisual(resourcePath: string, resourceQuery: string, content: string, 80 log: LogInfo[], source: string): string { 81 let code: string | null = getParsedContent(content, resourcePath, log); 82 if (!code) { 83 return content; 84 } 85 const result: ReplaceResult = sourceReplace(code, resourcePath); 86 code = result.content; 87 log.concat(result.log); 88 const resultLog: LogInfo[] = validateUISyntax(source, code, resourcePath, resourceQuery); 89 log.concat(resultLog); 90 if (!log.length) { 91 generateSourceMapForNewAndOriEtsFile(resourcePath, source); 92 } 93 return code; 94} 95 96function parseStatement(statement: ts.Statement, content: string, log: LogInfo[], 97 visualContent: any): string { 98 if (statement.kind === ts.SyntaxKind.StructDeclaration && statement.name) { 99 if (statement.members) { 100 statement.members.forEach(member => { 101 if (member.kind && member.kind === ts.SyntaxKind.MethodDeclaration) { 102 content = parseMember(statement, member, content, log, visualContent); 103 } 104 }); 105 } 106 } 107 return content; 108} 109 110function parseMember(statement: ts.Statement, member: ts.MethodDeclaration, content: string, 111 log: LogInfo[], visualContent: any): string { 112 let newContent: string = content; 113 if (member.name && member.name.getText() === 'build') { 114 const buildBody: string = member.getText(); 115 if (buildBody.replace(/\ +/g, '').replace(/[\r\n]/g, '') === 'build(){}') { 116 newContent = insertVisualCode(statement, member, visualContent, newContent); 117 } else { 118 log.push({ 119 type: LogType.ERROR, 120 message: `when the corresponding visual file exists,` + 121 ` the build function of the entry component must be empty.`, 122 pos: member.pos 123 }); 124 } 125 } 126 return newContent; 127} 128 129function insertVisualCode(statement: ts.Statement, member: ts.MethodDeclaration, 130 visualContent: any, content: string): string { 131 let newContent: string = content; 132 newContent = insertImport(visualContent, newContent); 133 newContent = insertVarAndFunc(member, visualContent, newContent, content); 134 newContent = insertBuild(member, visualContent, newContent, content); 135 newContent = insertAboutToAppear(statement, member, visualContent, newContent, content); 136 return newContent; 137} 138 139function insertImport(visualContent: any, content: string): string { 140 if (!visualContent.etsImport) { 141 return content; 142 } 143 const mediaQueryImport: string = visualContent.etsImport + '\n'; 144 const newContent: string = mediaQueryImport + content; 145 slotMap.set(0, mediaQueryImport.length); 146 visualMap.set(0, mediaQueryImport.split('\n').length - 1); 147 return newContent; 148} 149 150function insertVarAndFunc(build: ts.MethodDeclaration, visualContent: any, 151 content: string, oriContent: string): string { 152 const visualVarAndFunc: string = (visualContent.etsVariable ? visualContent.etsVariable : '') + 153 (visualContent.etsFunction ? visualContent.etsFunction : ''); 154 return visualVarAndFunc ? insertVisualCodeBeforePos(build, '\n' + visualVarAndFunc, content, 155 oriContent) : content; 156} 157 158function insertBuild(build: ts.MethodDeclaration, visualContent: any, content: string, 159 oriContent: string): string { 160 return visualContent.build ? insertVisualCodeAfterPos(build.body, 161 '\n' + visualContent.build + '\n', content, oriContent) : content; 162} 163 164function insertAboutToAppear(statement: ts.Statement, build: ts.MethodDeclaration, 165 visualContent: any, content: string, oriContent: string): string { 166 if (!visualContent.aboutToAppear) { 167 return content; 168 } 169 for (const member of statement.members) { 170 const hasAboutToAppear: boolean = member.kind && member.kind === ts.SyntaxKind.MethodDeclaration && 171 member.name && member.name.getText() === 'aboutToAppear'; 172 if (hasAboutToAppear) { 173 return insertVisualCodeAfterPos(member.body, '\n' + visualContent.aboutToAppear, content, 174 oriContent); 175 } 176 } 177 178 const aboutToAppearFunc: string = '\n aboutToAppear() {\n' + visualContent.aboutToAppear + 179 ' }\n'; 180 return insertVisualCodeBeforePos(build, aboutToAppearFunc, content, oriContent); 181} 182 183function insertVisualCodeAfterPos(member: ts.Block, visualContent: string, content: string, 184 oriContent: string): string { 185 const contentBeforePos: string = oriContent.substring(0, member.getStart() + 1); 186 const originEtsFileLineNumber: number = contentBeforePos.split('\n').length; 187 const visualLines: number = visualContent.split('\n').length - 1; 188 const insertedLineNumbers: number = visualMap.get(originEtsFileLineNumber); 189 visualMap.set(originEtsFileLineNumber, insertedLineNumbers ? insertedLineNumbers + visualLines : 190 visualLines); 191 192 let newPos: number = member.getStart() + 1; 193 for (const [key, value] of slotMap) { 194 if (member.getStart() >= key) { 195 newPos += value; 196 } 197 } 198 199 const newContent: string = content.substring(0, newPos) + visualContent + 200 content.substring(newPos); 201 slotMap.set(member.getStart(), visualContent.length); 202 return newContent; 203} 204 205function insertVisualCodeBeforePos(member: ts.MethodDeclaration, visualContent: string, 206 content: string, oriContent: string): string { 207 const contentBeforePos: string = oriContent.substring(0, member.pos); 208 const originEtsFileLineNumber: number = contentBeforePos.split('\n').length; 209 const visualLines: number = visualContent.split('\n').length - 1; 210 const insertedLineNumbers: number = visualMap.get(originEtsFileLineNumber); 211 visualMap.set(originEtsFileLineNumber, insertedLineNumbers ? insertedLineNumbers + visualLines : 212 visualLines); 213 let newPos: number = member.pos; 214 for (const [key, value] of slotMap) { 215 if (member.pos >= key) { 216 newPos += value; 217 } 218 } 219 const newContent: string = content.substring(0, newPos) + visualContent + 220 content.substring(newPos); 221 slotMap.set(member.pos, visualContent.length); 222 return newContent; 223} 224 225function generateSourceMapForNewAndOriEtsFile(resourcePath: string, content: string) { 226 if (!process.env.cachePath) { 227 return; 228 } 229 const sourcemap: SourceMapGenerator = new SourceMapGenerator({ 230 file: resourcePath 231 }); 232 const lines: Array<string> = content.split('\n'); 233 const originEtsFileLines: number = lines.length; 234 for (let l: number = 1; l <= originEtsFileLines; l++) { 235 let newEtsFileLineNumber: number = l; 236 for (const [originEtsFileLineNumber, visualLines] of visualMap) { 237 if (l > originEtsFileLineNumber) { 238 newEtsFileLineNumber += visualLines; 239 } 240 } 241 sourcemap.addMapping({ 242 generated: { 243 line: newEtsFileLineNumber, 244 column: 0 245 }, 246 source: resourcePath, 247 original: { 248 line: l, 249 column: 0 250 } 251 }); 252 } 253 const visualMapName: string = path.parse(resourcePath).name + SUPERVISUAL_SOURCEMAP_EXT; 254 const visualDirPath: string = path.parse(resourcePath).dir; 255 const etsDirPath: string = path.parse(projectConfig.projectPath).dir; 256 let visualMapDirPath: string = path.resolve(process.env.cachePath, SUPERVISUAL + 257 visualDirPath.replace(etsDirPath, '')); 258 if (!visualDirPath.includes(etsDirPath)) { 259 const projectRootPath = getProjectRootPath(); 260 visualMapDirPath = path.resolve(process.env.cachePath, SUPERVISUAL + 261 visualDirPath.replace(projectRootPath, '')); 262 } 263 if (!(fs.existsSync(visualMapDirPath) && fs.statSync(visualMapDirPath).isDirectory())) { 264 mkDir(visualMapDirPath); 265 } 266 fs.writeFile(path.resolve(visualMapDirPath, visualMapName), sourcemap.toString(), (err) => { 267 if (err) { 268 console.error(red, 'ERROR: Failed to write visual.js.map', reset); 269 } 270 }); 271} 272 273function getProjectRootPath(): string { 274 let projectRootPath = projectConfig.projectRootPath; 275 if (!projectRootPath) { 276 if (!projectConfig.aceModuleJsonPath) { 277 projectRootPath = path.resolve(projectConfig.projectPath, '../../../../../'); 278 } else { 279 projectRootPath = path.resolve(projectConfig.projectPath, '../../../../'); 280 } 281 } 282 return projectRootPath; 283} 284 285export function findVisualFile(filePath: string): string { 286 if (!/\.ets$/.test(filePath)) { 287 return ''; 288 } 289 let etsDirPath: string = path.parse(projectConfig.projectPath).dir; 290 let visualDirPath: string = path.parse(projectConfig.aceSuperVisualPath).dir; 291 let resolvePath = filePath.replace(projectConfig.projectPath, projectConfig.aceSuperVisualPath) 292 .replace(etsDirPath, visualDirPath).replace(/\.ets$/, '.visual'); 293 if (fs.existsSync(resolvePath)) { 294 return resolvePath; 295 } 296 try { 297 const projectRootPath = getProjectRootPath(); 298 let moduleName = ''; 299 const relativePath = filePath.replace(projectRootPath, ''); 300 const moduleNames = relativePath.split(path.sep); 301 for (let i = 0; i < moduleNames.length; ++i) { 302 if (moduleNames[i] === 'src') { 303 if (i >= moduleNames.length - 2) { 304 break; 305 } 306 const modulePath = path.join(moduleNames[i], moduleNames[i + 1], moduleNames[i + 2]); 307 if (modulePath === MODULE_ETS_PATH) { 308 break; 309 } 310 } 311 moduleName = path.join(moduleName, moduleNames[i]); 312 } 313 etsDirPath = path.join(projectRootPath, moduleName, MODULE_ETS_PATH); 314 visualDirPath = path.join(projectRootPath, moduleName, MODULE_VISUAL_PATH); 315 resolvePath = filePath.replace(etsDirPath, visualDirPath).replace(/\.ets$/, '.visual'); 316 return resolvePath; 317 } catch (e) { 318 // avoid projectConfig attributes has undefined value 319 return ''; 320 } 321} 322 323function getVisualContent(visualPath: string, log: LogInfo[]): any { 324 const parseContent: any = genETS(fs.readFileSync(visualPath, 'utf-8')); 325 if (parseContent && parseContent.errorType && parseContent.errorType !== '') { 326 log.push({ 327 type: LogType.ERROR, 328 message: parseContent.errorMessage 329 }); 330 } 331 return parseContent ? parseContent.ets : null; 332} 333 334function getParsedContent(code: string, id: string, log: LogInfo[]): string | null { 335 if (!projectConfig.aceSuperVisualPath || 336 !(componentCollection.entryComponent || componentCollection.customComponents)) { 337 return null; 338 } 339 const visualPath: string = findVisualFile(id); 340 if (!visualPath || !fs.existsSync(visualPath)) { 341 return null; 342 } 343 const visualContent: any = getVisualContent(visualPath, log); 344 if (!visualContent) { 345 return null; 346 } 347 clearVisualSlotMap(); 348 const sourceFile: ts.SourceFile = ts.createSourceFile( 349 id, 350 code, 351 ts.ScriptTarget.Latest, 352 true, 353 ts.ScriptKind.ETS, 354 compilerOptions 355 ); 356 let content: string = code; 357 if (sourceFile.statements) { 358 sourceFile.statements.forEach(statement => { 359 content = parseStatement(statement, content, log, visualContent); 360 }); 361 } 362 return content; 363} 364 365function clearVisualSlotMap(): void { 366 visualMap.clear(); 367 slotMap.clear(); 368}