From 3e2d350938ee2806bd8910d4cfd869ca8b9cdab1 Mon Sep 17 00:00:00 2001 From: xiaodemen Date: Wed, 16 Mar 2022 18:24:02 +0800 Subject: [PATCH] chore: add the diff-api script Change-Id: Ic66702b08b4e18290ba7d1403c8344655e35060b --- one/build/diff-api.js | 313 ++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 137 ++++++++++++++++-- package.json | 3 + 3 files changed, 444 insertions(+), 9 deletions(-) create mode 100644 one/build/diff-api.js diff --git a/one/build/diff-api.js b/one/build/diff-api.js new file mode 100644 index 0000000..98a30e8 --- /dev/null +++ b/one/build/diff-api.js @@ -0,0 +1,313 @@ +import { readFileSync, readdirSync, writeFileSync, statSync, existsSync } from 'fs' +import { join, isAbsolute, dirname } from 'path' +import cheerio from 'cheerio' +import { render } from './page' +import { camelCase, union } from 'lodash' +import { + ScriptSnapshot, + ModuleKind, + getDefaultLibFilePath, + resolveModuleName, + sys, + createDocumentRegistry, + createLanguageService, + SyntaxKind, + forEachChild +} from 'typescript' + +const docPath = join(__dirname, '../docs/components') +const typesDir = join(resolveLib('veui'), 'types') +const Ls = createLs() + +function getApiFromDocs () { + return readdirSync(docPath) + .reduce((acc, name) => { + const match = name.match(/(.+)\.md$/) + if (match) { + const absPath = join(docPath, name) + acc[match[1]] = getComponentApiFromDoc(absPath) + } + return acc + }, {}) +} + +function getComponentApiFromDoc (docFile) { + const raw = readFileSync(docFile, 'utf8') + const { contents } = render(raw, docFile) + const $ = cheerio.load(contents) + const props = $('h3:contains(属性)+table > tbody > tr') + .map((i, el) => { + const tds = $(el).children() + return { + name: camelCase(tds.eq(0).text().trim()), + type: tds.eq(1).text().trim() + // defaultValue: tds.eq(2).text().trim() + } + }) + .toArray() + .reduce((acc, { name, type }) => { + acc[name] = type + return acc + }, {}) + + const slots = $('h3:contains(插槽)+table > tbody > tr') + .map((i, el) => { + const tds = $(el).children() + return tds.eq(0).text().trim() + }) + .toArray() + .reduce((acc, name) => { + acc[name] = null + return acc + }, {}) + + const emits = $('h3:contains(事件)+table > tbody > tr') + .map((i, el) => { + const tds = $(el).children() + return tds.eq(0).text().trim() + }) + .toArray() + .reduce((acc, name) => { + acc[name] = null + return acc + }, {}) + + return { + props, + slots, + emits + } +} + +function getApiFromVeuiTypes () { + const program = Ls.getProgram() + const inputs = readdirpSync(join(typesDir, 'components')).map(i => join(typesDir, 'components', i)) + const re = /([^/]+)\.d\.ts$/ + const result = {} + inputs.forEach(input => { + const name = re.exec(input)[1] + const save = api => { + result[name] = api + } + forEachChild(program.getSourceFile(input), (...args) => visit(save, ...args)) + }) + return result +} + +function visit (saveApi, node) { + if (node.kind === SyntaxKind.ExportAssignment) { + const ck = Ls.getProgram().getTypeChecker() + const sym = ck.getSymbolAtLocation(node.expression) // node.expression: id to Autocomplete + const type = ck.getTypeAtLocation(sym.declarations[0].name) + const rt = type.getConstructSignatures()[0].getReturnType() + const all = rt.getProperties() + const props = all.find(sy => sy.escapedName === '$props') + let result = {} + + result.props = ck + .getTypeOfSymbolAtLocation(props, node) + .getProperties() + .reduce((acc, sy) => { + acc[sy.escapedName] = ck.typeToString(ck.getTypeOfSymbolAtLocation(sy, node)) + return acc + }, {}) + + const emits = all.find(sy => sy.escapedName === '$emit') + const emitsType = ck.getTypeOfSymbolAtLocation(emits, node) + const emitsCollection = emitsType.isIntersection() + ? emitsType.types + : [emitsType] + + result.emits = emitsCollection + .map(ty => { + return ty.getCallSignatures()[0] + .getParameters() + .reduce((acc, argSy) => { + const argType = ck.getTypeOfSymbolAtLocation(argSy, node) + + const tstr = ck.typeToString(argType) + const matched = /^"([^"]+)"$/.exec(tstr) + acc[argSy.escapedName] = matched ? matched[1] : tstr + return acc + }, {}) + }) + .reduce((acc, { event, args }) => { + acc[event] = args + return acc + }, {}) + + const slots = all.find(sy => sy.escapedName === '$scopedSlots') + const slotsType = ck + .getTypeOfSymbolAtLocation(slots, node) + .getProperties() + .reduce((acc, symbol) => { + acc[symbol.escapedName] = getScope(symbol, node, ck) + return acc + }, {}) + + result.slots = slotsType + + saveApi(result) + } +} + +function getScope (symbol, node, checker) { + const scopeSy = checker.getTypeOfSymbolAtLocation(symbol, node) + .getCallSignatures()[0].getParameters()[0] + if (!scopeSy) { + return {} + } + const type = checker.getTypeOfSymbolAtLocation(scopeSy, node) + .getProperties().reduce((acc, argSy) => { + const argType = checker.getTypeOfSymbolAtLocation(argSy, node) + + acc[argSy.escapedName] = checker.typeToString(argType) + return acc + }, {}) + return type +} + +const typeDeps = [ + '@vue/runtime-dom', + 'vue-router', + '@vue/reactivity', + '@vue/shared', + '@vue/runtime-core' +] + +function createLs () { + const options = { + module: ModuleKind.ESNext + } + const host = { + getScriptSnapshot: fileName => { + fileName = isAbsolute(fileName) ? fileName : join(typesDir, fileName) + if (!existsSync(fileName)) { + return undefined + } + const content = readFileSync(fileName).toString() + return ScriptSnapshot.fromString(content) + }, + getScriptFileNames: () => readdirpSync(typesDir), + getScriptVersion: () => '1', + getCurrentDirectory: () => typesDir, + getCompilationSettings: () => options, + getDefaultLibFileName: options => getDefaultLibFilePath(options), + resolveModuleNames: (moduleNames, containingFile) => { + return moduleNames.map(moduleName => { + let { resolvedModule } = resolveModuleName(moduleName, containingFile, options, { + fileExists: sys.fileExists, + readFile: sys.readFile + }) + + if (resolvedModule) { + return resolvedModule + } + + if (typeDeps.indexOf(moduleName) >= 0) { + const p = resolveLib(moduleName, containingFile, true) + if (p) { + return { resolvedFileName: p } + } + } + + if (moduleName.startsWith('.')) { + const resolved = resolveRel(moduleName, containingFile) + if (resolved) { + return { resolvedFileName: resolved } + } + } + }) + } + } + return createLanguageService(host, createDocumentRegistry()) +} + +function readdirpSync (toRead, prefix = '') { + return readdirSync(toRead) + .reduce((acc, file) => { + const realFile = join(toRead, file) + if (statSync(realFile).isDirectory()) { + acc = acc.concat(readdirpSync(realFile, `${file}/`)) + } else { + acc.push(`${prefix}${file}`) + } + return acc + }, []) +} + +function resolveLib (libName, containingFile, types) { + const options = containingFile + ? { paths: [containingFile] } + : undefined + let libDir = dirname(require.resolve(libName, options)) + let pkgPath = join(libDir, 'package.json') + while (!existsSync(pkgPath)) { + libDir = dirname(libDir) + pkgPath = join(libDir, 'package.json') + } + + if (types) { + const pkg = require(pkgPath) + if (pkg.types || pkg.typings) { + return join(dirname(pkgPath), pkg.types || pkg.typings) + } + } + return libDir +} + +function resolveRel (moduleName, containingFile) { + let target = join(dirname(containingFile), moduleName) + + if (statSync(target).isDirectory() && existsSync(join(target, 'index.d.ts'))) { + return join(target, 'index.d.ts') + } +} + +function diffApi (tsApi, docApi) { + const fallback = { + props: {}, + slots: {}, + emits: {} + } + return union( + Object.keys(tsApi), + Object.keys(docApi) + ).map(compName => { + const { props, slots, emits } = tsApi[compName] || fallback + const { props: dProps, slots: dSlots, emits: dEmits } = docApi[compName] || fallback + return { + component: compName, + props: diffPart(props, dProps, true), // 这里是false可以检查props类型 + slots: diffPart(slots, dSlots, true), + emits: diffPart(emits, dEmits, true) + } + }) +} + +function diffPart (ts = {}, doc = {}, loose = false) { + return union( + Object.keys(ts), + Object.keys(doc) + ).map(key => { + return { + key, + ts: typeof ts[key] === 'undefined' ? 'undefined' : ts[key], // undefined 表示缺失 + doc: typeof doc[key] === 'undefined' ? 'undefined' : doc[key], + match: loose + ? ts.hasOwnProperty(key) && doc.hasOwnProperty(key) + : ts[key] === doc[key] + } + }).filter(({ match }) => !match) +} + +function writeDiffFile () { + const tsApi = getApiFromVeuiTypes() + const docApi = getApiFromDocs() + const diff = diffApi(tsApi, docApi) + writeFileSync(join(__dirname, 'diff.json'), JSON.stringify(diff, null, ' '), 'utf8') +} + +if (require.main === module) { + writeDiffFile() +} diff --git a/package-lock.json b/package-lock.json index 76dc62d..067dcbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "veui-docs", "version": "1.0.0", "dependencies": { - "express": "^4.16.2" + "@vue/runtime-dom": "^3.2.31", + "express": "^4.16.2", + "typescript": "^4.6.2" }, "devDependencies": { "@docsearch/css": "^3.0.0-alpha.39", @@ -4248,6 +4250,48 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, + "node_modules/@vue/reactivity": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.31.tgz", + "integrity": "sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==", + "dependencies": { + "@vue/shared": "3.2.31" + } + }, + "node_modules/@vue/reactivity/node_modules/@vue/shared": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz", + "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==" + }, + "node_modules/@vue/runtime-core": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.31.tgz", + "integrity": "sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA==", + "dependencies": { + "@vue/reactivity": "3.2.31", + "@vue/shared": "3.2.31" + } + }, + "node_modules/@vue/runtime-core/node_modules/@vue/shared": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz", + "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==" + }, + "node_modules/@vue/runtime-dom": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz", + "integrity": "sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g==", + "dependencies": { + "@vue/runtime-core": "3.2.31", + "@vue/shared": "3.2.31", + "csstype": "^2.6.8" + } + }, + "node_modules/@vue/runtime-dom/node_modules/@vue/shared": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz", + "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==" + }, "node_modules/@vue/shared": { "version": "3.2.26", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.26.tgz", @@ -7731,6 +7775,11 @@ "node": ">=0.10.0" } }, + "node_modules/csstype": { + "version": "2.6.20", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz", + "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==" + }, "node_modules/cuint": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", @@ -16368,6 +16417,19 @@ "string-width": "^2.1.1" } }, + "node_modules/prettier-eslint/node_modules/typescript": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/prettier-eslint/node_modules/vue-eslint-parser": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz", @@ -20335,10 +20397,9 @@ } }, "node_modules/typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", - "dev": true, + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", + "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -26122,6 +26183,54 @@ } } }, + "@vue/reactivity": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.31.tgz", + "integrity": "sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==", + "requires": { + "@vue/shared": "3.2.31" + }, + "dependencies": { + "@vue/shared": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz", + "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==" + } + } + }, + "@vue/runtime-core": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.31.tgz", + "integrity": "sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA==", + "requires": { + "@vue/reactivity": "3.2.31", + "@vue/shared": "3.2.31" + }, + "dependencies": { + "@vue/shared": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz", + "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==" + } + } + }, + "@vue/runtime-dom": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz", + "integrity": "sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g==", + "requires": { + "@vue/runtime-core": "3.2.31", + "@vue/shared": "3.2.31", + "csstype": "^2.6.8" + }, + "dependencies": { + "@vue/shared": { + "version": "3.2.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz", + "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==" + } + } + }, "@vue/shared": { "version": "3.2.26", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.26.tgz", @@ -28929,6 +29038,11 @@ } } }, + "csstype": { + "version": "2.6.20", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz", + "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==" + }, "cuint": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", @@ -35878,6 +35992,12 @@ "string-width": "^2.1.1" } }, + "typescript": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "dev": true + }, "vue-eslint-parser": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz", @@ -39047,10 +39167,9 @@ } }, "typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", - "dev": true + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", + "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==" }, "typescript-eslint-parser": { "version": "16.0.1", diff --git a/package.json b/package.json index 1cfffe9..8150670 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "npm run docs && NODE_OPTIONS=--max_old_space_size=4096 HOST=0.0.0.0 nuxt & node -r esm ./one/build/watch.js", "build": "NODE_ENV=production npm run docs && nuxt build", "docs": "node -r esm ./one/build/generate.js", + "diff": "node -r esm ./one/build/diff-api.js", "start": "nuxt start", "generate": "npm run docs && nuxt generate", "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", @@ -23,6 +24,7 @@ "@docsearch/js": "^3.0.0-alpha.39", "@justfork/vue-monaco": "^0.3.1", "@stackblitz/sdk": "^1.5.2", + "@vue/runtime-dom": "^3.2.31", "babel-eslint": "^10.1.0", "babel-plugin-lodash": "^3.3.4", "babel-plugin-veui": "^2.5.5", @@ -81,6 +83,7 @@ "stylelint-plugin-stylus": "^0.9.0", "stylus": "^0.54.5", "stylus-loader": "^3.0.2", + "typescript": "^4.6.2", "unist-util-remove": "^1.0.1", "unist-util-visit": "^1.4.0", "veui": "^2.5.5",