From b20902412f7c7344e4221a1bcce878fc101a5d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A8=80=E8=82=86?= <18x@loacg.com> Date: Fri, 12 Mar 2021 14:41:09 +0800 Subject: [PATCH] feat: add IDE types tips generator (#3756) * feat: add IDE types tips generator * fix: webtypes default lang en-US * chore: rename npmpublish allow files --- .gitignore | 3 + antd-tools/generator-types/README.md | 1 + antd-tools/generator-types/index.js | 19 ++++ antd-tools/generator-types/src/formatter.ts | 119 ++++++++++++++++++++ antd-tools/generator-types/src/index.ts | 52 +++++++++ antd-tools/generator-types/src/parser.ts | 113 +++++++++++++++++++ antd-tools/generator-types/src/type.ts | 63 +++++++++++ antd-tools/generator-types/src/utils.ts | 30 +++++ antd-tools/generator-types/src/vetur.ts | 30 +++++ antd-tools/generator-types/src/web-types.ts | 18 +++ antd-tools/generator-types/tsconfig.json | 13 +++ package.json | 12 +- 12 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 antd-tools/generator-types/README.md create mode 100644 antd-tools/generator-types/index.js create mode 100644 antd-tools/generator-types/src/formatter.ts create mode 100644 antd-tools/generator-types/src/index.ts create mode 100644 antd-tools/generator-types/src/parser.ts create mode 100644 antd-tools/generator-types/src/type.ts create mode 100644 antd-tools/generator-types/src/utils.ts create mode 100644 antd-tools/generator-types/src/vetur.ts create mode 100644 antd-tools/generator-types/src/web-types.ts create mode 100644 antd-tools/generator-types/tsconfig.json diff --git a/.gitignore b/.gitignore index 665bb9350..a924c26cb 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,6 @@ package-lock.json list.txt site/dev.js + +# IDE 语法提示临时文件 +vetur/ diff --git a/antd-tools/generator-types/README.md b/antd-tools/generator-types/README.md new file mode 100644 index 000000000..199c688ba --- /dev/null +++ b/antd-tools/generator-types/README.md @@ -0,0 +1 @@ +fork github.com/youzan/vant packages/generator-types diff --git a/antd-tools/generator-types/index.js b/antd-tools/generator-types/index.js new file mode 100644 index 000000000..90be7a5e7 --- /dev/null +++ b/antd-tools/generator-types/index.js @@ -0,0 +1,19 @@ +const path = require('path'); +const pkg = require('../../package.json'); +const { parseAndWrite } = require('./lib/index.js'); +const rootPath = path.resolve(__dirname, '../../'); + +try { + parseAndWrite({ + version: pkg.version, + name: 'types', + path: path.resolve(rootPath, './v2-doc/src/docs'), + // default match lang + test: /en-US\.md/, + outputDir: path.resolve(rootPath, './vetur'), + tagPrefix: 'a-', + }); + console.log('generator types success'); +} catch (e) { + console.error('generator types error', e); +} diff --git a/antd-tools/generator-types/src/formatter.ts b/antd-tools/generator-types/src/formatter.ts new file mode 100644 index 000000000..d70f5510a --- /dev/null +++ b/antd-tools/generator-types/src/formatter.ts @@ -0,0 +1,119 @@ +/* eslint-disable no-continue */ +import { Artical, Articals } from './parser'; +import { formatType, removeVersion, toKebabCase } from './utils'; +import { VueTag } from './type'; + +function getComponentName(name: string, tagPrefix: string) { + if (name) { + return tagPrefix + toKebabCase(name.split(' ')[0]); + } + return ''; +} + +function parserProps(tag: VueTag, line: any) { + const [name, desc, type, defaultVal] = line; + if ( + type && + (type.includes('v-slot') || + type.includes('slot') || + type.includes('slots') || + type.includes('slot-scoped')) + ) { + tag.slots!.push({ + name: removeVersion(name), + description: desc, + }); + } + tag.attributes!.push({ + name: removeVersion(name), + default: defaultVal, + description: desc, + value: { + type: formatType(type || ''), + kind: 'expression', + }, + }); +} + +export function formatter(articals: Articals, componentName: string, tagPrefix: string = '') { + if (!articals.length) { + return; + } + + const tags: VueTag[] = []; + const tag: VueTag = { + name: getComponentName(componentName, tagPrefix), + slots: [], + events: [], + attributes: [], + }; + tags.push(tag); + + const tables = articals.filter(artical => artical.type === 'table'); + + tables.forEach(item => { + const { table } = item; + const prevIndex = articals.indexOf(item) - 1; + const prevArtical = articals[prevIndex]; + + if (!prevArtical || !prevArtical.content || !table || !table.body) { + return; + } + + const tableTitle = prevArtical.content; + + if (tableTitle.includes('API')) { + table.body.forEach(line => { + parserProps(tag, line); + }); + return; + } + + if (tableTitle.includes('events') && !tableTitle.includes(componentName)) { + table.body.forEach(line => { + const [name, desc] = line; + tag.events!.push({ + name: removeVersion(name), + description: desc, + }); + }); + return; + } + + // 额外的子组件 + if (tableTitle.includes(componentName) && !tableTitle.includes('events')) { + const childTag: VueTag = { + name: getComponentName(tableTitle.replace('.', ''), tagPrefix), + slots: [], + events: [], + attributes: [], + }; + table.body.forEach(line => { + parserProps(childTag, line); + }); + tags.push(childTag); + return; + } + // 额外的子组件事件 + if (tableTitle.includes(componentName) && tableTitle.includes('events')) { + const childTagName = getComponentName( + tableTitle.replace('.', '').replace('events', ''), + tagPrefix, + ); + const childTag: VueTag | undefined = tags.find(item => item.name === childTagName.trim()); + if (!childTag) { + return; + } + table.body.forEach(line => { + const [name, desc] = line; + childTag.events!.push({ + name: removeVersion(name), + description: desc, + }); + }); + return; + } + }); + + return tags; +} diff --git a/antd-tools/generator-types/src/index.ts b/antd-tools/generator-types/src/index.ts new file mode 100644 index 000000000..62598960e --- /dev/null +++ b/antd-tools/generator-types/src/index.ts @@ -0,0 +1,52 @@ +import glob from 'fast-glob'; +import { join, dirname, basename } from 'path'; +import { mdParser } from './parser'; +import { formatter } from './formatter'; +import { genWebTypes } from './web-types'; +import { readFileSync, outputFileSync } from 'fs-extra'; +import { Options, VueTag } from './type'; +import { normalizePath, getComponentName } from './utils'; +import { genVeturTags, genVeturAttributes } from './vetur'; + +async function readMarkdown(options: Options) { + // const mds = await glob(normalizePath(`${options.path}/**/*.md`)) + const mds = await glob(normalizePath(`${options.path}/**/*.md`)); + return mds + .filter(md => options.test.test(md)) + .map(path => { + const docPath = dirname(path); + const componentName = docPath.substring(docPath.lastIndexOf('/') + 1); + return { + componentName: getComponentName(componentName || ''), + md: readFileSync(path, 'utf-8'), + }; + }); +} + +export async function parseAndWrite(options: Options) { + if (!options.outputDir) { + throw new Error('outputDir can not be empty.'); + } + + const docs = await readMarkdown(options); + const datas = docs + .map(doc => formatter(mdParser(doc.md), doc.componentName, options.tagPrefix)) + .filter(item => item) as VueTag[][]; + const tags: VueTag[] = []; + datas.forEach(arr => { + tags.push(...arr); + }); + + const webTypes = genWebTypes(tags, options); + const veturTags = genVeturTags(tags); + const veturAttributes = genVeturAttributes(tags); + + outputFileSync(join(options.outputDir, 'tags.json'), JSON.stringify(veturTags, null, 2)); + outputFileSync( + join(options.outputDir, 'attributes.json'), + JSON.stringify(veturAttributes, null, 2), + ); + outputFileSync(join(options.outputDir, 'web-types.json'), JSON.stringify(webTypes, null, 2)); +} + +export default { parseAndWrite }; diff --git a/antd-tools/generator-types/src/parser.ts b/antd-tools/generator-types/src/parser.ts new file mode 100644 index 000000000..a09bc250d --- /dev/null +++ b/antd-tools/generator-types/src/parser.ts @@ -0,0 +1,113 @@ +/* eslint-disable no-cond-assign */ +const TITLE_REG = /^(#+)\s+([^\n]*)/; +const TABLE_REG = /^\|.+\r?\n\|\s*-+/; +const TD_REG = /\s*`[^`]+`\s*|([^|`]+)/g; +const TABLE_SPLIT_LINE_REG = /^\|\s*-/; + +type TableContent = { + head: string[]; + body: string[][]; +}; + +export type Artical = { + type: string; + content?: string; + table?: TableContent; + level?: number; +}; + +export type Articals = Artical[]; + +function readLine(input: string) { + const end = input.indexOf('\n'); + + return input.substr(0, end !== -1 ? end : input.length); +} + +function splitTableLine(line: string) { + line = line.replace('\\|', 'JOIN'); + + const items = line.split('|').map(item => item.trim().replace('JOIN', '|')); + + // remove pipe character on both sides + items.pop(); + items.shift(); + + return items; +} + +function tableParse(input: string) { + let start = 0; + let isHead = true; + + const end = input.length; + const table: TableContent = { + head: [], + body: [], + }; + + while (start < end) { + const target = input.substr(start); + const line = readLine(target); + + if (!/^\|/.test(target)) { + break; + } + + if (TABLE_SPLIT_LINE_REG.test(target)) { + isHead = false; + } else if (!isHead && line.includes('|')) { + const matched = line.trim().match(TD_REG); + + if (matched) { + table.body.push(splitTableLine(line)); + } + } + + start += line.length + 1; + } + + return { + table, + usedLength: start, + }; +} + +export function mdParser(input: string): Articals { + const artical = []; + let start = 0; + const end = input.length; + // artical.push({ + // type: 'title', + // content: title, + // level: 0, + // }); + + while (start < end) { + const target = input.substr(start); + + let match; + if ((match = TITLE_REG.exec(target))) { + artical.push({ + type: 'title', + content: match[2].replace('\r', ''), + level: match[1].length, + }); + + start += match.index + match[0].length; + } else if ((match = TABLE_REG.exec(target))) { + const { table, usedLength } = tableParse(target.substr(match.index)); + artical.push({ + type: 'table', + table, + }); + + start += match.index + usedLength; + } else { + start += readLine(target).length + 1; + } + } + + // artical[0].content = title + return artical; +} diff --git a/antd-tools/generator-types/src/type.ts b/antd-tools/generator-types/src/type.ts new file mode 100644 index 000000000..248eb98ab --- /dev/null +++ b/antd-tools/generator-types/src/type.ts @@ -0,0 +1,63 @@ +import { PathLike } from 'fs'; + +export type VueSlot = { + name: string; + description: string; +}; + +export type VueEventArgument = { + name: string; + type: string; +}; + +export type VueEvent = { + name: string; + description?: string; + arguments?: VueEventArgument[]; +}; + +export type VueAttribute = { + name: string; + default: string; + description: string; + value: { + kind: 'expression'; + type: string; + }; +}; + +export type VueTag = { + name: string; + slots?: VueSlot[]; + events?: VueEvent[]; + attributes?: VueAttribute[]; + description?: string; +}; + +export type VeturTag = { + description?: string; + attributes: string[]; +}; + +export type VeturTags = Record; + +export type VeturAttribute = { + type: string; + description: string; +}; + +export type VeturAttributes = Record; + +export type VeturResult = { + tags: VeturTags; + attributes: VeturAttributes; +}; + +export type Options = { + name: string; + path: PathLike; + test: RegExp; + version: string; + outputDir?: string; + tagPrefix?: string; +}; diff --git a/antd-tools/generator-types/src/utils.ts b/antd-tools/generator-types/src/utils.ts new file mode 100644 index 000000000..67fc1e8a6 --- /dev/null +++ b/antd-tools/generator-types/src/utils.ts @@ -0,0 +1,30 @@ +// myName -> my-name +export function toKebabCase(input: string): string { + return input.replace(/[A-Z]/g, (val, index) => (index === 0 ? '' : '-') + val.toLowerCase()); +} + +// name `v2.0.0` -> name +export function removeVersion(str: string) { + return str.replace(/`(\w|\.)+`/g, '').trim(); +} + +// *boolean* -> boolean +// _boolean_ -> boolean +export function formatType(type: string) { + return type + .replace(/(^(\*|_))|((\*|_)$)/g, '') + .replace('\\', '') + .replace('\\', ''); +} + +export function getComponentName(name: string) { + let title = name + .split('-') + .map(it => it.substring(0, 1) + it.substring(1)) + .join(''); + return title.substring(0, 1).toUpperCase() + title.substring(1); +} + +export function normalizePath(path: string): string { + return path.replace(/\\/g, '/'); +} diff --git a/antd-tools/generator-types/src/vetur.ts b/antd-tools/generator-types/src/vetur.ts new file mode 100644 index 000000000..7d2ba38d1 --- /dev/null +++ b/antd-tools/generator-types/src/vetur.ts @@ -0,0 +1,30 @@ +import { VueTag, VeturTags, VeturAttributes } from './type'; + +export function genVeturTags(tags: VueTag[]) { + const veturTags: VeturTags = {}; + + tags.forEach(tag => { + veturTags[tag.name] = { + attributes: tag.attributes ? tag.attributes.map(item => item.name) : [], + }; + }); + + return veturTags; +} + +export function genVeturAttributes(tags: VueTag[]) { + const veturAttributes: VeturAttributes = {}; + + tags.forEach(tag => { + if (tag.attributes) { + tag.attributes.forEach(attr => { + veturAttributes[`${tag.name}/${attr.name}`] = { + type: attr.value.type, + description: `${attr.description}, Default: ${attr.default}`, + }; + }); + } + }); + + return veturAttributes; +} diff --git a/antd-tools/generator-types/src/web-types.ts b/antd-tools/generator-types/src/web-types.ts new file mode 100644 index 000000000..1ac46af41 --- /dev/null +++ b/antd-tools/generator-types/src/web-types.ts @@ -0,0 +1,18 @@ +import { VueTag, Options } from './type'; + +// create web-types.json to provide autocomplete in JetBrains IDEs +export function genWebTypes(tags: VueTag[], options: Options) { + return { + $schema: 'https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json', + framework: 'vue', + name: options.name, + version: options.version, + contributions: { + html: { + tags, + attributes: [], + 'types-syntax': 'typescript', + }, + }, + }; +} diff --git a/antd-tools/generator-types/tsconfig.json b/antd-tools/generator-types/tsconfig.json new file mode 100644 index 000000000..83790d350 --- /dev/null +++ b/antd-tools/generator-types/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2017", + "outDir": "./lib", + "module": "commonjs", + "strict": true, + "declaration": true, + "skipLibCheck": true, + "esModuleInterop": true, + "lib": ["esnext"] + }, + "include": ["src/**/*"] +} diff --git a/package.json b/package.json index c6b581d1f..20870842d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "dist", "lib", "es", - "scripts" + "scripts", + "vetur" ], "scripts": { "dev": "webpack-dev-server", @@ -32,6 +33,7 @@ "test": "cross-env NODE_ENV=test WORKFLOW=true jest --config .jest.js", "test:dev": "cross-env NODE_ENV=test jest --config .jest.js", "compile": "node antd-tools/cli/run.js compile", + "generator-webtypes": "tsc -p antd-tools/generator-types/tsconfig.json && node antd-tools/generator-types/index.js", "pub": "node antd-tools/cli/run.js pub", "pub-with-ci": "node antd-tools/cli/run.js pub-with-ci", "prepublish": "node antd-tools/cli/run.js guard", @@ -82,6 +84,7 @@ "@commitlint/cli": "^8.0.0", "@commitlint/config-conventional": "^8.0.0", "@octokit/rest": "^16.0.0", + "@types/fs-extra": "^9.0.8", "@types/lodash-es": "^4.17.3", "@types/raf": "^3.4.0", "@typescript-eslint/eslint-plugin": "^4.1.0", @@ -229,5 +232,10 @@ "es/**/style/*", "lib/**/style/*", "*.less" - ] + ], + "vetur": { + "tags": "vetur/tags.json", + "attributes": "vetur/attributes.json" + }, + "web-types": "vetur/web-types.json" }