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