Browse Source

feat: add IDE types tips generator (#3756)

* feat: add IDE types tips generator

* fix: webtypes default lang en-US

* chore: rename npmpublish allow files
pull/3197/head^2
言肆 4 years ago committed by GitHub
parent
commit
b20902412f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 1
      antd-tools/generator-types/README.md
  3. 19
      antd-tools/generator-types/index.js
  4. 119
      antd-tools/generator-types/src/formatter.ts
  5. 52
      antd-tools/generator-types/src/index.ts
  6. 113
      antd-tools/generator-types/src/parser.ts
  7. 63
      antd-tools/generator-types/src/type.ts
  8. 30
      antd-tools/generator-types/src/utils.ts
  9. 30
      antd-tools/generator-types/src/vetur.ts
  10. 18
      antd-tools/generator-types/src/web-types.ts
  11. 13
      antd-tools/generator-types/tsconfig.json
  12. 12
      package.json

3
.gitignore vendored

@ -69,3 +69,6 @@ package-lock.json
list.txt
site/dev.js
# IDE 语法提示临时文件
vetur/

1
antd-tools/generator-types/README.md

@ -0,0 +1 @@
fork github.com/youzan/vant packages/generator-types

19
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);
}

119
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;
}

52
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 };

113
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;
}

63
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<string, VeturTag>;
export type VeturAttribute = {
type: string;
description: string;
};
export type VeturAttributes = Record<string, VeturAttribute>;
export type VeturResult = {
tags: VeturTags;
attributes: VeturAttributes;
};
export type Options = {
name: string;
path: PathLike;
test: RegExp;
version: string;
outputDir?: string;
tagPrefix?: string;
};

30
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, '/');
}

30
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;
}

18
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',
},
},
};
}

13
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/**/*"]
}

12
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"
}

Loading…
Cancel
Save