• 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} 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