1/* 2 * Copyright (c) 2021-2022 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 16const WebSocket = require('ws'); 17const ts = require('typescript'); 18const path = require('path'); 19const fs = require('fs'); 20const { spawn, execSync } = require('child_process'); 21const _ = require('lodash'); 22 23const { processComponentChild } = require('../lib/process_component_build'); 24const { createWatchCompilerHost } = require('../lib/ets_checker'); 25const { writeFileSync } = require('../lib/utils'); 26const { projectConfig } = require('../main'); 27const { props } = require('../lib/compile_info'); 28const { 29 isResource, 30 processResourceData, 31 isAnimateTo, 32 processAnimateTo 33} = require('../lib/process_ui_syntax'); 34const { dollarCollection } = require('../lib/ets_checker'); 35 36const WebSocketServer = WebSocket.Server; 37 38let pluginSocket = ''; 39 40let supplement = { 41 isAcceleratePreview: false, 42 line: 0, 43 column: 0, 44 fileName: '' 45}; 46 47const pluginCommandChannelMessageHandlers = { 48 'compileComponent': handlePluginCompileComponent, 49 'default': () => {} 50}; 51let es2abcFilePath = path.join(__dirname, '../bin/ark/build-win/bin/es2abc'); 52 53let previewCacheFilePath; 54let previewJsFilePath; 55let previewAbcFilePath; 56const messages = []; 57let start = false; 58let checkStatus = false; 59let compileStatus = false; 60let receivedMsg_; 61let errorInfo = []; 62let compileWithCheck; 63let globalVariable = []; 64let propertyVariable = []; 65let globalDeclaration = new Map(); 66let connectNum = 0; 67const maxConnectNum = 8; 68let codeOverMaxlength = false; 69 70let callback = undefined; 71 72function buildPipeServer() { 73 return { 74 init(cachePath, buildPath, cb) { 75 previewCacheFilePath = path.join(cachePath || buildPath, 'preview.ets'); 76 const rootFileNames = []; 77 writeFileSync(previewCacheFilePath, ''); 78 rootFileNames.push(previewCacheFilePath); 79 ts.createWatchProgram( 80 createWatchCompilerHost(rootFileNames, resolveDiagnostic, delayPrintLogCount, ()=>{}, true)); 81 callback = cb; 82 }, 83 compileComponent(jsonData) { 84 handlePluginCompileComponent(jsonData); 85 } 86 }; 87} 88 89function init(port) { 90 previewCacheFilePath = 91 path.join(projectConfig.cachePath || projectConfig.buildPath, 'preview.ets'); 92 previewJsFilePath = 93 path.join(projectConfig.cachePath || projectConfig.buildPath, 'preview.js'); 94 previewAbcFilePath = 95 path.join(projectConfig.cachePath || projectConfig.buildPath, 'preview.abc'); 96 const rootFileNames = []; 97 writeFileSync(previewCacheFilePath, ''); 98 rootFileNames.push(previewCacheFilePath); 99 ts.createWatchProgram( 100 createWatchCompilerHost(rootFileNames, resolveDiagnostic, delayPrintLogCount, ()=>{}, true)); 101 const wss = new WebSocketServer({ 102 port: port, 103 host: '127.0.0.1' 104 }); 105 wss.on('connection', function(ws) { 106 if (connectNum < maxConnectNum) { 107 connectNum++; 108 handlePluginConnect(ws); 109 } else { 110 ws.terminate(); 111 } 112 }); 113} 114 115function handlePluginConnect(ws) { 116 ws.on('message', function(message) { 117 pluginSocket = ws; 118 try { 119 const jsonData = JSON.parse(message); 120 handlePluginCommand(jsonData); 121 } catch (e) { 122 } 123 }); 124} 125 126function handlePluginCommand(jsonData) { 127 pluginCommandChannelMessageHandlers[jsonData.command] 128 ? pluginCommandChannelMessageHandlers[jsonData.command](jsonData) 129 : pluginCommandChannelMessageHandlers['default'](jsonData); 130} 131 132function handlePluginCompileComponent(jsonData) { 133 if (jsonData) { 134 messages.push(jsonData); 135 if (receivedMsg_) { 136 return; 137 } 138 } else if (messages.length > 0) { 139 jsonData = messages[0]; 140 } else { 141 return; 142 } 143 start = true; 144 const receivedMsg = _.cloneDeep(jsonData); 145 const compilerOptions = ts.readConfigFile( 146 path.resolve(__dirname, '../tsconfig.json'), ts.sys.readFile).config.compilerOptions; 147 Object.assign(compilerOptions, { 148 'sourceMap': false 149 }); 150 const sourceNode = ts.createSourceFile('preview.ets', 151 'struct preview{build(){' + receivedMsg.data.script.replace(/new\s+\b(Circle|Ellipse|Rect|Path)\b/g, 152 (item, item1) => { 153 return 'special' + item1 + 'ForPreview'; 154 }) + '}}', 155 ts.ScriptTarget.Latest, true, ts.ScriptKind.ETS, compilerOptions); 156 compileWithCheck = jsonData.data.compileWithCheck || 'true'; 157 receivedMsg.data.variableScript = ''; 158 checkPreparation(receivedMsg, sourceNode); 159 const previewStatements = []; 160 const log = []; 161 supplement = { 162 isAcceleratePreview: true, 163 line: parseInt(JSON.parse(receivedMsg.data.offset).line), 164 column: parseInt(JSON.parse(receivedMsg.data.offset).column), 165 fileName: receivedMsg.data.filePath || '' 166 }; 167 processComponentChild(sourceNode.statements[0].members[1].body, previewStatements, log, supplement); 168 supplement.isAcceleratePreview = false; 169 const newSource = ts.factory.updateSourceFile(sourceNode, previewStatements); 170 const transformedSourceFile = transformResourceNode(newSource, log); 171 const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 172 const result = printer.printNode(ts.EmitHint.Unspecified, transformedSourceFile, transformedSourceFile); 173 receivedMsg.data.script = ts.transpileModule(result, {}).outputText.replace( 174 /\bspecial(Circle|Ellipse|Rect|Path)ForPreview\b/g, (item, item1) => { 175 return 'new ' + item1; 176 }); 177 receivedMsg.data.log = log; 178 if (receivedMsg.data.viewID) { 179 receivedMsg.data.script = receivedMsg.data.variableScript + `function quickPreview(context) { 180 const fastPreview = function build(){ 181 ${receivedMsg.data.script} 182 }.bind(context); 183 fastPreview(); 184 } 185 quickPreview(GetRootView().findChildByIdForPreview(${receivedMsg.data.viewID}))`; 186 } 187 callEs2abc(receivedMsg); 188} 189 190function transformResourceNode(newSource, log) { 191 const transformerFactory = (context) => { 192 return (rootNode) => { 193 function visit(node) { 194 node = ts.visitEachChild(node, visit, context); 195 return processResourceNode(node, log); 196 } 197 return ts.visitNode(rootNode, visit); 198 }; 199 }; 200 const transformationResult = ts.transform(newSource, [transformerFactory]); 201 return transformationResult.transformed[0]; 202} 203 204function processResourceNode(node, log) { 205 if (isResource(node)) { 206 return processResourceData(node, {isAcceleratePreview: true, log: log}); 207 } else if (isAnimateTo(node)) { 208 return processAnimateTo(node); 209 } else { 210 return node; 211 } 212} 213 214function checkPreparation(receivedMsg, sourceNode) { 215 let variableScript = ''; 216 if (previewCacheFilePath && fs.existsSync(previewCacheFilePath) && compileWithCheck === 'true') { 217 globalVariable = receivedMsg.data.globalVariable || globalVariable; 218 globalVariable = globalVariable.map((item) => { 219 globalDeclaration.set(item.identifier, item.declaration); 220 return item.identifier; 221 }); 222 for (const [key, value] of sourceNode.identifiers) { 223 if (globalVariable.includes(key)) { 224 variableScript += globalDeclaration.get(key) + '\n'; 225 } else if (key.startsWith('$$') && globalVariable.includes(key.substring(2))) { 226 variableScript += globalDeclaration.get(key.substring(2)) + '\n'; 227 } 228 } 229 propertyVariable = receivedMsg.data.propertyVariable || propertyVariable; 230 receivedMsg.data.variableScript = ts.transpileModule(variableScript, {}).outputText; 231 writeFileSync(previewCacheFilePath, variableScript + 'struct preview{build(){' + receivedMsg.data.script + '}}'); 232 } 233} 234 235function callEs2abc(receivedMsg) { 236 if (fs.existsSync(es2abcFilePath + '.exe') || fs.existsSync(es2abcFilePath)) { 237 es2abc(receivedMsg); 238 } else { 239 es2abcFilePath = path.join(__dirname, '../bin/ark/build-mac/bin/es2abc'); 240 if (fs.existsSync(es2abcFilePath)) { 241 es2abc(receivedMsg); 242 } 243 } 244} 245 246function es2abc(receivedMsg) { 247 try { 248 const transCode = spawn(es2abcFilePath, 249 ['--base64Input', Buffer.from(receivedMsg.data.script).toString('base64'), '--base64Output'], {windowsHide: true}); 250 transCode.stdout.on('data', (data) => { 251 receivedMsg.data.script = data.toString(); 252 nextResponse(receivedMsg); 253 }); 254 transCode.stderr.on('data', (data) => { 255 receivedMsg.data.script = ''; 256 nextResponse(receivedMsg); 257 }); 258 } catch (e) { 259 if (checkStatus) { 260 getOverLengthCode(receivedMsg); 261 } else { 262 codeOverMaxlength = true; 263 receivedMsg_ = receivedMsg; 264 } 265 } 266} 267 268function getOverLengthCode(receivedMsg) { 269 writeFileSync(previewJsFilePath, receivedMsg.data.script); 270 const cmd = '"' + es2abcFilePath + '" ' + previewJsFilePath + ' --output ' + previewAbcFilePath; 271 execSync(cmd, {windowsHide: true}); 272 if (fs.existsSync(previewAbcFilePath)) { 273 receivedMsg.data.script = fs.readFileSync(previewAbcFilePath).toString('base64'); 274 } else { 275 receivedMsg.data.script = ''; 276 } 277 nextResponse(receivedMsg); 278} 279 280function nextResponse(receivedMsg) { 281 compileStatus = true; 282 receivedMsg_ = receivedMsg; 283 responseToPlugin(); 284} 285 286function resolveDiagnostic(diagnostic) { 287 const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 288 if (validateError(message)) { 289 if (diagnostic.file) { 290 const { line, character } = 291 diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); 292 errorInfo.push( 293 `ArkTS:ERROR File: ${diagnostic.file.fileName}:${line + 1}:${character + 1}\n ${message}\n`); 294 } else { 295 errorInfo.push(`ArkTS:ERROR: ${message}`); 296 } 297 } 298} 299 300function delayPrintLogCount() { 301 if (start == true) { 302 checkStatus = true; 303 if (codeOverMaxlength && !errorInfo.length && !receivedMsg_.data.log.length) { 304 getOverLengthCode(receivedMsg_); 305 } else { 306 if (codeOverMaxlength) { 307 compileStatus = true; 308 } 309 responseToPlugin(); 310 } 311 } 312} 313 314function responseToPlugin() { 315 if ((compileWithCheck !== 'true' && compileStatus == true) || 316 (compileWithCheck === 'true' && compileStatus == true && checkStatus == true)) { 317 if (receivedMsg_) { 318 if (errorInfo && errorInfo.length) { 319 receivedMsg_.data.log = receivedMsg_.data.log || []; 320 receivedMsg_.data.log.push(...errorInfo); 321 } 322 if (callback) { 323 callback(JSON.stringify(receivedMsg_)); 324 afterResponse(); 325 } else { 326 pluginSocket.send(JSON.stringify(receivedMsg_), (err) => { 327 afterResponse(); 328 }); 329 } 330 } 331 } 332} 333 334function afterResponse() { 335 start = false; 336 checkStatus = false; 337 compileStatus = false; 338 codeOverMaxlength = false; 339 errorInfo = []; 340 receivedMsg_ = undefined; 341 globalDeclaration.clear(); 342 messages.shift(); 343 if (messages.length > 0) { 344 handlePluginCompileComponent(); 345 } 346} 347 348function validateError(message) { 349 const stateInfoReg = /Property\s*'(\$?[_a-zA-Z0-9]+)' does not exist on type/; 350 const $$InfoReg = /Cannot find name\s*'(\$\$[_a-zA-Z0-9]+)'/; 351 if (matchMessage(message, [...propertyVariable, ...props], stateInfoReg) || 352 matchMessage(message, [...dollarCollection], $$InfoReg)) { 353 return false; 354 } 355 return true; 356} 357 358function matchMessage(message, nameArr, reg) { 359 if (reg.test(message)) { 360 const match = message.match(reg); 361 if (match[1] && nameArr.includes(match[1])) { 362 return true; 363 } 364 } 365 return false; 366} 367 368module.exports = { 369 init, 370 buildPipeServer 371}; 372