1/* 2 * Copyright (c) 2021 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 ReplaceResult, 23 sourceReplace, 24 validateUISyntax, 25 processSystemApi, 26 componentCollection 27} from './validate_ui_syntax'; 28import { 29 LogType, 30 LogInfo, 31 emitLogInfo, 32 mkDir 33} from './utils'; 34import { 35 BUILD_ON, 36 SUPERVISUAL, 37 SUPERVISUAL_SOURCEMAP_EXT 38} from './pre_define'; 39import { projectConfig } from '../main.js'; 40import { genETS } from '../codegen/codegen_ets.js'; 41 42const visualMap: Map<number, number> = new Map(); 43const slotMap: Map<number, number> = new Map(); 44 45const red: string = '\u001b[31m'; 46const reset: string = '\u001b[39m'; 47 48function preProcess(source: string): string { 49 process.env.compiler = BUILD_ON; 50 if (/\.ets$/.test(this.resourcePath)) { 51 const result: ReplaceResult = sourceReplace(source, this.resourcePath); 52 let newContent: string = result.content; 53 const log: LogInfo[] = result.log.concat(validateUISyntax(source, newContent, 54 this.resourcePath, this.resourceQuery)); 55 newContent = parseVisual(this.resourcePath, this.resourceQuery, newContent, log, source); 56 if (log.length) { 57 emitLogInfo(this, log); 58 } 59 return newContent; 60 } else { 61 return processSystemApi(source, false, this.resourcePath); 62 } 63} 64 65function parseVisual(resourcePath: string, resourceQuery: string, content: string, 66 log: LogInfo[], source: string): string { 67 if (!(componentCollection.entryComponent || componentCollection.customComponents) || !projectConfig.aceSuperVisualPath) { 68 return content; 69 } 70 const visualPath: string = findVisualFile(resourcePath); 71 if (!visualPath || !fs.existsSync(visualPath)) { 72 return content; 73 } 74 const visualContent: any = getVisualContent(visualPath, log); 75 if (!visualContent) { 76 return content; 77 } 78 visualMap.clear(); 79 slotMap.clear(); 80 const compilerOptions = ts.readConfigFile( 81 path.resolve(__dirname, '../tsconfig.json'), ts.sys.readFile).config.compilerOptions; 82 Object.assign(compilerOptions, { 83 'sourceMap': false 84 }); 85 const sourceFile: ts.SourceFile = ts.createSourceFile(resourcePath, content, 86 ts.ScriptTarget.Latest, true, ts.ScriptKind.ETS, compilerOptions); 87 let newContent: string = content; 88 if (sourceFile.statements) { 89 sourceFile.statements.forEach(statement => { 90 newContent = parseStatement(statement, newContent, log, visualContent); 91 }); 92 } 93 const result: ReplaceResult = sourceReplace(newContent, resourcePath); 94 newContent = result.content; 95 const resultLog: LogInfo[] = result.log.concat(validateUISyntax(source, newContent, 96 resourcePath, resourceQuery)); 97 log.concat(resultLog); 98 if (!log.length) { 99 generateSourceMapForNewAndOriEtsFile(resourcePath, source); 100 } 101 return newContent; 102} 103 104function parseStatement(statement: ts.Statement, content: string, log: LogInfo[], 105 visualContent: any): string { 106 if (statement.kind === ts.SyntaxKind.StructDeclaration && statement.name) { 107 if (statement.members) { 108 statement.members.forEach(member => { 109 if (member.kind && member.kind === ts.SyntaxKind.MethodDeclaration) { 110 content = parseMember(statement, member, content, log, visualContent); 111 } 112 }); 113 } 114 } 115 return content; 116} 117 118function parseMember(statement: ts.Statement, member: ts.MethodDeclaration, content: string, 119 log: LogInfo[], visualContent: any): string { 120 let newContent: string = content; 121 if (member.name && member.name.getText() === 'build') { 122 const buildBody: string = member.getText(); 123 if (buildBody.replace(/\ +/g, '').replace(/[\r\n]/g, '') === 'build(){}') { 124 newContent = insertVisualCode(statement, member, visualContent, newContent); 125 } else { 126 log.push({ 127 type: LogType.ERROR, 128 message: `when the corresponding visual file exists,` + 129 ` the build function of the entry component must be empty.`, 130 pos: member.pos 131 }); 132 } 133 } 134 return newContent; 135} 136 137function insertVisualCode(statement: ts.Statement, member: ts.MethodDeclaration, 138 visualContent: any, content: string): string { 139 let newContent: string = content; 140 newContent = insertImport(visualContent, newContent); 141 newContent = insertVarAndFunc(member, visualContent, newContent, content); 142 newContent = insertBuild(member, visualContent, newContent, content); 143 newContent = insertAboutToAppear(statement, member, visualContent, newContent, content); 144 return newContent; 145} 146 147function insertImport(visualContent: any, content: string): string { 148 if (!visualContent.etsImport) { 149 return content; 150 } 151 const mediaQueryImport: string = visualContent.etsImport + '\n'; 152 const newContent: string = mediaQueryImport + content; 153 slotMap.set(0, mediaQueryImport.length); 154 visualMap.set(0, mediaQueryImport.split('\n').length - 1); 155 return newContent; 156} 157 158function insertVarAndFunc(build: ts.MethodDeclaration, visualContent: any, 159 content: string, oriContent: string): string { 160 const visualVarAndFunc: string = (visualContent.etsVariable ? visualContent.etsVariable : '') + 161 (visualContent.etsFunction ? visualContent.etsFunction : ''); 162 return visualVarAndFunc ? insertVisualCodeBeforePos(build, '\n' + visualVarAndFunc, content, 163 oriContent) : content; 164} 165 166function insertBuild(build: ts.MethodDeclaration, visualContent: any, content: string, 167 oriContent: string): string { 168 return visualContent.build ? insertVisualCodeAfterPos(build.body, 169 '\n' + visualContent.build + '\n', content, oriContent) : content; 170} 171 172function insertAboutToAppear(statement: ts.Statement, build: ts.MethodDeclaration, 173 visualContent: any, content: string, oriContent: string): string { 174 if (!visualContent.aboutToAppear) { 175 return content; 176 } 177 for (const member of statement.members) { 178 const hasAboutToAppear: boolean = member.kind && member.kind === ts.SyntaxKind.MethodDeclaration 179 && member.name && member.name.getText() === 'aboutToAppear'; 180 if (hasAboutToAppear) { 181 return insertVisualCodeAfterPos(member.body, '\n' + visualContent.aboutToAppear, content, 182 oriContent); 183 } 184 } 185 186 const aboutToAppearFunc: string = '\n aboutToAppear() {\n' + visualContent.aboutToAppear + 187 ' }\n'; 188 return insertVisualCodeBeforePos(build, aboutToAppearFunc, content, oriContent); 189} 190 191function insertVisualCodeAfterPos(member: ts.Block, visualContent: string, content: string, 192 oriContent: string): string { 193 const contentBeforePos: string = oriContent.substring(0, member.getStart() + 1); 194 const originEtsFileLineNumber: number = contentBeforePos.split('\n').length; 195 const visualLines: number = visualContent.split('\n').length - 1; 196 const insertedLineNumbers: number = visualMap.get(originEtsFileLineNumber); 197 visualMap.set(originEtsFileLineNumber, insertedLineNumbers ? insertedLineNumbers + visualLines : 198 visualLines); 199 200 let newPos: number = member.getStart() + 1; 201 for (const [key, value] of slotMap) { 202 if (member.getStart() >= key) { 203 newPos += value; 204 } 205 } 206 207 const newContent: string = content.substring(0, newPos) + visualContent + 208 content.substring(newPos); 209 slotMap.set(member.getStart(), visualContent.length); 210 return newContent; 211} 212 213function insertVisualCodeBeforePos(member: ts.MethodDeclaration, visualContent: string, 214 content: string, oriContent: string): string { 215 const contentBeforePos: string = oriContent.substring(0, member.pos); 216 const originEtsFileLineNumber: number = contentBeforePos.split('\n').length; 217 const visualLines: number = visualContent.split('\n').length - 1; 218 const insertedLineNumbers: number = visualMap.get(originEtsFileLineNumber); 219 visualMap.set(originEtsFileLineNumber, insertedLineNumbers ? insertedLineNumbers + visualLines : 220 visualLines); 221 let newPos: number = member.pos; 222 for (const [key, value] of slotMap) { 223 if (member.pos >= key) { 224 newPos += value; 225 } 226 } 227 const newContent: string = content.substring(0, newPos) + visualContent + 228 content.substring(newPos); 229 slotMap.set(member.pos, visualContent.length); 230 return newContent; 231} 232 233function generateSourceMapForNewAndOriEtsFile(resourcePath: string, content: string) { 234 if (!process.env.cachePath) { 235 return; 236 } 237 const sourcemap: SourceMapGenerator = new SourceMapGenerator({ 238 file: resourcePath 239 }); 240 const lines: Array<string> = content.split('\n'); 241 const originEtsFileLines: number = lines.length; 242 for (let l: number = 1; l <= originEtsFileLines; l++) { 243 let newEtsFileLineNumber: number = l; 244 for (const [originEtsFileLineNumber, visualLines] of visualMap) { 245 if (l > originEtsFileLineNumber) { 246 newEtsFileLineNumber += visualLines; 247 } 248 } 249 sourcemap.addMapping({ 250 generated: { 251 line: newEtsFileLineNumber, 252 column: 0 253 }, 254 source: resourcePath, 255 original: { 256 line: l, 257 column: 0 258 } 259 }); 260 } 261 const visualMapName: string = path.parse(resourcePath).name + SUPERVISUAL_SOURCEMAP_EXT; 262 const visualDirPath: string = path.parse(resourcePath).dir; 263 const etsDirPath: string = path.parse(projectConfig.projectPath).dir; 264 const visualMapDirPath: string = path.resolve(process.env.cachePath, SUPERVISUAL + 265 visualDirPath.replace(etsDirPath, '')); 266 if (!(fs.existsSync(visualMapDirPath) && fs.statSync(visualMapDirPath).isDirectory())) { 267 mkDir(visualMapDirPath); 268 } 269 fs.writeFile(path.resolve(visualMapDirPath, visualMapName), sourcemap.toString(), (err) => { 270 if (err) { 271 return console.error(red, 'ERROR: Failed to write visual.js.map', reset); 272 } 273 }); 274} 275 276function findVisualFile(filePath: string): string { 277 const etsDirPath: string = path.parse(projectConfig.projectPath).dir; 278 const visualDirPath: string = path.parse(projectConfig.aceSuperVisualPath).dir; 279 return filePath 280 .replace(projectConfig.projectPath, projectConfig.aceSuperVisualPath) 281 .replace(etsDirPath, visualDirPath).replace(/\.ets$/, '.visual'); 282} 283 284function getVisualContent(visualPath: string, log: LogInfo[]): any { 285 const parseContent: any = genETS(fs.readFileSync(visualPath, 'utf-8')); 286 if (parseContent && parseContent.errorType && parseContent.errorType !== '') { 287 log.push({ 288 type: LogType.ERROR, 289 message: parseContent.message 290 }); 291 } 292 return parseContent ? parseContent.ets : null; 293} 294 295module.exports = preProcess; 296