create-sprinkles 0.2.3 → 0.3.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Malstrom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # create-sprinkles
2
+
3
+ Get started with development by creating projects from templates quickly.
4
+
5
+ ## Prerequisites
6
+
7
+ create-sprinkles requires [Vite+](https://viteplus.dev). Install it with Homebrew on macOS:
8
+
9
+ ```sh
10
+ brew install markmals/tap/vite-plus
11
+ ```
12
+
13
+ Or use the official installer script:
14
+
15
+ ```sh
16
+ # macOS/Linux
17
+ curl -fsSL https://vite.plus | bash
18
+
19
+ # Windows
20
+ irm https://vite.plus/ps1 | iex
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```sh
26
+ vp create sprinkles
27
+ ```
28
+
29
+ The interactive prompts will guide you through:
30
+
31
+ 1. **Directory** — where to create the project
32
+ 2. **Project kind** — choose a template (see below)
33
+ 3. **Optional features** — add-ons specific to the chosen template
34
+ 4. **GitHub owner** — your GitHub user or organization
35
+
36
+ ## Templates
37
+
38
+ ### React Router — SPA
39
+
40
+ Single-page app with React Router, Tailwind CSS, and Vite+.
41
+
42
+ - Optional: [Convex](https://convex.dev) backend
43
+
44
+ ### React Router — SSR
45
+
46
+ Server-rendered app with React Router, Tailwind CSS, Vite+, and Cloudflare deployment.
47
+
48
+ - Optional: [Convex](https://convex.dev) backend
49
+
50
+ ### React Router — RSC
51
+
52
+ React Server Components with React Router, Tailwind CSS, Vite+, and Cloudflare deployment.
53
+
54
+ - Optional: Content-layer plugin for MDX-based content
55
+
56
+ ## License
57
+
58
+ MIT
package/dist/bin.mjs CHANGED
@@ -4,6 +4,9 @@ import { createTemplate, runTemplate } from "bingo";
4
4
  import path from "node:path";
5
5
  import { handlebars } from "bingo-handlebars";
6
6
  import { z } from "zod";
7
+ //#region src/metadata.ts
8
+ const NAME = "create-sprinkles";
9
+ //#endregion
7
10
  //#region src/context.ts
8
11
  function buildContext(opts) {
9
12
  const isSPA = opts.kind === "react-router-spa";
@@ -13,6 +16,7 @@ function buildContext(opts) {
13
16
  ...opts,
14
17
  hasContentLayer: isRSC && Boolean(opts.contentLayer),
15
18
  hasConvex: (isSPA || isSSR) && Boolean(opts.convex),
19
+ hasSEA: isRSC && Boolean(opts.sea),
16
20
  isPackage: opts.kind === "ts-package",
17
21
  isRSC,
18
22
  isReactRouter: opts.kind !== "ts-package",
@@ -62,12 +66,16 @@ function buildDependencyCommands(context) {
62
66
  commands.push("vp add react react-dom react-router");
63
67
  commands.push("vp add -D @types/react @types/react-dom @react-router/dev @tailwindcss/vite tailwindcss vite-plugin-devtools-json");
64
68
  commands.push("vp add -D @rolldown/plugin-babel @vitejs/plugin-react babel-plugin-react-compiler");
65
- commands.push("vp add -D @cloudflare/vite-plugin wrangler");
69
+ if (!context.hasSEA) commands.push("vp add -D @cloudflare/vite-plugin wrangler");
66
70
  commands.push("vp add -D eslint-plugin-perfectionist eslint-plugin-react-hooks");
67
71
  }
68
72
  if (context.isReactRouter) commands.push("vp add @react-router/node isbot");
69
73
  if (context.hasConvex) commands.push("vp add convex @convex-dev/react-query @tanstack/react-query");
70
74
  if (context.isRSC) commands.push("vp add -D @vitejs/plugin-rsc");
75
+ if (context.hasSEA) {
76
+ commands.push("vp add @remix-run/node-fetch-server mime");
77
+ commands.push("vp add -D tsdown");
78
+ }
71
79
  if (context.hasContentLayer) {
72
80
  commands.push("vp add jsr:@std/jsonc jsr:@std/yaml gray-matter github-slugger @remix-run/data-schema");
73
81
  commands.push("vp add -D @mdx-js/rollup");
@@ -86,7 +94,7 @@ function buildScripts(context) {
86
94
  commands: phase0Commands,
87
95
  phase: 0
88
96
  });
89
- if (context.isRSC) scripts.push({
97
+ if (context.isRSC && !context.hasSEA) scripts.push({
90
98
  commands: ["vpx wrangler types -c wrangler.rsc.jsonc"],
91
99
  phase: 1
92
100
  });
@@ -119,7 +127,8 @@ function buildScripts(context) {
119
127
  function buildSuggestions(context) {
120
128
  const suggestions = [];
121
129
  if (context.hasConvex) suggestions.push("Open the Convex dashboard: https://dashboard.convex.dev");
122
- if (context.isSSR || context.isRSC) suggestions.push("Log in to Cloudflare: vpx wrangler login");
130
+ if ((context.isSSR || context.isRSC) && !context.hasSEA) suggestions.push("Log in to Cloudflare: vpx wrangler login");
131
+ if (context.hasSEA) suggestions.push("Build the executable: vp run build");
123
132
  if (context.isReactRouter) suggestions.push("Start the dev server: vp dev");
124
133
  else suggestions.push("Start development: vp run dev");
125
134
  return suggestions;
@@ -147,6 +156,8 @@ async function collectAddonLayers(context) {
147
156
  if (context.isPackage && context.sea) addons.push(await tryHandlebars("ts-package-sea", context));
148
157
  if (context.hasConvex) addons.push(await tryHandlebars("react-router-convex", context));
149
158
  if (context.isSSR && context.hasConvex) addons.push(await tryHandlebars("react-router-ssr-convex", context));
159
+ if (context.isRSC && !context.hasSEA) addons.push(await tryHandlebars("react-router-rsc-cloudflare", context));
160
+ if (context.hasSEA) addons.push(await tryHandlebars("react-router-rsc-sea", context));
150
161
  if (context.hasContentLayer) addons.push(await tryHandlebars("react-router-rsc-content-layer", context));
151
162
  return addons;
152
163
  }
@@ -176,9 +187,6 @@ var template_default = createTemplate({
176
187
  }
177
188
  });
178
189
  //#endregion
179
- //#region src/index.ts
180
- const NAME = "create-sprinkles";
181
- //#endregion
182
190
  //#region bin/index.ts
183
191
  prompts.intro(NAME);
184
192
  const directory = await prompts.text({
@@ -225,12 +233,20 @@ if (kind === "react-router-spa" || kind === "react-router-ssr") {
225
233
  convex = answer;
226
234
  }
227
235
  if (kind === "react-router-rsc") {
228
- const answer = await prompts.confirm({
229
- initialValue: false,
230
- message: "Include content-layer plugin?"
236
+ const features = await prompts.multiselect({
237
+ message: "Include optional features?",
238
+ options: [{
239
+ label: "Content-layer plugin",
240
+ value: "contentLayer"
241
+ }, {
242
+ label: "Single Executable Application (SEA)",
243
+ value: "sea"
244
+ }],
245
+ required: false
231
246
  });
232
- if (prompts.isCancel(answer)) process.exit(0);
233
- contentLayer = answer;
247
+ if (prompts.isCancel(features)) process.exit(0);
248
+ contentLayer = features.includes("contentLayer");
249
+ sea = features.includes("sea");
234
250
  }
235
251
  if (kind === "ts-package") {
236
252
  const features = await prompts.multiselect({
package/dist/index.d.mts CHANGED
@@ -36,11 +36,9 @@ interface TemplateContext extends Options {
36
36
  isReactRouter: boolean;
37
37
  hasConvex: boolean;
38
38
  hasContentLayer: boolean;
39
+ hasSEA: boolean;
39
40
  ssr: boolean;
40
41
  }
41
42
  declare function buildContext(opts: Options): TemplateContext;
42
43
  //#endregion
43
- //#region src/index.d.ts
44
- declare const NAME = "create-sprinkles";
45
- //#endregion
46
- export { NAME, type TemplateContext, buildContext, options, _default as template };
44
+ export { type TemplateContext, buildContext, options, _default as template };
package/dist/index.mjs CHANGED
@@ -11,6 +11,7 @@ function buildContext(opts) {
11
11
  ...opts,
12
12
  hasContentLayer: isRSC && Boolean(opts.contentLayer),
13
13
  hasConvex: (isSPA || isSSR) && Boolean(opts.convex),
14
+ hasSEA: isRSC && Boolean(opts.sea),
14
15
  isPackage: opts.kind === "ts-package",
15
16
  isRSC,
16
17
  isReactRouter: opts.kind !== "ts-package",
@@ -34,6 +35,9 @@ function mergeFiles(...layers) {
34
35
  return result;
35
36
  }
36
37
  //#endregion
38
+ //#region src/metadata.ts
39
+ const NAME = "create-sprinkles";
40
+ //#endregion
37
41
  //#region src/options.ts
38
42
  const kind = z.enum([
39
43
  "react-router-spa",
@@ -60,12 +64,16 @@ function buildDependencyCommands(context) {
60
64
  commands.push("vp add react react-dom react-router");
61
65
  commands.push("vp add -D @types/react @types/react-dom @react-router/dev @tailwindcss/vite tailwindcss vite-plugin-devtools-json");
62
66
  commands.push("vp add -D @rolldown/plugin-babel @vitejs/plugin-react babel-plugin-react-compiler");
63
- commands.push("vp add -D @cloudflare/vite-plugin wrangler");
67
+ if (!context.hasSEA) commands.push("vp add -D @cloudflare/vite-plugin wrangler");
64
68
  commands.push("vp add -D eslint-plugin-perfectionist eslint-plugin-react-hooks");
65
69
  }
66
70
  if (context.isReactRouter) commands.push("vp add @react-router/node isbot");
67
71
  if (context.hasConvex) commands.push("vp add convex @convex-dev/react-query @tanstack/react-query");
68
72
  if (context.isRSC) commands.push("vp add -D @vitejs/plugin-rsc");
73
+ if (context.hasSEA) {
74
+ commands.push("vp add @remix-run/node-fetch-server mime");
75
+ commands.push("vp add -D tsdown");
76
+ }
69
77
  if (context.hasContentLayer) {
70
78
  commands.push("vp add jsr:@std/jsonc jsr:@std/yaml gray-matter github-slugger @remix-run/data-schema");
71
79
  commands.push("vp add -D @mdx-js/rollup");
@@ -84,7 +92,7 @@ function buildScripts(context) {
84
92
  commands: phase0Commands,
85
93
  phase: 0
86
94
  });
87
- if (context.isRSC) scripts.push({
95
+ if (context.isRSC && !context.hasSEA) scripts.push({
88
96
  commands: ["vpx wrangler types -c wrangler.rsc.jsonc"],
89
97
  phase: 1
90
98
  });
@@ -117,7 +125,8 @@ function buildScripts(context) {
117
125
  function buildSuggestions(context) {
118
126
  const suggestions = [];
119
127
  if (context.hasConvex) suggestions.push("Open the Convex dashboard: https://dashboard.convex.dev");
120
- if (context.isSSR || context.isRSC) suggestions.push("Log in to Cloudflare: vpx wrangler login");
128
+ if ((context.isSSR || context.isRSC) && !context.hasSEA) suggestions.push("Log in to Cloudflare: vpx wrangler login");
129
+ if (context.hasSEA) suggestions.push("Build the executable: vp run build");
121
130
  if (context.isReactRouter) suggestions.push("Start the dev server: vp dev");
122
131
  else suggestions.push("Start development: vp run dev");
123
132
  return suggestions;
@@ -145,6 +154,8 @@ async function collectAddonLayers(context) {
145
154
  if (context.isPackage && context.sea) addons.push(await tryHandlebars("ts-package-sea", context));
146
155
  if (context.hasConvex) addons.push(await tryHandlebars("react-router-convex", context));
147
156
  if (context.isSSR && context.hasConvex) addons.push(await tryHandlebars("react-router-ssr-convex", context));
157
+ if (context.isRSC && !context.hasSEA) addons.push(await tryHandlebars("react-router-rsc-cloudflare", context));
158
+ if (context.hasSEA) addons.push(await tryHandlebars("react-router-rsc-sea", context));
148
159
  if (context.hasContentLayer) addons.push(await tryHandlebars("react-router-rsc-content-layer", context));
149
160
  return addons;
150
161
  }
@@ -174,7 +185,4 @@ var template_default = createTemplate({
174
185
  }
175
186
  });
176
187
  //#endregion
177
- //#region src/index.ts
178
- const NAME = "create-sprinkles";
179
- //#endregion
180
- export { NAME, buildContext, options, template_default as template };
188
+ export { buildContext, options, template_default as template };
package/package.json CHANGED
@@ -1,45 +1,54 @@
1
1
  {
2
- "name": "create-sprinkles",
3
- "version": "0.2.3",
4
- "description": "Get started with development by creating projects from templates quickly.",
5
- "homepage": "https://github.com/withsprinkles/workbench#readme",
6
- "bugs": {
7
- "url": "https://github.com/withsprinkles/workbench/issues"
8
- },
9
- "license": "MIT",
10
- "author": "Mark Malstrom <mark@malstrom.me>",
11
- "repository": {
12
- "type": "git",
13
- "url": "git+https://github.com/withsprinkles/workbench.git"
14
- },
15
- "bin": {
16
- "create-sprinkles": "dist/bin.mjs"
17
- },
18
- "files": [
19
- "dist",
20
- "templates"
21
- ],
22
- "type": "module",
23
- "types": "./dist/index.d.mts",
24
- "exports": {
25
- ".": "./dist/index.mjs",
26
- "./package.json": "./package.json"
27
- },
28
- "dependencies": {
29
- "@bomb.sh/args": "^0.3.1",
30
- "@clack/prompts": "^1.1.0",
31
- "bingo": "^0.9.2",
32
- "bingo-fs": "^0.5.6",
33
- "bingo-handlebars": "^0.1.0",
34
- "zod": "^3.25.76"
35
- },
36
- "devDependencies": {
37
- "@types/node": "^25.3.5",
38
- "@typescript/native-preview": "latest",
39
- "bingo-testers": "^0.5.8",
40
- "bumpp": "^10.4.1",
41
- "vite-plus": "latest",
42
- "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
43
- },
44
- "scripts": {}
45
- }
2
+ "name": "create-sprinkles",
3
+ "version": "0.3.0",
4
+ "description": "Get started with development by creating projects from templates quickly.",
5
+ "homepage": "https://github.com/withsprinkles/create-sprinkles#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/withsprinkles/create-sprinkles/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "Mark Malstrom <mark@malstrom.me>",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/withsprinkles/create-sprinkles.git"
14
+ },
15
+ "bin": {
16
+ "create-sprinkles": "dist/bin.mjs"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "templates"
21
+ ],
22
+ "type": "module",
23
+ "types": "./dist/index.d.mts",
24
+ "exports": {
25
+ ".": "./dist/index.mjs",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "scripts": {
29
+ "prepublishOnly": "vp run build"
30
+ },
31
+ "dependencies": {
32
+ "@bomb.sh/args": "^0.3.1",
33
+ "@clack/prompts": "^1.1.0",
34
+ "bingo": "^0.9.2",
35
+ "bingo-fs": "^0.5.6",
36
+ "bingo-handlebars": "^0.1.0",
37
+ "zod": "^3.25.76"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.3.5",
41
+ "@typescript/native-preview": "latest",
42
+ "bingo-testers": "^0.5.8",
43
+ "bumpp": "^10.4.1",
44
+ "vite-plus": "latest",
45
+ "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
46
+ },
47
+ "packageManager": "pnpm@10.32.1",
48
+ "pnpm": {
49
+ "overrides": {
50
+ "vite": "npm:@voidzero-dev/vite-plus-core@latest",
51
+ "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,53 @@
1
+ import { readdirSync, writeFileSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { build } from "tsdown";
4
+
5
+ import packageJson from "../package.json" with { type: "json" };
6
+
7
+ const CLIENT_DIR = "dist/client";
8
+
9
+ // ── Collect every file under dist/client/ ──────────────────────
10
+ function collectFiles(dir: string, base = dir) {
11
+ const files = [] as string[];
12
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
13
+ const full = join(dir, entry.name);
14
+ if (entry.isDirectory()) {
15
+ files.push(...collectFiles(full, base));
16
+ } else {
17
+ // Key = path relative to CLIENT_DIR, e.g. "assets/index-abc123.js"
18
+ files.push(relative(base, full));
19
+ }
20
+ }
21
+ return files;
22
+ }
23
+
24
+ let clientFiles = collectFiles(CLIENT_DIR);
25
+
26
+ // ── Write manifest so server.ts knows which keys exist ──────────
27
+ writeFileSync("dist/asset-manifest.json", JSON.stringify(clientFiles, null, 2));
28
+
29
+ // ── Build the SEA asset map: key → file path on disk ────────────
30
+ // node:sea's getAsset(key) uses these keys at runtime
31
+ let assets = Object.fromEntries(clientFiles.map(file => [file, join(CLIENT_DIR, file)]));
32
+
33
+ console.log(`Embedding ${clientFiles.length} client assets into executable`);
34
+
35
+ // ── Run tsdown with exe enabled ─────────────────────────────────
36
+ await build({
37
+ entry: ["server.ts"],
38
+ outDir: "build",
39
+ format: "esm",
40
+ target: "node25",
41
+ // Bundle everything — the React Router server build, @remix-run/node-fetch-server, etc.
42
+ noExternal: [/.*/],
43
+ exe: {
44
+ fileName: packageJson.name,
45
+ seaConfig: {
46
+ disableExperimentalSEAWarning: true,
47
+ useCodeCache: false,
48
+ assets, // ← every client file, keyed by its URL path
49
+ },
50
+ },
51
+ });
52
+
53
+ console.log(`Done — executable written to build/${packageJson.name}`);
@@ -0,0 +1,52 @@
1
+ import { createRequestListener } from "@remix-run/node-fetch-server";
2
+ import mime from "mime";
3
+ import * as http from "node:http";
4
+ import sea from "node:sea";
5
+
6
+ // @ts-expect-error: tsdown bundles this — it's the output of `vp build`
7
+ import app from "./dist/server/index.js";
8
+
9
+ // Generated by scripts/build-exe.ts before tsdown runs
10
+ import assetManifest from "./dist/asset-manifest.json" with { type: "json" };
11
+
12
+ function mimeType(path: string): string {
13
+ let ext = path.substring(path.lastIndexOf("."));
14
+ return mime.getType(ext) ?? "application/octet-stream";
15
+ }
16
+
17
+ // Set of embedded asset paths for O(1) lookup
18
+ let embeddedAssets = new Set<string>(assetManifest);
19
+
20
+ async function handler(request: Request): Promise<Response> {
21
+ let url = new URL(request.url);
22
+ // Strip leading slash to match SEA asset keys
23
+ let assetPath = url.pathname.slice(1);
24
+
25
+ // Serve embedded client assets straight from the binary
26
+ if (sea.isSea() && embeddedAssets.has(assetPath)) {
27
+ let body = sea.getAsset(assetPath); // ArrayBuffer
28
+
29
+ // Vite fingerprints asset filenames, so anything under /assets/
30
+ // is safe to cache forever. Everything else gets a shorter TTL.
31
+ let immutable = assetPath.startsWith("assets/");
32
+
33
+ return new Response(body, {
34
+ headers: {
35
+ "Content-Type": mimeType(assetPath),
36
+ "Cache-Control": immutable
37
+ ? "public, max-age=31536000, immutable"
38
+ : "public, max-age=3600",
39
+ },
40
+ });
41
+ }
42
+
43
+ // Everything else → React Router RSC
44
+ return app.fetch(request);
45
+ }
46
+
47
+ const port = Number.parseInt(process.env.PORT || "1612");
48
+ const server = http.createServer(createRequestListener(handler));
49
+
50
+ server.listen(port, () => {
51
+ console.log(`Listening on http://localhost:${port}`);
52
+ });
@@ -1,6 +1,9 @@
1
1
  import type { Config } from "@react-router/dev/config";
2
2
 
3
3
  export default {
4
+ {{#if hasSEA}}
5
+ buildDirectory: "dist",
6
+ {{/if}}
4
7
  future: {
5
8
  v8_middleware: true,
6
9
  v8_viteEnvironmentApi: true,
@@ -1,5 +1,7 @@
1
1
  {{#if isRSC}}
2
+ {{#unless hasSEA}}
2
3
  import { cloudflare } from "@cloudflare/vite-plugin";
4
+ {{/unless}}
3
5
  {{#if hasContentLayer}}
4
6
  import mdx from "@mdx-js/rollup";
5
7
  {{/if}}
@@ -26,7 +28,9 @@ import { defineConfig } from "vite-plus";
26
28
  {{/if}}
27
29
 
28
30
  {{#if isReactRouter}}
31
+ {{#unless hasSEA}}
29
32
  const IS_TEST = Boolean(process.env.VITEST);
33
+ {{/unless}}
30
34
 
31
35
  {{/if}}
32
36
  export default defineConfig({
@@ -107,7 +111,9 @@ export default defineConfig({
107
111
  node: true,
108
112
  },
109
113
  {{#if isRSC}}
114
+ {{#unless hasSEA}}
110
115
  ignorePatterns: ["**/worker-configuration.d.ts"],
116
+ {{/unless}}
111
117
  {{/if}}
112
118
  {{#if isReactRouter}}
113
119
  jsPlugins: [
@@ -281,6 +287,19 @@ export default defineConfig({
281
287
  },
282
288
  {{/if}}
283
289
  {{#if isRSC}}
290
+ {{#if hasSEA}}
291
+ plugins: [
292
+ {{#if hasContentLayer}}
293
+ contentLayer(),
294
+ mdx(),
295
+ {{/if}}
296
+ reactRouter(),
297
+ rsc(),
298
+ reactCompiler(),
299
+ tailwindcss(),
300
+ devtoolsJson(),
301
+ ],
302
+ {{else}}
284
303
  plugins: [
285
304
  {{#if hasContentLayer}}
286
305
  contentLayer(),
@@ -307,6 +326,7 @@ export default defineConfig({
307
326
  tailwindcss(),
308
327
  devtoolsJson(),
309
328
  ],
329
+ {{/if}}
310
330
  {{else if isSSR}}
311
331
  plugins: [
312
332
  tailwindcss(),
@@ -338,6 +358,22 @@ export default defineConfig({
338
358
  run: {
339
359
  tasks: {
340
360
  {{#if isRSC}}
361
+ {{#if hasSEA}}
362
+ build: {
363
+ // Compound command — each half is cached independently.
364
+ // vp build runs React Router's production build,
365
+ // then the script bundles everything into a SEA.
366
+ command: "vp build && node scripts/build-exe.ts",
367
+ env: ["NODE_ENV"],
368
+ },
369
+ typecheck: {
370
+ command: "tsgo --noEmit",
371
+ dependsOn: ["typegen:react-router"],
372
+ },
373
+ "typegen:react-router": {
374
+ command: "react-router typegen",
375
+ },
376
+ {{else}}
341
377
  deploy: {
342
378
  cache: false,
343
379
  command: "vp build && wrangler deploy",
@@ -352,6 +388,7 @@ export default defineConfig({
352
388
  "typegen:react-router": {
353
389
  command: "react-router typegen",
354
390
  },
391
+ {{/if}}
355
392
  {{else if isReactRouter}}
356
393
  deploy: {
357
394
  cache: false,