diff --git a/apps/website/auto-api.ts b/apps/website/auto-api.ts new file mode 100644 index 000000000..d4f2bafe2 --- /dev/null +++ b/apps/website/auto-api.ts @@ -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; +type SubComponents = SubComponent[]; +export type SubComponent = Record; +export type PublicType = Record; +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); +} diff --git a/apps/website/src/components/api-table/api-table.tsx b/apps/website/src/components/api-table/api-table.tsx index d5dbfdd06..65f1fe672 100644 --- a/apps/website/src/components/api-table/api-table.tsx +++ b/apps/website/src/components/api-table/api-table.tsx @@ -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; }[]; diff --git a/apps/website/src/components/api-table/auto-api.tsx b/apps/website/src/components/api-table/auto-api.tsx new file mode 100644 index 000000000..526b6b33c --- /dev/null +++ b/apps/website/src/components/api-table/auto-api.tsx @@ -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; +type SubComponents = SubComponent[]; +type SubComponent = Record; +type PublicType = Record; +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 ( + <> +

{subHeader}

+ + ); +}); + +const removeQuestionMarkFromProp = $((text: string) => { + return text.replace('?', ''); +}); +const defaultConfig: AutoAPIConfig = { + topHeader: currentHeader, + subHeader: currentSubHeader, + props: removeQuestionMarkFromProp, +}; +export const AutoAPI = component$( + ({ api, config = defaultConfig }) => { + if (api === undefined) { + return null; + } + const key = Object.keys(api)[0]; + const topHeaderSig = useSignal(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) => ( + + ))} + + ); + }, +); + +const SubComponent = component$(({ subComponent, config }) => { + const subComponentKey = Object.keys(subComponent)[0]; + const comments = subComponent[subComponentKey]; + return ( + <> + {comments.map((e) => ( + <> + + + ))} + + ); +}); + +const ParsedComments = component$(({ parsedProps, config }) => { + const key = Object.keys(parsedProps)[0]; + const subHeaderSig = useSignal(key); + useTask$(async () => { + if (config.subHeader) { + subHeaderSig.value = await config.subHeader(key as string); + } + }); + const appliedPropsSig = useSignal(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 && ( + + )} + + ); +}); diff --git a/apps/website/src/components/mdx-components/index.tsx b/apps/website/src/components/mdx-components/index.tsx index 3a1d9e7ee..603655d39 100644 --- a/apps/website/src/components/mdx-components/index.tsx +++ b/apps/website/src/components/mdx-components/index.tsx @@ -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'; @@ -132,4 +133,5 @@ export const components: Record = { Note, StatusBanner, Showcase, + AutoAPI, }; diff --git a/apps/website/src/routes/docs/headless/select/index.mdx b/apps/website/src/routes/docs/headless/select/index.mdx index d42ead181..d92d0c4bc 100644 --- a/apps/website/src/routes/docs/headless/select/index.mdx +++ b/apps/website/src/routes/docs/headless/select/index.mdx @@ -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'; diff --git a/apps/website/vite.config.ts b/apps/website/vite.config.ts index 702ab3aeb..57617084c 100644 --- a/apps/website/vite.config.ts +++ b/apps/website/vite.config.ts @@ -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'); @@ -29,6 +30,7 @@ export default defineConfig(async () => { return { plugins: [ + autoAPI(), qwikCity({ mdxPlugins: { rehypeSyntaxHighlight: false, diff --git a/packages/kit-headless/src/components/select/select-root.tsx b/packages/kit-headless/src/components/select/select-root.tsx index d0af4308a..449c9d812 100644 --- a/packages/kit-headless/src/components/select/select-root.tsx +++ b/packages/kit-headless/src/components/select/select-root.tsx @@ -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 = Omit< PropsOf<'div'>,