@sntlr/registry-shell 1.1.3 → 2.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/src/cli/build.ts CHANGED
@@ -1,15 +1,17 @@
1
1
  /**
2
- * `registry-shell build` — run `next build` against the user's project.
2
+ * `registry-shell build` — produces a Next.js static export tree in
3
+ * `<user-project>/out/`.
3
4
  *
4
- * Strategy: `next build` writes to its default `<next-project>/.next`
5
- * location (inside our node_modules). After a successful build, MOVE that
6
- * directory to `<user-project>/<paths.buildOutput ?? ".next">`.
7
- *
8
- * Why not use Next's `distDir` config? Next silently ignores absolute
9
- * paths that point outside the Next project dir (the output ends up back
10
- * in the default location). The env-var approach we tried first worked
11
- * locally on Windows but failed on Vercel. A post-build move is
12
- * platform-independent.
5
+ * Pipeline:
6
+ * 1. Overlay user's public/ onto the shell's bundled public/ (so Next
7
+ * sees user's registry manifests at /r/*.json, /a11y/*.json, etc.).
8
+ * 2. Run build-time generators that write derived JSON into the merged
9
+ * public/ currently just `api/search-index.json`.
10
+ * 3. Run `next build` with `output: "export"` (set in next.config.ts).
11
+ * Next writes static HTML/JS/CSS to `<shell>/out/`.
12
+ * 4. Copy `<shell>/out/` `<user-project>/out/`.
13
+ * 5. Restore shell's public/ to its pristine state (remove anything we
14
+ * added in step 1/2) so repeated builds are idempotent.
13
15
  */
14
16
  import fs from "node:fs"
15
17
  import path from "node:path"
@@ -22,53 +24,112 @@ import {
22
24
  nextAppDir,
23
25
  writeUserSourcesCss,
24
26
  } from "./shared.js"
27
+ import { generateSearchIndex } from "./generate-search-index.js"
25
28
 
26
29
  export async function run(args: string[]): Promise<void> {
27
30
  const loaded = loadUserConfig()
31
+ if (!loaded) {
32
+ console.error(
33
+ "[registry-shell] No registry-shell.config.ts found — `build` requires a registry.",
34
+ )
35
+ process.exit(1)
36
+ }
37
+
28
38
  clearStaleNextCacheIfModeChanged(loaded)
29
39
  writeUserSourcesCss(loaded)
30
40
 
31
41
  const shellNextApp = nextAppDir()
42
+ const shellPublic = path.join(shellNextApp, "public")
43
+ const userPublic = path.join(loaded.root, "public")
44
+
45
+ // Step 1: Snapshot shell's public/ before overlay so we can restore it.
46
+ const shellOwnEntries: Set<string> = new Set(
47
+ fs.existsSync(shellPublic) ? fs.readdirSync(shellPublic) : [],
48
+ )
49
+
50
+ // Step 2: Overlay user's public/ onto shell's public/ (user files win).
51
+ const overlaidEntries: string[] = []
52
+ if (fs.existsSync(userPublic)) {
53
+ for (const entry of fs.readdirSync(userPublic)) {
54
+ const src = path.join(userPublic, entry)
55
+ const dest = path.join(shellPublic, entry)
56
+ fs.cpSync(src, dest, { recursive: true, force: true })
57
+ overlaidEntries.push(entry)
58
+ }
59
+ }
60
+
61
+ // Apply the shell env vars to THIS process so build-time generators can
62
+ // call `loadResolvedConfig()` (which reads USER_REGISTRY_ROOT etc.).
63
+ // The same env is also forwarded to the spawn child below.
32
64
  const env = { ...process.env, ...buildEnvVars(loaded) }
33
- const child = spawn(
65
+ Object.assign(process.env, buildEnvVars(loaded))
66
+
67
+ // Step 3: Pre-build generators (writes into shell's public/ so Next picks up).
68
+ try {
69
+ await generateSearchIndex(loaded, shellPublic)
70
+ } catch (err) {
71
+ console.warn(
72
+ `[registry-shell] search-index generation failed: ${(err as Error).message}`,
73
+ )
74
+ }
75
+
76
+ // Step 4: next build (static export — writes to <shellNextApp>/out/).
77
+ const buildChild = spawn(
34
78
  process.execPath,
35
79
  [NEXT_BIN, "build", shellNextApp, ...args],
36
80
  { stdio: "inherit", env },
37
81
  )
38
82
 
39
- child.on("exit", (code) => {
83
+ buildChild.on("exit", (code) => {
40
84
  if (code !== 0) {
85
+ restoreShellPublic(shellPublic, shellOwnEntries, overlaidEntries)
41
86
  process.exit(code ?? 1)
42
87
  }
43
- if (loaded) relocateBuildOutput(shellNextApp, loaded.root, loaded.config.paths?.buildOutput)
88
+
89
+ // Step 5: Copy out/ to user project root.
90
+ const src = path.join(shellNextApp, "out")
91
+ const dest = path.resolve(loaded.root, "out")
92
+ if (fs.existsSync(src)) {
93
+ if (fs.existsSync(dest)) {
94
+ fs.rmSync(dest, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
95
+ }
96
+ fs.cpSync(src, dest, { recursive: true })
97
+ // Remove the build output from inside node_modules so it doesn't
98
+ // accumulate stale copies across releases.
99
+ fs.rmSync(src, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
100
+ console.log(`[registry-shell] Static build ready at ${dest}`)
101
+ }
102
+
103
+ // Step 6: Restore shell's public/ (remove overlay).
104
+ restoreShellPublic(shellPublic, shellOwnEntries, overlaidEntries)
44
105
  process.exit(0)
45
106
  })
46
107
  }
47
108
 
48
109
  /**
49
- * Moves `<shellNextApp>/.next` `<userRoot>/<outputName ?? ".next">`.
50
- * Idempotent: removes stale destination first. Noop if source is missing
51
- * (e.g. Next's distDir *did* happen to work) or source and dest are the
52
- * same path.
110
+ * Removes anything from `shellPublic` that wasn't there before the overlay.
111
+ * Keeps shell's bundled favicons + anything else that was part of the
112
+ * shipped package. Runs on both success and failure paths.
53
113
  */
54
- function relocateBuildOutput(
55
- shellNextApp: string,
56
- userRoot: string,
57
- outputName: string | undefined,
114
+ function restoreShellPublic(
115
+ shellPublic: string,
116
+ shellOwnEntries: Set<string>,
117
+ overlaidEntries: string[],
58
118
  ): void {
59
- const src = path.join(shellNextApp, ".next")
60
- const dest = path.resolve(userRoot, outputName ?? ".next")
61
- if (src === dest) return
62
- if (!fs.existsSync(src)) return
63
-
64
- if (fs.existsSync(dest)) {
65
- fs.rmSync(dest, {
66
- recursive: true,
67
- force: true,
68
- maxRetries: 5,
69
- retryDelay: 200,
70
- })
119
+ if (!fs.existsSync(shellPublic)) return
120
+ for (const entry of overlaidEntries) {
121
+ if (!shellOwnEntries.has(entry)) {
122
+ // Entry didn't exist before overlay — remove cleanly.
123
+ fs.rmSync(path.join(shellPublic, entry), {
124
+ recursive: true,
125
+ force: true,
126
+ maxRetries: 5,
127
+ retryDelay: 200,
128
+ })
129
+ }
130
+ // Entry existed in shell's own public/ — we overwrote it, can't
131
+ // cleanly restore without reinstalling. Leave it; on next build the
132
+ // user's version replaces it again. Mostly affects favicon.* files
133
+ // the user might customize.
71
134
  }
72
- fs.renameSync(src, dest)
73
- console.log(`[registry-shell] Build output moved to ${dest}`)
74
135
  }
package/src/cli/dev.ts CHANGED
@@ -1,6 +1,19 @@
1
1
  /**
2
2
  * `registry-shell dev` — run `next dev` against the user's project.
3
+ *
4
+ * Dev mode runs Next in its normal dynamic-rendering mode (so hot reload
5
+ * / live MDX / interactive previews all work). The `output: "export"`
6
+ * config only affects `next build`, so dev behaves identically to pre-v2.
7
+ *
8
+ * We still overlay the user's `public/` onto the shell's bundled `public/`
9
+ * before starting Next, so dynamic registry assets (`/r/*.json`,
10
+ * `/a11y/*.json`, `/props/*.json`, `/tests/*.json`, `/api/search-index.json`,
11
+ * plus any user-custom static files) are served the same way they will be
12
+ * in a production build. Note: changes to the user's `public/` during a
13
+ * dev session require a restart to refresh the overlay.
3
14
  */
15
+ import fs from "node:fs"
16
+ import path from "node:path"
4
17
  import { spawn } from "node:child_process"
5
18
  import {
6
19
  NEXT_BIN,
@@ -10,6 +23,7 @@ import {
10
23
  nextAppDir,
11
24
  writeUserSourcesCss,
12
25
  } from "./shared.js"
26
+ import { generateSearchIndex } from "./generate-search-index.js"
13
27
 
14
28
  export async function run(args: string[]): Promise<void> {
15
29
  const loaded = loadUserConfig()
@@ -21,6 +35,22 @@ export async function run(args: string[]): Promise<void> {
21
35
 
22
36
  clearStaleNextCacheIfModeChanged(loaded)
23
37
  writeUserSourcesCss(loaded)
38
+
39
+ const shellNextApp = nextAppDir()
40
+
41
+ // Overlay user's public/ onto shell's public/ so dev-mode URLs match
42
+ // what production will serve statically. Skipped in shell-only mode.
43
+ if (loaded) {
44
+ overlayUserPublic(loaded.root, shellNextApp)
45
+ try {
46
+ await generateSearchIndex(loaded, path.join(shellNextApp, "public"))
47
+ } catch (err) {
48
+ console.warn(
49
+ `[registry-shell] search-index generation failed: ${(err as Error).message}`,
50
+ )
51
+ }
52
+ }
53
+
24
54
  const env = { ...process.env, ...buildEnvVars(loaded) }
25
55
  const portArgs = loaded?.config.port ? ["-p", String(loaded.config.port)] : []
26
56
  // Webpack by default. Turbopack currently can't compile files reached via
@@ -30,9 +60,20 @@ export async function run(args: string[]): Promise<void> {
30
60
  const turbopackArgs = process.env.UI_SHELL_TURBOPACK ? ["--turbopack"] : []
31
61
  const child = spawn(
32
62
  process.execPath,
33
- [NEXT_BIN, "dev", nextAppDir(), ...turbopackArgs, ...portArgs, ...args],
63
+ [NEXT_BIN, "dev", shellNextApp, ...turbopackArgs, ...portArgs, ...args],
34
64
  { stdio: "inherit", env },
35
65
  )
36
66
 
37
67
  child.on("exit", (code) => process.exit(code ?? 0))
38
68
  }
69
+
70
+ function overlayUserPublic(userRoot: string, shellNextApp: string): void {
71
+ const userPublic = path.join(userRoot, "public")
72
+ const shellPublic = path.join(shellNextApp, "public")
73
+ if (!fs.existsSync(userPublic)) return
74
+ for (const entry of fs.readdirSync(userPublic)) {
75
+ const src = path.join(userPublic, entry)
76
+ const dest = path.join(shellPublic, entry)
77
+ fs.cpSync(src, dest, { recursive: true, force: true })
78
+ }
79
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Pre-build generator: writes `public/api/search-index.json` into the
3
+ * user's public tree. Replaces the former `/api/search-index` route
4
+ * handler — the shell's static-export mode can't serve dynamic route
5
+ * handlers, so we bake the search index at build time instead.
6
+ *
7
+ * The generator runs in a child `tsx` process so the shell's own build
8
+ * pipeline (which ships as compiled `.js`) can invoke user-registry-side
9
+ * .ts files (docs loaders, adapters) without the user's `node_modules/`
10
+ * needing to be on our resolution path.
11
+ */
12
+ import fs from "node:fs"
13
+ import path from "node:path"
14
+ import { createJiti } from "jiti"
15
+ import type { LoadedConfig } from "./shared.js"
16
+ import type { ResolvedShellConfig } from "../config-loader.js"
17
+
18
+ interface SearchItem {
19
+ label: string
20
+ href: string
21
+ group: string
22
+ }
23
+
24
+ /**
25
+ * Writes the search index JSON into `targetPublicDir/api/search-index.json`.
26
+ * `targetPublicDir` is expected to be the shell's bundled `public/` dir
27
+ * (inside node_modules). The build pipeline overlays the user's public/
28
+ * onto this dir so the merged tree is what `next build` sees.
29
+ */
30
+ export async function generateSearchIndex(
31
+ loaded: LoadedConfig,
32
+ targetPublicDir: string,
33
+ ): Promise<void> {
34
+ const { config } = loaded
35
+ void config
36
+
37
+ // Load the shell's registry-adapter + config-loader via jiti so we can
38
+ // evaluate the same filesystem-walking logic the server used to do at
39
+ // request time.
40
+ const jiti = createJiti(import.meta.url, { interopDefault: true })
41
+ const resolved: ResolvedShellConfig | null = (
42
+ jiti("../config-loader.js") as {
43
+ loadResolvedConfig: () => ResolvedShellConfig | null
44
+ }
45
+ ).loadResolvedConfig()
46
+
47
+ if (!resolved) {
48
+ console.warn("[registry-shell] generate-search-index: no resolved config, skipping")
49
+ return
50
+ }
51
+
52
+ const { createDefaultAdapter } = jiti("../adapter/default.js") as {
53
+ createDefaultAdapter: (r: ResolvedShellConfig) => {
54
+ getAllComponents: () => { name: string; label: string }[]
55
+ getAllDocs: () => { slug: string; title: string }[]
56
+ }
57
+ }
58
+ const adapter = createDefaultAdapter(resolved)
59
+
60
+ const docs: SearchItem[] = adapter.getAllDocs().map((doc) => ({
61
+ label: doc.title,
62
+ href: `/docs/${doc.slug}/`,
63
+ group: "Documentation",
64
+ }))
65
+
66
+ const components: SearchItem[] = adapter.getAllComponents().map((comp) => ({
67
+ label: comp.label,
68
+ href: `/components/${comp.name}/`,
69
+ group: "Components",
70
+ }))
71
+
72
+ const items = [...docs, ...components]
73
+
74
+ const outDir = path.join(targetPublicDir, "api")
75
+ fs.mkdirSync(outDir, { recursive: true })
76
+ const outPath = path.join(outDir, "search-index.json")
77
+ fs.writeFileSync(outPath, JSON.stringify(items), "utf-8")
78
+
79
+ console.log(
80
+ `[registry-shell] Wrote search index (${items.length} items) → ${outPath}`,
81
+ )
82
+ }
package/src/cli/index.ts CHANGED
@@ -3,10 +3,13 @@
3
3
  * @sntlr/registry-shell CLI entry.
4
4
  *
5
5
  * Commands:
6
- * init Scaffold registry-shell.config.ts and add a script to package.json.
7
- * dev Run `next dev` pointed at the user's current directory.
8
- * build Run `next build` pointed at the user's current directory.
9
- * start Run `next start` for a built shell.
6
+ * init Scaffold registry-shell.config.ts and add scripts to package.json.
7
+ * dev Run `next dev` for local iteration on the registry.
8
+ * build Produce a static export in `<user-project>/out/` (Storybook-style).
9
+ *
10
+ * `start` was removed in v2.0.0 — `next build` now produces a static site,
11
+ * so there's no server to "start". Serve `out/` with any static host
12
+ * (`npx serve out`, Vercel, Netlify, S3, etc.).
10
13
  */
11
14
 
12
15
  const USAGE = `Usage: registry-shell <command> [args]
@@ -14,8 +17,7 @@ const USAGE = `Usage: registry-shell <command> [args]
14
17
  Commands:
15
18
  init Scaffold registry-shell.config.ts in the current project.
16
19
  dev Start the shell in dev mode on http://localhost:3000.
17
- build Build the shell for production.
18
- start Start the production server against a prior build.
20
+ build Produce a static export in ./out (deploy anywhere).
19
21
  `
20
22
 
21
23
  async function main() {
@@ -37,8 +39,10 @@ async function main() {
37
39
  await (await import("./build.js")).run(args)
38
40
  break
39
41
  case "start":
40
- await (await import("./start.js")).run(args)
41
- break
42
+ console.error(
43
+ "[registry-shell] `start` was removed in v2.0.0. `build` now produces a static site — serve `./out` with any static host (e.g. `npx serve out`).",
44
+ )
45
+ process.exit(1)
42
46
  default:
43
47
  console.error(`Unknown command: ${cmd}\n`)
44
48
  console.log(USAGE)
@@ -3,11 +3,15 @@
3
3
  * the user doesn't need a build step or TS tooling of their own — the shell
4
4
  * parses TS on the fly.
5
5
  *
6
- * Called once at Next.js server startup (see src/next-app/registry.config.ts).
6
+ * Called from two places:
7
+ * • Next.js server startup (src/next-app/registry.config.ts) — runtime.
8
+ * • The shell CLI's build pipeline (src/cli/generate-search-index.ts) —
9
+ * pre-build. The CLI runs in plain Node, not inside Next, so we don't
10
+ * use Next's `server-only` guard here.
11
+ *
7
12
  * The returned `ResolvedShellConfig` contains absolute paths and defaults
8
13
  * applied.
9
14
  */
10
- import "server-only"
11
15
  import path from "node:path"
12
16
  import fs from "node:fs"
13
17
  import { createJiti } from "jiti"
@@ -24,7 +24,7 @@ let fetchPromise: Promise<void> | null = null
24
24
 
25
25
  function preloadSearchIndex() {
26
26
  if (cachedItems || fetchPromise) return
27
- fetchPromise = fetch("/api/search-index")
27
+ fetchPromise = fetch("/api/search-index.json")
28
28
  .then((r) => r.json())
29
29
  .then((data) => { cachedItems = data })
30
30
  .catch(() => {})
@@ -46,15 +46,20 @@ function resolveUserModule(relativePath: string, fallback: string): string {
46
46
  const USER_PREVIEWS = resolveUserModule("components/previews", "fallback/previews.ts")
47
47
 
48
48
  const nextConfig: NextConfig = {
49
- output: process.env.BUILD_STANDALONE === "true" ? "standalone" : undefined,
49
+ // Static export (Storybook model) the shell produces a pure static
50
+ // HTML/JS/CSS tree under `out/`, deployable to any static host (Vercel,
51
+ // Netlify, S3, GitHub Pages). No serverless functions, no runtime file
52
+ // tracing, no pnpm symlink hazards. Dev mode (`next dev`) still runs
53
+ // normally; export only affects `next build`.
54
+ output: "export",
50
55
 
51
- // Next's file tracer figures out which node_modules files each
52
- // serverless function needs. With pnpm (and our setup, where Next
53
- // runs from inside node_modules), the tracer can't walk pnpm's
54
- // virtual store without knowing the real project root. Point it at
55
- // the user's project that's the dir that contains `node_modules/.pnpm`
56
- // on Vercel and any other pnpm-installed host.
57
- ...(USER_ROOT ? { outputFileTracingRoot: toPosix(USER_ROOT) } : {}),
56
+ // `next/image` runtime optimization requires a server. Disable for
57
+ // static export images are served as-is from `public/`.
58
+ images: { unoptimized: true },
59
+
60
+ // URLs end with `/` (e.g. `/components/button/`). Makes static hosts
61
+ // serve `components/button/index.html` correctly.
62
+ trailingSlash: true,
58
63
 
59
64
  // When installed as an external package (link:../registry-shell, npm, etc.),
60
65
  // the shell's TSX lives under the user's node_modules and Next won't
@@ -1 +0,0 @@
1
- export declare function run(args: string[]): Promise<void>;
package/dist/cli/start.js DELETED
@@ -1,47 +0,0 @@
1
- /**
2
- * `registry-shell start` — run `next start` for a prior `registry-shell build`.
3
- *
4
- * Build writes `.next/` to the user's project root (so Vercel / Netlify /
5
- * generic Next hosts find it). `next start` expects it at its default
6
- * location inside the Next project dir. Move it back before starting.
7
- * Safe to run even if the move has already happened on a prior start.
8
- */
9
- import fs from "node:fs";
10
- import path from "node:path";
11
- import { spawn } from "node:child_process";
12
- import { NEXT_BIN, buildEnvVars, loadUserConfig, nextAppDir } from "./shared.js";
13
- export async function run(args) {
14
- const loaded = loadUserConfig();
15
- const env = { ...process.env, ...buildEnvVars(loaded) };
16
- const portArgs = loaded?.config.port ? ["-p", String(loaded.config.port)] : [];
17
- const shellNextApp = nextAppDir();
18
- if (loaded) {
19
- restoreBuildOutput(shellNextApp, loaded.root, loaded.config.paths?.buildOutput);
20
- }
21
- const child = spawn(process.execPath, [NEXT_BIN, "start", shellNextApp, ...portArgs, ...args], { stdio: "inherit", env });
22
- child.on("exit", (code) => process.exit(code ?? 0));
23
- }
24
- /**
25
- * Inverse of build.ts's relocation: moves `<userRoot>/<buildOutput>` back
26
- * to `<shellNextApp>/.next` so `next start` can find it. Noop when the
27
- * user-side `.next` is missing — we assume they ran `registry-shell start`
28
- * against a shell-side build or are otherwise in a good state.
29
- */
30
- function restoreBuildOutput(shellNextApp, userRoot, outputName) {
31
- const dest = path.join(shellNextApp, ".next");
32
- const src = path.resolve(userRoot, outputName ?? ".next");
33
- if (src === dest)
34
- return;
35
- if (!fs.existsSync(src))
36
- return;
37
- if (fs.existsSync(dest)) {
38
- fs.rmSync(dest, {
39
- recursive: true,
40
- force: true,
41
- maxRetries: 5,
42
- retryDelay: 200,
43
- });
44
- }
45
- fs.renameSync(src, dest);
46
- }
47
- //# sourceMappingURL=start.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"start.js","sourceRoot":"","sources":["../../src/cli/start.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEhF,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,IAAc;IACtC,MAAM,MAAM,GAAG,cAAc,EAAE,CAAA;IAC/B,MAAM,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,EAAE,CAAA;IACvD,MAAM,QAAQ,GAAG,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC9E,MAAM,YAAY,GAAG,UAAU,EAAE,CAAA;IAEjC,IAAI,MAAM,EAAE,CAAC;QACX,kBAAkB,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,CAAA;IACjF,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CACjB,OAAO,CAAC,QAAQ,EAChB,CAAC,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,CAAC,EACvD,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAC1B,CAAA;IACD,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAA;AACrD,CAAC;AAED;;;;;GAKG;AACH,SAAS,kBAAkB,CACzB,YAAoB,EACpB,QAAgB,EAChB,UAA8B;IAE9B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;IAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,IAAI,OAAO,CAAC,CAAA;IACzD,IAAI,GAAG,KAAK,IAAI;QAAE,OAAM;IACxB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAM;IAE/B,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;YACd,SAAS,EAAE,IAAI;YACf,KAAK,EAAE,IAAI;YACX,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,GAAG;SAChB,CAAC,CAAA;IACJ,CAAC;IACD,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;AAC1B,CAAC"}
package/src/cli/start.ts DELETED
@@ -1,57 +0,0 @@
1
- /**
2
- * `registry-shell start` — run `next start` for a prior `registry-shell build`.
3
- *
4
- * Build writes `.next/` to the user's project root (so Vercel / Netlify /
5
- * generic Next hosts find it). `next start` expects it at its default
6
- * location inside the Next project dir. Move it back before starting.
7
- * Safe to run even if the move has already happened on a prior start.
8
- */
9
- import fs from "node:fs"
10
- import path from "node:path"
11
- import { spawn } from "node:child_process"
12
- import { NEXT_BIN, buildEnvVars, loadUserConfig, nextAppDir } from "./shared.js"
13
-
14
- export async function run(args: string[]): Promise<void> {
15
- const loaded = loadUserConfig()
16
- const env = { ...process.env, ...buildEnvVars(loaded) }
17
- const portArgs = loaded?.config.port ? ["-p", String(loaded.config.port)] : []
18
- const shellNextApp = nextAppDir()
19
-
20
- if (loaded) {
21
- restoreBuildOutput(shellNextApp, loaded.root, loaded.config.paths?.buildOutput)
22
- }
23
-
24
- const child = spawn(
25
- process.execPath,
26
- [NEXT_BIN, "start", shellNextApp, ...portArgs, ...args],
27
- { stdio: "inherit", env },
28
- )
29
- child.on("exit", (code) => process.exit(code ?? 0))
30
- }
31
-
32
- /**
33
- * Inverse of build.ts's relocation: moves `<userRoot>/<buildOutput>` back
34
- * to `<shellNextApp>/.next` so `next start` can find it. Noop when the
35
- * user-side `.next` is missing — we assume they ran `registry-shell start`
36
- * against a shell-side build or are otherwise in a good state.
37
- */
38
- function restoreBuildOutput(
39
- shellNextApp: string,
40
- userRoot: string,
41
- outputName: string | undefined,
42
- ): void {
43
- const dest = path.join(shellNextApp, ".next")
44
- const src = path.resolve(userRoot, outputName ?? ".next")
45
- if (src === dest) return
46
- if (!fs.existsSync(src)) return
47
-
48
- if (fs.existsSync(dest)) {
49
- fs.rmSync(dest, {
50
- recursive: true,
51
- force: true,
52
- maxRetries: 5,
53
- retryDelay: 200,
54
- })
55
- }
56
- fs.renameSync(src, dest)
57
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Catch-all route that serves arbitrary files from the user registry's
3
- * `public/` directory. Fires only when no other app route matches, so it
4
- * doesn't interfere with static routes like `/`, `/docs/...`, `/r/...`,
5
- * etc. The shell's own `public/` assets are served first by Next.js's
6
- * built-in static handler; this route handles the user's project's assets
7
- * since Next only serves the bundled package's `public/`.
8
- *
9
- * Examples of what this enables:
10
- * - `![foo](./my-image.png)` inside user MDX resolving to
11
- * `{userRoot}/public/my-image.png`
12
- * - Custom favicons referenced in `branding.faviconDark` that live under
13
- * `{userRoot}/public/`
14
- */
15
- import "server-only"
16
- import fs from "node:fs"
17
- import path from "node:path"
18
- import { NextRequest } from "next/server"
19
-
20
- const MIME: Record<string, string> = {
21
- ".png": "image/png",
22
- ".jpg": "image/jpeg",
23
- ".jpeg": "image/jpeg",
24
- ".gif": "image/gif",
25
- ".webp": "image/webp",
26
- ".avif": "image/avif",
27
- ".svg": "image/svg+xml",
28
- ".ico": "image/x-icon",
29
- ".woff": "font/woff",
30
- ".woff2": "font/woff2",
31
- ".ttf": "font/ttf",
32
- ".otf": "font/otf",
33
- ".json": "application/json",
34
- ".txt": "text/plain; charset=utf-8",
35
- ".xml": "application/xml",
36
- ".pdf": "application/pdf",
37
- ".mp4": "video/mp4",
38
- ".webm": "video/webm",
39
- }
40
-
41
- export async function GET(
42
- _request: NextRequest,
43
- { params }: { params: Promise<{ asset: string[] }> },
44
- ) {
45
- const { asset } = await params
46
- const userRoot = process.env.USER_REGISTRY_ROOT
47
- if (!userRoot || asset.length === 0) {
48
- return new Response("Not found", { status: 404 })
49
- }
50
-
51
- // Reject traversal.
52
- if (asset.some((seg) => seg.includes("..") || seg.includes("\0"))) {
53
- return new Response("Forbidden", { status: 403 })
54
- }
55
-
56
- const publicDir = path.join(userRoot, "public")
57
- const filePath = path.join(publicDir, ...asset)
58
-
59
- // Defense-in-depth: make sure the resolved path is still under public/.
60
- const resolved = path.resolve(filePath)
61
- if (!resolved.startsWith(path.resolve(publicDir) + path.sep)) {
62
- return new Response("Forbidden", { status: 403 })
63
- }
64
-
65
- try {
66
- const stat = await fs.promises.stat(resolved)
67
- if (!stat.isFile()) return new Response("Not found", { status: 404 })
68
- const body = await fs.promises.readFile(resolved)
69
- const type = MIME[path.extname(resolved).toLowerCase()] ?? "application/octet-stream"
70
- return new Response(new Uint8Array(body), {
71
- status: 200,
72
- headers: {
73
- "Content-Type": type,
74
- "Content-Length": String(stat.size),
75
- "Cache-Control": "public, max-age=0, must-revalidate",
76
- },
77
- })
78
- } catch {
79
- return new Response("Not found", { status: 404 })
80
- }
81
- }
@@ -1,19 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server"
2
- import { registry } from "@shell/registry.config"
3
-
4
- /**
5
- * Serves the user registry's `public/a11y/{name}.json` files. The shell's
6
- * own `public/` doesn't hold these (they live in the user's project), so a
7
- * route handler proxies the read through the adapter.
8
- */
9
- export async function GET(
10
- _request: NextRequest,
11
- { params }: { params: Promise<{ name: string }> },
12
- ) {
13
- const { name } = await params
14
- const data = registry?.getA11yData ? await registry.getA11yData(name) : null
15
- if (!data) {
16
- return NextResponse.json({ error: "A11y data not found" }, { status: 404 })
17
- }
18
- return NextResponse.json(data)
19
- }
@@ -1,19 +0,0 @@
1
- import { NextResponse } from "next/server"
2
- import { getAllDocs } from "@shell/lib/docs"
3
- import { getAllComponents } from "@shell/lib/components-nav"
4
-
5
- export function GET() {
6
- const docs = getAllDocs().map((doc) => ({
7
- label: doc.title,
8
- href: `/docs/${doc.slug}`,
9
- group: "Documentation",
10
- }))
11
-
12
- const components = getAllComponents().map((comp) => ({
13
- label: comp.label,
14
- href: `/components/${comp.name}`,
15
- group: "Components",
16
- }))
17
-
18
- return NextResponse.json([...docs, ...components])
19
- }