Skip to content

Commit 3e2d350

Browse files
xiaodemenJustineo
authored andcommitted
chore: add the diff-api script
Change-Id: Ic66702b08b4e18290ba7d1403c8344655e35060b
1 parent 3adf036 commit 3e2d350

File tree

3 files changed

+444
-9
lines changed

3 files changed

+444
-9
lines changed

one/build/diff-api.js

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
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

Comments
 (0)