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