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 let absPath: string = path.resolve(projectConfig.projectPath, key.toString().replace('.js.map', '.js')); 134 if (sourcemapNamesCollection && absPath) { 135 let map: Map<string, string> = sourcemapNamesCollection.get(absPath); 136 if (map && map.size !== 0) { 137 let names: Array<string> = Array.from(map).flat(); 138 let sourcemapObj: any = JSON.parse(assets[key]._value); 139 sourcemapObj.nameMap = names; 140 assets[key]._value = JSON.stringify(sourcemapObj); 141 } 142 } 143 } 144 }); 145 } 146 ); 147 148 compilation.hooks.succeedModule.tap('findModule', (module) => { 149 if (module && module.error) { 150 const errorLog: string = module.error.toString(); 151 if (module.resourceResolveData && module.resourceResolveData.path && 152 /Module parse failed/.test(errorLog) && /Invalid regular expression:/.test(errorLog)) { 153 this.mErrorCount++; 154 const errorInfos: string[] = errorLog.split('\n>')[1].split(';'); 155 if (errorInfos && errorInfos.length > 0 && errorInfos[0]) { 156 const errorInformation: string = `ERROR in ${module.resourceResolveData.path}\n The following syntax is incorrect.\n > ${errorInfos[0]}`; 157 this.printErrorMessage(parseErrorMessage(errorInformation), false, module.error); 158 } 159 } 160 } 161 }); 162 163 compilation.hooks.buildModule.tap('findModule', (module) => { 164 if (module.context) { 165 if (module.context.indexOf(projectConfig.projectPath) >= 0) { 166 return; 167 } 168 const modulePath: string = path.join(module.context); 169 const srcIndex: number = modulePath.lastIndexOf(MODULE_ETS_PATH); 170 if (srcIndex < 0) { 171 return; 172 } 173 const moduleSharePath: string = path.resolve(modulePath.substring(0, srcIndex), MODULE_SHARE_PATH); 174 if (fs.existsSync(moduleSharePath)) { 175 this.moduleSharePaths.add(moduleSharePath); 176 } 177 } 178 }); 179 180 compilation.hooks.finishModules.tap('finishModules', handleFinishModules.bind(this)); 181 }); 182 183 compiler.hooks.afterCompile.tap('copyFindModule', () => { 184 this.moduleSharePaths.forEach(modulePath => { 185 circularFile(modulePath, path.resolve(projectConfig.buildPath, BUILD_SHARE_PATH)); 186 }); 187 }); 188 189 compiler.hooks.compilation.tap('CommonAsset', compilation => { 190 compilation.hooks.processAssets.tap( 191 { 192 name: 'GLOBAL_COMMON_MODULE_CACHE', 193 stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS 194 }, 195 (assets) => { 196 const GLOBAL_COMMON_MODULE_CACHE = ` 197 globalThis["__common_module_cache__${projectConfig.hashProjectPath}"] =` + 198 ` globalThis["__common_module_cache__${projectConfig.hashProjectPath}"] || {};`; 199 if (assets['commons.js']) { 200 assets['commons.js'] = new CachedSource( 201 new ConcatSource(assets['commons.js'], GLOBAL_COMMON_MODULE_CACHE)); 202 } else if (assets['vendors.js']) { 203 assets['vendors.js'] = new CachedSource( 204 new ConcatSource(assets['vendors.js'], GLOBAL_COMMON_MODULE_CACHE)); 205 } 206 }); 207 }); 208 209 compiler.hooks.compilation.tap('Require', compilation => { 210 JavascriptModulesPlugin.getCompilationHooks(compilation).renderRequire.tap('renderRequire', 211 (source) => { 212 return `var commonCachedModule = globalThis` + 213 `["__common_module_cache__${projectConfig.hashProjectPath}"] ? ` + 214 `globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` + 215 `[moduleId]: null;\n` + 216 `if (commonCachedModule) { return commonCachedModule.exports; }\n` + 217 source.replace('// Execute the module function', 218 `function isCommonModue(moduleId) { 219 if (globalThis["webpackChunk${projectConfig.hashProjectPath}"]) { 220 const length = globalThis["webpackChunk${projectConfig.hashProjectPath}"].length; 221 switch (length) { 222 case 1: 223 return globalThis["webpackChunk${projectConfig.hashProjectPath}"][0][1][moduleId]; 224 case 2: 225 return globalThis["webpackChunk${projectConfig.hashProjectPath}"][0][1][moduleId] || 226 globalThis["webpackChunk${projectConfig.hashProjectPath}"][1][1][moduleId]; 227 } 228 } 229 return undefined; 230 }\n` + 231 `if (globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` + 232 ` && String(moduleId).indexOf("?name=") < 0 && isCommonModue(moduleId)) {\n` + 233 ` globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` + 234 `[moduleId] = module;\n}`); 235 }); 236 }); 237 238 compiler.hooks.entryOption.tap('beforeRun', () => { 239 const rootFileNames: string[] = []; 240 Object.values(projectConfig.entryObj).forEach((fileName: string) => { 241 rootFileNames.push(fileName.replace('?entry', '')); 242 }); 243 if (process.env.watchMode === 'true') { 244 globalProgram.watchProgram = ts.createWatchProgram( 245 createWatchCompilerHost(rootFileNames, printDiagnostic, 246 this.delayPrintLogCount.bind(this), this.resetTsErrorCount)); 247 } else { 248 serviceChecker(rootFileNames); 249 } 250 setChecker(); 251 }); 252 253 compiler.hooks.watchRun.tap('WatchRun', (comp) => { 254 process.env.watchEts = 'start'; 255 checkErrorMessage.clear(); 256 this.clearCount(); 257 comp.modifiedFiles = comp.modifiedFiles || []; 258 comp.removedFiles = comp.removedFiles || []; 259 const watchModifiedFiles: string[] = [...comp.modifiedFiles]; 260 let watchRemovedFiles: string[] = [...comp.removedFiles]; 261 if (watchRemovedFiles.length) { 262 this.removedFiles = watchRemovedFiles; 263 } 264 if (watchModifiedFiles.length) { 265 watchModifiedFiles.some((item: string) => { 266 if (fs.statSync(item).isFile() && !/.(ts|ets)$/.test(item)) { 267 process.env.watchTs = 'end'; 268 return true; 269 } 270 return false; 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): void { 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, undefined); 486 const jsBuildFilePath: string = genTemporaryPath(filePath, projectConfig.moduleRootPath, 487 projectConfig.buildPath, projectConfig, undefined, 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