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