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 +21 -0
- package/README.md +58 -0
- package/dist/bin.mjs +27 -11
- package/dist/index.d.mts +2 -4
- package/dist/index.mjs +15 -7
- package/package.json +53 -44
- package/templates/react-router-rsc-sea/scripts/build-exe.ts.hbs +53 -0
- package/templates/react-router-rsc-sea/server.ts.hbs +52 -0
- package/templates/react-shared/public/favicon.ico +0 -0
- package/templates/react-shared/react-router.config.ts.hbs +3 -0
- package/templates/shared/vite.config.ts.hbs +37 -0
- /package/templates/{react-router-rsc → react-router-rsc-cloudflare}/workers/entry.rsc.tsx +0 -0
- /package/templates/{react-router-rsc → react-router-rsc-cloudflare}/workers/entry.ssr.tsx +0 -0
- /package/templates/{react-router-rsc → react-router-rsc-cloudflare}/wrangler.rsc.jsonc.hbs +0 -0
- /package/templates/{react-router-rsc → react-router-rsc-cloudflare}/wrangler.ssr.jsonc.hbs +0 -0
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
|
|
229
|
-
|
|
230
|
-
|
|
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(
|
|
233
|
-
contentLayer =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
});
|
|
Binary file
|
|
@@ -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,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|