Skip to content

Commit 05aa9ea

Browse files
authored
docs: publish a static registry for vuetify add (#721)
* docs: publish a static registry for vuetify add v0 ships a real package, so a consumer can already install it — what they cannot get is a working, styled file, because a bare import of a headless primitive renders nothing. This publishes the docs examples as a registry a CLI can read, so `vuetify add dialog` writes a runnable demo. The source of truth is the `::: gn-example` directive already present on every feature page: it declares an ordered file manifest, external `@import` dependencies, a title, and prose. Building on it means the registry inherits the curation instead of guessing from a directory listing, and it picks up the supporting `.ts` files that `examples.json` drops today. Emits `registry/index.json` (a 30 KB catalog, versus the 521 KB examples.json blob, so resolving a name is cheap), `registry/{type}/{name}.json` per item, and `registry/tokens.json` carrying the semantic token contract plus the UnoCSS and Tailwind snippets that map it — one source of truth the CLI reads rather than a copy that drifts. Examples may only use tokens the create-vuetify0 templates map, since anything else renders unstyled once copied out of the docs site. Generation now fails a production build on a violation; `bg-accent` in two observer examples is swapped for portable colors accordingly. * refactor(theme): move the semantic color contract into packages/0 The list of semantic colors was declared in the docs registry builder, but the guarantee is the theme layer's — those are the names `createThemePlugin` emits and a consumer maps onto utility classes. Leaving it in `apps/docs/build` implied the docs site owned it, when the docs site is one of three consumers alongside the create-vuetify0 templates and the CLI, all synced by hand. It moves to `packages/0/src/theme/tokens.ts`, imported by the builder the same way `maturity.json` already is. The generation itself stays in `apps/docs` — it reads `src/pages` and `src/examples`, so hoisting it into a package would have a package reaching up into an app to read its content. Deliberately not exported from `@vuetify/v0/theme`. Nothing outside this repo consumes it, and making it public is a minor bump that belongs with the change that gives the templates a reason to import it. Emitted registry is unchanged: 93 items, 144 examples, 20 tokens. * docs(registry): harden vuetify add portability Warn on case-colliding basenames (fatal in prod), skip incomplete multi-file blocks, invalidate the dev memo on page/example edits, and surface Uno icon utilities as soft deps. Rename six PascalCase/entry pairs that collided on case-insensitive filesystems. * docs(registry): close review gaps in the add registry builder Derive example ids and dirs from the entry file, join multi-line prose, skip fence-local paths, confine reads to the examples root, reload maturity/package.json per build, and broaden dev-cache invalidation. * docs(registry): shape icons as collections for install soft-deps Emit icons.collections (lucide, mdi, …) as the actionable install unit and keep classes for audit. Frame the module as the official seed catalog for the local component-library lifecycle. * docs(registry): include Plugin and Transformer docs pages features.category Plugin/Transformer were skipped (only Component/Composable matched), so useTheme and the rest of the plugins family never entered the seed catalog. Map them onto the composables bucket where their examples live. * docs(registry): plugin install recipes and CORS for playground Emit install recipes (factory/label/file) on plugin items so the CLI can wire create*Plugin without a hard-coded map, include install-only plugins with zero examples, and allow cross-origin /registry/* reads from the playground. * docs(registry): harden path confinement and install recipe shape Reject path segments (..), resolve examples through realpath so git symlinks cannot smuggle host files into the payload, validate feature names and create*Plugin install recipes before emit, and restrict the dev /registry middleware to GET (+ OPTIONS). * docs(registry): tighten install label and catalog integrity Sanitize plugin install labels to plain identifiers, warn on duplicate type/name keys, align nginx CORS headers with the dev middleware, and drop a stale comment about reflected 404 paths.
1 parent 6b3c7e2 commit 05aa9ea

25 files changed

Lines changed: 1065 additions & 32 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* Vite plugin to publish the official seed registry for `vuetify add`.
3+
*
4+
* Serves `/registry/*` in dev and emits the same files as build assets so the
5+
* CLI can read a plain static origin. This is the first-party catalog of docs
6+
* examples — not a user's library (that is `vuetify.json` + optional self-hosted
7+
* registries later).
8+
*
9+
* Endpoints:
10+
* - `/registry/index.json` slim catalog (names, types, example ids)
11+
* - `/registry/tokens.json` semantic token contract + config snippets
12+
* - `/registry/{type}/{name}.json` one item, file contents included
13+
*/
14+
15+
import { build, contract } from './registry'
16+
17+
// Types
18+
import type { Registry } from './registry'
19+
import type { Plugin } from 'vite'
20+
21+
/**
22+
* Docs-only tokens in an example render unstyled once copied into a consumer
23+
* project, so they fail the production build rather than shipping broken. Dev
24+
* only warns — an author mid-edit should not be blocked.
25+
*/
26+
function report (warnings: string[], fatal: boolean) {
27+
if (warnings.length === 0) return
28+
29+
for (const warning of warnings) console.warn(warning)
30+
31+
if (fatal) {
32+
throw new Error(
33+
`[generate-registry] ${warnings.length} registry problem(s). `
34+
+ `See warnings above (missing files, case collisions, docs-only tokens, …).`,
35+
)
36+
}
37+
}
38+
39+
export default function generateRegistryPlugin (): Plugin {
40+
let registry: Registry | null = null
41+
let pending: Promise<Registry> | null = null
42+
let dev = false
43+
44+
async function get () {
45+
if (registry) return registry
46+
pending ??= (async () => {
47+
try {
48+
const result = await build()
49+
console.log(`[generate-registry] ${result.items.length} items`)
50+
report(result.warnings, !dev)
51+
return result
52+
} catch (error) {
53+
pending = null
54+
throw error
55+
}
56+
})()
57+
58+
registry = await pending
59+
return registry
60+
}
61+
62+
return {
63+
name: 'generate-registry',
64+
65+
configureServer (server) {
66+
dev = true
67+
68+
// Mirror generate-nav / generate-llms-full: drop the memo when source
69+
// pages or examples change so a local CLI against the dev origin sees
70+
// fresh bodies without a server restart. `add`/`unlink` matter when an
71+
// author creates or deletes an example file mid-session.
72+
function invalidate (file: string) {
73+
// Vite may report Windows paths with `\`; normalize before matching.
74+
const normalized = file.replaceAll('\\', '/')
75+
const data = normalized.endsWith('maturity.json')
76+
|| normalized.endsWith('package.json')
77+
|| normalized.endsWith('uno.config.ts')
78+
const docs = (normalized.includes('/pages/') || normalized.includes('/examples/'))
79+
&& (normalized.endsWith('.md') || normalized.endsWith('.vue') || normalized.endsWith('.ts'))
80+
if (!data && !docs) return
81+
registry = null
82+
pending = null
83+
}
84+
85+
for (const event of ['change', 'add', 'unlink'] as const) {
86+
server.watcher.on(event, invalidate)
87+
}
88+
89+
server.middlewares.use(async (req, res, next) => {
90+
const url = req.url?.split('?')[0]
91+
if (!url?.startsWith('/registry/') || !url.endsWith('.json')) return next()
92+
93+
// Playground (v0play / localhost) fetches this origin cross-site.
94+
res.setHeader('Access-Control-Allow-Origin', '*')
95+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
96+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
97+
res.setHeader('Allow', 'GET, OPTIONS')
98+
99+
if (req.method === 'OPTIONS') {
100+
res.statusCode = 204
101+
res.end()
102+
return
103+
}
104+
105+
if (req.method !== 'GET') {
106+
res.statusCode = 405
107+
res.end('Method Not Allowed')
108+
return
109+
}
110+
111+
try {
112+
const data = await get()
113+
const path = url.slice('/registry/'.length, -'.json'.length)
114+
115+
const body = path === 'index'
116+
? data.index
117+
: (path === 'tokens'
118+
? contract()
119+
: data.items.find(item => `${item.type}/${item.name}` === path))
120+
121+
if (!body) {
122+
res.statusCode = 404
123+
res.end('Unknown registry item')
124+
return
125+
}
126+
127+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
128+
res.end(JSON.stringify(body))
129+
} catch (error) {
130+
console.error('[generate-registry] Error:', error)
131+
res.statusCode = 500
132+
res.end('Error generating registry')
133+
}
134+
})
135+
},
136+
137+
async generateBundle (_, bundle) {
138+
// Skip if this is the main entry (avoid duplicate emission)
139+
if (Object.keys(bundle).some(k => k.includes('main.mjs'))) return
140+
141+
const data = await get()
142+
143+
this.emitFile({
144+
type: 'asset',
145+
fileName: 'registry/index.json',
146+
source: JSON.stringify(data.index),
147+
})
148+
149+
this.emitFile({
150+
type: 'asset',
151+
fileName: 'registry/tokens.json',
152+
source: JSON.stringify(contract()),
153+
})
154+
155+
for (const item of data.items) {
156+
this.emitFile({
157+
type: 'asset',
158+
fileName: `registry/${item.type}/${item.name}.json`,
159+
source: JSON.stringify(item),
160+
})
161+
}
162+
},
163+
164+
buildEnd () {
165+
registry = null
166+
pending = null
167+
},
168+
}
169+
}

0 commit comments

Comments
 (0)