@sntlr/registry-shell 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/adapter/custom.d.ts +47 -0
- package/dist/adapter/custom.js +53 -0
- package/dist/adapter/custom.js.map +1 -0
- package/dist/adapter/default.d.ts +40 -0
- package/dist/adapter/default.js +202 -0
- package/dist/adapter/default.js.map +1 -0
- package/dist/cli/build.d.ts +1 -0
- package/dist/cli/build.js +31 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/dev.d.ts +1 -0
- package/dist/cli/dev.js +26 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +70 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/shared.d.ts +33 -0
- package/dist/cli/shared.js +278 -0
- package/dist/cli/shared.js.map +1 -0
- package/dist/cli/start.d.ts +1 -0
- package/dist/cli/start.js +24 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/config-loader.d.ts +49 -0
- package/dist/config-loader.js +140 -0
- package/dist/define-config.d.ts +188 -0
- package/dist/define-config.js +21 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/package.json +124 -0
- package/src/adapter/custom.ts +90 -0
- package/src/adapter/default.ts +241 -0
- package/src/cli/build.ts +38 -0
- package/src/cli/dev.ts +38 -0
- package/src/cli/index.ts +52 -0
- package/src/cli/init.ts +76 -0
- package/src/cli/shared.ts +306 -0
- package/src/cli/start.ts +28 -0
- package/src/config-loader.ts +190 -0
- package/src/define-config.ts +206 -0
- package/src/index.ts +17 -0
- package/src/next-app/app/[...asset]/route.ts +81 -0
- package/src/next-app/app/_user-global.css +6 -0
- package/src/next-app/app/_user-sources.css +9 -0
- package/src/next-app/app/a11y/[name]/route.ts +19 -0
- package/src/next-app/app/api/search-index/route.ts +19 -0
- package/src/next-app/app/components/[name]/page.tsx +61 -0
- package/src/next-app/app/components/layout.tsx +18 -0
- package/src/next-app/app/docs/[slug]/page.tsx +53 -0
- package/src/next-app/app/docs/layout.tsx +18 -0
- package/src/next-app/app/globals.css +329 -0
- package/src/next-app/app/layout.tsx +102 -0
- package/src/next-app/app/page.tsx +9 -0
- package/src/next-app/app/preview-snapshot/[name]/page.tsx +20 -0
- package/src/next-app/app/preview-snapshot/layout.tsx +17 -0
- package/src/next-app/app/props/[name]/route.ts +19 -0
- package/src/next-app/app/r/[name]/route.ts +14 -0
- package/src/next-app/app/tests/[name]/route.ts +19 -0
- package/src/next-app/components/a11y-info.tsx +287 -0
- package/src/next-app/components/a11y-provider.tsx +39 -0
- package/src/next-app/components/component-breadcrumb.tsx +55 -0
- package/src/next-app/components/component-icon.tsx +140 -0
- package/src/next-app/components/component-preview.tsx +13 -0
- package/src/next-app/components/component-tabs.tsx +209 -0
- package/src/next-app/components/docs-toc.tsx +86 -0
- package/src/next-app/components/global-mobile-sidebar.tsx +35 -0
- package/src/next-app/components/header.tsx +188 -0
- package/src/next-app/components/heading-anchor.tsx +52 -0
- package/src/next-app/components/homepage-demo.tsx +180 -0
- package/src/next-app/components/locale-toggle.tsx +35 -0
- package/src/next-app/components/localized-mdx-client.tsx +14 -0
- package/src/next-app/components/localized-mdx.tsx +27 -0
- package/src/next-app/components/mobile-sidebar.tsx +22 -0
- package/src/next-app/components/nav-data-provider.tsx +37 -0
- package/src/next-app/components/navigation-progress.tsx +62 -0
- package/src/next-app/components/preview-canvas.tsx +368 -0
- package/src/next-app/components/preview-controls.tsx +94 -0
- package/src/next-app/components/preview-layout.tsx +218 -0
- package/src/next-app/components/props-table.tsx +134 -0
- package/src/next-app/components/resizable-preview.tsx +101 -0
- package/src/next-app/components/search.tsx +177 -0
- package/src/next-app/components/settings-modal.tsx +98 -0
- package/src/next-app/components/shell-ui/accordion.tsx +70 -0
- package/src/next-app/components/shell-ui/backdrop.tsx +29 -0
- package/src/next-app/components/shell-ui/badge.tsx +55 -0
- package/src/next-app/components/shell-ui/breadcrumb.tsx +120 -0
- package/src/next-app/components/shell-ui/button.tsx +64 -0
- package/src/next-app/components/shell-ui/card.tsx +127 -0
- package/src/next-app/components/shell-ui/checkbox.tsx +33 -0
- package/src/next-app/components/shell-ui/dialog.tsx +171 -0
- package/src/next-app/components/shell-ui/empty-state.tsx +66 -0
- package/src/next-app/components/shell-ui/input.tsx +27 -0
- package/src/next-app/components/shell-ui/kbd.tsx +30 -0
- package/src/next-app/components/shell-ui/label.tsx +25 -0
- package/src/next-app/components/shell-ui/select.tsx +204 -0
- package/src/next-app/components/shell-ui/separator.tsx +32 -0
- package/src/next-app/components/shell-ui/skeleton.tsx +18 -0
- package/src/next-app/components/shell-ui/table.tsx +124 -0
- package/src/next-app/components/shell-ui/tabs.tsx +102 -0
- package/src/next-app/components/shell-ui/toggle.tsx +56 -0
- package/src/next-app/components/sidebar-layout.tsx +37 -0
- package/src/next-app/components/sidebar-provider.tsx +75 -0
- package/src/next-app/components/sidebar.tsx +222 -0
- package/src/next-app/components/snapshot-preview.tsx +28 -0
- package/src/next-app/components/test-info.tsx +155 -0
- package/src/next-app/components/theme-provider.tsx +16 -0
- package/src/next-app/components/theme-toggle.tsx +21 -0
- package/src/next-app/components/translated-text.tsx +8 -0
- package/src/next-app/fallback/homepage.tsx +112 -0
- package/src/next-app/fallback/previews.ts +17 -0
- package/src/next-app/hooks/use-active-section.ts +23 -0
- package/src/next-app/hooks/use-controls.ts +72 -0
- package/src/next-app/hooks/use-mobile.ts +19 -0
- package/src/next-app/lib/branding.ts +52 -0
- package/src/next-app/lib/components-nav.ts +8 -0
- package/src/next-app/lib/docs.ts +16 -0
- package/src/next-app/lib/github.ts +38 -0
- package/src/next-app/lib/i18n.tsx +630 -0
- package/src/next-app/lib/locales.ts +17 -0
- package/src/next-app/lib/preview-loader.ts +7 -0
- package/src/next-app/lib/registry-adapter.ts +199 -0
- package/src/next-app/lib/utils.ts +6 -0
- package/src/next-app/next-env.d.ts +6 -0
- package/src/next-app/next.config.ts +101 -0
- package/src/next-app/postcss.config.mjs +7 -0
- package/src/next-app/public/favicon.ico +0 -0
- package/src/next-app/public/favicon_dark.svg +3 -0
- package/src/next-app/public/favicon_light.svg +3 -0
- package/src/next-app/registry.config.ts +50 -0
- package/src/next-app/tsconfig.json +29 -0
- package/src/next-app/user-aliases.d.ts +17 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared across CLI commands: locating the shell's bundled Next app,
|
|
3
|
+
* finding the user's config file, and injecting the env vars the Next app
|
|
4
|
+
* reads at boot.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs"
|
|
7
|
+
import path from "node:path"
|
|
8
|
+
import { createRequire } from "node:module"
|
|
9
|
+
import { fileURLToPath } from "node:url"
|
|
10
|
+
import { createJiti } from "jiti"
|
|
11
|
+
import type { ShellConfig } from "../define-config.js"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Absolute path to the Next.js CLI binary shipped with the shell package.
|
|
15
|
+
* Resolves against the shell's own node_modules so a consumer doesn't need
|
|
16
|
+
* to declare Next as a direct dep — they depend on `@sntlr/registry-shell`
|
|
17
|
+
* and transitively get Next.
|
|
18
|
+
*/
|
|
19
|
+
const requireFromHere = createRequire(import.meta.url)
|
|
20
|
+
export const NEXT_BIN = requireFromHere.resolve("next/dist/bin/next")
|
|
21
|
+
|
|
22
|
+
// `path` / `fs` already imported above; ensure both stay usable below.
|
|
23
|
+
|
|
24
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
|
|
26
|
+
const CONFIG_FILE_CANDIDATES = [
|
|
27
|
+
"registry-shell.config.ts",
|
|
28
|
+
"registry-shell.config.js",
|
|
29
|
+
"registry-shell.config.mjs",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/** Walk upward from cwd looking for a config file. Returns null if none. */
|
|
33
|
+
export function findConfigFile(cwd: string = process.cwd()): string | null {
|
|
34
|
+
let dir = cwd
|
|
35
|
+
while (true) {
|
|
36
|
+
for (const name of CONFIG_FILE_CANDIDATES) {
|
|
37
|
+
const candidate = path.join(dir, name)
|
|
38
|
+
if (fs.existsSync(candidate)) return candidate
|
|
39
|
+
}
|
|
40
|
+
const parent = path.dirname(dir)
|
|
41
|
+
if (parent === dir) return null
|
|
42
|
+
dir = parent
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Absolute path to the Next.js app bundled inside the shell package. */
|
|
47
|
+
export function nextAppDir(): string {
|
|
48
|
+
// When running from dist (published), this file is at dist/cli/shared.js.
|
|
49
|
+
// When running from source via tsx, it's at src/cli/shared.ts.
|
|
50
|
+
// In both cases, next-app lives at ../next-app relative to the cli dir.
|
|
51
|
+
const distNextApp = path.resolve(HERE, "../next-app")
|
|
52
|
+
if (fs.existsSync(distNextApp)) return distNextApp
|
|
53
|
+
|
|
54
|
+
const srcNextApp = path.resolve(HERE, "../../src/next-app")
|
|
55
|
+
if (fs.existsSync(srcNextApp)) return srcNextApp
|
|
56
|
+
|
|
57
|
+
throw new Error(
|
|
58
|
+
`[registry-shell] Couldn't locate the bundled Next app. Looked in:\n ${distNextApp}\n ${srcNextApp}`,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LoadedConfig {
|
|
63
|
+
configPath: string
|
|
64
|
+
root: string
|
|
65
|
+
config: ShellConfig
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Read + parse the user's config file via jiti (zero-build TS). Returns null
|
|
70
|
+
* when no config is found (shell-only mode).
|
|
71
|
+
*/
|
|
72
|
+
export function loadUserConfig(): LoadedConfig | null {
|
|
73
|
+
const configPath = findConfigFile()
|
|
74
|
+
if (!configPath) return null
|
|
75
|
+
|
|
76
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true })
|
|
77
|
+
const loaded = jiti(configPath) as unknown
|
|
78
|
+
const config = (
|
|
79
|
+
loaded && typeof loaded === "object" && "default" in loaded
|
|
80
|
+
? (loaded as { default: ShellConfig }).default
|
|
81
|
+
: (loaded as ShellConfig)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if (!config?.branding) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`[registry-shell] Invalid config at ${configPath}: missing required \`branding\`.`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
configPath,
|
|
92
|
+
root: path.dirname(configPath),
|
|
93
|
+
config,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generate a CSS file containing `@source` directives that tell Tailwind v4
|
|
99
|
+
* to scan the user's component/block/doc files for utility classes. Without
|
|
100
|
+
* this, classes used only in the user's files don't end up in the bundled
|
|
101
|
+
* stylesheet. Rewritten every time the CLI boots so path changes in the
|
|
102
|
+
* user's config take effect immediately.
|
|
103
|
+
*/
|
|
104
|
+
/**
|
|
105
|
+
* The shell's `.next/` build output is keyed to the active config. When the
|
|
106
|
+
* user switches from a wired registry to shell-only (or swaps registries
|
|
107
|
+
* altogether), the old build has eager references to files that may no
|
|
108
|
+
* longer resolve. Detect that by stamping the current mode and clearing
|
|
109
|
+
* `.next/` when it changes.
|
|
110
|
+
*/
|
|
111
|
+
export function clearStaleNextCacheIfModeChanged(loaded: LoadedConfig | null): void {
|
|
112
|
+
const nextApp = nextAppDir()
|
|
113
|
+
const nextDir = path.join(nextApp, ".next")
|
|
114
|
+
const stampPath = path.join(nextApp, ".registry-shell-mode")
|
|
115
|
+
const currentMode = loaded?.configPath ?? "<shell-only>"
|
|
116
|
+
|
|
117
|
+
let previousMode = "<never>"
|
|
118
|
+
if (fs.existsSync(stampPath)) {
|
|
119
|
+
try {
|
|
120
|
+
previousMode = fs.readFileSync(stampPath, "utf-8").trim()
|
|
121
|
+
} catch {
|
|
122
|
+
/* ignore */
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (fs.existsSync(nextDir) && previousMode !== currentMode) {
|
|
127
|
+
console.log(
|
|
128
|
+
`[registry-shell] Mode changed (${previousMode} → ${currentMode}) — clearing .next cache.`,
|
|
129
|
+
)
|
|
130
|
+
// Windows can hold file locks (antivirus, IDE indexers) for a moment
|
|
131
|
+
// after the previous Next process exits. Retry a few times; if the
|
|
132
|
+
// directory still won't remove, clear its contents in place instead.
|
|
133
|
+
const tryRemove = (attempt = 0): boolean => {
|
|
134
|
+
try {
|
|
135
|
+
fs.rmSync(nextDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
136
|
+
return true
|
|
137
|
+
} catch {
|
|
138
|
+
if (attempt < 5) return tryRemove(attempt + 1)
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!tryRemove()) {
|
|
143
|
+
try {
|
|
144
|
+
for (const entry of fs.readdirSync(nextDir)) {
|
|
145
|
+
fs.rmSync(path.join(nextDir, entry), {
|
|
146
|
+
recursive: true,
|
|
147
|
+
force: true,
|
|
148
|
+
maxRetries: 5,
|
|
149
|
+
retryDelay: 200,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
console.log("[registry-shell] Directory locked — cleared contents instead.")
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.warn(
|
|
155
|
+
`[registry-shell] Couldn't clear .next cache cleanly (${(err as Error).message}). ` +
|
|
156
|
+
`Continuing; you may see stale-build warnings.`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(stampPath, currentMode + "\n", "utf-8")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function writeUserSourcesCss(loaded: LoadedConfig | null): void {
|
|
166
|
+
const nextApp = nextAppDir()
|
|
167
|
+
const appDir = path.join(nextApp, "app")
|
|
168
|
+
const sourcesTarget = path.join(appDir, "_user-sources.css")
|
|
169
|
+
const globalTarget = path.join(appDir, "_user-global.css")
|
|
170
|
+
|
|
171
|
+
// Tailwind 4 resolves `@source` paths relative to the CSS file on disk and
|
|
172
|
+
// handles platform-specific separators. Use relative paths so Windows
|
|
173
|
+
// drive-letter handling doesn't trip the scanner.
|
|
174
|
+
const rel = (abs: string) => {
|
|
175
|
+
const r = path.relative(appDir, abs).replace(/\\/g, "/")
|
|
176
|
+
return r.startsWith(".") ? r : `./${r}`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── _user-sources.css — `@source` directives only ──────────────────
|
|
180
|
+
// Imported near the TOP of globals.css so Tailwind's class scanner
|
|
181
|
+
// picks up utility usage from the listed dirs.
|
|
182
|
+
const sources = ["/* Auto-generated by @sntlr/registry-shell. Do not edit. */"]
|
|
183
|
+
sources.push(`@source "${rel(path.join(nextApp, "app"))}";`)
|
|
184
|
+
sources.push(`@source "${rel(path.join(nextApp, "components"))}";`)
|
|
185
|
+
sources.push(`@source "${rel(path.join(nextApp, "lib"))}";`)
|
|
186
|
+
sources.push(`@source "${rel(path.join(nextApp, "hooks"))}";`)
|
|
187
|
+
sources.push(`@source "${rel(path.join(nextApp, "fallback"))}";`)
|
|
188
|
+
|
|
189
|
+
if (loaded) {
|
|
190
|
+
const paths = loaded.config.paths ?? {}
|
|
191
|
+
const resolve = (r: string | undefined, fallback: string) =>
|
|
192
|
+
path.resolve(loaded.root, r ?? fallback)
|
|
193
|
+
|
|
194
|
+
sources.push(`@source "${rel(resolve(paths.components, "components/ui"))}";`)
|
|
195
|
+
sources.push(`@source "${rel(resolve(paths.blocks, "registry/new-york/blocks"))}";`)
|
|
196
|
+
sources.push(
|
|
197
|
+
`@source "${rel(resolve(paths.previews?.replace(/\/index\.[tj]sx?$/, ""), "components/previews"))}";`,
|
|
198
|
+
)
|
|
199
|
+
if (loaded.config.homePage) {
|
|
200
|
+
sources.push(`@source "${rel(resolve(loaded.config.homePage, ""))}";`)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
fs.writeFileSync(sourcesTarget, sources.join("\n") + "\n", "utf-8")
|
|
204
|
+
|
|
205
|
+
// ── _user-global.css — user's extra theme/tokens ──────────────────
|
|
206
|
+
// Imported at the BOTTOM of globals.css so `:root { --primary: ... }`
|
|
207
|
+
// style overrides win the cascade over the shell's defaults. Absent
|
|
208
|
+
// when paths.globalCss is not configured (file is written as a no-op
|
|
209
|
+
// so the `@import` in globals.css never 404s).
|
|
210
|
+
const globalLines = ["/* Auto-generated by @sntlr/registry-shell. Do not edit. */"]
|
|
211
|
+
if (loaded) {
|
|
212
|
+
const userGlobal = loaded.config.paths?.globalCss
|
|
213
|
+
if (userGlobal) {
|
|
214
|
+
const abs = path.resolve(loaded.root, userGlobal)
|
|
215
|
+
if (!fs.existsSync(abs)) {
|
|
216
|
+
console.warn(
|
|
217
|
+
`[registry-shell] paths.globalCss points at ${abs} but the file doesn't exist — skipping.`,
|
|
218
|
+
)
|
|
219
|
+
} else {
|
|
220
|
+
globalLines.push(`@import "${rel(abs)}";`)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
fs.writeFileSync(globalTarget, globalLines.join("\n") + "\n", "utf-8")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Build the env-var bag the Next app reads at startup. The CLI spreads this
|
|
229
|
+
* into child `next` processes.
|
|
230
|
+
*/
|
|
231
|
+
/**
|
|
232
|
+
* Resolve the locale list the client's locale toggle should offer. Uses the
|
|
233
|
+
* explicit `locales` array if provided, otherwise auto-scans subfolders of
|
|
234
|
+
* the registry's docs path. Mirrors the logic in the server config-loader
|
|
235
|
+
* but runs client-side via inlined env vars.
|
|
236
|
+
*/
|
|
237
|
+
function resolveLocaleList(root: string, config: ShellConfig): string[] {
|
|
238
|
+
if (!config.multilocale) return []
|
|
239
|
+
if (config.locales && config.locales.length > 0) return [...config.locales]
|
|
240
|
+
|
|
241
|
+
const docsAbs = path.resolve(root, config.paths?.docs ?? "content/docs")
|
|
242
|
+
if (!fs.existsSync(docsAbs)) {
|
|
243
|
+
return config.defaultLocale ? [config.defaultLocale] : []
|
|
244
|
+
}
|
|
245
|
+
const found = fs
|
|
246
|
+
.readdirSync(docsAbs, { withFileTypes: true })
|
|
247
|
+
.filter((d) => d.isDirectory())
|
|
248
|
+
.map((d) => d.name)
|
|
249
|
+
|
|
250
|
+
if (config.defaultLocale && found.includes(config.defaultLocale)) {
|
|
251
|
+
return [config.defaultLocale, ...found.filter((l) => l !== config.defaultLocale)]
|
|
252
|
+
}
|
|
253
|
+
return found
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function buildEnvVars(loaded: LoadedConfig | null): Record<string, string> {
|
|
257
|
+
// Always set SHELL_APP_ROOT so the Next app can resolve its own bundled
|
|
258
|
+
// files (fallbacks, globals.css) independently of process.cwd().
|
|
259
|
+
const base: Record<string, string> = { SHELL_APP_ROOT: nextAppDir() }
|
|
260
|
+
if (!loaded) return base
|
|
261
|
+
|
|
262
|
+
const { configPath, root, config } = loaded
|
|
263
|
+
const b = config.branding
|
|
264
|
+
|
|
265
|
+
const env: Record<string, string> = {
|
|
266
|
+
...base,
|
|
267
|
+
USER_CONFIG_PATH: configPath,
|
|
268
|
+
USER_REGISTRY_ROOT: root,
|
|
269
|
+
NEXT_PUBLIC_SHELL_SITE_NAME: b.siteName,
|
|
270
|
+
NEXT_PUBLIC_SHELL_SHORT_NAME: b.shortName,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (b.siteUrl) env.NEXT_PUBLIC_SHELL_SITE_URL = b.siteUrl
|
|
274
|
+
if (b.description) env.NEXT_PUBLIC_SHELL_DESCRIPTION = b.description
|
|
275
|
+
if (b.ogImage) env.NEXT_PUBLIC_SHELL_OG_IMAGE = b.ogImage
|
|
276
|
+
if (b.twitterHandle) env.NEXT_PUBLIC_SHELL_TWITTER_HANDLE = b.twitterHandle
|
|
277
|
+
if (b.github?.owner) env.NEXT_PUBLIC_SHELL_GITHUB_OWNER = b.github.owner
|
|
278
|
+
if (b.github?.repo) env.NEXT_PUBLIC_SHELL_GITHUB_REPO = b.github.repo
|
|
279
|
+
if (b.github?.label) env.NEXT_PUBLIC_SHELL_GITHUB_LABEL = b.github.label
|
|
280
|
+
if (b.github && b.github.showStars === false) {
|
|
281
|
+
env.NEXT_PUBLIC_SHELL_GITHUB_SHOW_STARS = "false"
|
|
282
|
+
}
|
|
283
|
+
if (b.logoAlt) env.NEXT_PUBLIC_SHELL_LOGO_ALT = b.logoAlt
|
|
284
|
+
if (b.faviconDark) env.NEXT_PUBLIC_SHELL_FAVICON_DARK = b.faviconDark
|
|
285
|
+
if (b.faviconLight) env.NEXT_PUBLIC_SHELL_FAVICON_LIGHT = b.faviconLight
|
|
286
|
+
if (b.faviconIco) env.NEXT_PUBLIC_SHELL_FAVICON_ICO = b.faviconIco
|
|
287
|
+
|
|
288
|
+
if (config.homePage) env.USER_HOMEPAGE_PATH = config.homePage
|
|
289
|
+
if (config.installCommandTemplate !== undefined) {
|
|
290
|
+
env.NEXT_PUBLIC_SHELL_INSTALL_CMD = config.installCommandTemplate
|
|
291
|
+
}
|
|
292
|
+
if (config.transpilePackages && config.transpilePackages.length > 0) {
|
|
293
|
+
env.USER_TRANSPILE_PACKAGES = config.transpilePackages.join(",")
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Multilocale signalling for the locale toggle. Resolves the toggle's
|
|
297
|
+
// locale set from explicit config or auto-scan of doc subfolders so the
|
|
298
|
+
// client has a complete list at build time.
|
|
299
|
+
if (config.multilocale && config.defaultLocale) {
|
|
300
|
+
env.NEXT_PUBLIC_SHELL_DEFAULT_LOCALE = config.defaultLocale
|
|
301
|
+
const locales = resolveLocaleList(loaded.root, config)
|
|
302
|
+
env.NEXT_PUBLIC_SHELL_LOCALES = locales.join(",")
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return env
|
|
306
|
+
}
|
package/src/cli/start.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `registry-shell start` — run `next start` for a prior `registry-shell build`.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors build.ts: passes `USER_DIST_DIR` pointing at `<user-project>/.next`
|
|
5
|
+
* so `next start` reads the build output from where build.ts wrote it.
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path"
|
|
8
|
+
import { spawn } from "node:child_process"
|
|
9
|
+
import { NEXT_BIN, buildEnvVars, loadUserConfig, nextAppDir } from "./shared.js"
|
|
10
|
+
|
|
11
|
+
export async function run(args: string[]): Promise<void> {
|
|
12
|
+
const loaded = loadUserConfig()
|
|
13
|
+
const userDistDir = loaded
|
|
14
|
+
? path.resolve(loaded.root, loaded.config.paths?.buildOutput ?? ".next")
|
|
15
|
+
: undefined
|
|
16
|
+
const env = {
|
|
17
|
+
...process.env,
|
|
18
|
+
...buildEnvVars(loaded),
|
|
19
|
+
...(userDistDir ? { USER_DIST_DIR: userDistDir } : {}),
|
|
20
|
+
}
|
|
21
|
+
const portArgs = loaded?.config.port ? ["-p", String(loaded.config.port)] : []
|
|
22
|
+
const child = spawn(
|
|
23
|
+
process.execPath,
|
|
24
|
+
[NEXT_BIN, "start", nextAppDir(), ...portArgs, ...args],
|
|
25
|
+
{ stdio: "inherit", env },
|
|
26
|
+
)
|
|
27
|
+
child.on("exit", (code) => process.exit(code ?? 0))
|
|
28
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads the user's `registry-shell.config.ts` at server boot. Uses `jiti` so
|
|
3
|
+
* the user doesn't need a build step or TS tooling of their own — the shell
|
|
4
|
+
* parses TS on the fly.
|
|
5
|
+
*
|
|
6
|
+
* Called once at Next.js server startup (see src/next-app/registry.config.ts).
|
|
7
|
+
* The returned `ResolvedShellConfig` contains absolute paths and defaults
|
|
8
|
+
* applied.
|
|
9
|
+
*/
|
|
10
|
+
import "server-only"
|
|
11
|
+
import path from "node:path"
|
|
12
|
+
import fs from "node:fs"
|
|
13
|
+
import { createJiti } from "jiti"
|
|
14
|
+
import type { ShellConfig, ShellPaths, BrandingConfig } from "./define-config.js"
|
|
15
|
+
|
|
16
|
+
// `skipBlocks`, `globalCss`, and `buildOutput` are excluded from the
|
|
17
|
+
// "must have a default" shape: skipBlocks is always an array (empty
|
|
18
|
+
// default); globalCss is purely opt-in; buildOutput is read by the CLI
|
|
19
|
+
// directly (with its own `.next` default) and doesn't flow through the
|
|
20
|
+
// server-side resolver.
|
|
21
|
+
const DEFAULT_PATHS: Required<
|
|
22
|
+
Omit<ShellPaths, "skipBlocks" | "globalCss" | "buildOutput">
|
|
23
|
+
> & { skipBlocks: string[] } = {
|
|
24
|
+
components: "components/ui",
|
|
25
|
+
blocks: "registry/new-york/blocks",
|
|
26
|
+
previews: "components/previews/index.ts",
|
|
27
|
+
docs: "content/docs",
|
|
28
|
+
registryJson: "public/r",
|
|
29
|
+
a11y: "public/a11y",
|
|
30
|
+
tests: "public/tests",
|
|
31
|
+
props: "public/props",
|
|
32
|
+
skipBlocks: [],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_BRANDING: BrandingConfig = {
|
|
36
|
+
siteName: "UI Registry",
|
|
37
|
+
shortName: "UI",
|
|
38
|
+
siteUrl: "",
|
|
39
|
+
logoAlt: "UI",
|
|
40
|
+
faviconDark: "/favicon_dark.svg",
|
|
41
|
+
faviconLight: "/favicon_light.svg",
|
|
42
|
+
faviconIco: "/favicon.ico",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ResolvedShellConfig {
|
|
46
|
+
/** Absolute path to the user's registry root (dir containing the config). */
|
|
47
|
+
root: string
|
|
48
|
+
/** Absolute path to the config file itself. */
|
|
49
|
+
configPath: string
|
|
50
|
+
branding: Required<BrandingConfig>
|
|
51
|
+
/** All paths resolved to absolute locations. */
|
|
52
|
+
paths: {
|
|
53
|
+
components: string
|
|
54
|
+
blocks: string
|
|
55
|
+
previews: string
|
|
56
|
+
docs: string
|
|
57
|
+
registryJson: string
|
|
58
|
+
a11y: string
|
|
59
|
+
tests: string
|
|
60
|
+
props: string
|
|
61
|
+
skipBlocks: Set<string>
|
|
62
|
+
/** Absolute path to user's extra global CSS, or null when not configured. */
|
|
63
|
+
globalCss: string | null
|
|
64
|
+
}
|
|
65
|
+
/** Absolute path to a custom homepage module, or null. */
|
|
66
|
+
homePage: string | null
|
|
67
|
+
/** Absolute path to a custom adapter module, or null. */
|
|
68
|
+
adapter: string | null
|
|
69
|
+
extraTranslations: Record<string, Record<string, string>>
|
|
70
|
+
/** When true, docs live under per-locale subfolders. */
|
|
71
|
+
multilocale: boolean
|
|
72
|
+
/** Locale subfolder containing the canonical doc set. Empty when multilocale is off. */
|
|
73
|
+
defaultLocale: string
|
|
74
|
+
/** Explicit locale list (resolved — may be empty in single-locale mode). */
|
|
75
|
+
locales: string[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read env vars set by the CLI, load the config file, resolve paths. Returns
|
|
80
|
+
* `null` when no config is set (shell-only dev mode) — the Next app falls
|
|
81
|
+
* back to built-in shell docs.
|
|
82
|
+
*/
|
|
83
|
+
export function loadResolvedConfig(): ResolvedShellConfig | null {
|
|
84
|
+
const configPath = process.env.USER_CONFIG_PATH
|
|
85
|
+
const root = process.env.USER_REGISTRY_ROOT
|
|
86
|
+
if (!configPath || !root) return null
|
|
87
|
+
|
|
88
|
+
if (!fs.existsSync(configPath)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`[registry-shell] Config file not found: ${configPath}. ` +
|
|
91
|
+
`Check USER_CONFIG_PATH.`,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true })
|
|
96
|
+
const loaded = jiti(configPath) as unknown
|
|
97
|
+
const config = extractDefault(loaded) as ShellConfig
|
|
98
|
+
|
|
99
|
+
if (!config?.branding) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`[registry-shell] Invalid config at ${configPath}: missing required \`branding\`.`,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
if (config.multilocale && !config.defaultLocale) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`[registry-shell] Invalid config at ${configPath}: \`multilocale\` is true but \`defaultLocale\` is missing.`,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rootAbs = path.resolve(root)
|
|
111
|
+
const cfgPaths = config.paths ?? {}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
root: rootAbs,
|
|
115
|
+
configPath,
|
|
116
|
+
branding: applyBrandingDefaults(config.branding),
|
|
117
|
+
paths: {
|
|
118
|
+
components: path.resolve(rootAbs, cfgPaths.components ?? DEFAULT_PATHS.components),
|
|
119
|
+
blocks: path.resolve(rootAbs, cfgPaths.blocks ?? DEFAULT_PATHS.blocks),
|
|
120
|
+
previews: path.resolve(rootAbs, cfgPaths.previews ?? DEFAULT_PATHS.previews),
|
|
121
|
+
docs: path.resolve(rootAbs, cfgPaths.docs ?? DEFAULT_PATHS.docs),
|
|
122
|
+
registryJson: path.resolve(rootAbs, cfgPaths.registryJson ?? DEFAULT_PATHS.registryJson),
|
|
123
|
+
a11y: path.resolve(rootAbs, cfgPaths.a11y ?? DEFAULT_PATHS.a11y),
|
|
124
|
+
tests: path.resolve(rootAbs, cfgPaths.tests ?? DEFAULT_PATHS.tests),
|
|
125
|
+
props: path.resolve(rootAbs, cfgPaths.props ?? DEFAULT_PATHS.props),
|
|
126
|
+
skipBlocks: new Set(cfgPaths.skipBlocks ?? []),
|
|
127
|
+
globalCss: cfgPaths.globalCss ? path.resolve(rootAbs, cfgPaths.globalCss) : null,
|
|
128
|
+
},
|
|
129
|
+
homePage: config.homePage ? path.resolve(rootAbs, config.homePage) : null,
|
|
130
|
+
adapter: config.adapter ? path.resolve(rootAbs, config.adapter) : null,
|
|
131
|
+
extraTranslations: config.extraTranslations ?? {},
|
|
132
|
+
multilocale: Boolean(config.multilocale),
|
|
133
|
+
defaultLocale: config.defaultLocale ?? "",
|
|
134
|
+
locales: resolveLocales(rootAbs, cfgPaths.docs ?? DEFAULT_PATHS.docs, config),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the locale list for the locale toggle. In single-locale mode we
|
|
140
|
+
* return an empty array (the toggle hides itself). In multilocale mode we
|
|
141
|
+
* use the explicit `locales` config if present, otherwise auto-scan
|
|
142
|
+
* subfolders under `paths.docs`.
|
|
143
|
+
*/
|
|
144
|
+
function resolveLocales(rootAbs: string, docsRel: string, config: ShellConfig): string[] {
|
|
145
|
+
if (!config.multilocale) return []
|
|
146
|
+
if (config.locales && config.locales.length > 0) return [...config.locales]
|
|
147
|
+
|
|
148
|
+
const docsAbs = path.resolve(rootAbs, docsRel)
|
|
149
|
+
if (!fs.existsSync(docsAbs)) return config.defaultLocale ? [config.defaultLocale] : []
|
|
150
|
+
const found = fs
|
|
151
|
+
.readdirSync(docsAbs, { withFileTypes: true })
|
|
152
|
+
.filter((d) => d.isDirectory())
|
|
153
|
+
.map((d) => d.name)
|
|
154
|
+
|
|
155
|
+
// Put the default locale first so the toggle cycles predictably.
|
|
156
|
+
if (config.defaultLocale && found.includes(config.defaultLocale)) {
|
|
157
|
+
return [config.defaultLocale, ...found.filter((l) => l !== config.defaultLocale)]
|
|
158
|
+
}
|
|
159
|
+
return found
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function extractDefault(loaded: unknown): unknown {
|
|
163
|
+
if (loaded && typeof loaded === "object" && "default" in loaded) {
|
|
164
|
+
return (loaded as { default: unknown }).default
|
|
165
|
+
}
|
|
166
|
+
return loaded
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function applyBrandingDefaults(b: BrandingConfig): Required<BrandingConfig> {
|
|
170
|
+
return {
|
|
171
|
+
siteName: b.siteName,
|
|
172
|
+
shortName: b.shortName,
|
|
173
|
+
siteUrl: b.siteUrl ?? "",
|
|
174
|
+
description: b.description ?? "",
|
|
175
|
+
ogImage: b.ogImage ?? "",
|
|
176
|
+
twitterHandle: b.twitterHandle ?? "",
|
|
177
|
+
github: b.github
|
|
178
|
+
? {
|
|
179
|
+
owner: b.github.owner,
|
|
180
|
+
repo: b.github.repo,
|
|
181
|
+
label: b.github.label ?? "Github",
|
|
182
|
+
showStars: b.github.showStars ?? true,
|
|
183
|
+
}
|
|
184
|
+
: { owner: "", repo: "", label: "", showStars: false },
|
|
185
|
+
logoAlt: b.logoAlt ?? b.siteName,
|
|
186
|
+
faviconDark: b.faviconDark ?? DEFAULT_BRANDING.faviconDark!,
|
|
187
|
+
faviconLight: b.faviconLight ?? DEFAULT_BRANDING.faviconLight!,
|
|
188
|
+
faviconIco: b.faviconIco ?? DEFAULT_BRANDING.faviconIco!,
|
|
189
|
+
}
|
|
190
|
+
}
|