• 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 path from 'path';
17import ts from 'typescript';
18import fs from 'fs';
19import os from 'os';
20import uglifyJS from 'uglify-js';
21
22import {
23  projectConfig,
24  partialUpdateConfig
25} from '../main';
26import { createHash } from 'crypto';
27import {
28  AUXILIARY,
29  EXTNAME_ETS,
30  EXTNAME_CJS,
31  EXTNAME_MJS,
32  EXTNAME_JS,
33  MAIN,
34  FAIL,
35  TEMPORARY,
36  ESMODULE,
37  $$
38} from './pre_define';
39
40export enum LogType {
41  ERROR = 'ERROR',
42  WARN = 'WARN',
43  NOTE = 'NOTE'
44}
45export const TEMPORARYS: string = 'temporarys';
46export const BUILD: string = 'build';
47export const SRC_MAIN: string = 'src/main';
48
49const red: string = '\u001b[31m';
50const reset: string = '\u001b[39m';
51
52const WINDOWS: string = 'Windows_NT';
53const LINUX: string = 'Linux';
54const MAC: string = 'Darwin';
55
56export interface LogInfo {
57  type: LogType,
58  message: string,
59  pos?: number,
60  line?: number,
61  column?: number,
62  fileName?: string
63}
64
65export const repeatLog: Map<string, LogInfo> = new Map();
66
67export class FileLog {
68  private _sourceFile: ts.SourceFile;
69  private _errors: LogInfo[] = [];
70
71  public get sourceFile() {
72    return this._sourceFile;
73  }
74
75  public set sourceFile(newValue: ts.SourceFile) {
76    this._sourceFile = newValue;
77  }
78
79  public get errors() {
80    return this._errors;
81  }
82
83  public set errors(newValue: LogInfo[]) {
84    this._errors = newValue;
85  }
86}
87
88export function emitLogInfo(loader: any, infos: LogInfo[], fastBuild: boolean = false,
89  resourcePath: string = null): void {
90  if (infos && infos.length) {
91    infos.forEach((item) => {
92      switch (item.type) {
93        case LogType.ERROR:
94          fastBuild ? loader.error('\u001b[31m' + getMessage(item.fileName || resourcePath, item, true)) :
95            loader.emitError(getMessage(item.fileName || loader.resourcePath, item));
96          break;
97        case LogType.WARN:
98          fastBuild ? loader.warn('\u001b[33m' + getMessage(item.fileName || resourcePath, item, true)) :
99            loader.emitWarning(getMessage(item.fileName || loader.resourcePath, item));
100          break;
101        case LogType.NOTE:
102          fastBuild ? loader.info('\u001b[34m' + getMessage(item.fileName || resourcePath, item, true)) :
103            loader.emitWarning(getMessage(loader.resourcePath, item));
104          break;
105      }
106    });
107  }
108}
109
110export function addLog(type: LogType, message: string, pos: number, log: LogInfo[],
111  sourceFile: ts.SourceFile) {
112  const posOfNode: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(pos);
113  log.push({
114    type: type,
115    message: message,
116    line: posOfNode.line + 1,
117    column: posOfNode.character + 1,
118    fileName: sourceFile.fileName
119  });
120}
121
122export function getMessage(fileName: string, info: LogInfo, fastBuild: boolean = false): string {
123  let message: string;
124  if (info.line && info.column) {
125    message = `BUILD${info.type} File: ${fileName}:${info.line}:${info.column}\n ${info.message}`;
126  } else {
127    message = `BUILD${info.type} File: ${fileName}\n ${info.message}`;
128  }
129  if (fastBuild) {
130    message = message.replace(/^BUILD/, 'ArkTS:');
131  }
132  return message;
133}
134
135export function getTransformLog(transformLog: FileLog): LogInfo[] {
136  const sourceFile: ts.SourceFile = transformLog.sourceFile;
137  const logInfos: LogInfo[] = transformLog.errors.map((item) => {
138    if (item.pos) {
139      if (!item.column || !item.line) {
140        const posOfNode: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(item.pos);
141        item.line = posOfNode.line + 1;
142        item.column = posOfNode.character + 1;
143      }
144    } else {
145      item.line = item.line || undefined;
146      item.column = item.column || undefined;
147    }
148    if (!item.fileName) {
149      item.fileName = sourceFile.fileName;
150    }
151    return item;
152  });
153  return logInfos;
154}
155
156class ComponentInfo {
157  private _id: number = 0;
158  private _componentNames: Set<string> = new Set(['ForEach']);
159  public set id(id: number) {
160    this._id = id;
161  }
162  public get id() {
163    return this._id;
164  }
165  public set componentNames(componentNames: Set<string>) {
166    this._componentNames = componentNames;
167  }
168  public get componentNames() {
169    return this._componentNames;
170  }
171}
172
173export const componentInfo: ComponentInfo = new ComponentInfo();
174
175export function hasDecorator(node: ts.MethodDeclaration | ts.FunctionDeclaration |
176  ts.StructDeclaration | ts.ClassDeclaration, decortorName: string, customBuilder?: ts.Decorator[]): boolean {
177  if (node.decorators && node.decorators.length) {
178    for (let i = 0; i < node.decorators.length; i++) {
179      if (node.decorators[i].getText().replace(/\(.*\)$/, '').trim() === decortorName) {
180        if (customBuilder) {
181          customBuilder.push(...node.decorators.slice(i + 1), ...node.decorators.slice(0, i));
182        }
183        return true;
184      }
185    }
186  }
187  return false;
188}
189
190const STATEMENT_EXPECT: number = 1128;
191const SEMICOLON_EXPECT: number = 1005;
192const STATESTYLES_EXPECT: number = 1003;
193export const IGNORE_ERROR_CODE: number[] = [STATEMENT_EXPECT, SEMICOLON_EXPECT, STATESTYLES_EXPECT];
194
195export function readFile(dir: string, utFiles: string[]) {
196  try {
197    const files: string[] = fs.readdirSync(dir);
198    files.forEach((element) => {
199      const filePath: string = path.join(dir, element);
200      const status: fs.Stats = fs.statSync(filePath);
201      if (status.isDirectory()) {
202        readFile(filePath, utFiles);
203      } else {
204        utFiles.push(filePath);
205      }
206    });
207  } catch (e) {
208    console.error(red, 'ArkTS ERROR: ' + e, reset);
209  }
210}
211
212export function circularFile(inputPath: string, outputPath: string): void {
213  if (!inputPath || !outputPath) {
214    return;
215  }
216  fs.readdir(inputPath, function(err, files) {
217    if (!files) {
218      return;
219    }
220    files.forEach(file => {
221      const inputFile: string = path.resolve(inputPath, file);
222      const outputFile: string = path.resolve(outputPath, file);
223      const fileStat: fs.Stats = fs.statSync(inputFile);
224      if (fileStat.isFile()) {
225        copyFile(inputFile, outputFile);
226      } else {
227        circularFile(inputFile, outputFile);
228      }
229    });
230  });
231}
232
233function copyFile(inputFile: string, outputFile: string): void {
234  try {
235    const parent: string = path.join(outputFile, '..');
236    if (!(fs.existsSync(parent) && fs.statSync(parent).isDirectory())) {
237      mkDir(parent);
238    }
239    if (fs.existsSync(outputFile)) {
240      return;
241    }
242    const readStream: fs.ReadStream = fs.createReadStream(inputFile);
243    const writeStream: fs.WriteStream = fs.createWriteStream(outputFile);
244    readStream.pipe(writeStream);
245    readStream.on('close', function() {
246      writeStream.end();
247    });
248  } catch (err) {
249    throw err.message;
250  }
251}
252
253export function mkDir(path_: string): void {
254  const parent: string = path.join(path_, '..');
255  if (!(fs.existsSync(parent) && !fs.statSync(parent).isFile())) {
256    mkDir(parent);
257  }
258  fs.mkdirSync(path_);
259}
260
261export function toUnixPath(data: string): string {
262  if (/^win/.test(require('os').platform())) {
263    const fileTmps: string[] = data.split(path.sep);
264    const newData: string = path.posix.join(...fileTmps);
265    return newData;
266  }
267  return data;
268}
269
270export function toHashData(path: string): any {
271  const content: string = fs.readFileSync(path).toString();
272  const hash: any = createHash('sha256');
273  hash.update(content);
274  return hash.digest('hex');
275}
276
277export function writeFileSync(filePath: string, content: string): void {
278  if (!fs.existsSync(filePath)) {
279    const parent: string = path.join(filePath, '..');
280    if (!(fs.existsSync(parent) && !fs.statSync(parent).isFile())) {
281      mkDir(parent);
282    }
283  }
284  fs.writeFileSync(filePath, content);
285}
286
287export function genTemporaryPath(filePath: string, projectPath: string, buildPath: string,
288  projectConfig: any, buildInHar: boolean = false): string {
289  filePath = toUnixPath(filePath).replace(/\.[cm]js$/, EXTNAME_JS);
290  projectPath = toUnixPath(projectPath);
291
292  if (process.env.compileTool === 'rollup') {
293    const projectRootPath: string = toUnixPath(buildInHar ? projectPath : projectConfig.projectRootPath);
294    const relativeFilePath: string = filePath.replace(projectRootPath, '');
295    const output: string = path.join(buildPath, relativeFilePath);
296    return output;
297  }
298
299  if (isPackageModulesFile(filePath, projectConfig)) {
300    const packageDir: string = projectConfig.packageDir;
301    const fakePkgModulesPath: string = toUnixPath(path.join(projectConfig.projectRootPath, packageDir));
302    let output: string = '';
303    if (filePath.indexOf(fakePkgModulesPath) === -1) {
304      const hapPath: string = toUnixPath(projectConfig.projectRootPath);
305      const tempFilePath: string = filePath.replace(hapPath, '');
306      const relativeFilePath: string = tempFilePath.substring(tempFilePath.indexOf(packageDir) + packageDir.length + 1);
307      output = path.join(buildPath, buildInHar ? '' : TEMPORARY, packageDir, MAIN, relativeFilePath);
308    } else {
309      output = filePath.replace(fakePkgModulesPath,
310        path.join(buildPath, buildInHar ? '' : TEMPORARY, packageDir, AUXILIARY));
311    }
312    return output;
313  }
314
315  if (filePath.indexOf(projectPath) !== -1) {
316    const relativeFilePath: string = filePath.replace(projectPath, '');
317    const output: string = path.join(buildPath, buildInHar ? '' : TEMPORARY, relativeFilePath);
318    return output;
319  }
320
321  return '';
322}
323
324export function isPackageModulesFile(filePath: string, projectConfig: any): boolean {
325  filePath = toUnixPath(filePath);
326  const hapPath: string = toUnixPath(projectConfig.projectRootPath);
327  const tempFilePath: string = filePath.replace(hapPath, '');
328  const packageDir: string = projectConfig.packageDir;
329  if (tempFilePath.indexOf(packageDir) !== -1) {
330    const fakePkgModulesPath: string = toUnixPath(path.resolve(projectConfig.projectRootPath, packageDir));
331    if (filePath.indexOf(fakePkgModulesPath) !== -1) {
332      return true;
333    }
334    if (projectConfig.modulePathMap) {
335      for (const key in projectConfig.modulePathMap) {
336        const value: string = projectConfig.modulePathMap[key];
337        const fakeModulePkgModulesPath: string = toUnixPath(path.resolve(value, packageDir));
338        if (filePath.indexOf(fakeModulePkgModulesPath) !== -1) {
339          return true;
340        }
341      }
342    }
343  }
344
345  return false;
346}
347
348export function mkdirsSync(dirname: string): boolean {
349  if (fs.existsSync(dirname)) {
350    return true;
351  } else if (mkdirsSync(path.dirname(dirname))) {
352    fs.mkdirSync(dirname);
353    return true;
354  }
355
356  return false;
357}
358
359export function generateSourceFilesInHar(sourcePath: string, sourceContent: string, suffix: string, projectConfig: any) {
360  // compileShared: compile shared har of project
361  let jsFilePath: string = genTemporaryPath(sourcePath,
362    projectConfig.compileShared ? projectConfig.projectRootPath : projectConfig.moduleRootPath,
363    projectConfig.compileShared ? path.resolve(projectConfig.aceModuleBuild, '../etsFortgz') : projectConfig.cachePath,
364    projectConfig, projectConfig.compileShared);
365  if (!jsFilePath.match(new RegExp(projectConfig.packageDir))) {
366    jsFilePath = jsFilePath.replace(/\.ets$/, suffix).replace(/\.ts$/, suffix);
367    mkdirsSync(path.dirname(jsFilePath));
368    if (projectConfig.obfuscateHarType === 'uglify' && suffix === '.js') {
369      sourceContent = uglifyJS.minify(sourceContent).code;
370    }
371    fs.writeFileSync(jsFilePath, sourceContent);
372  }
373}
374
375export function nodeLargeOrEqualTargetVersion(targetVersion: number): boolean {
376  const currentNodeVersion: number = parseInt(process.versions.node.split('.')[0]);
377  if (currentNodeVersion >= targetVersion) {
378    return true;
379  }
380
381  return false;
382}
383
384export function removeDir(dirName: string): void {
385  if (fs.existsSync(dirName)) {
386    if (nodeLargeOrEqualTargetVersion(16)) {
387      fs.rmSync(dirName, { recursive: true});
388    } else {
389      fs.rmdirSync(dirName, { recursive: true});
390    }
391  }
392}
393
394export function parseErrorMessage(message: string): string {
395  const messageArrary: string[] = message.split('\n');
396  let logContent: string = '';
397  messageArrary.forEach(element => {
398    if (!(/^at/.test(element.trim()))) {
399      logContent = logContent + element + '\n';
400    }
401  });
402  return logContent;
403}
404
405export function isWindows(): boolean {
406  return os.type() === WINDOWS;
407}
408
409export function isLinux(): boolean {
410  return os.type() === LINUX;
411}
412
413export function isMac(): boolean {
414  return os.type() === MAC;
415}
416
417export function maxFilePathLength(): number {
418  if (isWindows()) {
419    return 32766;
420  } else if (isLinux()) {
421    return 4095;
422  } else if (isMac()) {
423    return 1016;
424  } else {
425    return -1;
426  }
427}
428
429export function validateFilePathLength(filePath: string, logger: any): boolean {
430  if (maxFilePathLength() < 0) {
431    logger.error(red, "Unknown OS platform", reset);
432    process.exitCode = FAIL;
433    return false;
434  } else if (filePath.length > 0 && filePath.length <= maxFilePathLength()) {
435    return true;
436  } else if (filePath.length > maxFilePathLength()) {
437    logger.error(red, `The length of ${filePath} exceeds the limitation of current platform, which is ` +
438      `${maxFilePathLength()}. Please try moving the project folder to avoid deeply nested file path and try again`,
439    reset);
440    process.exitCode = FAIL;
441    return false;
442  } else {
443    logger.error(red, "Validate file path failed", reset);
444    process.exitCode = FAIL;
445    return false;
446  }
447}
448
449export function validateFilePathLengths(filePaths: Array<string>, logger: any): boolean {
450  filePaths.forEach((filePath) => {
451    if (!validateFilePathLength(filePath, logger)) {
452      return false;
453    }
454  })
455  return true;
456}
457
458export function unlinkSync(filePath: string): void {
459  if (fs.existsSync(filePath)) {
460    fs.unlinkSync(filePath);
461  }
462}
463
464export function getExtensionIfUnfullySpecifiedFilepath(filePath: string): string {
465  if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
466    return "";
467  }
468
469  let extension: string = EXTNAME_ETS;
470  if (fs.existsSync(filePath + '.ts') && fs.statSync(filePath + '.ts').isFile()) {
471    extension = '.ts';
472  } else if (fs.existsSync(filePath + '.d.ts') && fs.statSync(filePath + '.d.ts').isFile()) {
473    extension = '.d.ts';
474  } else if (fs.existsSync(filePath + '.d.ets') && fs.statSync(filePath + '.d.ets').isFile()) {
475    extension = '.d.ets';
476  } else if (fs.existsSync(filePath + '.js') && fs.statSync(filePath + '.js').isFile()) {
477    extension = '.js';
478  } else if (fs.existsSync(filePath + '.json') && fs.statSync(filePath + '.json').isFile()) {
479    extension = '.json';
480  }
481
482  return extension;
483}
484
485export function shouldWriteChangedList(watchModifiedFiles: string[],
486  watchRemovedFiles: string[]): boolean {
487  if (projectConfig.compileMode === ESMODULE && process.env.watchMode === 'true' && !projectConfig.isPreview &&
488    projectConfig.changedFileList && (watchRemovedFiles.length + watchModifiedFiles.length)) {
489    if (process.env.compileTool !== 'rollup') {
490      if (!(watchModifiedFiles.length === 1 &&
491        watchModifiedFiles[0] === projectConfig.projectPath && !watchRemovedFiles.length)) {
492        return true;
493      } else {
494        return false;
495      }
496    }
497    return true;
498  }
499  return false;
500}
501
502interface HotReloadIncrementalTime {
503  hotReloadIncrementalStartTime: string;
504  hotReloadIncrementalEndTime: string;
505}
506
507export const hotReloadIncrementalTime: HotReloadIncrementalTime = {
508  hotReloadIncrementalStartTime: '',
509  hotReloadIncrementalEndTime: ''
510};
511
512interface FilesObj {
513  modifiedFiles: string[],
514  removedFiles: string[]
515}
516
517let allModifiedFiles: Set<string> = new Set();
518
519export function getHotReloadFiles(watchModifiedFiles: string[],
520  watchRemovedFiles: string[], hotReloadSupportFiles: Set<string>): FilesObj {
521  hotReloadIncrementalTime.hotReloadIncrementalStartTime = new Date().getTime().toString();
522  watchRemovedFiles = watchRemovedFiles.map(file => path.relative(projectConfig.projectPath, file));
523  allModifiedFiles = new Set([...allModifiedFiles, ...watchModifiedFiles
524    .filter(file => fs.statSync(file).isFile() &&
525      (hotReloadSupportFiles.has(file) || !['.ets', '.ts', '.js'].includes(path.extname(file))))
526    .map(file => path.relative(projectConfig.projectPath, file))]
527    .filter(file => !watchRemovedFiles.includes(file)));
528  return {
529    modifiedFiles: [...allModifiedFiles],
530    removedFiles: [...watchRemovedFiles]
531  };
532}
533
534export function getResolveModules(projectPath: string, faMode: boolean): string[] {
535  if (faMode) {
536    return [
537      path.resolve(projectPath, '../../../../../'),
538      path.resolve(projectPath, '../../../../' + projectConfig.packageDir),
539      path.resolve(projectPath, '../../../../../' + projectConfig.packageDir),
540      path.resolve(projectPath, '../../')
541    ];
542  } else {
543    return [
544      path.resolve(projectPath, '../../../../'),
545      path.resolve(projectPath, '../../../' + projectConfig.packageDir),
546      path.resolve(projectPath, '../../../../' + projectConfig.packageDir),
547      path.resolve(projectPath, '../')
548    ];
549  }
550}
551
552export function writeUseOSFiles(useOSFiles: Set<string>): void {
553  let info: string = '';
554  if (!fs.existsSync(projectConfig.aceSoPath)) {
555    const parent: string = path.resolve(projectConfig.aceSoPath, '..');
556    if (!(fs.existsSync(parent) && !fs.statSync(parent).isFile())) {
557      mkDir(parent);
558    }
559  } else {
560    info = fs.readFileSync(projectConfig.aceSoPath, 'utf-8') + '\n';
561  }
562  fs.writeFileSync(projectConfig.aceSoPath, info + Array.from(useOSFiles).join('\n'));
563}
564
565export function getPossibleBuilderTypeParameter(parameters: ts.ParameterDeclaration[]): string[] {
566  const parameterNames: string[] = [];
567  if (!partialUpdateConfig.builderCheck) {
568    if (is$$Parameter(parameters)) {
569      parameters[0].type.members.forEach((member) => {
570        if (member.name && ts.isIdentifier(member.name)) {
571          parameterNames.push(member.name.escapedText.toString());
572        }
573      });
574    } else {
575      parameters.forEach((parameter) => {
576        if (parameter.name && ts.isIdentifier(parameter.name)) {
577          parameterNames.push(parameter.name.escapedText.toString());
578        }
579      });
580    }
581  }
582  return parameterNames;
583}
584
585function is$$Parameter(parameters: ts.ParameterDeclaration[]): boolean {
586  return parameters.length === 1 && parameters[0].name && ts.isIdentifier(parameters[0].name) &&
587    parameters[0].name.escapedText.toString() === $$ && parameters[0].type && ts.isTypeLiteralNode(parameters[0].type) &&
588    parameters[0].type.members && parameters[0].type.members.length > 0;
589}
590
591// Global Information
592class ProcessFileInfo {
593  buildStart: boolean = true;
594  wholeFileInfo: {[id: string]: SpecialArkTSFileInfo} = {}; // Saved ArkTS file's infomation
595  transformedFiles: string[] = []; // ArkTS Files which should be transformed in this compilation
596  cachedFiles: string[] = []; // ArkTS Files which should not be transformed in this compilation
597  shouldHaveEntry: string[] = []; // Which file should have @Entry decorator
598
599  addFileCacheInfo(id: string, fileCacheInfo: fileInfo): void {
600    if (id.match(/(?<!\.d)\.(ets)$/)) {
601      this.wholeFileInfo[id] = new SpecialArkTSFileInfo(fileCacheInfo);
602    }
603  }
604
605  collectTransformedFiles(id: string): void {
606    if (id.match(/(?<!\.d)\.(ets)$/)) {
607      this.transformedFiles.push(id);
608    }
609  }
610
611  collectCachedFiles(id: string): void {
612    if (id.match(/(?<!\.d)\.(ets)$/)) {
613      this.cachedFiles.push(id);
614    }
615  }
616
617  judgeShouldHaveEntryFiles(entryFileWithoutEntryDecorator: string[]): void {
618    this.shouldHaveEntry = Object.values(projectConfig.entryObj as string[]).filter((item) => {
619      return !entryFileWithoutEntryDecorator.includes(item.toLowerCase()) && item.match(/(?<!\.d)\.(ets)$/);
620    });
621  }
622
623  saveCacheFileInfo(cache) {
624    const cacheInfo: {[id: string]: fileInfo} = cache.get('fileCacheInfo') || {};
625    for (const id of this.transformedFiles) {
626      cacheInfo[id] = this.wholeFileInfo[id].fileInfo;
627    }
628    cache.set('fileCacheInfo', cacheInfo);
629  }
630}
631
632export const storedFileInfo: ProcessFileInfo = new ProcessFileInfo();
633
634export interface fileInfo {
635  hasEntry: boolean; // Has @Entry decorator or not
636}
637
638// Save single file information
639class SpecialArkTSFileInfo {
640  fileInfo: fileInfo = {
641    hasEntry: false
642  };
643  constructor(cacheInfo: fileInfo) {
644    this.fileInfo = cacheInfo || this.fileInfo;
645  }
646  get hasEntry() {
647    return this.fileInfo.hasEntry;
648  }
649  set hasEntry(value: boolean) {
650    this.fileInfo.hasEntry = value;
651  }
652}
653