• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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