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;