create-sprinkles 0.2.4 → 0.3.1

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,59 @@
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
+ - Optional: Single Executable Application (SEA) — bundles the server and all client assets into a single portable Node.js binary via `node:sea`
56
+
57
+ ## License
58
+
59
+ MIT
package/dist/bin.mjs CHANGED
@@ -4,6 +4,7 @@ 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
+ import { readFileSync } from "node:fs";
7
8
  //#region src/metadata.ts
8
9
  const NAME = "create-sprinkles";
9
10
  //#endregion
@@ -16,6 +17,7 @@ function buildContext(opts) {
16
17
  ...opts,
17
18
  hasContentLayer: isRSC && Boolean(opts.contentLayer),
18
19
  hasConvex: (isSPA || isSSR) && Boolean(opts.convex),
20
+ hasSEA: isRSC && Boolean(opts.sea),
19
21
  isPackage: opts.kind === "ts-package",
20
22
  isRSC,
21
23
  isReactRouter: opts.kind !== "ts-package",
@@ -58,6 +60,7 @@ const options = {
58
60
  };
59
61
  //#endregion
60
62
  //#region src/scripts.ts
63
+ const faviconBase64 = readFileSync(path.join(import.meta.dirname, "assets/favicon.ico")).toString("base64");
61
64
  function buildDependencyCommands(context) {
62
65
  const commands = [];
63
66
  commands.push("vp add -D @types/node @typescript/native-preview");
@@ -65,12 +68,16 @@ function buildDependencyCommands(context) {
65
68
  commands.push("vp add react react-dom react-router");
66
69
  commands.push("vp add -D @types/react @types/react-dom @react-router/dev @tailwindcss/vite tailwindcss vite-plugin-devtools-json");
67
70
  commands.push("vp add -D @rolldown/plugin-babel @vitejs/plugin-react babel-plugin-react-compiler");
68
- commands.push("vp add -D @cloudflare/vite-plugin wrangler");
71
+ if (!context.hasSEA) commands.push("vp add -D @cloudflare/vite-plugin wrangler");
69
72
  commands.push("vp add -D eslint-plugin-perfectionist eslint-plugin-react-hooks");
70
73
  }
71
74
  if (context.isReactRouter) commands.push("vp add @react-router/node isbot");
72
75
  if (context.hasConvex) commands.push("vp add convex @convex-dev/react-query @tanstack/react-query");
73
76
  if (context.isRSC) commands.push("vp add -D @vitejs/plugin-rsc");
77
+ if (context.hasSEA) {
78
+ commands.push("vp add @remix-run/node-fetch-server mime");
79
+ commands.push("vp add -D tsdown");
80
+ }
74
81
  if (context.hasContentLayer) {
75
82
  commands.push("vp add jsr:@std/jsonc jsr:@std/yaml gray-matter github-slugger @remix-run/data-schema");
76
83
  commands.push("vp add -D @mdx-js/rollup");
@@ -89,7 +96,7 @@ function buildScripts(context) {
89
96
  commands: phase0Commands,
90
97
  phase: 0
91
98
  });
92
- if (context.isRSC) scripts.push({
99
+ if (context.isRSC && !context.hasSEA) scripts.push({
93
100
  commands: ["vpx wrangler types -c wrangler.rsc.jsonc"],
94
101
  phase: 1
95
102
  });
@@ -106,6 +113,10 @@ function buildScripts(context) {
106
113
  phase: 3,
107
114
  silent: true
108
115
  });
116
+ if (context.isReactRouter) scripts.push({
117
+ commands: ["mkdir -p public", `echo '${faviconBase64}' | base64 -d > public/favicon.ico`],
118
+ phase: 3
119
+ });
109
120
  scripts.push({
110
121
  commands: [
111
122
  "ln -sf AGENTS.md CLAUDE.md",
@@ -122,7 +133,8 @@ function buildScripts(context) {
122
133
  function buildSuggestions(context) {
123
134
  const suggestions = [];
124
135
  if (context.hasConvex) suggestions.push("Open the Convex dashboard: https://dashboard.convex.dev");
125
- if (context.isSSR || context.isRSC) suggestions.push("Log in to Cloudflare: vpx wrangler login");
136
+ if ((context.isSSR || context.isRSC) && !context.hasSEA) suggestions.push("Log in to Cloudflare: vpx wrangler login");
137
+ if (context.hasSEA) suggestions.push("Build the executable: vp run build");
126
138
  if (context.isReactRouter) suggestions.push("Start the dev server: vp dev");
127
139
  else suggestions.push("Start development: vp run dev");
128
140
  return suggestions;
@@ -150,6 +162,8 @@ async function collectAddonLayers(context) {
150
162
  if (context.isPackage && context.sea) addons.push(await tryHandlebars("ts-package-sea", context));
151
163
  if (context.hasConvex) addons.push(await tryHandlebars("react-router-convex", context));
152
164
  if (context.isSSR && context.hasConvex) addons.push(await tryHandlebars("react-router-ssr-convex", context));
165
+ if (context.isRSC && !context.hasSEA) addons.push(await tryHandlebars("react-router-rsc-cloudflare", context));
166
+ if (context.hasSEA) addons.push(await tryHandlebars("react-router-rsc-sea", context));
153
167
  if (context.hasContentLayer) addons.push(await tryHandlebars("react-router-rsc-content-layer", context));
154
168
  return addons;
155
169
  }
@@ -225,12 +239,20 @@ if (kind === "react-router-spa" || kind === "react-router-ssr") {
225
239
  convex = answer;
226
240
  }
227
241
  if (kind === "react-router-rsc") {
228
- const answer = await prompts.confirm({
229
- initialValue: false,
230
- message: "Include content-layer plugin?"
242
+ const features = await prompts.multiselect({
243
+ message: "Include optional features?",
244
+ options: [{
245
+ label: "Content-layer plugin",
246
+ value: "contentLayer"
247
+ }, {
248
+ label: "Single Executable Application (SEA)",
249
+ value: "sea"
250
+ }],
251
+ required: false
231
252
  });
232
- if (prompts.isCancel(answer)) process.exit(0);
233
- contentLayer = answer;
253
+ if (prompts.isCancel(features)) process.exit(0);
254
+ contentLayer = features.includes("contentLayer");
255
+ sea = features.includes("sea");
234
256
  }
235
257
  if (kind === "ts-package") {
236
258
  const features = await prompts.multiselect({
package/dist/index.d.mts CHANGED
@@ -36,6 +36,7 @@ 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;
package/dist/index.mjs CHANGED
@@ -2,6 +2,7 @@ import { createTemplate } from "bingo";
2
2
  import { handlebars } from "bingo-handlebars";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
+ import { readFileSync } from "node:fs";
5
6
  //#region src/context.ts
6
7
  function buildContext(opts) {
7
8
  const isSPA = opts.kind === "react-router-spa";
@@ -11,6 +12,7 @@ function buildContext(opts) {
11
12
  ...opts,
12
13
  hasContentLayer: isRSC && Boolean(opts.contentLayer),
13
14
  hasConvex: (isSPA || isSSR) && Boolean(opts.convex),
15
+ hasSEA: isRSC && Boolean(opts.sea),
14
16
  isPackage: opts.kind === "ts-package",
15
17
  isRSC,
16
18
  isReactRouter: opts.kind !== "ts-package",
@@ -20,9 +22,6 @@ function buildContext(opts) {
20
22
  };
21
23
  }
22
24
  //#endregion
23
- //#region src/metadata.ts
24
- const NAME = "create-sprinkles";
25
- //#endregion
26
25
  //#region src/merge.ts
27
26
  function isDirectory(value) {
28
27
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -37,6 +36,9 @@ function mergeFiles(...layers) {
37
36
  return result;
38
37
  }
39
38
  //#endregion
39
+ //#region src/metadata.ts
40
+ const NAME = "create-sprinkles";
41
+ //#endregion
40
42
  //#region src/options.ts
41
43
  const kind = z.enum([
42
44
  "react-router-spa",
@@ -56,6 +58,7 @@ const options = {
56
58
  };
57
59
  //#endregion
58
60
  //#region src/scripts.ts
61
+ const faviconBase64 = readFileSync(path.join(import.meta.dirname, "assets/favicon.ico")).toString("base64");
59
62
  function buildDependencyCommands(context) {
60
63
  const commands = [];
61
64
  commands.push("vp add -D @types/node @typescript/native-preview");
@@ -63,12 +66,16 @@ function buildDependencyCommands(context) {
63
66
  commands.push("vp add react react-dom react-router");
64
67
  commands.push("vp add -D @types/react @types/react-dom @react-router/dev @tailwindcss/vite tailwindcss vite-plugin-devtools-json");
65
68
  commands.push("vp add -D @rolldown/plugin-babel @vitejs/plugin-react babel-plugin-react-compiler");
66
- commands.push("vp add -D @cloudflare/vite-plugin wrangler");
69
+ if (!context.hasSEA) commands.push("vp add -D @cloudflare/vite-plugin wrangler");
67
70
  commands.push("vp add -D eslint-plugin-perfectionist eslint-plugin-react-hooks");
68
71
  }
69
72
  if (context.isReactRouter) commands.push("vp add @react-router/node isbot");
70
73
  if (context.hasConvex) commands.push("vp add convex @convex-dev/react-query @tanstack/react-query");
71
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
+ }
72
79
  if (context.hasContentLayer) {
73
80
  commands.push("vp add jsr:@std/jsonc jsr:@std/yaml gray-matter github-slugger @remix-run/data-schema");
74
81
  commands.push("vp add -D @mdx-js/rollup");
@@ -87,7 +94,7 @@ function buildScripts(context) {
87
94
  commands: phase0Commands,
88
95
  phase: 0
89
96
  });
90
- if (context.isRSC) scripts.push({
97
+ if (context.isRSC && !context.hasSEA) scripts.push({
91
98
  commands: ["vpx wrangler types -c wrangler.rsc.jsonc"],
92
99
  phase: 1
93
100
  });
@@ -104,6 +111,10 @@ function buildScripts(context) {
104
111
  phase: 3,
105
112
  silent: true
106
113
  });
114
+ if (context.isReactRouter) scripts.push({
115
+ commands: ["mkdir -p public", `echo '${faviconBase64}' | base64 -d > public/favicon.ico`],
116
+ phase: 3
117
+ });
107
118
  scripts.push({
108
119
  commands: [
109
120
  "ln -sf AGENTS.md CLAUDE.md",
@@ -120,7 +131,8 @@ function buildScripts(context) {
120
131
  function buildSuggestions(context) {
121
132
  const suggestions = [];
122
133
  if (context.hasConvex) suggestions.push("Open the Convex dashboard: https://dashboard.convex.dev");
123
- if (context.isSSR || context.isRSC) suggestions.push("Log in to Cloudflare: vpx wrangler login");
134
+ if ((context.isSSR || context.isRSC) && !context.hasSEA) suggestions.push("Log in to Cloudflare: vpx wrangler login");
135
+ if (context.hasSEA) suggestions.push("Build the executable: vp run build");
124
136
  if (context.isReactRouter) suggestions.push("Start the dev server: vp dev");
125
137
  else suggestions.push("Start development: vp run dev");
126
138
  return suggestions;
@@ -148,6 +160,8 @@ async function collectAddonLayers(context) {
148
160
  if (context.isPackage && context.sea) addons.push(await tryHandlebars("ts-package-sea", context));
149
161
  if (context.hasConvex) addons.push(await tryHandlebars("react-router-convex", context));
150
162
  if (context.isSSR && context.hasConvex) addons.push(await tryHandlebars("react-router-ssr-convex", context));
163
+ if (context.isRSC && !context.hasSEA) addons.push(await tryHandlebars("react-router-rsc-cloudflare", context));
164
+ if (context.hasSEA) addons.push(await tryHandlebars("react-router-rsc-sea", context));
151
165
  if (context.hasContentLayer) addons.push(await tryHandlebars("react-router-rsc-content-layer", context));
152
166
  return addons;
153
167
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sprinkles",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "Get started with development by creating projects from templates quickly.",
5
5
  "homepage": "https://github.com/withsprinkles/create-sprinkles#readme",
6
6
  "bugs": {
@@ -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,