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