Skip to content

Commit

Permalink
Custom automatic api (#835)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
TheMcnafaha authored Sep 29, 2024
1 parent af6aff9 commit 27474ea
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 8 deletions.
118 changes: 118 additions & 0 deletions apps/website/auto-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as fs from 'fs';
import { resolve } from 'path';
import { inspect } from 'util';
import { ViteDevServer } from 'vite';
export default function autoAPI() {
return {
name: 'watch-monorepo-changes',
configureServer(server: ViteDevServer) {
const watchPath = resolve(__dirname, '../../packages/kit-headless');
server.watcher.on('change', (file: string) => {
if (file.startsWith(watchPath)) {
loopOnAllChildFiles(file);
}
});
},
};
}
// the object should have this general structure, arranged from parent to child
// componentName:[subComponent,subcomponent,...]
// subComponentName:[publicType,publicType,...]
// publicType:[{ comment,prop,type },{ comment,prop,type },...]
// THEY UPPER-MOST KEY IS ALWAYS USED AS A HEADING
export type ComponentParts = Record<string, SubComponents>;
type SubComponents = SubComponent[];
export type SubComponent = Record<string, PublicType[]>;
export type PublicType = Record<string, ParsedProps[]>;
type ParsedProps = {
comment: string;
prop: string;
type: string;
};
function parseSingleComponentFromDir(path: string, ref: SubComponents) {
const component_name = /\/([\w-]*).tsx/.exec(path);
if (component_name === null || component_name[1] === null) {
// may need better behavior
return;
}
const sourceCode = fs.readFileSync(path, 'utf-8');
const comments = extractPublicTypes(sourceCode);
const parsed: PublicType[] = [];
for (const comment of comments) {
const api = extractComments(comment.string);
const pair: PublicType = { [comment.label]: api };
parsed.push(pair);
}
const completeSubComponent: SubComponent = { [component_name[1]]: parsed };
ref.push(completeSubComponent);
return ref;
}

function extractPublicTypes(strg: string) {
const getPublicTypes = /type Public([A-Z][\w]*)*[\w\W]*?{([\w|\W]*?)}(;| &)/gm;
const cms = [];
let groups;
while ((groups = getPublicTypes.exec(strg)) !== null) {
const string = groups[2];
cms.push({ label: groups[1], string });
}
return cms;
}
function extractComments(strg: string): ParsedProps[] {
const magical_regex =
/^\s*?\/[*]{2}\n?([\w|\W|]*?)\s*[*]{1,2}[/]\n[ ]*([\w|\W]*?): ([\w|\W]*?);?$/gm;

const cms = [];
let groups;

while ((groups = magical_regex.exec(strg)) !== null) {
const trimStart = /^ *|(\* *)/g;
const comment = groups[1].replaceAll(trimStart, '');
const prop = groups[2];
const type = groups[3];
cms.push({ comment, prop, type });
}
return cms;
}
function writeToDocs(fullPath: string, componentName: string, api: ComponentParts) {
if (fullPath.includes('kit-headless')) {
const relDocPath = `../website/src/routes//docs/headless/${componentName}`;
const fullDocPath = resolve(__dirname, relDocPath);
const dirPath = fullDocPath.concat('/auto-api');

if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const json = JSON.stringify(api, null, 2);
const hacky = `export const api=${json}`;

try {
fs.writeFileSync(dirPath.concat('/api.ts'), hacky);
console.log('auto-api: succesfully genereated new json!!! :)');
} catch (err) {
return;
}
}
}
function loopOnAllChildFiles(filePath: string) {
const childComponentRegex = /\/([\w-]*).tsx$/.exec(filePath);
if (childComponentRegex === null) {
return;
}
const parentDir = filePath.replace(childComponentRegex[0], '');
const componentRegex = /\/(\w*)$/.exec(parentDir);
if (!fs.existsSync(parentDir) || componentRegex == null) {
return;
}
const componentName = componentRegex[1];
const allParts: SubComponents = [];
const store: ComponentParts = { [componentName]: allParts };
fs.readdirSync(parentDir).forEach((fileName) => {
if (/tsx$/.test(fileName)) {
const fullPath = parentDir + '/' + fileName;
parseSingleComponentFromDir(fullPath, store[componentName]);
}
});

writeToDocs(filePath, componentName, store);
}
4 changes: 2 additions & 2 deletions apps/website/src/components/api-table/api-table.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { component$ } from '@builder.io/qwik';
import { InfoPopup } from '../info-popup/info-popup';
type APITableProps = {
export type APITableProps = {
propDescriptors: {
name: string;
info: string;
info?: string;
type: string;
description: string;
}[];
Expand Down
134 changes: 134 additions & 0 deletions apps/website/src/components/api-table/auto-api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { JSXOutput, component$, $, QRL, useTask$, useSignal } from '@builder.io/qwik';
import { APITable, type APITableProps } from './api-table';
import { packages } from '../install-snippet/install-snippet';

//This is a workaround for not being able to export across packages due to nx rule:
// https://nx.dev/features/enforce-module-boundaries#enforce-module-boundaries
type ComponentParts = Record<string, SubComponents>;
type SubComponents = SubComponent[];
type SubComponent = Record<string, PublicType[]>;
type PublicType = Record<string, ParsedProps[]>;
type ParsedProps = {
comment: string;
prop: string;
type: string;
};
type AutoAPIConfig = {
topHeader?: QRL<(text: string) => JSXOutput>;
subHeader?: QRL<(text: string) => JSXOutput>;
props?: QRL<(text: string) => string>;
};

type AnatomyTableProps = {
api?: ComponentParts;
config: AutoAPIConfig;
};

type SubComponentProps = {
subComponent: SubComponent;
config: AutoAPIConfig;
};
type ParsedCommentsProps = {
parsedProps: PublicType;
config: AutoAPIConfig;
};
const currentHeader = $((_: string) => {
//cannot send h2 from here because current TOC can only read md
return null;
});

const currentSubHeader = $((text: string) => {
let subHeader = text.replace(/(p|P)rops/, '');
const hasCapital = /[a-z][A-Z]/.exec(subHeader)?.index;
if (hasCapital != undefined) {
subHeader =
subHeader.slice(0, hasCapital + 1) + '.' + subHeader.slice(hasCapital + 1);
}
return (
<>
<h3 class="mb-6 mt-8 scroll-mt-20 text-xl font-semibold">{subHeader}</h3>
</>
);
});

const removeQuestionMarkFromProp = $((text: string) => {
return text.replace('?', '');
});
const defaultConfig: AutoAPIConfig = {
topHeader: currentHeader,
subHeader: currentSubHeader,
props: removeQuestionMarkFromProp,
};
export const AutoAPI = component$<AnatomyTableProps>(
({ api, config = defaultConfig }) => {
if (api === undefined) {
return null;
}
const key = Object.keys(api)[0];
const topHeaderSig = useSignal<string | JSXOutput>(key);
const subComponents = api[key].filter((e) => e[Object.keys(e)[0]].length > 0);
useTask$(async () => {
if (config.topHeader) {
topHeaderSig.value = await config.topHeader(key as string);
}
});
return (
<>
{topHeaderSig.value}
{subComponents.map((e) => (
<SubComponent subComponent={e} config={config} />
))}
</>
);
},
);

const SubComponent = component$<SubComponentProps>(({ subComponent, config }) => {
const subComponentKey = Object.keys(subComponent)[0];
const comments = subComponent[subComponentKey];
return (
<>
{comments.map((e) => (
<>
<ParsedComments parsedProps={e} config={config} />
</>
))}
</>
);
});

const ParsedComments = component$<ParsedCommentsProps>(({ parsedProps, config }) => {
const key = Object.keys(parsedProps)[0];
const subHeaderSig = useSignal<string | JSXOutput>(key);
useTask$(async () => {
if (config.subHeader) {
subHeaderSig.value = await config.subHeader(key as string);
}
});
const appliedPropsSig = useSignal<null | APITableProps>(null);
useTask$(async () => {
const translation: APITableProps = {
propDescriptors: parsedProps[key].map((e) => {
return {
name: e.prop,
type: e.type,
description: e.comment,
};
}),
};
if (config.props) {
for (const props of translation.propDescriptors) {
props.name = await config.props(props.name);
}
}
appliedPropsSig.value = translation;
});
return (
<>
{subHeaderSig.value}
{appliedPropsSig.value?.propDescriptors && (
<APITable propDescriptors={appliedPropsSig.value?.propDescriptors} />
)}
</>
);
});
2 changes: 2 additions & 0 deletions apps/website/src/components/mdx-components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, PropsOf, Slot, component$ } from '@builder.io/qwik';
import { cn } from '@qwik-ui/utils';
import { AnatomyTable } from '../anatomy-table/anatomy-table';
import { APITable } from '../api-table/api-table';
import { AutoAPI } from '../api-table/auto-api';
import { CodeCopy } from '../code-copy/code-copy';
import { CodeSnippet } from '../code-snippet/code-snippet';
import { FeatureList } from '../feature-list/feature-list';
Expand Down Expand Up @@ -132,4 +133,5 @@ export const components: Record<string, Component> = {
Note,
StatusBanner,
Showcase,
AutoAPI,
};
1 change: 1 addition & 0 deletions apps/website/src/routes/docs/headless/select/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
title: Qwik UI | Select
---

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

Expand Down
2 changes: 2 additions & 0 deletions apps/website/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { qwikVite } from '@builder.io/qwik/optimizer';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import { recmaProvideComponents } from './recma-provide-components';
import autoAPI from './auto-api';

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

return {
plugins: [
autoAPI(),
qwikCity({
mdxPlugins: {
rehypeSyntaxHighlight: false,
Expand Down
12 changes: 6 additions & 6 deletions packages/kit-headless/src/components/select/select-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ type TMultiValue =

type TStringOrArray =
| {
multiple?: true;
onChange$?: QRL<(value: string[]) => void>;
}
multiple?: true;
onChange$?: QRL<(value: string[]) => void>;
}
| {
multiple?: false;
onChange$?: QRL<(value: string) => void>;
};
multiple?: false;
onChange$?: QRL<(value: string) => void>;
};

export type SelectProps<M extends boolean = boolean> = Omit<
PropsOf<'div'>,
Expand Down

0 comments on commit 27474ea

Please sign in to comment.