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 path from 'path'; 26import fs from 'fs'; 27import CachedSource from 'webpack-sources/lib/CachedSource'; 28import ConcatSource from 'webpack-sources/lib/ConcatSource'; 29 30import { transformLog } from './process_ui_syntax'; 31import { 32 useOSFiles, 33 sourcemapNamesCollection 34} from './validate_ui_syntax'; 35import { 36 circularFile, 37 writeUseOSFiles, 38 writeFileSync, 39 parseErrorMessage, 40 genTemporaryPath, 41 shouldWriteChangedList, 42 getHotReloadFiles, 43 setChecker, 44} from './utils'; 45import { 46 MODULE_ETS_PATH, 47 MODULE_SHARE_PATH, 48 BUILD_SHARE_PATH, 49 EXTNAME_JS, 50 EXTNAME_JS_MAP 51} from './pre_define'; 52import { 53 serviceChecker, 54 createWatchCompilerHost, 55 hotReloadSupportFiles, 56 printDiagnostic, 57 checkerResult, 58 incrementWatchFile, 59 warnCheckerResult 60} from './ets_checker'; 61import { 62 globalProgram, 63 projectConfig 64} from '../main'; 65import cluster from 'cluster'; 66 67configure({ 68 appenders: { 'ETS': {type: 'stderr', layout: {type: 'messagePassThrough'}}}, 69 categories: {'default': {appenders: ['ETS'], level: 'info'}} 70}); 71export const logger = getLogger('ETS'); 72 73const checkErrorMessage: Set<string | Info> = new Set([]); 74 75interface Info { 76 message?: string; 77 issue?: { 78 message: string, 79 file: string, 80 location: { start?: { line: number, column: number } } 81 }; 82} 83 84export interface CacheFileName { 85 mtimeMs: number, 86 children: string[], 87 parent: string[], 88 error: boolean 89} 90 91interface hotReloadIncrementalTime { 92 hotReloadIncrementalStartTime: string; 93 hotReloadIncrementalEndTime: string; 94} 95 96export class ResultStates { 97 private mStats: Stats; 98 private mErrorCount: number = 0; 99 private mPreErrorCount: number = 0; 100 private mWarningCount: number = 0; 101 private warningCount: number = 0; 102 private noteCount: number = 0; 103 private red: string = '\u001b[31m'; 104 private yellow: string = '\u001b[33m'; 105 private blue: string = '\u001b[34m'; 106 private reset: string = '\u001b[39m'; 107 private moduleSharePaths: Set<string> = new Set([]); 108 private removedFiles: string[] = []; 109 private hotReloadIncrementalTime: hotReloadIncrementalTime = { 110 hotReloadIncrementalStartTime: '', 111 hotReloadIncrementalEndTime: '' 112 } 113 private incrementalFileInHar: Map<string, string> = new Map(); 114 115 public apply(compiler: Compiler): void { 116 compiler.hooks.compilation.tap('SourcemapFixer', compilation => { 117 compilation.hooks.processAssets.tap('RemoveHar', (assets) => { 118 if (!projectConfig.compileHar) { 119 return; 120 } 121 Object.keys(compilation.assets).forEach(key => { 122 if (path.extname(key) === EXTNAME_JS || path.extname(key) === EXTNAME_JS_MAP) { 123 delete assets[key]; 124 } 125 }); 126 }); 127 128 compilation.hooks.afterProcessAssets.tap('SourcemapFixer', assets => { 129 Reflect.ownKeys(assets).forEach(key => { 130 if (/\.map$/.test(key.toString()) && assets[key]._value) { 131 assets[key]._value = assets[key]._value.toString().replace('.ets?entry', '.ets'); 132 assets[key]._value = assets[key]._value.toString().replace('.ts?entry', '.ts'); 133 134 let absPath: string = path.resolve(projectConfig.projectPath, key.toString().replace('.js.map','.js')); 135 if (sourcemapNamesCollection && absPath) { 136 let map: Map<string, string> = sourcemapNamesCollection.get(absPath); 137 if (map && map.size != 0) { 138 let names: Array<string> = Array.from(map).flat(); 139 let sourcemapObj: any = JSON.parse(assets[key]._value); 140 sourcemapObj.nameMap = names; 141 assets[key]._value = JSON.stringify(sourcemapObj); 142 } 143 } 144 } 145 }); 146 } 147 ); 148 149 compilation.hooks.succeedModule.tap('findModule', (module) => { 150 if (module && module.error) { 151 const errorLog: string = module.error.toString(); 152 if (module.resourceResolveData && module.resourceResolveData.path && 153 /Module parse failed/.test(errorLog) && /Invalid regular expression:/.test(errorLog)) { 154 this.mErrorCount++; 155 const errorInfos: string[] = errorLog.split('\n>')[1].split(';'); 156 if (errorInfos && errorInfos.length > 0 && errorInfos[0]) { 157 const errorInformation: string = `ERROR in ${module.resourceResolveData.path}\n The following syntax is incorrect.\n > ${errorInfos[0]}`; 158 this.printErrorMessage(parseErrorMessage(errorInformation), false, module.error); 159 } 160 } 161 } 162 }); 163 164 compilation.hooks.buildModule.tap('findModule', (module) => { 165 if (module.context) { 166 if (module.context.indexOf(projectConfig.projectPath) >= 0) { 167 return; 168 } 169 const modulePath: string = path.join(module.context); 170 const srcIndex: number = modulePath.lastIndexOf(MODULE_ETS_PATH); 171 if (srcIndex < 0) { 172 return; 173 } 174 const moduleSharePath: string = path.resolve(modulePath.substring(0, srcIndex), MODULE_SHARE_PATH); 175 if (fs.existsSync(moduleSharePath)) { 176 this.moduleSharePaths.add(moduleSharePath); 177 } 178 } 179 }); 180 181 compilation.hooks.finishModules.tap('finishModules', handleFinishModules.bind(this)); 182 }); 183 184 compiler.hooks.afterCompile.tap('copyFindModule', () => { 185 this.moduleSharePaths.forEach(modulePath => { 186 circularFile(modulePath, path.resolve(projectConfig.buildPath, BUILD_SHARE_PATH)); 187 }); 188 }); 189 190 compiler.hooks.compilation.tap('CommonAsset', compilation => { 191 compilation.hooks.processAssets.tap( 192 { 193 name: 'GLOBAL_COMMON_MODULE_CACHE', 194 stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS 195 }, 196 (assets) => { 197 const GLOBAL_COMMON_MODULE_CACHE = ` 198 globalThis["__common_module_cache__${projectConfig.hashProjectPath}"] =` + 199 ` globalThis["__common_module_cache__${projectConfig.hashProjectPath}"] || {};`; 200 if (assets['commons.js']) { 201 assets['commons.js'] = new CachedSource( 202 new ConcatSource(assets['commons.js'], GLOBAL_COMMON_MODULE_CACHE)); 203 } else if (assets['vendors.js']) { 204 assets['vendors.js'] = new CachedSource( 205 new ConcatSource(assets['vendors.js'], GLOBAL_COMMON_MODULE_CACHE)); 206 } 207 }); 208 }); 209 210 compiler.hooks.compilation.tap('Require', compilation => { 211 JavascriptModulesPlugin.getCompilationHooks(compilation).renderRequire.tap('renderRequire', 212 (source) => { 213 return `var commonCachedModule = globalThis` + 214 `["__common_module_cache__${projectConfig.hashProjectPath}"] ? ` + 215 `globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` + 216 `[moduleId]: null;\n` + 217 `if (commonCachedModule) { return commonCachedModule.exports; }\n` + 218 source.replace('// Execute the module function', 219 `function isCommonModue(moduleId) { 220 if (globalThis["webpackChunk${projectConfig.hashProjectPath}"]) { 221 const length = globalThis["webpackChunk${projectConfig.hashProjectPath}"].length; 222 switch (length) { 223 case 1: 224 return globalThis["webpackChunk${projectConfig.hashProjectPath}"][0][1][moduleId]; 225 case 2: 226 return globalThis["webpackChunk${projectConfig.hashProjectPath}"][0][1][moduleId] || 227 globalThis["webpackChunk${projectConfig.hashProjectPath}"][1][1][moduleId]; 228 } 229 } 230 return undefined; 231 }\n` + 232 `if (globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` + 233 ` && String(moduleId).indexOf("?name=") < 0 && isCommonModue(moduleId)) {\n` + 234 ` globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` + 235 `[moduleId] = module;\n}`); 236 }); 237 }); 238 239 compiler.hooks.entryOption.tap('beforeRun', () => { 240 const rootFileNames: string[] = []; 241 Object.values(projectConfig.entryObj).forEach((fileName: string) => { 242 rootFileNames.push(fileName.replace('?entry', '')); 243 }); 244 if (process.env.watchMode === 'true') { 245 globalProgram.watchProgram = ts.createWatchProgram( 246 createWatchCompilerHost(rootFileNames, printDiagnostic, 247 this.delayPrintLogCount.bind(this), this.resetTsErrorCount)); 248 } else { 249 serviceChecker(rootFileNames); 250 } 251 setChecker(); 252 }); 253 254 compiler.hooks.watchRun.tap('WatchRun', (comp) => { 255 process.env.watchEts = 'start'; 256 checkErrorMessage.clear(); 257 this.clearCount(); 258 comp.modifiedFiles = comp.modifiedFiles || []; 259 comp.removedFiles = comp.removedFiles || []; 260 const watchModifiedFiles: string[] = [...comp.modifiedFiles]; 261 let watchRemovedFiles: string[] = [...comp.removedFiles]; 262 if (watchRemovedFiles.length) { 263 this.removedFiles = watchRemovedFiles; 264 } 265 if (watchModifiedFiles.length) { 266 watchModifiedFiles.some((item: string) => { 267 if (fs.statSync(item).isFile() && !/.(ts|ets)$/.test(item)) { 268 process.env.watchTs = 'end'; 269 return true; 270 } 271 }); 272 } 273 if (shouldWriteChangedList(watchModifiedFiles, watchRemovedFiles)) { 274 writeFileSync(projectConfig.changedFileList, JSON.stringify( 275 getHotReloadFiles(watchModifiedFiles, watchRemovedFiles, hotReloadSupportFiles))); 276 } 277 incrementWatchFile(watchModifiedFiles, watchRemovedFiles); 278 }); 279 280 compiler.hooks.done.tap('Result States', (stats: Stats) => { 281 if (projectConfig.isPreview && projectConfig.aceSoPath && 282 useOSFiles && useOSFiles.size > 0) { 283 writeUseOSFiles(useOSFiles); 284 } 285 if (projectConfig.compileHar) { 286 this.incrementalFileInHar.forEach((jsBuildFilePath, jsCacheFilePath) => { 287 const sourceCode: string = fs.readFileSync(jsCacheFilePath, 'utf-8'); 288 writeFileSync(jsBuildFilePath, sourceCode); 289 }); 290 } 291 this.mStats = stats; 292 this.warningCount = 0; 293 this.noteCount = 0; 294 if (this.mStats.compilation.warnings) { 295 this.mWarningCount = this.mStats.compilation.warnings.length; 296 } 297 this.printResult(); 298 }); 299 } 300 301 private resetTsErrorCount(): void { 302 checkerResult.count = 0; 303 warnCheckerResult.count = 0; 304 } 305 306 private printResult(): void { 307 this.printWarning(); 308 this.printError(); 309 if (process.env.watchMode === 'true') { 310 process.env.watchEts = 'end'; 311 this.delayPrintLogCount(true); 312 } else { 313 this.printLogCount(); 314 } 315 } 316 317 private delayPrintLogCount(isCompile: boolean = false) { 318 if (process.env.watchEts === 'end' && process.env.watchTs === 'end') { 319 this.printLogCount(); 320 process.env.watchTs = 'start'; 321 this.removedFiles = []; 322 } else if (isCompile && this.removedFiles.length && this.mErrorCount === 0 && this.mPreErrorCount > 0) { 323 this.printLogCount(); 324 } 325 this.mPreErrorCount = this.mErrorCount; 326 } 327 328 private printLogCount(): void { 329 let errorCount: number = this.mErrorCount + checkerResult.count; 330 const warnCount: number = this.warningCount + warnCheckerResult.count; 331 if (errorCount + warnCount + this.noteCount > 0 || process.env.abcCompileSuccess === 'false') { 332 let result: string; 333 let resultInfo: string = ''; 334 if (errorCount > 0) { 335 resultInfo += `ERROR:${errorCount}`; 336 result = 'FAIL '; 337 process.exitCode = 1; 338 } else { 339 result = 'SUCCESS '; 340 } 341 if (process.env.abcCompileSuccess === 'false') { 342 result = 'FAIL '; 343 } 344 if (warnCount > 0) { 345 resultInfo += ` WARN:${warnCount}`; 346 } 347 if (this.noteCount > 0) { 348 resultInfo += ` NOTE:${this.noteCount}`; 349 } 350 if (result === 'SUCCESS ' && process.env.watchMode === 'true') { 351 this.printPreviewResult(resultInfo); 352 } else { 353 logger.info(this.blue, 'COMPILE RESULT:' + result + `{${resultInfo}}`, this.reset); 354 } 355 } else { 356 if (process.env.watchMode === 'true') { 357 this.printPreviewResult(); 358 } else { 359 console.info(this.blue, 'COMPILE RESULT:SUCCESS ', this.reset); 360 } 361 } 362 } 363 364 private clearCount(): void { 365 this.mErrorCount = 0; 366 this.warningCount = 0; 367 this.noteCount = 0; 368 process.env.abcCompileSuccess = 'true'; 369 } 370 371 private printPreviewResult(resultInfo: string = ''): void { 372 const workerNum: number = Object.keys(cluster.workers).length; 373 const blue: string = this.blue; 374 const reset: string = this.reset; 375 if (workerNum === 0) { 376 this.printSuccessInfo(blue, reset, resultInfo); 377 } 378 } 379 380 private printSuccessInfo(blue: string, reset: string, resultInfo: string): void { 381 if (projectConfig.hotReload) { 382 this.hotReloadIncrementalTime.hotReloadIncrementalEndTime = new Date().getTime().toString(); 383 console.info(blue, 'Incremental build start: ' + this.hotReloadIncrementalTime.hotReloadIncrementalStartTime 384 +'\n' + 'Incremental build end: ' + this.hotReloadIncrementalTime.hotReloadIncrementalEndTime, reset); 385 } 386 if (resultInfo.length === 0) { 387 console.info(blue, 'COMPILE RESULT:SUCCESS ', reset); 388 } else { 389 console.info(blue, 'COMPILE RESULT:SUCCESS ' + `{${resultInfo}}`, reset); 390 } 391 } 392 393 private printWarning(): void { 394 if (this.mWarningCount > 0) { 395 const warnings: Info[] = this.mStats.compilation.warnings; 396 const length: number = warnings.length; 397 for (let index = 0; index < length; index++) { 398 const message: string = warnings[index].message.replace(/^Module Warning\s*.*:\n/, '') 399 .replace(/\(Emitted value instead of an instance of Error\) BUILD/, ''); 400 if (/^NOTE/.test(message)) { 401 if (!checkErrorMessage.has(message)) { 402 this.noteCount++; 403 logger.info(this.blue, message.replace(/^NOTE/, 'ArkTS:NOTE'), this.reset, '\n'); 404 checkErrorMessage.add(message); 405 } 406 } else { 407 if (!checkErrorMessage.has(message)) { 408 this.warningCount++; 409 logger.warn(this.yellow, message.replace(/^WARN/, 'ArkTS:WARN'), this.reset, '\n'); 410 checkErrorMessage.add(message); 411 } 412 } 413 } 414 if (this.mWarningCount > length) { 415 this.warningCount = this.warningCount + this.mWarningCount - length; 416 } 417 } 418 } 419 420 private printError(): void { 421 if (this.mStats.compilation.errors.length > 0) { 422 const errors: Info[] = [...this.mStats.compilation.errors]; 423 for (let index = 0; index < errors.length; index++) { 424 if (errors[index].issue) { 425 if (!checkErrorMessage.has(errors[index].issue)) { 426 this.mErrorCount++; 427 const position: string = errors[index].issue.location 428 ? `:${errors[index].issue.location.start.line}:${errors[index].issue.location.start.column}` 429 : ''; 430 const location: string = errors[index].issue.file.replace(/\\/g, '/') + position; 431 const detail: string = errors[index].issue.message; 432 logger.error(this.red, 'ArkTS:ERROR File: ' + location, this.reset); 433 logger.error(this.red, detail, this.reset, '\n'); 434 checkErrorMessage.add(errors[index].issue); 435 } 436 } else if (/BUILDERROR/.test(errors[index].message)) { 437 if (!checkErrorMessage.has(errors[index].message)) { 438 this.mErrorCount++; 439 const errorMessage: string = errors[index].message.replace(/^Module Error\s*.*:\n/, '') 440 .replace(/\(Emitted value instead of an instance of Error\) BUILD/, '') 441 .replace(/^ERROR/, 'ArkTS:ERROR'); 442 this.printErrorMessage(errorMessage, true, errors[index]); 443 checkErrorMessage.add(errors[index].message); 444 } 445 } else if (!/TS[0-9]+:/.test(errors[index].message.toString()) && 446 !/Module parse failed/.test(errors[index].message.toString())) { 447 this.mErrorCount++; 448 let errorMessage: string = `${errors[index].message.replace(/\[tsl\]\s*/, '') 449 .replace(/\u001b\[.*?m/g, '').replace(/\.ets\.ts/g, '.ets').trim()}\n`; 450 errorMessage = this.filterModuleError(errorMessage) 451 .replace(/^ERROR in /, 'ArkTS:ERROR File: ').replace(/\s{6}TS/g, ' TS') 452 .replace(/\(([0-9]+),([0-9]+)\)/, ':$1:$2'); 453 this.printErrorMessage(parseErrorMessage(errorMessage), false, errors[index]); 454 } 455 } 456 } 457 } 458 private printErrorMessage(errorMessage: string, lineFeed: boolean, errorInfo: Info): void { 459 const formatErrMsg = errorMessage.replace(/\\/g, '/'); 460 if (lineFeed) { 461 logger.error(this.red, formatErrMsg + '\n', this.reset); 462 } else { 463 logger.error(this.red, formatErrMsg, this.reset); 464 } 465 } 466 private filterModuleError(message: string): string { 467 if (/You may need an additional loader/.test(message) && transformLog && transformLog.sourceFile) { 468 const fileName: string = transformLog.sourceFile.fileName; 469 const errorInfos: string[] = message.split('You may need an additional loader to handle the result of these loaders.'); 470 if (errorInfos && errorInfos.length > 1 && errorInfos[1]) { 471 message = `ERROR in ${fileName}\n The following syntax is incorrect.${errorInfos[1]}`; 472 } 473 } 474 return message; 475 } 476} 477 478function handleFinishModules(modules, callback) { 479 if (projectConfig.compileHar) { 480 modules.forEach(module => { 481 if (module !== undefined && module.resourceResolveData !== undefined) { 482 const filePath: string = module.resourceResolveData.path; 483 if (!filePath.match(/node_modules/)) { 484 const jsCacheFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath, process.env.cachePath, 485 projectConfig); 486 const jsBuildFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath, 487 projectConfig.buildPath, projectConfig, true); 488 if (filePath.match(/\.e?ts$/)) { 489 this.incrementalFileInHar.set(jsCacheFilePath.replace(/\.ets$/, '.d.ets').replace(/\.ts$/, '.d.ts'), 490 jsBuildFilePath.replace(/\.ets$/, '.d.ets').replace(/\.ts$/, '.d.ts')); 491 this.incrementalFileInHar.set(jsCacheFilePath.replace(/\.e?ts$/, '.js'), jsBuildFilePath.replace(/\.e?ts$/, '.js')); 492 } else { 493 this.incrementalFileInHar.set(jsCacheFilePath, jsBuildFilePath); 494 } 495 } 496 } 497 }); 498 } 499} 500