• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023 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
16const { Project, Sdk, FileSystem, Logger } = require('./utils');
17const { ApiWriter, ApiExcelWriter } = require('./api_writer');
18const { SystemApiRecognizer } = require('./api_recognizer');
19const { ReporterFormat } = require('./configs');
20const ts = require('typescript');
21const fs = require('fs');
22const path = require('path');
23
24class ProgramFactory {
25  setLibPath(libPath) {
26    this.libPath = libPath;
27  }
28
29  getETSOptions(componentLibs) {
30    const tsconfig = require('../tsconfig.json');
31    const etsConfig = tsconfig.compilerOptions.ets;
32    etsConfig.libs = [...componentLibs];
33    return etsConfig;
34  }
35
36  createProgram(rootNames, apiLibs, componentLibs, esLibs) {
37    const compilerOption = {
38      target: ts.ScriptTarget.ES2017,
39      ets: this.getETSOptions([]),
40      allowJs: false,
41      lib: [...apiLibs, ...componentLibs, ...esLibs],
42      module: ts.ModuleKind.CommonJS,
43    };
44    this.compilerHost = this.createCompilerHost({
45      resolveModuleName: (moduleName) => {
46        return this.resolveModuleName(moduleName, apiLibs);
47      },
48    }, compilerOption);
49
50    if (this.libPath && fs.existsSync(this.libPath)) {
51      Logger.info('ProgramFactory', `set default lib location: ${this.libPath}`);
52      this.compilerHost.getDefaultLibLocation = () => {
53        return this.libPath;
54      };
55    }
56    return ts.createProgram({
57      rootNames: [...rootNames],
58      options: compilerOption,
59      host: this.compilerHost,
60    });
61  }
62
63  resolveModuleName(moduleName, libs) {
64    if (moduleName.startsWith('@')) {
65      const moduleFileName = `${moduleName}.d.ts`;
66      const etsModuleFileName = `${moduleName}.d.ets`;
67      for (const lib of libs) {
68        if (lib.endsWith(moduleFileName) || lib.endsWith(etsModuleFileName)) {
69          return lib;
70        }
71      }
72    }
73    return undefined;
74  }
75
76  createCompilerHost(moduleResolver, compilerOption) {
77    const compilerHost = ts.createCompilerHost(compilerOption);
78    compilerHost.resolveModuleNames = this.getResolveModuleNames(moduleResolver);
79    return compilerHost;
80  }
81
82  getResolveModuleNames(moduleResolver) {
83    return (moduleNames, containingFile, reusedNames, redirectedReference, options) => {
84      const resolvedModules = [];
85      for (const moduleName of moduleNames) {
86        const moduleLookupLocaton = ts.resolveModuleName(moduleName, containingFile, options, {
87          fileExists: (fileName) => {
88            return fileName && ts.sys.fileExists(fileName);
89          },
90          readFile: (fileName) => {
91            ts.sys.readFile(fileName);
92          },
93        });
94        if (moduleLookupLocaton.resolvedModule) {
95          resolvedModules.push(moduleLookupLocaton.resolvedModule);
96        } else {
97          const modulePath = moduleResolver.resolveModuleName(moduleName);
98          const resolved = modulePath && ts.sys.fileExists(modulePath) ? { resolvedFileName: modulePath } : undefined;
99          resolvedModules.push(resolved);
100        }
101      }
102      return resolvedModules;
103    };
104  }
105}
106
107class ApiCollector {
108  constructor(argv) {
109    const appProject = argv.app ? argv.app : (argv.dir ? argv.dir : undefined);
110    if (!appProject) {
111      throw 'app not found';
112    }
113    this.project = new Project(appProject, argv.dir !== undefined);
114    this.sdk = new Sdk(this.project, argv.sdk, argv.sdkRoot);
115    this.formatFlag = ReporterFormat.getFlag(argv.format);
116    this.outputPath = !argv.output ? appProject : argv.output;
117    this.logTag = 'ApiCollector';
118    this.debugFlag = argv.debug;
119    this.noRepeat = argv.noRepeat ? true : false;
120  }
121
122  setLibPath(libPath) {
123    this.libPath = libPath;
124    if (libPath && !fs.existsSync(this.libPath)) {
125      Logger.warn(this.logTag, `${libPath} is not exist`);
126    } else {
127      Logger.info(this.logTag, `set lib path ${libPath}`);
128    }
129    return this;
130  }
131
132  setIncludeTest(isIncludeTest) {
133    this.isIncludeTest = isIncludeTest;
134    return this;
135  }
136
137  async start() {
138    const sdkPath = this.sdk.getPath();
139    if (!sdkPath || !fs.existsSync(sdkPath)) {
140      return;
141    }
142    const handleFilePath = path.join(sdkPath, '/api/@internal/full/global.d.ts');
143    const originalContent = fs.readFileSync(handleFilePath, 'utf-8');
144    let newContent = originalContent.replace(/\import|export/g, '');
145    fs.writeFileSync(handleFilePath, newContent);
146    Logger.info(this.logTag, `scan app ${this.project.getPath()}`);
147    Logger.info(this.logTag, `sdk is in ${sdkPath}`);
148    const apiLibs = this.sdk.getApiLibs();
149    const componentLibs = this.sdk.getComponentLibs();
150    const eslibs = this.sdk.getESLibs(this.libPath);
151    const appSourceSet = this.project.getAppSources(this.isIncludeTest);
152    const programFactory = new ProgramFactory();
153    programFactory.setLibPath(this.libPath);
154    let program = programFactory.createProgram(appSourceSet, apiLibs, componentLibs, eslibs);
155
156    if (this.debugFlag) {
157      program.getSourceFiles().forEach((sf) => {
158        Logger.info('ApiCollector', sf.fileName);
159      });
160    }
161
162    let systemApiRecognizer = new SystemApiRecognizer(sdkPath);
163    systemApiRecognizer.setTypeChecker(program.getTypeChecker());
164    Logger.info(this.logTag, `start scanning ${this.project.getPath()}`);
165    appSourceSet.forEach((appCodeFilePath) => {
166      const canonicalFileName = programFactory.compilerHost.getCanonicalFileName(appCodeFilePath);
167      const sourceFile = program.getSourceFileByPath(canonicalFileName);
168      if (sourceFile) {
169        if (this.debugFlag) {
170          Logger.info(this.logTag, `scan ${sourceFile.fileName}`);
171        }
172        systemApiRecognizer.visitNode(sourceFile, sourceFile.fileName);
173      } else {
174        Logger.warn(this.logTag, `no sourceFile ${appCodeFilePath}`);
175      }
176    });
177    Logger.info(this.logTag, `end scan ${this.project.getPath()}`);
178    const apiWriter = this.getApiWriter();
179    apiWriter.add(systemApiRecognizer.getApiInformations());
180    // avoid oom
181    systemApiRecognizer = undefined;
182    program = undefined;
183    await apiWriter.flush();
184    fs.writeFileSync(handleFilePath, originalContent);
185  }
186
187  getApiWriter() {
188    if (!this.apiWriter) {
189      this.apiWriter = new ApiWriter(this.outputPath, this.formatFlag, this.noRepeat);
190    }
191    return this.apiWriter;
192  }
193
194  setApiWriter(apiWriter) {
195    this.apiWriter = apiWriter;
196  }
197}
198
199class MultiProjectApiCollector {
200  constructor(argv) {
201    this.argv = argv;
202  }
203
204  setLibPath(libPath) {
205    this.libPath = libPath;
206    if (libPath && !fs.existsSync(this.libPath)) {
207      Logger.warn(this.logTag, `${libPath} is not exist`);
208    } else {
209      Logger.info(this.logTag, `set lib path ${libPath}`);
210    }
211    return this;
212  }
213
214  setIncludeTest(isIncludeTest) {
215    this.isIncludeTest = isIncludeTest;
216    return this;
217  }
218
219  async start() {
220    const allApps = FileSystem.listAllAppDirs(this.argv.appDir);
221    if (allApps.length === 0) {
222      Logger.info('MultiProjectApiCollector', `project not found in ${this.argv.appDir}`);
223      return;
224    }
225    const output = !this.argv.output ? this.argv.appDir : this.argv.output;
226    const apiExcelWriter = new ApiExcelWriter(output);
227    apiExcelWriter.close();
228    allApps.forEach((app) => {
229      if (app) {
230        this.argv.app = app;
231        const apiCollector = new ApiCollector(this.argv);
232        apiCollector.setApiWriter(apiExcelWriter);
233        apiCollector.setLibPath(this.libPath).setIncludeTest(this.isIncludeTest).start();
234      }
235    });
236    apiExcelWriter.open();
237    await apiExcelWriter.flush();
238  }
239}
240
241exports.ApiCollector = ApiCollector;
242exports.MultiProjectApiCollector = MultiProjectApiCollector;