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