|
| 1 | +import { readFileSync, readdirSync, writeFileSync, statSync, existsSync } from 'fs' |
| 2 | +import { join, isAbsolute, dirname } from 'path' |
| 3 | +import cheerio from 'cheerio' |
| 4 | +import { render } from './page' |
| 5 | +import { camelCase, union } from 'lodash' |
| 6 | +import { |
| 7 | + ScriptSnapshot, |
| 8 | + ModuleKind, |
| 9 | + getDefaultLibFilePath, |
| 10 | + resolveModuleName, |
| 11 | + sys, |
| 12 | + createDocumentRegistry, |
| 13 | + createLanguageService, |
| 14 | + SyntaxKind, |
| 15 | + forEachChild |
| 16 | +} from 'typescript' |
| 17 | + |
| 18 | +const docPath = join(__dirname, '../docs/components') |
| 19 | +const typesDir = join(resolveLib('veui'), 'types') |
| 20 | +const Ls = createLs() |
| 21 | + |
| 22 | +function getApiFromDocs () { |
| 23 | + return readdirSync(docPath) |
| 24 | + .reduce((acc, name) => { |
| 25 | + const match = name.match(/(.+)\.md$/) |
| 26 | + if (match) { |
| 27 | + const absPath = join(docPath, name) |
| 28 | + acc[match[1]] = getComponentApiFromDoc(absPath) |
| 29 | + } |
| 30 | + return acc |
| 31 | + }, {}) |
| 32 | +} |
| 33 | + |
| 34 | +function getComponentApiFromDoc (docFile) { |
| 35 | + const raw = readFileSync(docFile, 'utf8') |
| 36 | + const { contents } = render(raw, docFile) |
| 37 | + const $ = cheerio.load(contents) |
| 38 | + const props = $('h3:contains(属性)+table > tbody > tr') |
| 39 | + .map((i, el) => { |
| 40 | + const tds = $(el).children() |
| 41 | + return { |
| 42 | + name: camelCase(tds.eq(0).text().trim()), |
| 43 | + type: tds.eq(1).text().trim() |
| 44 | + // defaultValue: tds.eq(2).text().trim() |
| 45 | + } |
| 46 | + }) |
| 47 | + .toArray() |
| 48 | + .reduce((acc, { name, type }) => { |
| 49 | + acc[name] = type |
| 50 | + return acc |
| 51 | + }, {}) |
| 52 | + |
| 53 | + const slots = $('h3:contains(插槽)+table > tbody > tr') |
| 54 | + .map((i, el) => { |
| 55 | + const tds = $(el).children() |
| 56 | + return tds.eq(0).text().trim() |
| 57 | + }) |
| 58 | + .toArray() |
| 59 | + .reduce((acc, name) => { |
| 60 | + acc[name] = null |
| 61 | + return acc |
| 62 | + }, {}) |
| 63 | + |
| 64 | + const emits = $('h3:contains(事件)+table > tbody > tr') |
| 65 | + .map((i, el) => { |
| 66 | + const tds = $(el).children() |
| 67 | + return tds.eq(0).text().trim() |
| 68 | + }) |
| 69 | + .toArray() |
| 70 | + .reduce((acc, name) => { |
| 71 | + acc[name] = null |
| 72 | + return acc |
| 73 | + }, {}) |
| 74 | + |
| 75 | + return { |
| 76 | + props, |
| 77 | + slots, |
| 78 | + emits |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +function getApiFromVeuiTypes () { |
| 83 | + const program = Ls.getProgram() |
| 84 | + const inputs = readdirpSync(join(typesDir, 'components')).map(i => join(typesDir, 'components', i)) |
| 85 | + const re = /([^/]+)\.d\.ts$/ |
| 86 | + const result = {} |
| 87 | + inputs.forEach(input => { |
| 88 | + const name = re.exec(input)[1] |
| 89 | + const save = api => { |
| 90 | + result[name] = api |
| 91 | + } |
| 92 | + forEachChild(program.getSourceFile(input), (...args) => visit(save, ...args)) |
| 93 | + }) |
| 94 | + return result |
| 95 | +} |
| 96 | + |
| 97 | +function visit (saveApi, node) { |
| 98 | + if (node.kind === SyntaxKind.ExportAssignment) { |
| 99 | + const ck = Ls.getProgram().getTypeChecker() |
| 100 | + const sym = ck.getSymbolAtLocation(node.expression) // node.expression: id to Autocomplete |
| 101 | + const type = ck.getTypeAtLocation(sym.declarations[0].name) |
| 102 | + const rt = type.getConstructSignatures()[0].getReturnType() |
| 103 | + const all = rt.getProperties() |
| 104 | + const props = all.find(sy => sy.escapedName === '$props') |
| 105 | + let result = {} |
| 106 | + |
| 107 | + result.props = ck |
| 108 | + .getTypeOfSymbolAtLocation(props, node) |
| 109 | + .getProperties() |
| 110 | + .reduce((acc, sy) => { |
| 111 | + acc[sy.escapedName] = ck.typeToString(ck.getTypeOfSymbolAtLocation(sy, node)) |
| 112 | + return acc |
| 113 | + }, {}) |
| 114 | + |
| 115 | + const emits = all.find(sy => sy.escapedName === '$emit') |
| 116 | + const emitsType = ck.getTypeOfSymbolAtLocation(emits, node) |
| 117 | + const emitsCollection = emitsType.isIntersection() |
| 118 | + ? emitsType.types |
| 119 | + : [emitsType] |
| 120 | + |
| 121 | + result.emits = emitsCollection |
| 122 | + .map(ty => { |
| 123 | + return ty.getCallSignatures()[0] |
| 124 | + .getParameters() |
| 125 | + .reduce((acc, argSy) => { |
| 126 | + const argType = ck.getTypeOfSymbolAtLocation(argSy, node) |
| 127 | + |
| 128 | + const tstr = ck.typeToString(argType) |
| 129 | + const matched = /^"([^"]+)"$/.exec(tstr) |
| 130 | + acc[argSy.escapedName] = matched ? matched[1] : tstr |
| 131 | + return acc |
| 132 | + }, {}) |
| 133 | + }) |
| 134 | + .reduce((acc, { event, args }) => { |
| 135 | + acc[event] = args |
| 136 | + return acc |
| 137 | + }, {}) |
| 138 | + |
| 139 | + const slots = all.find(sy => sy.escapedName === '$scopedSlots') |
| 140 | + const slotsType = ck |
| 141 | + .getTypeOfSymbolAtLocation(slots, node) |
| 142 | + .getProperties() |
| 143 | + .reduce((acc, symbol) => { |
| 144 | + acc[symbol.escapedName] = getScope(symbol, node, ck) |
| 145 | + return acc |
| 146 | + }, {}) |
| 147 | + |
| 148 | + result.slots = slotsType |
| 149 | + |
| 150 | + saveApi(result) |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +function getScope (symbol, node, checker) { |
| 155 | + const scopeSy = checker.getTypeOfSymbolAtLocation(symbol, node) |
| 156 | + .getCallSignatures()[0].getParameters()[0] |
| 157 | + if (!scopeSy) { |
| 158 | + return {} |
| 159 | + } |
| 160 | + const type = checker.getTypeOfSymbolAtLocation(scopeSy, node) |
| 161 | + .getProperties().reduce((acc, argSy) => { |
| 162 | + const argType = checker.getTypeOfSymbolAtLocation(argSy, node) |
| 163 | + |
| 164 | + acc[argSy.escapedName] = checker.typeToString(argType) |
| 165 | + return acc |
| 166 | + }, {}) |
| 167 | + return type |
| 168 | +} |
| 169 | + |
| 170 | +const typeDeps = [ |
| 171 | + '@vue/runtime-dom', |
| 172 | + 'vue-router', |
| 173 | + '@vue/reactivity', |
| 174 | + '@vue/shared', |
| 175 | + '@vue/runtime-core' |
| 176 | +] |
| 177 | + |
| 178 | +function createLs () { |
| 179 | + const options = { |
| 180 | + module: ModuleKind.ESNext |
| 181 | + } |
| 182 | + const host = { |
| 183 | + getScriptSnapshot: fileName => { |
| 184 | + fileName = isAbsolute(fileName) ? fileName : join(typesDir, fileName) |
| 185 | + if (!existsSync(fileName)) { |
| 186 | + return undefined |
| 187 | + } |
| 188 | + const content = readFileSync(fileName).toString() |
| 189 | + return ScriptSnapshot.fromString(content) |
| 190 | + }, |
| 191 | + getScriptFileNames: () => readdirpSync(typesDir), |
| 192 | + getScriptVersion: () => '1', |
| 193 | + getCurrentDirectory: () => typesDir, |
| 194 | + getCompilationSettings: () => options, |
| 195 | + getDefaultLibFileName: options => getDefaultLibFilePath(options), |
| 196 | + resolveModuleNames: (moduleNames, containingFile) => { |
| 197 | + return moduleNames.map(moduleName => { |
| 198 | + let { resolvedModule } = resolveModuleName(moduleName, containingFile, options, { |
| 199 | + fileExists: sys.fileExists, |
| 200 | + readFile: sys.readFile |
| 201 | + }) |
| 202 | + |
| 203 | + if (resolvedModule) { |
| 204 | + return resolvedModule |
| 205 | + } |
| 206 | + |
| 207 | + if (typeDeps.indexOf(moduleName) >= 0) { |
| 208 | + const p = resolveLib(moduleName, containingFile, true) |
| 209 | + if (p) { |
| 210 | + return { resolvedFileName: p } |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + if (moduleName.startsWith('.')) { |
| 215 | + const resolved = resolveRel(moduleName, containingFile) |
| 216 | + if (resolved) { |
| 217 | + return { resolvedFileName: resolved } |
| 218 | + } |
| 219 | + } |
| 220 | + }) |
| 221 | + } |
| 222 | + } |
| 223 | + return createLanguageService(host, createDocumentRegistry()) |
| 224 | +} |
| 225 | + |
| 226 | +function readdirpSync (toRead, prefix = '') { |
| 227 | + return readdirSync(toRead) |
| 228 | + .reduce((acc, file) => { |
| 229 | + const realFile = join(toRead, file) |
| 230 | + if (statSync(realFile).isDirectory()) { |
| 231 | + acc = acc.concat(readdirpSync(realFile, `${file}/`)) |
| 232 | + } else { |
| 233 | + acc.push(`${prefix}${file}`) |
| 234 | + } |
| 235 | + return acc |
| 236 | + }, []) |
| 237 | +} |
| 238 | + |
| 239 | +function resolveLib (libName, containingFile, types) { |
| 240 | + const options = containingFile |
| 241 | + ? { paths: [containingFile] } |
| 242 | + : undefined |
| 243 | + let libDir = dirname(require.resolve(libName, options)) |
| 244 | + let pkgPath = join(libDir, 'package.json') |
| 245 | + while (!existsSync(pkgPath)) { |
| 246 | + libDir = dirname(libDir) |
| 247 | + pkgPath = join(libDir, 'package.json') |
| 248 | + } |
| 249 | + |
| 250 | + if (types) { |
| 251 | + const pkg = require(pkgPath) |
| 252 | + if (pkg.types || pkg.typings) { |
| 253 | + return join(dirname(pkgPath), pkg.types || pkg.typings) |
| 254 | + } |
| 255 | + } |
| 256 | + return libDir |
| 257 | +} |
| 258 | + |
| 259 | +function resolveRel (moduleName, containingFile) { |
| 260 | + let target = join(dirname(containingFile), moduleName) |
| 261 | + |
| 262 | + if (statSync(target).isDirectory() && existsSync(join(target, 'index.d.ts'))) { |
| 263 | + return join(target, 'index.d.ts') |
| 264 | + } |
| 265 | +} |
| 266 | + |
| 267 | +function diffApi (tsApi, docApi) { |
| 268 | + const fallback = { |
| 269 | + props: {}, |
| 270 | + slots: {}, |
| 271 | + emits: {} |
| 272 | + } |
| 273 | + return union( |
| 274 | + Object.keys(tsApi), |
| 275 | + Object.keys(docApi) |
| 276 | + ).map(compName => { |
| 277 | + const { props, slots, emits } = tsApi[compName] || fallback |
| 278 | + const { props: dProps, slots: dSlots, emits: dEmits } = docApi[compName] || fallback |
| 279 | + return { |
| 280 | + component: compName, |
| 281 | + props: diffPart(props, dProps, true), // 这里是false可以检查props类型 |
| 282 | + slots: diffPart(slots, dSlots, true), |
| 283 | + emits: diffPart(emits, dEmits, true) |
| 284 | + } |
| 285 | + }) |
| 286 | +} |
| 287 | + |
| 288 | +function diffPart (ts = {}, doc = {}, loose = false) { |
| 289 | + return union( |
| 290 | + Object.keys(ts), |
| 291 | + Object.keys(doc) |
| 292 | + ).map(key => { |
| 293 | + return { |
| 294 | + key, |
| 295 | + ts: typeof ts[key] === 'undefined' ? 'undefined' : ts[key], // undefined 表示缺失 |
| 296 | + doc: typeof doc[key] === 'undefined' ? 'undefined' : doc[key], |
| 297 | + match: loose |
| 298 | + ? ts.hasOwnProperty(key) && doc.hasOwnProperty(key) |
| 299 | + : ts[key] === doc[key] |
| 300 | + } |
| 301 | + }).filter(({ match }) => !match) |
| 302 | +} |
| 303 | + |
| 304 | +function writeDiffFile () { |
| 305 | + const tsApi = getApiFromVeuiTypes() |
| 306 | + const docApi = getApiFromDocs() |
| 307 | + const diff = diffApi(tsApi, docApi) |
| 308 | + writeFileSync(join(__dirname, 'diff.json'), JSON.stringify(diff, null, ' '), 'utf8') |
| 309 | +} |
| 310 | + |
| 311 | +if (require.main === module) { |
| 312 | + writeDiffFile() |
| 313 | +} |
0 commit comments