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 * as ts from 'typescript'; 17import Stats from 'webpack/lib/Stats'; 18import Compiler from 'webpack/lib/Compiler'; 19import Compilation from 'webpack/lib/Compilation'; 20import JavascriptModulesPlugin from 'webpack/lib/javascript/JavascriptModulesPlugin'; 21import { 22 configure, 23 getLogger 24} from 'log4js'; 25import RawSource from 'webpack-sources/lib/RawSource'; 26import path from 'path'; 27import fs from 'fs'; 28import CachedSource from 'webpack-sources/lib/CachedSource'; 29import ConcatSource from 'webpack-sources/lib/ConcatSource'; 30 31import { transformLog } from './process_ui_syntax'; 32import { 33 moduleCollection, 34 useOSFiles 35} from './validate_ui_syntax'; 36import { 37 circularFile, 38 mkDir, 39 writeFileSync, 40 parseErrorMessage 41} from './utils'; 42import { 43 MODULE_ETS_PATH, 44 MODULE_SHARE_PATH, 45 BUILD_SHARE_PATH 46} from './pre_define'; 47import { 48 createLanguageService, 49 appComponentCollection, 50 importModuleCollection, 51 createWatchCompilerHost 52} from './ets_checker'; 53import { 54 globalProgram, 55 projectConfig 56} from '../main'; 57 58configure({ 59 appenders: { 'ETS': {type: 'stderr', layout: {type: 'messagePassThrough'}}}, 60 categories: {'default': {appenders: ['ETS'], level: 'info'}} 61}); 62export const logger = getLogger('ETS'); 63 64export const props: string[] = []; 65 66interface Info { 67 message?: string; 68 issue?: { 69 message: string, 70 file: string, 71 location: { start?: { line: number, column: number } } 72 }; 73} 74 75export interface CacheFileName { 76 mtimeMs: number, 77 children: string[], 78 parent: string[], 79 error: boolean 80} 81 82interface NeedUpdateFlag { 83 flag: boolean; 84} 85 86export let cache: Cache = {}; 87export const shouldResolvedFiles: Set<string> = new Set() 88const checkErrorMessage: Set<string | Info> = new Set([]); 89type Cache = Record<string, CacheFileName>; 90 91export class ResultStates { 92 private mStats: Stats; 93 private mErrorCount: number = 0; 94 private mPreErrorCount: number = 0; 95 private tsErrorCount: number = 0; 96 private mWarningCount: number = 0; 97 private warningCount: number = 0; 98 private noteCount: number = 0; 99 private red: string = '\u001b[31m'; 100 private yellow: string = '\u001b[33m'; 101 private blue: string = '\u001b[34m'; 102 private reset: string = '\u001b[39m'; 103 private moduleSharePaths: Set<string> = new Set([]); 104 private removedFiles: string[] = []; 105 106 public apply(compiler: Compiler): void { 107 compiler.hooks.compilation.tap('SourcemapFixer', compilation => { 108 compilation.hooks.afterProcessAssets.tap('SourcemapFixer', assets => { 109 Reflect.ownKeys(assets).forEach(key => { 110 if (/\.map$/.test(key.toString()) && assets[key]._value) { 111 assets[key]._value = assets[key]._value.toString().replace('.ets?entry', '.ets'); 112 assets[key]._value = assets[key]._value.toString().replace('.ts?entry', '.ts'); 113 } 114 }); 115 } 116 ); 117 118 compilation.hooks.succeedModule.tap('findModule', (module) => { 119 if (module && module.error) { 120 const errorLog: string = module.error.toString(); 121 if (module.resourceResolveData && module.resourceResolveData.path && 122 /Module parse failed/.test(errorLog) && /Invalid regular expression:/.test(errorLog)) { 123 this.mErrorCount++; 124 const errorInfos: string[] = errorLog.split('\n>')[1].split(';'); 125 if (errorInfos && errorInfos.length > 0 && errorInfos[0]) { 126 const errorInformation: string = `ERROR in ${module.resourceResolveData.path}\n The following syntax is incorrect.\n > ${errorInfos[0]}`; 127 this.printErrorMessage(parseErrorMessage(errorInformation), false, module.error); 128 } 129 } 130 } 131 }); 132 133 compilation.hooks.buildModule.tap('findModule', (module) => { 134 if (module.context) { 135 if (module.context.indexOf(projectConfig.projectPath) >= 0) { 136 return; 137 } 138 const modulePath: string = path.join(module.context); 139 const srcIndex: number = modulePath.lastIndexOf(MODULE_ETS_PATH); 140 if (srcIndex < 0) { 141 return; 142 } 143 const moduleSharePath: string = path.resolve(modulePath.substring(0, srcIndex), MODULE_SHARE_PATH); 144 if (fs.existsSync(moduleSharePath)) { 145 this.moduleSharePaths.add(moduleSharePath); 146 } 147 } 148 }); 149 }); 150 151 compiler.hooks.afterCompile.tap('copyFindModule', () => { 152 this.moduleSharePaths.forEach(modulePath => { 153 circularFile(modulePath, path.resolve(projectConfig.buildPath, BUILD_SHARE_PATH)); 154 }); 155 }); 156 157 compiler.hooks.compilation.tap('CommonAsset', compilation => { 158 compilation.hooks.processAssets.tap( 159 { 160 name: 'GLOBAL_COMMON_MODULE_CACHE', 161 stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS 162 }, 163 (assets) => { 164 const GLOBAL_COMMON_MODULE_CACHE = ` 165 globalThis["__common_module_cache__${projectConfig.hashProjectPath}"] =` + 166 ` globalThis["__common_module_cache__${projectConfig.hashProjectPath}"] || {};`; 167 168 if (assets['commons.js']) { 169 assets['commons.js'] = new CachedSource( 170 new ConcatSource(assets['commons.js'], GLOBAL_COMMON_MODULE_CACHE)); 171 } else if (assets['vendors.js']) { 172 assets['vendors.js'] = new CachedSource( 173 new ConcatSource(assets['vendors.js'], GLOBAL_COMMON_MODULE_CACHE)); 174 } 175 }); 176 }); 177 178 compiler.hooks.compilation.tap('Require', compilation => { 179 JavascriptModulesPlugin.getCompilationHooks(compilation).renderRequire.tap('renderRequire', 180 (source) => { 181 return `var commonCachedModule = globalThis` + 182 `["__common_module_cache__${projectConfig.hashProjectPath}"] ? ` + 183 `globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` + 184 `[moduleId]: null;\n` + 185 `if (commonCachedModule) { return commonCachedModule.exports; }\n` + 186 source.replace('// Execute the module function', 187 `function isCommonModue(moduleId) { 188 if (globalThis["webpackChunk${projectConfig.hashProjectPath}"]) { 189 const length = globalThis["webpackChunk${projectConfig.hashProjectPath}"].length; 190 switch (length) { 191 case 1: 192 return globalThis["webpackChunk${projectConfig.hashProjectPath}"][0][1][moduleId]; 193 case 2: 194 return globalThis["webpackChunk${projectConfig.hashProjectPath}"][0][1][moduleId] || 195 globalThis["webpackChunk${projectConfig.hashProjectPath}"][1][1][moduleId]; 196 } 197 } 198 return undefined; 199 }\n` + 200 `if (globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` + 201 ` && String(moduleId).indexOf("?name=") < 0 && isCommonModue(moduleId)) {\n` + 202 ` globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` + 203 `[moduleId] = module;\n}`); 204 }); 205 }); 206 207 compiler.hooks.entryOption.tap('beforeRun', () => { 208 const rootFileNames: string[] = []; 209 Object.values(projectConfig.entryObj).forEach((fileName: string) => { 210 rootFileNames.push(fileName.replace('?entry', '')); 211 }); 212 if (process.env.watchMode === 'true') { 213 globalProgram.watchProgram = ts.createWatchProgram( 214 createWatchCompilerHost(rootFileNames, this.printDiagnostic.bind(this), 215 this.delayPrintLogCount.bind(this), this.resetTsErrorCount.bind(this))); 216 } else { 217 let languageService: ts.LanguageService = null; 218 let cacheFile: string = null; 219 if (projectConfig.xtsMode) { 220 languageService = createLanguageService(rootFileNames); 221 } else { 222 cacheFile = path.resolve(projectConfig.cachePath, '../.ts_checker_cache'); 223 cache = fs.existsSync(cacheFile) ? JSON.parse(fs.readFileSync(cacheFile).toString()) : {}; 224 const filterFiles: string[] = filterInput(rootFileNames); 225 languageService = createLanguageService(filterFiles); 226 } 227 globalProgram.program = languageService.getProgram(); 228 const allDiagnostics: ts.Diagnostic[] = globalProgram.program 229 .getSyntacticDiagnostics() 230 .concat(globalProgram.program.getSemanticDiagnostics()) 231 .concat(globalProgram.program.getDeclarationDiagnostics()); 232 allDiagnostics.forEach((diagnostic: ts.Diagnostic) => { 233 this.printDiagnostic(diagnostic); 234 }); 235 if (process.env.watchMode !== 'true' && !projectConfig.xtsMode) { 236 fs.writeFileSync(cacheFile, JSON.stringify(cache, null, 2)); 237 } 238 } 239 }); 240 241 compiler.hooks.done.tap('Result States', (stats: Stats) => { 242 if (projectConfig.isPreview && projectConfig.aceSoPath && 243 useOSFiles && useOSFiles.size > 0) { 244 this.writeUseOSFiles(); 245 } 246 this.mStats = stats; 247 this.warningCount = 0; 248 this.noteCount = 0; 249 if (this.mStats.compilation.warnings) { 250 this.mWarningCount = this.mStats.compilation.warnings.length; 251 } 252 this.printResult(); 253 }); 254 255 compiler.hooks.watchRun.tap('Listening State', (comp: Compiler) => { 256 checkErrorMessage.clear(); 257 this.clearCount(); 258 process.env.watchEts = 'start'; 259 comp.modifiedFiles = comp.modifiedFiles || []; 260 comp.removedFiles = comp.removedFiles || []; 261 const watchModifiedFiles: string[] = [...comp.modifiedFiles]; 262 const watchRemovedFiles: string[] = [...comp.removedFiles]; 263 if (watchRemovedFiles.length) { 264 this.removedFiles = watchRemovedFiles; 265 } 266 if (watchModifiedFiles.length) { 267 watchModifiedFiles.some((item: string) => { 268 if (fs.statSync(item).isFile() && !/.(ts|ets)$/.test(item)) { 269 process.env.watchTs = 'end'; 270 return true; 271 } 272 }); 273 } 274 const changedFiles: string[] = [...watchModifiedFiles, ...watchRemovedFiles]; 275 if (changedFiles.length) { 276 shouldResolvedFiles.clear(); 277 } 278 changedFiles.forEach((file) => { 279 this.judgeFileShouldResolved(file, shouldResolvedFiles) 280 }) 281 }); 282 283 if (!projectConfig.isPreview) { 284 compiler.hooks.compilation.tap('Collect Components And Modules', compilation => { 285 compilation.hooks.additionalAssets.tapAsync('Collect Components And Modules', callback => { 286 this.generateCollectionFile(); 287 callback(); 288 }); 289 }); 290 } 291 } 292 293 private judgeFileShouldResolved(file: string, shouldResolvedFiles: Set<string>): void { 294 if (shouldResolvedFiles.has(file)) { 295 return; 296 } 297 shouldResolvedFiles.add(file); 298 if (cache && cache[file] && cache[file].parent) { 299 cache[file].parent.forEach((item)=>{ 300 this.judgeFileShouldResolved(item, shouldResolvedFiles); 301 }) 302 cache[file].parent = []; 303 } 304 if (cache && cache[file] && cache[file].children) { 305 cache[file].children.forEach((item)=>{ 306 this.judgeFileShouldResolved(item, shouldResolvedFiles); 307 }) 308 cache[file].children = []; 309 } 310 } 311 312 private generateCollectionFile() { 313 if (projectConfig.aceSuperVisualPath && fs.existsSync(projectConfig.aceSuperVisualPath)) { 314 appComponentCollection.clear(); 315 } 316 if (fs.existsSync(path.resolve(projectConfig.buildPath, './module_collection.txt'))) { 317 const lastModuleCollection: string = 318 fs.readFileSync(path.resolve(projectConfig.buildPath, './module_collection.txt')).toString(); 319 if (lastModuleCollection && lastModuleCollection !== 'NULL') { 320 lastModuleCollection.split(',').forEach(item => { 321 moduleCollection.add(item); 322 }) 323 } 324 } 325 const moduleContent: string = 326 moduleCollection.size === 0 ? 'NULL' : Array.from(moduleCollection).join(','); 327 writeFileSync(path.resolve(projectConfig.buildPath, './module_collection.txt'), 328 moduleContent); 329 const componentContent: string = Array.from(appComponentCollection).join(','); 330 writeFileSync(path.resolve(projectConfig.buildPath, './component_collection.txt'), 331 componentContent); 332 } 333 334 private printDiagnostic(diagnostic: ts.Diagnostic): void { 335 const message: string = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 336 if (this.validateError(message)) { 337 if (process.env.watchMode !== 'true' && !projectConfig.xtsMode) { 338 updateErrorFileCache(diagnostic); 339 } 340 this.tsErrorCount += 1; 341 if (diagnostic.file) { 342 const { line, character }: ts.LineAndCharacter = 343 diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); 344 logger.error(this.red, 345 `ArkTS:ERROR File: ${diagnostic.file.fileName}:${line + 1}:${character + 1}\n ${message}\n`, this.reset); 346 } else { 347 logger.error(this.red, `ArkTS:ERROR: ${message}`, this.reset); 348 } 349 } 350 } 351 352 private resetTsErrorCount(): void { 353 this.tsErrorCount = 0; 354 } 355 356 private writeUseOSFiles(): void { 357 let info: string = ''; 358 if (!fs.existsSync(projectConfig.aceSoPath)) { 359 const parent: string = path.join(projectConfig.aceSoPath, '..'); 360 if (!(fs.existsSync(parent) && !fs.statSync(parent).isFile())) { 361 mkDir(parent); 362 } 363 } else { 364 info = fs.readFileSync(projectConfig.aceSoPath, 'utf-8') + '\n'; 365 } 366 fs.writeFileSync(projectConfig.aceSoPath, info + Array.from(useOSFiles).join('\n')); 367 } 368 369 private printResult(): void { 370 this.printWarning(); 371 this.printError(); 372 if (process.env.watchMode === 'true') { 373 process.env.watchEts = 'end'; 374 this.delayPrintLogCount(true); 375 } else { 376 this.printLogCount(); 377 } 378 } 379 380 private delayPrintLogCount(isCompile: boolean = false) { 381 if (process.env.watchEts === 'end' && process.env.watchTs === 'end') { 382 this.printLogCount(); 383 process.env.watchTs = 'start'; 384 this.removedFiles = []; 385 } else if (isCompile && this.removedFiles.length && this.mErrorCount === 0 && 386 this.mPreErrorCount > 0) { 387 this.printLogCount(); 388 } 389 this.mPreErrorCount = this.mErrorCount; 390 } 391 392 private printLogCount(): void { 393 this.mErrorCount += this.tsErrorCount; 394 if (this.mErrorCount + this.warningCount + this.noteCount > 0) { 395 let result: string; 396 let resultInfo: string = ''; 397 if (this.mErrorCount > 0) { 398 resultInfo += `ERROR:${this.mErrorCount}`; 399 result = 'FAIL '; 400 if (!/ets_loader_ark$/.test(path.resolve(__dirname, '..'))) { 401 process.exitCode = 1; 402 } 403 } else { 404 result = 'SUCCESS '; 405 } 406 if (this.warningCount > 0) { 407 resultInfo += ` WARN:${this.warningCount}`; 408 } 409 if (this.noteCount > 0) { 410 resultInfo += ` NOTE:${this.noteCount}`; 411 } 412 logger.info(this.blue, 'COMPILE RESULT:' + result + `{${resultInfo}}`, this.reset); 413 } else { 414 console.info(this.blue, 'COMPILE RESULT:SUCCESS ', this.reset); 415 } 416 } 417 418 private clearCount(): void { 419 this.mErrorCount = 0; 420 this.warningCount = 0; 421 this.noteCount = 0; 422 } 423 424 private printWarning(): void { 425 if (this.mWarningCount > 0) { 426 const warnings: Info[] = this.mStats.compilation.warnings; 427 const length: number = warnings.length; 428 for (let index = 0; index < length; index++) { 429 const message: string = warnings[index].message.replace(/^Module Warning\s*.*:\n/, '') 430 .replace(/\(Emitted value instead of an instance of Error\) BUILD/, ''); 431 if (/^NOTE/.test(message)) { 432 if (!checkErrorMessage.has(message)) { 433 this.noteCount++; 434 logger.info(this.blue, message, this.reset, '\n'); 435 checkErrorMessage.add(message); 436 } 437 } else { 438 if (!checkErrorMessage.has(message)) { 439 this.warningCount++; 440 logger.warn(this.yellow, message.replace(/^WARN/, 'ArkTS:WARN'), this.reset, '\n'); 441 checkErrorMessage.add(message); 442 } 443 } 444 } 445 if (this.mWarningCount > length) { 446 this.warningCount = this.warningCount + this.mWarningCount - length; 447 } 448 } 449 } 450 451 private printError(): void { 452 if (this.mStats.compilation.errors.length > 0) { 453 const errors: Info[] = [...this.mStats.compilation.errors]; 454 for (let index = 0; index < errors.length; index++) { 455 if (errors[index].issue) { 456 if (!checkErrorMessage.has(errors[index].issue)) { 457 this.mErrorCount++; 458 const position: string = errors[index].issue.location 459 ? `:${errors[index].issue.location.start.line}:${errors[index].issue.location.start.column}` 460 : ''; 461 const location: string = errors[index].issue.file.replace(/\\/g, '/') + position; 462 const detail: string = errors[index].issue.message; 463 logger.error(this.red, 'ArkTS:ERROR File: ' + location, this.reset); 464 logger.error(this.red, detail, this.reset, '\n'); 465 checkErrorMessage.add(errors[index].issue); 466 } 467 } else if (/BUILDERROR/.test(errors[index].message)) { 468 if (!checkErrorMessage.has(errors[index].message)) { 469 this.mErrorCount++; 470 const errorMessage: string = errors[index].message.replace(/^Module Error\s*.*:\n/, '') 471 .replace(/\(Emitted value instead of an instance of Error\) BUILD/, '') 472 .replace(/^ERROR/, 'ArkTS:ERROR'); 473 this.printErrorMessage(errorMessage, true, errors[index]); 474 checkErrorMessage.add(errors[index].message); 475 } 476 } else if (!/TS[0-9]+:/.test(errors[index].message.toString()) && 477 !/Module parse failed/.test(errors[index].message.toString())) { 478 this.mErrorCount++; 479 let errorMessage: string = `${errors[index].message.replace(/\[tsl\]\s*/, '') 480 .replace(/\u001b\[.*?m/g, '').replace(/\.ets\.ts/g, '.ets').trim()}\n`; 481 errorMessage = this.filterModuleError(errorMessage) 482 .replace(/^ERROR in /, 'ArkTS:ERROR File: ').replace(/\s{6}TS/g, ' TS') 483 .replace(/\(([0-9]+),([0-9]+)\)/, ':$1:$2'); 484 this.printErrorMessage(parseErrorMessage(errorMessage), false, errors[index]); 485 } 486 } 487 } 488 } 489 private printErrorMessage(errorMessage: string, lineFeed: boolean, errorInfo: Info): void { 490 if (this.validateError(errorMessage)) { 491 const formatErrMsg = errorMessage.replace(/\\/g, '/'); 492 if (lineFeed) { 493 logger.error(this.red, formatErrMsg + '\n', this.reset); 494 } else { 495 logger.error(this.red, formatErrMsg, this.reset); 496 } 497 } else { 498 const errorsIndex = this.mStats.compilation.errors.indexOf(errorInfo); 499 this.mStats.compilation.errors.splice(errorsIndex, 1); 500 this.mErrorCount = this.mErrorCount - 1; 501 } 502 } 503 private validateError(message: string): boolean { 504 const propInfoReg: RegExp = /Cannot find name\s*'(\$?\$?[_a-zA-Z0-9]+)'/; 505 const stateInfoReg: RegExp = /Property\s*'(\$?[_a-zA-Z0-9]+)' does not exist on type/; 506 const importInfoReg: RegExp = /Cannot find namespace\s*'([_a-zA-Z0-9]+)'\./; 507 if (this.matchMessage(message, props, propInfoReg) || 508 this.matchMessage(message, props, stateInfoReg)) { 509 return false; 510 } 511 return true; 512 } 513 private matchMessage(message: string, nameArr: any, reg: RegExp): boolean { 514 if (reg.test(message)) { 515 const match: string[] = message.match(reg); 516 if (match[1] && nameArr.includes(match[1])) { 517 return true; 518 } 519 } 520 return false; 521 } 522 private filterModuleError(message: string): string { 523 if (/You may need an additional loader/.test(message) && transformLog && transformLog.sourceFile) { 524 const fileName: string = transformLog.sourceFile.fileName; 525 const errorInfos: string[] = message.split('You may need an additional loader to handle the result of these loaders.'); 526 if (errorInfos && errorInfos.length > 1 && errorInfos[1]) { 527 message = `ERROR in ${fileName}\n The following syntax is incorrect.${errorInfos[1]}`; 528 } 529 } 530 return message; 531 } 532} 533 534function updateErrorFileCache(diagnostic: ts.Diagnostic): void { 535 if (diagnostic.file && cache[path.resolve(diagnostic.file.fileName)]) { 536 cache[path.resolve(diagnostic.file.fileName)].error = true; 537 } 538} 539 540function filterInput(rootFileNames: string[]): string[] { 541 return rootFileNames.filter((file: string) => { 542 const needUpdate: NeedUpdateFlag = { flag: false }; 543 const alreadyCheckedFiles: Set<string> = new Set(); 544 checkNeedUpdateFiles(path.resolve(file), needUpdate, alreadyCheckedFiles); 545 return needUpdate.flag; 546 }); 547} 548 549function checkNeedUpdateFiles(file: string, needUpdate: NeedUpdateFlag, alreadyCheckedFiles: Set<string>): void { 550 if (alreadyCheckedFiles.has(file)) { 551 return; 552 } else { 553 alreadyCheckedFiles.add(file); 554 } 555 556 if (needUpdate.flag) { 557 return; 558 } 559 560 const value: CacheFileName = cache[file]; 561 const mtimeMs: number = fs.statSync(file).mtimeMs; 562 if (value) { 563 if (value.error || value.mtimeMs !== mtimeMs) { 564 needUpdate.flag = true; 565 return; 566 } 567 for (let i = 0; i < value.children.length; ++i) { 568 if (fs.existsSync(value.children[i])) { 569 checkNeedUpdateFiles(value.children[i], needUpdate, alreadyCheckedFiles); 570 } else { 571 needUpdate.flag = true; 572 } 573 } 574 } else { 575 cache[file] = { mtimeMs, children: [], parent: [], error: false }; 576 needUpdate.flag = true; 577 } 578} 579