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/ets_checker'); 28const { 29 isResource, 30 processResourceData, 31 isAnimateToOrImmediately, 32 processAnimateToOrImmediately 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 try { 168 processComponentChild(sourceNode.statements[0].members[1].body, previewStatements, log, supplement); 169 } catch (e) { 170 log.push({ 171 message: e.stack 172 }); 173 } 174 supplement.isAcceleratePreview = false; 175 const newSource = ts.factory.updateSourceFile(sourceNode, previewStatements); 176 const transformedSourceFile = transformResourceNode(newSource, log); 177 const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 178 const result = printer.printNode(ts.EmitHint.Unspecified, transformedSourceFile, transformedSourceFile); 179 receivedMsg.data.script = ts.transpileModule(result, {}).outputText.replace( 180 /\bspecial(Circle|Ellipse|Rect|Path)ForPreview\b/g, (item, item1) => { 181 return 'new ' + item1; 182 }); 183 receivedMsg.data.log = log; 184 if (receivedMsg.data.viewID) { 185 receivedMsg.data.script = receivedMsg.data.variableScript + `function quickPreview(context) { 186 const fastPreview = function build(){ 187 ${receivedMsg.data.script} 188 }.bind(context); 189 fastPreview(); 190 } 191 quickPreview(GetRootView().findChildByIdForPreview(${receivedMsg.data.viewID}))`; 192 } 193 callEs2abc(receivedMsg); 194} 195 196function transformResourceNode(newSource, log) { 197 const transformerFactory = (context) => { 198 return (rootNode) => { 199 function visit(node) { 200 node = ts.visitEachChild(node, visit, context); 201 return processResourceNode(node, log); 202 } 203 return ts.visitNode(rootNode, visit); 204 }; 205 }; 206 const transformationResult = ts.transform(newSource, [transformerFactory]); 207 return transformationResult.transformed[0]; 208} 209 210function processResourceNode(node, log) { 211 if (isResource(node)) { 212 return processResourceData(node, '', {isAcceleratePreview: true, log: log}); 213 } else if (isAnimateToOrImmediately(node)) { 214 return processAnimateToOrImmediately(node); 215 } else { 216 return node; 217 } 218} 219 220function checkPreparation(receivedMsg, sourceNode) { 221 let variableScript = ''; 222 if (previewCacheFilePath && fs.existsSync(previewCacheFilePath) && compileWithCheck === 'true') { 223 globalVariable = receivedMsg.data.globalVariable || globalVariable; 224 globalVariable = globalVariable.map((item) => { 225 globalDeclaration.set(item.identifier, item.declaration); 226 return item.identifier; 227 }); 228 for (const [key, value] of sourceNode.identifiers) { 229 if (globalVariable.includes(key)) { 230 variableScript += globalDeclaration.get(key) + '\n'; 231 } else if (key.startsWith('$$') && globalVariable.includes(key.substring(2))) { 232 variableScript += globalDeclaration.get(key.substring(2)) + '\n'; 233 } 234 } 235 propertyVariable = receivedMsg.data.propertyVariable || propertyVariable; 236 receivedMsg.data.variableScript = ts.transpileModule(variableScript, {}).outputText; 237 writeFileSync(previewCacheFilePath, variableScript + 'struct preview{build(){' + receivedMsg.data.script + '}}'); 238 } 239} 240 241function callEs2abc(receivedMsg) { 242 if (fs.existsSync(es2abcFilePath + '.exe') || fs.existsSync(es2abcFilePath)) { 243 es2abc(receivedMsg); 244 } else { 245 es2abcFilePath = path.join(__dirname, '../bin/ark/build-mac/bin/es2abc'); 246 if (fs.existsSync(es2abcFilePath)) { 247 es2abc(receivedMsg); 248 } 249 } 250} 251 252function es2abc(receivedMsg) { 253 try { 254 const transCode = spawn(es2abcFilePath, 255 ['--base64Input', Buffer.from(receivedMsg.data.script).toString('base64'), '--base64Output'], {windowsHide: true}); 256 transCode.stdout.on('data', (data) => { 257 receivedMsg.data.script = data.toString(); 258 nextResponse(receivedMsg); 259 }); 260 transCode.stderr.on('data', (data) => { 261 receivedMsg.data.script = ''; 262 nextResponse(receivedMsg); 263 }); 264 } catch (e) { 265 if (checkStatus) { 266 getOverLengthCode(receivedMsg); 267 } else { 268 codeOverMaxlength = true; 269 receivedMsg_ = receivedMsg; 270 } 271 } 272} 273 274function getOverLengthCode(receivedMsg) { 275 writeFileSync(previewJsFilePath, receivedMsg.data.script); 276 const cmd = '"' + es2abcFilePath + '" ' + previewJsFilePath + ' --output ' + previewAbcFilePath; 277 execSync(cmd, {windowsHide: true}); 278 if (fs.existsSync(previewAbcFilePath)) { 279 receivedMsg.data.script = fs.readFileSync(previewAbcFilePath).toString('base64'); 280 } else { 281 receivedMsg.data.script = ''; 282 } 283 nextResponse(receivedMsg); 284} 285 286function nextResponse(receivedMsg) { 287 compileStatus = true; 288 receivedMsg_ = receivedMsg; 289 responseToPlugin(); 290} 291 292function resolveDiagnostic(diagnostic) { 293 const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 294 if (validateError(message)) { 295 if (diagnostic.file) { 296 const { line, character } = 297 diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); 298 errorInfo.push( 299 `ArkTS:ERROR File: ${diagnostic.file.fileName}:${line + 1}:${character + 1}\n ${message}\n`); 300 } else { 301 errorInfo.push(`ArkTS:ERROR: ${message}`); 302 } 303 } 304} 305 306function delayPrintLogCount() { 307 if (start == true) { 308 checkStatus = true; 309 if (codeOverMaxlength && !errorInfo.length && !receivedMsg_.data.log.length) { 310 getOverLengthCode(receivedMsg_); 311 } else { 312 if (codeOverMaxlength) { 313 compileStatus = true; 314 } 315 responseToPlugin(); 316 } 317 } 318} 319 320function responseToPlugin() { 321 if ((compileWithCheck !== 'true' && compileStatus == true) || 322 (compileWithCheck === 'true' && compileStatus == true && checkStatus == true)) { 323 if (receivedMsg_) { 324 if (errorInfo && errorInfo.length) { 325 receivedMsg_.data.log = receivedMsg_.data.log || []; 326 receivedMsg_.data.log.push(...errorInfo); 327 } 328 if (callback) { 329 callback(JSON.stringify(receivedMsg_)); 330 afterResponse(); 331 } else { 332 pluginSocket.send(JSON.stringify(receivedMsg_), (err) => { 333 afterResponse(); 334 }); 335 } 336 } 337 } 338} 339 340function afterResponse() { 341 start = false; 342 checkStatus = false; 343 compileStatus = false; 344 codeOverMaxlength = false; 345 errorInfo = []; 346 receivedMsg_ = undefined; 347 globalDeclaration.clear(); 348 messages.shift(); 349 if (messages.length > 0) { 350 handlePluginCompileComponent(); 351 } 352} 353 354function validateError(message) { 355 const stateInfoReg = /Property\s*'(\$?[_a-zA-Z0-9]+)' does not exist on type/; 356 const $$InfoReg = /Cannot find name\s*'(\$\$[_a-zA-Z0-9]+)'/; 357 if (matchMessage(message, [...propertyVariable, ...props], stateInfoReg) || 358 matchMessage(message, [...dollarCollection], $$InfoReg)) { 359 return false; 360 } 361 return true; 362} 363 364function matchMessage(message, nameArr, reg) { 365 if (reg.test(message)) { 366 const match = message.match(reg); 367 if (match[1] && nameArr.includes(match[1])) { 368 return true; 369 } 370 } 371 return false; 372} 373 374module.exports = { 375 init, 376 buildPipeServer 377}; 378