• 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 RawSource from 'webpack-sources/lib/RawSource';
26import path from 'path';
27import fs from 'fs';
28import CachedSource from 'webpack-sources/lib/CachedSource';
29import ConcatSource from 'webpack-sources/lib/ConcatSource';
30
31import { transformLog } from './process_ui_syntax';
32import {
33  moduleCollection,
34  useOSFiles
35} from './validate_ui_syntax';
36import {
37  circularFile,
38  mkDir,
39  writeFileSync,
40  parseErrorMessage
41} from './utils';
42import {
43  MODULE_ETS_PATH,
44  MODULE_SHARE_PATH,
45  BUILD_SHARE_PATH
46} from './pre_define';
47import {
48  createLanguageService,
49  appComponentCollection,
50  importModuleCollection,
51  createWatchCompilerHost
52} from './ets_checker';
53import {
54  globalProgram,
55  projectConfig
56} from '../main';
57
58configure({
59  appenders: { 'ETS': {type: 'stderr', layout: {type: 'messagePassThrough'}}},
60  categories: {'default': {appenders: ['ETS'], level: 'info'}}
61});
62export const logger = getLogger('ETS');
63
64export const props: string[] = [];
65
66interface Info {
67  message?: string;
68  issue?: {
69    message: string,
70    file: string,
71    location: { start?: { line: number, column: number } }
72  };
73}
74
75export interface CacheFileName {
76  mtimeMs: number,
77  children: string[],
78  parent: string[],
79  error: boolean
80}
81
82interface NeedUpdateFlag {
83  flag: boolean;
84}
85
86export let cache: Cache = {};
87export const shouldResolvedFiles: Set<string> = new Set()
88const checkErrorMessage: Set<string | Info> = new Set([]);
89type Cache = Record<string, CacheFileName>;
90
91export class ResultStates {
92  private mStats: Stats;
93  private mErrorCount: number = 0;
94  private mPreErrorCount: number = 0;
95  private tsErrorCount: number = 0;
96  private mWarningCount: number = 0;
97  private warningCount: number = 0;
98  private noteCount: number = 0;
99  private red: string = '\u001b[31m';
100  private yellow: string = '\u001b[33m';
101  private blue: string = '\u001b[34m';
102  private reset: string = '\u001b[39m';
103  private moduleSharePaths: Set<string> = new Set([]);
104  private removedFiles: string[] = [];
105
106  public apply(compiler: Compiler): void {
107    compiler.hooks.compilation.tap('SourcemapFixer', compilation => {
108      compilation.hooks.afterProcessAssets.tap('SourcemapFixer', assets => {
109        Reflect.ownKeys(assets).forEach(key => {
110          if (/\.map$/.test(key.toString()) && assets[key]._value) {
111            assets[key]._value = assets[key]._value.toString().replace('.ets?entry', '.ets');
112            assets[key]._value = assets[key]._value.toString().replace('.ts?entry', '.ts');
113          }
114        });
115      }
116      );
117
118      compilation.hooks.succeedModule.tap('findModule', (module) => {
119        if (module && module.error) {
120          const errorLog: string = module.error.toString();
121          if (module.resourceResolveData && module.resourceResolveData.path &&
122            /Module parse failed/.test(errorLog) && /Invalid regular expression:/.test(errorLog)) {
123            this.mErrorCount++;
124            const errorInfos: string[] = errorLog.split('\n>')[1].split(';');
125            if (errorInfos && errorInfos.length > 0 && errorInfos[0]) {
126              const errorInformation: string = `ERROR in ${module.resourceResolveData.path}\n The following syntax is incorrect.\n > ${errorInfos[0]}`;
127              this.printErrorMessage(parseErrorMessage(errorInformation), false, module.error);
128            }
129          }
130        }
131      });
132
133      compilation.hooks.buildModule.tap('findModule', (module) => {
134        if (module.context) {
135          if (module.context.indexOf(projectConfig.projectPath) >= 0) {
136            return;
137          }
138          const modulePath: string = path.join(module.context);
139          const srcIndex: number = modulePath.lastIndexOf(MODULE_ETS_PATH);
140          if (srcIndex < 0) {
141            return;
142          }
143          const moduleSharePath: string = path.resolve(modulePath.substring(0, srcIndex), MODULE_SHARE_PATH);
144          if (fs.existsSync(moduleSharePath)) {
145            this.moduleSharePaths.add(moduleSharePath);
146          }
147        }
148      });
149    });
150
151    compiler.hooks.afterCompile.tap('copyFindModule', () => {
152      this.moduleSharePaths.forEach(modulePath => {
153        circularFile(modulePath, path.resolve(projectConfig.buildPath, BUILD_SHARE_PATH));
154      });
155    });
156
157    compiler.hooks.compilation.tap('CommonAsset', compilation => {
158      compilation.hooks.processAssets.tap(
159        {
160          name: 'GLOBAL_COMMON_MODULE_CACHE',
161          stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
162        },
163        (assets) => {
164          const GLOBAL_COMMON_MODULE_CACHE = `
165          globalThis["__common_module_cache__${projectConfig.hashProjectPath}"] =` +
166          ` globalThis["__common_module_cache__${projectConfig.hashProjectPath}"] || {};`;
167
168          if (assets['commons.js']) {
169            assets['commons.js'] = new CachedSource(
170              new ConcatSource(assets['commons.js'], GLOBAL_COMMON_MODULE_CACHE));
171          } else if (assets['vendors.js']) {
172            assets['vendors.js'] = new CachedSource(
173              new ConcatSource(assets['vendors.js'], GLOBAL_COMMON_MODULE_CACHE));
174          }
175        });
176    });
177
178    compiler.hooks.compilation.tap('Require', compilation => {
179      JavascriptModulesPlugin.getCompilationHooks(compilation).renderRequire.tap('renderRequire',
180        (source) => {
181          return `var commonCachedModule = globalThis` +
182          `["__common_module_cache__${projectConfig.hashProjectPath}"] ? ` +
183            `globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` +
184            `[moduleId]: null;\n` +
185            `if (commonCachedModule) { return commonCachedModule.exports; }\n` +
186            source.replace('// Execute the module function',
187              `function isCommonModue(moduleId) {
188                if (globalThis["webpackChunk${projectConfig.hashProjectPath}"]) {
189                  const length = globalThis["webpackChunk${projectConfig.hashProjectPath}"].length;
190                  switch (length) {
191                    case 1:
192                      return globalThis["webpackChunk${projectConfig.hashProjectPath}"][0][1][moduleId];
193                    case 2:
194                      return globalThis["webpackChunk${projectConfig.hashProjectPath}"][0][1][moduleId] ||
195                      globalThis["webpackChunk${projectConfig.hashProjectPath}"][1][1][moduleId];
196                  }
197                }
198                return undefined;
199              }\n` +
200              `if (globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` +
201              ` && String(moduleId).indexOf("?name=") < 0 && isCommonModue(moduleId)) {\n` +
202              `  globalThis["__common_module_cache__${projectConfig.hashProjectPath}"]` +
203              `[moduleId] = module;\n}`);
204        });
205    });
206
207    compiler.hooks.entryOption.tap('beforeRun', () => {
208      const rootFileNames: string[] = [];
209      Object.values(projectConfig.entryObj).forEach((fileName: string) => {
210        rootFileNames.push(fileName.replace('?entry', ''));
211      });
212      if (process.env.watchMode === 'true') {
213        globalProgram.watchProgram = ts.createWatchProgram(
214          createWatchCompilerHost(rootFileNames, this.printDiagnostic.bind(this),
215            this.delayPrintLogCount.bind(this), this.resetTsErrorCount.bind(this)));
216      } else {
217        let languageService: ts.LanguageService = null;
218        let cacheFile: string = null;
219        if (projectConfig.xtsMode) {
220          languageService = createLanguageService(rootFileNames);
221        } else {
222          cacheFile = path.resolve(projectConfig.cachePath, '../.ts_checker_cache');
223          cache = fs.existsSync(cacheFile) ? JSON.parse(fs.readFileSync(cacheFile).toString()) : {};
224          const filterFiles: string[] = filterInput(rootFileNames);
225          languageService = createLanguageService(filterFiles);
226        }
227        globalProgram.program = languageService.getProgram();
228        const allDiagnostics: ts.Diagnostic[] = globalProgram.program
229          .getSyntacticDiagnostics()
230          .concat(globalProgram.program.getSemanticDiagnostics())
231          .concat(globalProgram.program.getDeclarationDiagnostics());
232        allDiagnostics.forEach((diagnostic: ts.Diagnostic) => {
233          this.printDiagnostic(diagnostic);
234        });
235        if (process.env.watchMode !== 'true' && !projectConfig.xtsMode) {
236          fs.writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
237        }
238      }
239    });
240
241    compiler.hooks.done.tap('Result States', (stats: Stats) => {
242      if (projectConfig.isPreview && projectConfig.aceSoPath &&
243        useOSFiles && useOSFiles.size > 0) {
244        this.writeUseOSFiles();
245      }
246      this.mStats = stats;
247      this.warningCount = 0;
248      this.noteCount = 0;
249      if (this.mStats.compilation.warnings) {
250        this.mWarningCount = this.mStats.compilation.warnings.length;
251      }
252      this.printResult();
253    });
254
255    compiler.hooks.watchRun.tap('Listening State', (comp: Compiler) => {
256      checkErrorMessage.clear();
257      this.clearCount();
258      process.env.watchEts = 'start';
259      comp.modifiedFiles = comp.modifiedFiles || [];
260      comp.removedFiles = comp.removedFiles || [];
261      const watchModifiedFiles: string[] = [...comp.modifiedFiles];
262      const 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      const changedFiles: string[] = [...watchModifiedFiles, ...watchRemovedFiles];
275      if (changedFiles.length) {
276        shouldResolvedFiles.clear();
277      }
278      changedFiles.forEach((file) => {
279        this.judgeFileShouldResolved(file, shouldResolvedFiles)
280      })
281    });
282
283    if (!projectConfig.isPreview) {
284      compiler.hooks.compilation.tap('Collect Components And Modules', compilation => {
285        compilation.hooks.additionalAssets.tapAsync('Collect Components And Modules', callback => {
286          this.generateCollectionFile();
287          callback();
288        });
289      });
290    }
291  }
292
293  private judgeFileShouldResolved(file: string, shouldResolvedFiles: Set<string>): void {
294    if (shouldResolvedFiles.has(file)) {
295      return;
296    }
297    shouldResolvedFiles.add(file);
298    if (cache && cache[file] && cache[file].parent) {
299      cache[file].parent.forEach((item)=>{
300        this.judgeFileShouldResolved(item, shouldResolvedFiles);
301      })
302      cache[file].parent = [];
303    }
304    if (cache && cache[file] && cache[file].children) {
305      cache[file].children.forEach((item)=>{
306        this.judgeFileShouldResolved(item, shouldResolvedFiles);
307      })
308      cache[file].children = [];
309    }
310  }
311
312  private generateCollectionFile() {
313    if (projectConfig.aceSuperVisualPath && fs.existsSync(projectConfig.aceSuperVisualPath)) {
314      appComponentCollection.clear();
315    }
316    if (fs.existsSync(path.resolve(projectConfig.buildPath, './module_collection.txt'))) {
317      const lastModuleCollection: string =
318        fs.readFileSync(path.resolve(projectConfig.buildPath, './module_collection.txt')).toString();
319      if (lastModuleCollection && lastModuleCollection !== 'NULL') {
320        lastModuleCollection.split(',').forEach(item => {
321          moduleCollection.add(item);
322        })
323      }
324    }
325    const moduleContent: string =
326      moduleCollection.size === 0 ? 'NULL' : Array.from(moduleCollection).join(',');
327    writeFileSync(path.resolve(projectConfig.buildPath, './module_collection.txt'),
328      moduleContent);
329    const componentContent: string = Array.from(appComponentCollection).join(',');
330    writeFileSync(path.resolve(projectConfig.buildPath, './component_collection.txt'),
331      componentContent);
332  }
333
334  private printDiagnostic(diagnostic: ts.Diagnostic): void {
335    const message: string = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
336    if (this.validateError(message)) {
337      if (process.env.watchMode !== 'true' && !projectConfig.xtsMode) {
338        updateErrorFileCache(diagnostic);
339      }
340      this.tsErrorCount += 1;
341      if (diagnostic.file) {
342        const { line, character }: ts.LineAndCharacter =
343          diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!);
344        logger.error(this.red,
345          `ArkTS:ERROR File: ${diagnostic.file.fileName}:${line + 1}:${character + 1}\n ${message}\n`, this.reset);
346      } else {
347        logger.error(this.red, `ArkTS:ERROR: ${message}`, this.reset);
348      }
349    }
350  }
351
352  private resetTsErrorCount(): void {
353    this.tsErrorCount = 0;
354  }
355
356  private writeUseOSFiles(): void {
357    let info: string = '';
358    if (!fs.existsSync(projectConfig.aceSoPath)) {
359      const parent: string = path.join(projectConfig.aceSoPath, '..');
360      if (!(fs.existsSync(parent) && !fs.statSync(parent).isFile())) {
361        mkDir(parent);
362      }
363    } else {
364      info = fs.readFileSync(projectConfig.aceSoPath, 'utf-8') + '\n';
365    }
366    fs.writeFileSync(projectConfig.aceSoPath, info + Array.from(useOSFiles).join('\n'));
367  }
368
369  private printResult(): void {
370    this.printWarning();
371    this.printError();
372    if (process.env.watchMode === 'true') {
373      process.env.watchEts = 'end';
374      this.delayPrintLogCount(true);
375    } else {
376      this.printLogCount();
377    }
378  }
379
380  private delayPrintLogCount(isCompile: boolean = false) {
381    if (process.env.watchEts === 'end' && process.env.watchTs === 'end') {
382      this.printLogCount();
383      process.env.watchTs = 'start';
384      this.removedFiles = [];
385    } else if (isCompile && this.removedFiles.length && this.mErrorCount === 0 &&
386      this.mPreErrorCount > 0) {
387      this.printLogCount();
388    }
389    this.mPreErrorCount = this.mErrorCount;
390  }
391
392  private printLogCount(): void {
393    this.mErrorCount += this.tsErrorCount;
394    if (this.mErrorCount + this.warningCount + this.noteCount > 0) {
395      let result: string;
396      let resultInfo: string = '';
397      if (this.mErrorCount > 0) {
398        resultInfo += `ERROR:${this.mErrorCount}`;
399        result = 'FAIL ';
400        if (!/ets_loader_ark$/.test(path.resolve(__dirname, '..'))) {
401          process.exitCode = 1;
402        }
403      } else {
404        result = 'SUCCESS ';
405      }
406      if (this.warningCount > 0) {
407        resultInfo += ` WARN:${this.warningCount}`;
408      }
409      if (this.noteCount > 0) {
410        resultInfo += ` NOTE:${this.noteCount}`;
411      }
412      logger.info(this.blue, 'COMPILE RESULT:' + result + `{${resultInfo}}`, this.reset);
413    } else {
414      console.info(this.blue, 'COMPILE RESULT:SUCCESS ', this.reset);
415    }
416  }
417
418  private clearCount(): void {
419    this.mErrorCount = 0;
420    this.warningCount = 0;
421    this.noteCount = 0;
422  }
423
424  private printWarning(): void {
425    if (this.mWarningCount > 0) {
426      const warnings: Info[] = this.mStats.compilation.warnings;
427      const length: number = warnings.length;
428      for (let index = 0; index < length; index++) {
429        const message: string = warnings[index].message.replace(/^Module Warning\s*.*:\n/, '')
430          .replace(/\(Emitted value instead of an instance of Error\) BUILD/, '');
431        if (/^NOTE/.test(message)) {
432          if (!checkErrorMessage.has(message)) {
433            this.noteCount++;
434            logger.info(this.blue, message, this.reset, '\n');
435            checkErrorMessage.add(message);
436          }
437        } else {
438          if (!checkErrorMessage.has(message)) {
439            this.warningCount++;
440            logger.warn(this.yellow, message.replace(/^WARN/, 'ArkTS:WARN'), this.reset, '\n');
441            checkErrorMessage.add(message);
442          }
443        }
444      }
445      if (this.mWarningCount > length) {
446        this.warningCount = this.warningCount + this.mWarningCount - length;
447      }
448    }
449  }
450
451  private printError(): void {
452    if (this.mStats.compilation.errors.length > 0) {
453      const errors: Info[] = [...this.mStats.compilation.errors];
454      for (let index = 0; index < errors.length; index++) {
455        if (errors[index].issue) {
456          if (!checkErrorMessage.has(errors[index].issue)) {
457            this.mErrorCount++;
458            const position: string = errors[index].issue.location
459              ? `:${errors[index].issue.location.start.line}:${errors[index].issue.location.start.column}`
460              : '';
461            const location: string = errors[index].issue.file.replace(/\\/g, '/') + position;
462            const detail: string = errors[index].issue.message;
463            logger.error(this.red, 'ArkTS:ERROR File: ' + location, this.reset);
464            logger.error(this.red, detail, this.reset, '\n');
465            checkErrorMessage.add(errors[index].issue);
466          }
467        } else if (/BUILDERROR/.test(errors[index].message)) {
468          if (!checkErrorMessage.has(errors[index].message)) {
469            this.mErrorCount++;
470            const errorMessage: string = errors[index].message.replace(/^Module Error\s*.*:\n/, '')
471              .replace(/\(Emitted value instead of an instance of Error\) BUILD/, '')
472              .replace(/^ERROR/, 'ArkTS:ERROR');
473            this.printErrorMessage(errorMessage, true, errors[index]);
474            checkErrorMessage.add(errors[index].message);
475          }
476        } else if (!/TS[0-9]+:/.test(errors[index].message.toString()) &&
477          !/Module parse failed/.test(errors[index].message.toString())) {
478          this.mErrorCount++;
479          let errorMessage: string = `${errors[index].message.replace(/\[tsl\]\s*/, '')
480            .replace(/\u001b\[.*?m/g, '').replace(/\.ets\.ts/g, '.ets').trim()}\n`;
481          errorMessage = this.filterModuleError(errorMessage)
482            .replace(/^ERROR in /, 'ArkTS:ERROR File: ').replace(/\s{6}TS/g, ' TS')
483            .replace(/\(([0-9]+),([0-9]+)\)/, ':$1:$2');
484          this.printErrorMessage(parseErrorMessage(errorMessage), false, errors[index]);
485        }
486      }
487    }
488  }
489  private printErrorMessage(errorMessage: string, lineFeed: boolean, errorInfo: Info): void {
490    if (this.validateError(errorMessage)) {
491      const formatErrMsg = errorMessage.replace(/\\/g, '/');
492      if (lineFeed) {
493        logger.error(this.red, formatErrMsg + '\n', this.reset);
494      } else {
495        logger.error(this.red, formatErrMsg, this.reset);
496      }
497    } else {
498      const errorsIndex = this.mStats.compilation.errors.indexOf(errorInfo);
499      this.mStats.compilation.errors.splice(errorsIndex, 1);
500      this.mErrorCount = this.mErrorCount - 1;
501    }
502  }
503  private validateError(message: string): boolean {
504    const propInfoReg: RegExp = /Cannot find name\s*'(\$?\$?[_a-zA-Z0-9]+)'/;
505    const stateInfoReg: RegExp = /Property\s*'(\$?[_a-zA-Z0-9]+)' does not exist on type/;
506    const importInfoReg: RegExp = /Cannot find namespace\s*'([_a-zA-Z0-9]+)'\./;
507    if (this.matchMessage(message, props, propInfoReg) ||
508      this.matchMessage(message, props, stateInfoReg)) {
509      return false;
510    }
511    return true;
512  }
513  private matchMessage(message: string, nameArr: any, reg: RegExp): boolean {
514    if (reg.test(message)) {
515      const match: string[] = message.match(reg);
516      if (match[1] && nameArr.includes(match[1])) {
517        return true;
518      }
519    }
520    return false;
521  }
522  private filterModuleError(message: string): string {
523    if (/You may need an additional loader/.test(message) && transformLog && transformLog.sourceFile) {
524      const fileName: string = transformLog.sourceFile.fileName;
525      const errorInfos: string[] = message.split('You may need an additional loader to handle the result of these loaders.');
526      if (errorInfos && errorInfos.length > 1 && errorInfos[1]) {
527        message = `ERROR in ${fileName}\n The following syntax is incorrect.${errorInfos[1]}`;
528      }
529    }
530    return message;
531  }
532}
533
534function updateErrorFileCache(diagnostic: ts.Diagnostic): void {
535  if (diagnostic.file && cache[path.resolve(diagnostic.file.fileName)]) {
536    cache[path.resolve(diagnostic.file.fileName)].error = true;
537  }
538}
539
540function filterInput(rootFileNames: string[]): string[] {
541  return rootFileNames.filter((file: string) => {
542    const needUpdate: NeedUpdateFlag = { flag: false };
543    const alreadyCheckedFiles: Set<string> = new Set();
544    checkNeedUpdateFiles(path.resolve(file), needUpdate, alreadyCheckedFiles);
545    return needUpdate.flag;
546  });
547}
548
549function checkNeedUpdateFiles(file: string, needUpdate: NeedUpdateFlag, alreadyCheckedFiles: Set<string>): void {
550  if (alreadyCheckedFiles.has(file)) {
551    return;
552  } else {
553    alreadyCheckedFiles.add(file);
554  }
555
556  if (needUpdate.flag) {
557    return;
558  }
559
560  const value: CacheFileName = cache[file];
561  const mtimeMs: number = fs.statSync(file).mtimeMs;
562  if (value) {
563    if (value.error || value.mtimeMs !== mtimeMs) {
564      needUpdate.flag = true;
565      return;
566    }
567    for (let i = 0; i < value.children.length; ++i) {
568      if (fs.existsSync(value.children[i])) {
569        checkNeedUpdateFiles(value.children[i], needUpdate, alreadyCheckedFiles);
570      } else {
571        needUpdate.flag = true;
572      }
573    }
574  } else {
575    cache[file] = { mtimeMs, children: [], parent: [], error: false };
576    needUpdate.flag = true;
577  }
578}
579