Skip to content

Commit 27474ea

Browse files
authored
Custom automatic api (#835)
* feat(automatin/api): makes obj from components * feat(automation/api): parses single line comments * feat(regex): found it * feat(regex): grouped and closed it * feat(regex): and now it respect white space * feat(automation): working regex-to-obj * feat(commentMerger): removes whitespace and * This is achieved by yet another regex™. It fully respects any intentional (or not) whitespace or line breaks after the fact. * feat(automation/regex): adds type extraction to regex * feat(automation api): fix type regex to correctly read from file * refactor(automation): splits file reading from comment regex * refactor(extractType): splits regex from file handle * refactor(automatic api): better file name and ouput * fix(automatic api): removes camel case * refactor(api automation): better fn name * fix(automatic api): prevents deleting comments * fix(automatic api): rm console log * feat(custom api): parses comments from single file * fix(custom api): type extentions correctly handles the second way a type could end * feat(auto api):group comments under type label Adds label prop to comment obj. Label prop is derived from the type that defines the comments but is not changed to match current API naming * feat(auto api): components use comment labels * feat(auto api): dynamically gets component name * feat(auto api): adds fn that returns output path * refactor(auto api): renames and shifts now the plugin is in one file, decluttering the vite.config file, and gives it a better name * refactor(custom api): removes old code new code is placed under a single file, can now be used in vite * refactor(auto api): move fns around Mostly default export being at the top. * fix(auto api): adds proper types * feat(auto-api): write api obj to file * fix(auto-api): removes semicolon from type * feat(auto-api): adds docs component * fix(auto api): removes unused import * feat(auto api): adds periods to sub component names * feat(auto api): adds fn for removing question marks * refactor(auto api): betters namings * fix(auto api): workaround error message Essentially, you can't import across package boundaries. Hacky fix is to duplicate types. * fix(auto api): removes unused import * fix(auto api): removes test code Accidentally commited code used to test the auto API.
1 parent af6aff9 commit 27474ea

File tree

7 files changed

+265
-8
lines changed

7 files changed

+265
-8
lines changed

apps/website/auto-api.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import * as fs from 'fs';
2+
import { resolve } from 'path';
3+
import { inspect } from 'util';
4+
import { ViteDevServer } from 'vite';
5+
export default function autoAPI() {
6+
return {
7+
name: 'watch-monorepo-changes',
8+
configureServer(server: ViteDevServer) {
9+
const watchPath = resolve(__dirname, '../../packages/kit-headless');
10+
server.watcher.on('change', (file: string) => {
11+
if (file.startsWith(watchPath)) {
12+
loopOnAllChildFiles(file);
13+
}
14+
});
15+
},
16+
};
17+
}
18+
// the object should have this general structure, arranged from parent to child
19+
// componentName:[subComponent,subcomponent,...]
20+
// subComponentName:[publicType,publicType,...]
21+
// publicType:[{ comment,prop,type },{ comment,prop,type },...]
22+
// THEY UPPER-MOST KEY IS ALWAYS USED AS A HEADING
23+
export type ComponentParts = Record<string, SubComponents>;
24+
type SubComponents = SubComponent[];
25+
export type SubComponent = Record<string, PublicType[]>;
26+
export type PublicType = Record<string, ParsedProps[]>;
27+
type ParsedProps = {
28+
comment: string;
29+
prop: string;
30+
type: string;
31+
};
32+
function parseSingleComponentFromDir(path: string, ref: SubComponents) {
33+
const component_name = /\/([\w-]*).tsx/.exec(path);
34+
if (component_name === null || component_name[1] === null) {
35+
// may need better behavior
36+
return;
37+
}
38+
const sourceCode = fs.readFileSync(path, 'utf-8');
39+
const comments = extractPublicTypes(sourceCode);
40+
const parsed: PublicType[] = [];
41+
for (const comment of comments) {
42+
const api = extractComments(comment.string);
43+
const pair: PublicType = { [comment.label]: api };
44+
parsed.push(pair);
45+
}
46+
const completeSubComponent: SubComponent = { [component_name[1]]: parsed };
47+
ref.push(completeSubComponent);
48+
return ref;
49+
}
50+
51+
function extractPublicTypes(strg: string) {
52+
const getPublicTypes = /type Public([A-Z][\w]*)*[\w\W]*?{([\w|\W]*?)}(;| &)/gm;
53+
const cms = [];
54+
let groups;
55+
while ((groups = getPublicTypes.exec(strg)) !== null) {
56+
const string = groups[2];
57+
cms.push({ label: groups[1], string });
58+
}
59+
return cms;
60+
}
61+
function extractComments(strg: string): ParsedProps[] {
62+
const magical_regex =
63+
/^\s*?\/[*]{2}\n?([\w|\W|]*?)\s*[*]{1,2}[/]\n[ ]*([\w|\W]*?): ([\w|\W]*?);?$/gm;
64+
65+
const cms = [];
66+
let groups;
67+
68+
while ((groups = magical_regex.exec(strg)) !== null) {
69+
const trimStart = /^ *|(\* *)/g;
70+
const comment = groups[1].replaceAll(trimStart, '');
71+
const prop = groups[2];
72+
const type = groups[3];
73+
cms.push({ comment, prop, type });
74+
}
75+
return cms;
76+
}
77+
function writeToDocs(fullPath: string, componentName: string, api: ComponentParts) {
78+
if (fullPath.includes('kit-headless')) {
79+
const relDocPath = `../website/src/routes//docs/headless/${componentName}`;
80+
const fullDocPath = resolve(__dirname, relDocPath);
81+
const dirPath = fullDocPath.concat('/auto-api');
82+
83+
if (!fs.existsSync(dirPath)) {
84+
fs.mkdirSync(dirPath);
85+
}
86+
const json = JSON.stringify(api, null, 2);
87+
const hacky = `export const api=${json}`;
88+
89+
try {
90+
fs.writeFileSync(dirPath.concat('/api.ts'), hacky);
91+
console.log('auto-api: succesfully genereated new json!!! :)');
92+
} catch (err) {
93+
return;
94+
}
95+
}
96+
}
97+
function loopOnAllChildFiles(filePath: string) {
98+
const childComponentRegex = /\/([\w-]*).tsx$/.exec(filePath);
99+
if (childComponentRegex === null) {
100+
return;
101+
}
102+
const parentDir = filePath.replace(childComponentRegex[0], '');
103+
const componentRegex = /\/(\w*)$/.exec(parentDir);
104+
if (!fs.existsSync(parentDir) || componentRegex == null) {
105+
return;
106+
}
107+
const componentName = componentRegex[1];
108+
const allParts: SubComponents = [];
109+
const store: ComponentParts = { [componentName]: allParts };
110+
fs.readdirSync(parentDir).forEach((fileName) => {
111+
if (/tsx$/.test(fileName)) {
112+
const fullPath = parentDir + '/' + fileName;
113+
parseSingleComponentFromDir(fullPath, store[componentName]);
114+
}
115+
});
116+
117+
writeToDocs(filePath, componentName, store);
118+
}

apps/website/src/components/api-table/api-table.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { component$ } from '@builder.io/qwik';
22
import { InfoPopup } from '../info-popup/info-popup';
3-
type APITableProps = {
3+
export type APITableProps = {
44
propDescriptors: {
55
name: string;
6-
info: string;
6+
info?: string;
77
type: string;
88
description: string;
99
}[];
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { JSXOutput, component$, $, QRL, useTask$, useSignal } from '@builder.io/qwik';
2+
import { APITable, type APITableProps } from './api-table';
3+
import { packages } from '../install-snippet/install-snippet';
4+
5+
//This is a workaround for not being able to export across packages due to nx rule:
6+
// https://nx.dev/features/enforce-module-boundaries#enforce-module-boundaries
7+
type ComponentParts = Record<string, SubComponents>;
8+
type SubComponents = SubComponent[];
9+
type SubComponent = Record<string, PublicType[]>;
10+
type PublicType = Record<string, ParsedProps[]>;
11+
type ParsedProps = {
12+
comment: string;
13+
prop: string;
14+
type: string;
15+
};
16+
type AutoAPIConfig = {
17+
topHeader?: QRL<(text: string) => JSXOutput>;
18+
subHeader?: QRL<(text: string) => JSXOutput>;
19+
props?: QRL<(text: string) => string>;
20+
};
21+
22+
type AnatomyTableProps = {
23+
api?: ComponentParts;
24+
config: AutoAPIConfig;
25+
};
26+
27+
type SubComponentProps = {
28+
subComponent: SubComponent;
29+
config: AutoAPIConfig;
30+
};
31+
type ParsedCommentsProps = {
32+
parsedProps: PublicType;
33+
config: AutoAPIConfig;
34+
};
35+
const currentHeader = $((_: string) => {
36+
//cannot send h2 from here because current TOC can only read md
37+
return null;
38+
});
39+
40+
const currentSubHeader = $((text: string) => {
41+
let subHeader = text.replace(/(p|P)rops/, '');
42+
const hasCapital = /[a-z][A-Z]/.exec(subHeader)?.index;
43+
if (hasCapital != undefined) {
44+
subHeader =
45+
subHeader.slice(0, hasCapital + 1) + '.' + subHeader.slice(hasCapital + 1);
46+
}
47+
return (
48+
<>
49+
<h3 class="mb-6 mt-8 scroll-mt-20 text-xl font-semibold">{subHeader}</h3>
50+
</>
51+
);
52+
});
53+
54+
const removeQuestionMarkFromProp = $((text: string) => {
55+
return text.replace('?', '');
56+
});
57+
const defaultConfig: AutoAPIConfig = {
58+
topHeader: currentHeader,
59+
subHeader: currentSubHeader,
60+
props: removeQuestionMarkFromProp,
61+
};
62+
export const AutoAPI = component$<AnatomyTableProps>(
63+
({ api, config = defaultConfig }) => {
64+
if (api === undefined) {
65+
return null;
66+
}
67+
const key = Object.keys(api)[0];
68+
const topHeaderSig = useSignal<string | JSXOutput>(key);
69+
const subComponents = api[key].filter((e) => e[Object.keys(e)[0]].length > 0);
70+
useTask$(async () => {
71+
if (config.topHeader) {
72+
topHeaderSig.value = await config.topHeader(key as string);
73+
}
74+
});
75+
return (
76+
<>
77+
{topHeaderSig.value}
78+
{subComponents.map((e) => (
79+
<SubComponent subComponent={e} config={config} />
80+
))}
81+
</>
82+
);
83+
},
84+
);
85+
86+
const SubComponent = component$<SubComponentProps>(({ subComponent, config }) => {
87+
const subComponentKey = Object.keys(subComponent)[0];
88+
const comments = subComponent[subComponentKey];
89+
return (
90+
<>
91+
{comments.map((e) => (
92+
<>
93+
<ParsedComments parsedProps={e} config={config} />
94+
</>
95+
))}
96+
</>
97+
);
98+
});
99+
100+
const ParsedComments = component$<ParsedCommentsProps>(({ parsedProps, config }) => {
101+
const key = Object.keys(parsedProps)[0];
102+
const subHeaderSig = useSignal<string | JSXOutput>(key);
103+
useTask$(async () => {
104+
if (config.subHeader) {
105+
subHeaderSig.value = await config.subHeader(key as string);
106+
}
107+
});
108+
const appliedPropsSig = useSignal<null | APITableProps>(null);
109+
useTask$(async () => {
110+
const translation: APITableProps = {
111+
propDescriptors: parsedProps[key].map((e) => {
112+
return {
113+
name: e.prop,
114+
type: e.type,
115+
description: e.comment,
116+
};
117+
}),
118+
};
119+
if (config.props) {
120+
for (const props of translation.propDescriptors) {
121+
props.name = await config.props(props.name);
122+
}
123+
}
124+
appliedPropsSig.value = translation;
125+
});
126+
return (
127+
<>
128+
{subHeaderSig.value}
129+
{appliedPropsSig.value?.propDescriptors && (
130+
<APITable propDescriptors={appliedPropsSig.value?.propDescriptors} />
131+
)}
132+
</>
133+
);
134+
});

apps/website/src/components/mdx-components/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Component, PropsOf, Slot, component$ } from '@builder.io/qwik';
22
import { cn } from '@qwik-ui/utils';
33
import { AnatomyTable } from '../anatomy-table/anatomy-table';
44
import { APITable } from '../api-table/api-table';
5+
import { AutoAPI } from '../api-table/auto-api';
56
import { CodeCopy } from '../code-copy/code-copy';
67
import { CodeSnippet } from '../code-snippet/code-snippet';
78
import { FeatureList } from '../feature-list/feature-list';
@@ -132,4 +133,5 @@ export const components: Record<string, Component> = {
132133
Note,
133134
StatusBanner,
134135
Showcase,
136+
AutoAPI,
135137
};

apps/website/src/routes/docs/headless/select/index.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: Qwik UI | Select
33
---
44

5+
import { api } from './auto-api/api';
56
import { FeatureList } from '~/components/feature-list/feature-list';
67
import { statusByComponent } from '~/_state/component-statuses';
78

apps/website/vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { qwikVite } from '@builder.io/qwik/optimizer';
33
import { defineConfig } from 'vite';
44
import tsconfigPaths from 'vite-tsconfig-paths';
55
import { recmaProvideComponents } from './recma-provide-components';
6+
import autoAPI from './auto-api';
67

78
export default defineConfig(async () => {
89
const { default: rehypePrettyCode } = await import('rehype-pretty-code');
@@ -29,6 +30,7 @@ export default defineConfig(async () => {
2930

3031
return {
3132
plugins: [
33+
autoAPI(),
3234
qwikCity({
3335
mdxPlugins: {
3436
rehypeSyntaxHighlight: false,

packages/kit-headless/src/components/select/select-root.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ type TMultiValue =
4444

4545
type TStringOrArray =
4646
| {
47-
multiple?: true;
48-
onChange$?: QRL<(value: string[]) => void>;
49-
}
47+
multiple?: true;
48+
onChange$?: QRL<(value: string[]) => void>;
49+
}
5050
| {
51-
multiple?: false;
52-
onChange$?: QRL<(value: string) => void>;
53-
};
51+
multiple?: false;
52+
onChange$?: QRL<(value: string) => void>;
53+
};
5454

5555
export type SelectProps<M extends boolean = boolean> = Omit<
5656
PropsOf<'div'>,

0 commit comments

Comments
 (0)