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 this.moduleLookupResolutionHost()); 88 if (moduleLookupLocaton.resolvedModule) { 89 resolvedModules.push(moduleLookupLocaton.resolvedModule); 90 } else { 91 const modulePath = moduleResolver.resolveModuleName(moduleName); 92 const resolved = modulePath && ts.sys.fileExists(modulePath) ? 93 { resolvedFileName: modulePath } : 94 undefined; 95 resolvedModules.push(resolved); 96 } 97 } 98 return resolvedModules; 99 }; 100 } 101 102 moduleLookupResolutionHost() { 103 return { 104 fileExists: (fileName) => { 105 return fileName && ts.sys.fileExists(fileName); 106 }, 107 readFile: (fileName) => { 108 ts.sys.readFile(fileName); 109 }, 110 }; 111 } 112} 113 114class ApiCollector { 115 constructor(argv) { 116 const appProject = argv.app ? argv.app : (argv.dir ? argv.dir : undefined); 117 if (!appProject) { 118 throw 'app not found'; 119 } 120 this.project = new Project(appProject, argv.dir !== undefined); 121 this.sdk = new Sdk(this.project, argv.sdk, argv.sdkRoot); 122 this.formatFlag = ReporterFormat.getFlag(argv.format); 123 this.outputPath = !argv.output ? appProject : argv.output; 124 this.logTag = 'ApiCollector'; 125 this.debugFlag = argv.debug; 126 this.noRepeat = argv.noRepeat ? true : false; 127 } 128 129 setLibPath(libPath) { 130 this.libPath = libPath; 131 if (libPath && !fs.existsSync(this.libPath)) { 132 Logger.warn(this.logTag, `${libPath} is not exist`); 133 } else { 134 Logger.info(this.logTag, `set lib path ${libPath}`); 135 } 136 return this; 137 } 138 139 setIncludeTest(isIncludeTest) { 140 this.isIncludeTest = isIncludeTest; 141 return this; 142 } 143 144 async start() { 145 const sdkPath = this.sdk.getPath(); 146 if (!sdkPath || !fs.existsSync(sdkPath)) { 147 return; 148 } 149 const handleFilePath = path.join(sdkPath, '/api/@internal/full/global.d.ts'); 150 const originalContent = fs.readFileSync(handleFilePath, 'utf-8'); 151 let newContent = originalContent.replace(/\import|export/g, ''); 152 fs.writeFileSync(handleFilePath, newContent); 153 Logger.info(this.logTag, `scan app ${this.project.getPath()}`); 154 Logger.info(this.logTag, `sdk is in ${sdkPath}`); 155 const apiLibs = this.sdk.getApiLibs(); 156 const componentLibs = this.sdk.getComponentLibs(); 157 const eslibs = this.sdk.getESLibs(this.libPath); 158 const appSourceSet = this.project.getAppSources(this.isIncludeTest); 159 const programFactory = new ProgramFactory(); 160 programFactory.setLibPath(this.libPath); 161 let program = programFactory.createProgram(appSourceSet, apiLibs, componentLibs, eslibs); 162 163 if (this.debugFlag) { 164 program.getSourceFiles().forEach((sf) => { 165 Logger.info('ApiCollector', sf.fileName); 166 }); 167 } 168 169 let systemApiRecognizer = new SystemApiRecognizer(sdkPath); 170 systemApiRecognizer.setTypeChecker(program.getTypeChecker()); 171 Logger.info(this.logTag, `start scanning ${this.project.getPath()}`); 172 appSourceSet.forEach((appCodeFilePath) => { 173 const canonicalFileName = programFactory.compilerHost.getCanonicalFileName(appCodeFilePath); 174 const sourceFile = program.getSourceFileByPath(canonicalFileName); 175 if (sourceFile) { 176 if (this.debugFlag) { 177 Logger.info(this.logTag, `scan ${sourceFile.fileName}`); 178 } 179 systemApiRecognizer.visitNode(sourceFile, sourceFile.fileName); 180 } else { 181 Logger.warn(this.logTag, `no sourceFile ${appCodeFilePath}`); 182 } 183 }); 184 Logger.info(this.logTag, `end scan ${this.project.getPath()}`); 185 const apiWriter = this.getApiWriter(); 186 apiWriter.add(systemApiRecognizer.getApiInformations()); 187 // avoid oom 188 systemApiRecognizer = undefined; 189 program = undefined; 190 await apiWriter.flush(); 191 fs.writeFileSync(handleFilePath, originalContent); 192 } 193 194 getApiWriter() { 195 if (!this.apiWriter) { 196 this.apiWriter = new ApiWriter(this.outputPath, this.formatFlag, this.noRepeat); 197 } 198 return this.apiWriter; 199 } 200 201 setApiWriter(apiWriter) { 202 this.apiWriter = apiWriter; 203 } 204} 205 206class MultiProjectApiCollector { 207 constructor(argv) { 208 this.argv = argv; 209 } 210 211 setLibPath(libPath) { 212 this.libPath = libPath; 213 if (libPath && !fs.existsSync(this.libPath)) { 214 Logger.warn(this.logTag, `${libPath} is not exist`); 215 } else { 216 Logger.info(this.logTag, `set lib path ${libPath}`); 217 } 218 return this; 219 } 220 221 setIncludeTest(isIncludeTest) { 222 this.isIncludeTest = isIncludeTest; 223 return this; 224 } 225 226 async start() { 227 const allApps = FileSystem.listAllAppDirs(this.argv.appDir); 228 if (allApps.length === 0) { 229 Logger.info('MultiProjectApiCollector', `project not found in ${this.argv.appDir}`); 230 return; 231 } 232 const output = !this.argv.output ? this.argv.appDir : this.argv.output; 233 const apiExcelWriter = new ApiExcelWriter(output); 234 apiExcelWriter.close(); 235 allApps.forEach((app) => { 236 if (app) { 237 this.argv.app = app; 238 const apiCollector = new ApiCollector(this.argv); 239 apiCollector.setApiWriter(apiExcelWriter); 240 apiCollector.setLibPath(this.libPath).setIncludeTest(this.isIncludeTest).start(); 241 } 242 }); 243 apiExcelWriter.open(); 244 await apiExcelWriter.flush(); 245 } 246} 247 248exports.ApiCollector = ApiCollector; 249exports.MultiProjectApiCollector = MultiProjectApiCollector;