create-rsc-kit 0.17.0 → 0.18.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/dist/index.js +29 -1
- package/dist/init.d.ts +2 -1
- package/dist/init.js +170 -28
- package/dist/options.d.ts +21 -1
- package/dist/options.js +22 -0
- package/dist/templates.d.ts +45 -7
- package/dist/templates.js +170 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,8 +9,9 @@ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
|
9
9
|
import { dirname, join, resolve } from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
12
13
|
import { argv, exit, stdout } from 'node:process';
|
|
13
|
-
import { DEFAULT_COMPILER, HELP, HOSTS, publishedCore, assertInsideCwd, assertUsableName, defaultCore, parseArgs, } from './options.js';
|
|
14
|
+
import { DEFAULT_COMPILER, HELP, HOSTS, publishedCore, assertInsideCwd, assertUsableName, defaultCore, parseArgs, VALIDATIONS } from './options.js';
|
|
14
15
|
import { Prompter, bold, cyan, dim } from './prompt.js';
|
|
15
16
|
import { checkForNewer, notifyIfStale, selfVersion } from './stale.js';
|
|
16
17
|
import * as t from './templates.js';
|
|
@@ -77,10 +78,13 @@ async function collect() {
|
|
|
77
78
|
compiler: flags.compiler ?? 'none',
|
|
78
79
|
tailwind: flags.tailwind ?? true,
|
|
79
80
|
lint: flags.lint ?? true,
|
|
81
|
+
validation: flags.validation ?? 'zod',
|
|
82
|
+
env: flags.env ?? (flags.validation ?? 'zod') !== 'none',
|
|
80
83
|
sourceDir: flags.sourceDir ?? 'src',
|
|
81
84
|
install: flags.install ?? true,
|
|
82
85
|
git: flags.git ?? true,
|
|
83
86
|
core,
|
|
87
|
+
backend: flags.backend,
|
|
84
88
|
};
|
|
85
89
|
}
|
|
86
90
|
// Nothing is attached to answer. readline would simply never resolve, and a
|
|
@@ -101,6 +105,11 @@ async function collect() {
|
|
|
101
105
|
const compiler = flags.compiler ?? ((await p.confirm('React Compiler', true)) ? DEFAULT_COMPILER : 'none');
|
|
102
106
|
const tailwind = flags.tailwind ?? (await p.confirm('Tailwind CSS', true));
|
|
103
107
|
const lint = flags.lint ?? (await p.confirm('oxlint', true));
|
|
108
|
+
const validation = flags.validation ?? (await p.select('Validation library', VALIDATIONS));
|
|
109
|
+
// Only worth asking once there is a library to validate with.
|
|
110
|
+
const env = validation === 'none'
|
|
111
|
+
? false
|
|
112
|
+
: (flags.env ?? (await p.confirm('Typed environment variables (@t3-oss/env-core)', true)));
|
|
104
113
|
return {
|
|
105
114
|
dir: resolve(dir),
|
|
106
115
|
name: basename(dir),
|
|
@@ -108,10 +117,13 @@ async function collect() {
|
|
|
108
117
|
compiler,
|
|
109
118
|
tailwind,
|
|
110
119
|
lint,
|
|
120
|
+
validation,
|
|
121
|
+
env,
|
|
111
122
|
sourceDir: flags.sourceDir ?? 'src',
|
|
112
123
|
install: flags.install ?? (await p.confirm('Install dependencies now', true)),
|
|
113
124
|
git: flags.git ?? true,
|
|
114
125
|
core,
|
|
126
|
+
backend: flags.backend,
|
|
115
127
|
};
|
|
116
128
|
}
|
|
117
129
|
finally {
|
|
@@ -148,8 +160,21 @@ function write(o) {
|
|
|
148
160
|
];
|
|
149
161
|
if (o.tailwind)
|
|
150
162
|
files.push(['src/app/styles.css', t.styles]);
|
|
163
|
+
if (o.env) {
|
|
164
|
+
files.push([`${o.sourceDir}/env.ts`, t.env(o)]);
|
|
165
|
+
files.push([`${o.sourceDir}/instrumentation.ts`, t.instrumentation(o)]);
|
|
166
|
+
files.push(['.env.example', t.envExample]);
|
|
167
|
+
}
|
|
151
168
|
if (o.lint)
|
|
152
169
|
files.push(['.oxlintrc.json', t.oxlintConfig(o)]);
|
|
170
|
+
// A backend answering host calls: the two lines that wire it, and a
|
|
171
|
+
// secret the backend is given once. The scaffold's .gitignore already
|
|
172
|
+
// keeps .env out of git.
|
|
173
|
+
if (o.backend) {
|
|
174
|
+
files.push(['.env', t.backendEnv(o.backend, randomBytes(32).toString('base64url'))]);
|
|
175
|
+
if (!o.env)
|
|
176
|
+
files.push(['.env.example', t.backendEnvExample(o.backend)]);
|
|
177
|
+
}
|
|
153
178
|
const replaced = [];
|
|
154
179
|
for (const [path, contents] of files) {
|
|
155
180
|
const full = join(o.dir, path);
|
|
@@ -193,6 +218,9 @@ function report(o) {
|
|
|
193
218
|
for (const step of steps)
|
|
194
219
|
stdout.write(` ${cyan(step)}\n`);
|
|
195
220
|
stdout.write(`\n${dim('Pages live in src/app. Where it deploys is the Nitro preset in vite.config.ts.')}\n\n`);
|
|
221
|
+
if (o.backend) {
|
|
222
|
+
stdout.write(`${bold('Then:')} ${t.backendStep(false)}\n\n`);
|
|
223
|
+
}
|
|
196
224
|
}
|
|
197
225
|
/** A path the user can paste, when it is under where they are. */
|
|
198
226
|
function relativeish(dir) {
|
package/dist/init.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface Detected {
|
|
|
4
4
|
deps: Record<string, string>;
|
|
5
5
|
/** A Laravel application: artisan and a composer manifest, both. */
|
|
6
6
|
laravel: boolean;
|
|
7
|
+
/** A Go module: the backend is Go, and answers host calls from its own server. */
|
|
8
|
+
go: boolean;
|
|
7
9
|
host: Host | null;
|
|
8
10
|
sourceDir: string | null;
|
|
9
11
|
viteConfig: string | null;
|
|
@@ -25,7 +27,6 @@ export interface Step {
|
|
|
25
27
|
* about things the project has already decided.
|
|
26
28
|
*/
|
|
27
29
|
export declare function detect(dir: string): Detected;
|
|
28
|
-
/** Everything, in the order a reader would want to hear about it. */
|
|
29
30
|
export declare function initialise(o: Options, found: Detected, dir: string): Step[];
|
|
30
31
|
/**
|
|
31
32
|
* Add RSC to a project that already exists.
|
package/dist/init.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// written, missing dependencies are added, and for anything already present
|
|
9
9
|
// the exact edit is printed for the reader to make. A tool that silently
|
|
10
10
|
// reformats a working server has to be right about more than it can know.
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
12
13
|
import { dirname, join } from 'node:path';
|
|
13
14
|
import { cwd, exit, stdout } from 'node:process';
|
|
14
15
|
import { DEFAULT_COMPILER, parseArgs, publishedCore } from './options.js';
|
|
@@ -47,12 +48,18 @@ export function detect(dir) {
|
|
|
47
48
|
return {
|
|
48
49
|
deps,
|
|
49
50
|
laravel,
|
|
51
|
+
go: existsSync(join(dir, 'go.mod')),
|
|
50
52
|
host,
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
53
|
+
// resources/js, the directory a Laravel app already keeps its JavaScript
|
|
54
|
+
// in, so the route tree is resources/js/app the way it is src/app
|
|
55
|
+
// everywhere else. The stock app.js and bootstrap.js beside it are files,
|
|
56
|
+
// not the app/ directory, and the route tree never looks at them. An app
|
|
57
|
+
// that init set up before this default - a tree at resources/js/rsc/app
|
|
58
|
+
// - keeps it: a second run must never move a tree.
|
|
54
59
|
sourceDir: laravel
|
|
55
|
-
? 'resources/js/rsc'
|
|
60
|
+
? existsSync(join(dir, 'resources/js/rsc/app'))
|
|
61
|
+
? 'resources/js/rsc'
|
|
62
|
+
: 'resources/js'
|
|
56
63
|
: ['src', 'app', 'resources/js'].find((d) => existsSync(join(dir, d))) ?? null,
|
|
57
64
|
viteConfig: ['vite.config.ts', 'vite.config.js', 'vite.config.mts'].find((f) => existsSync(join(dir, f))) ?? null,
|
|
58
65
|
hasReact: Boolean(deps.react),
|
|
@@ -86,6 +93,17 @@ function major(range) {
|
|
|
86
93
|
* asset pipeline too.
|
|
87
94
|
*/
|
|
88
95
|
function mergeDependencies(o, found) {
|
|
96
|
+
// laravel-vite-plugin served the Blade asset pipeline, which the renderer
|
|
97
|
+
// replaces; left installed it would still be resolvable from the moved-aside
|
|
98
|
+
// config, and nothing else. Removed from the manifest so `npm install` does
|
|
99
|
+
// not keep fetching it; the moved-aside config names it in its own steps.
|
|
100
|
+
if (o.host === 'laravel') {
|
|
101
|
+
for (const field of ['dependencies', 'devDependencies']) {
|
|
102
|
+
const bucket = found.packageJson[field];
|
|
103
|
+
if (bucket && 'laravel-vite-plugin' in bucket)
|
|
104
|
+
delete bucket['laravel-vite-plugin'];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
89
107
|
const pkg = found.packageJson;
|
|
90
108
|
const wanted = JSON.parse(t.packageJson(o));
|
|
91
109
|
const steps = [];
|
|
@@ -182,6 +200,15 @@ function mergeScripts(o, found) {
|
|
|
182
200
|
if (existing === command)
|
|
183
201
|
continue;
|
|
184
202
|
if (STOCK[name]?.includes(existing.trim())) {
|
|
203
|
+
// A stock script. On Laravel it was the Blade asset pipeline's, and
|
|
204
|
+
// the renderer owns the frontend now - one config, one pipeline - so
|
|
205
|
+
// it is replaced rather than run beside. Elsewhere the two are
|
|
206
|
+
// combined, so the command keeps doing what it did as well.
|
|
207
|
+
if (o.host === 'laravel') {
|
|
208
|
+
scripts[name] = command;
|
|
209
|
+
combined.push(name);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
185
212
|
scripts[name] = combine(name, existing.trim(), command);
|
|
186
213
|
combined.push(name);
|
|
187
214
|
needsConcurrently ||= name === 'dev';
|
|
@@ -200,7 +227,9 @@ function mergeScripts(o, found) {
|
|
|
200
227
|
steps.push({
|
|
201
228
|
kind: 'merged',
|
|
202
229
|
what: combined.join(' and '),
|
|
203
|
-
detail:
|
|
230
|
+
detail: o.host === 'laravel'
|
|
231
|
+
? 'the renderer, in place of the Blade pipeline they ran'
|
|
232
|
+
: 'now runs the asset pipeline AND the renderer',
|
|
204
233
|
});
|
|
205
234
|
}
|
|
206
235
|
// Declared here rather than in the template, because whether it is needed
|
|
@@ -223,32 +252,64 @@ function mergeScripts(o, found) {
|
|
|
223
252
|
/** The plugin entry, written if there is no config and printed if there is. */
|
|
224
253
|
function viteConfig(o, found, dir) {
|
|
225
254
|
const file = t.configFile(o);
|
|
226
|
-
|
|
227
|
-
// vite.config carries laravel-vite-plugin, and the two cannot share a config
|
|
228
|
-
// — so the question is whether the RSC config exists, not whether any does.
|
|
229
|
-
const existing = o.host === 'laravel' ? (existsSync(join(dir, file)) ? file : null) : found.viteConfig;
|
|
255
|
+
const existing = found.viteConfig;
|
|
230
256
|
if (existing === null) {
|
|
231
257
|
writeFileSync(join(dir, file), t.viteConfig(o));
|
|
232
258
|
return [{ kind: 'wrote', what: file }];
|
|
233
259
|
}
|
|
260
|
+
// A Laravel app's vite.config carries laravel-vite-plugin, which owns
|
|
261
|
+
// base, publicDir, outDir, the input list and the server origin - the same
|
|
262
|
+
// things this build owns - so the two cannot share a file. They do not
|
|
263
|
+
// need to: once the renderer owns the frontend there is no @vite
|
|
264
|
+
// directive and no Blade asset pipeline for that plugin to serve. The old
|
|
265
|
+
// config is kept beside the new one, for a Blade page or two that still
|
|
266
|
+
// needs it, rather than lost.
|
|
267
|
+
if (o.host === 'laravel') {
|
|
268
|
+
const source = readFileSync(join(dir, existing), 'utf-8');
|
|
269
|
+
// Already the renderer's: a second run, or a hand-written one.
|
|
270
|
+
if (source.includes('rscKit(')) {
|
|
271
|
+
return [{ kind: 'skipped', what: existing, detail: 'already has rscKit()' }];
|
|
272
|
+
}
|
|
273
|
+
// Some other config the build cannot own - not Blade's - so the edit is
|
|
274
|
+
// printed rather than the file replaced.
|
|
275
|
+
if (!source.includes('laravel-vite-plugin')) {
|
|
276
|
+
return [manualPluginStep(o, existing)];
|
|
277
|
+
}
|
|
278
|
+
const aside = existing.replace(/vite\.config/, 'vite.config.blade');
|
|
279
|
+
if (!existsSync(join(dir, aside)))
|
|
280
|
+
renameSync(join(dir, existing), join(dir, aside));
|
|
281
|
+
writeFileSync(join(dir, file), t.viteConfig(o));
|
|
282
|
+
return [
|
|
283
|
+
{ kind: 'wrote', what: file, detail: `the renderer owns the frontend now` },
|
|
284
|
+
{
|
|
285
|
+
kind: 'manual',
|
|
286
|
+
what: aside,
|
|
287
|
+
detail: `your previous config, moved aside. It served the Blade asset pipeline ` +
|
|
288
|
+
`(laravel-vite-plugin, @vite in a layout). Delete it once nothing uses that; ` +
|
|
289
|
+
`to keep a Blade page, build it with \`vite build --config ${aside}\`.`,
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
return [manualPluginStep(o, existing)];
|
|
294
|
+
}
|
|
295
|
+
/** The edit to make by hand when a config the build cannot own is already there. */
|
|
296
|
+
function manualPluginStep(o, existing) {
|
|
234
297
|
const p = t.paths(o);
|
|
235
298
|
const shown = [
|
|
236
299
|
`sourceDir: '${p.sourceDir}'`,
|
|
237
300
|
`outDir: '${p.outDir}'`,
|
|
238
301
|
...(p.hotFile ? [`hotFile: '${p.hotFile}'`] : []),
|
|
239
302
|
].join(', ');
|
|
240
|
-
return
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
},
|
|
251
|
-
];
|
|
303
|
+
return {
|
|
304
|
+
kind: 'manual',
|
|
305
|
+
what: existing,
|
|
306
|
+
detail: `add the plugin — it must come before any react() layer:\n` +
|
|
307
|
+
` import { rscKit } from '@rsc-kit/core/vite'\n\n` +
|
|
308
|
+
` plugins: [\n` +
|
|
309
|
+
` rscKit({ ${shown} }),\n` +
|
|
310
|
+
` …whatever you already have\n` +
|
|
311
|
+
` ]`,
|
|
312
|
+
};
|
|
252
313
|
}
|
|
253
314
|
/** The route tree, only where there is not one already. */
|
|
254
315
|
function routes(o, dir) {
|
|
@@ -302,6 +363,38 @@ function routes(o, dir) {
|
|
|
302
363
|
* takes `string` again instead of the route union, so a link to a page that
|
|
303
364
|
* does not exist compiles and 404s in the browser.
|
|
304
365
|
*/
|
|
366
|
+
/** JSON with comments, made JSON: comments outside strings removed, strings kept whole. */
|
|
367
|
+
function withoutComments(source) {
|
|
368
|
+
let out = '';
|
|
369
|
+
let i = 0;
|
|
370
|
+
while (i < source.length) {
|
|
371
|
+
const c = source[i];
|
|
372
|
+
if (c === '"') {
|
|
373
|
+
const end = source.indexOf('"', i + 1);
|
|
374
|
+
let j = end;
|
|
375
|
+
// A quote escaped inside the string is not its end.
|
|
376
|
+
while (j !== -1 && source[j - 1] === '\\')
|
|
377
|
+
j = source.indexOf('"', j + 1);
|
|
378
|
+
const close = j === -1 ? source.length : j + 1;
|
|
379
|
+
out += source.slice(i, close);
|
|
380
|
+
i = close;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (c === '/' && source[i + 1] === '/') {
|
|
384
|
+
const nl = source.indexOf('\n', i);
|
|
385
|
+
i = nl === -1 ? source.length : nl;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (c === '/' && source[i + 1] === '*') {
|
|
389
|
+
const end = source.indexOf('*/', i + 2);
|
|
390
|
+
i = end === -1 ? source.length : end + 2;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
out += c;
|
|
394
|
+
i++;
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
305
398
|
function tsconfig(o, found, dir) {
|
|
306
399
|
const path = join(dir, 'tsconfig.json');
|
|
307
400
|
if (!found.hasTypeScript) {
|
|
@@ -313,10 +406,14 @@ function tsconfig(o, found, dir) {
|
|
|
313
406
|
// formatting someone chose — a worse trade than one line of output, for a
|
|
314
407
|
// file this does not own.
|
|
315
408
|
const current = readFileSync(path, 'utf-8');
|
|
316
|
-
// Comments are legal here and JSON.parse does not take them.
|
|
409
|
+
// Comments are legal here and JSON.parse does not take them. Stripped
|
|
410
|
+
// outside strings only: a glob like ".rsc-kit/**/*" holds "/*", and a
|
|
411
|
+
// regex that did not know about strings read it as a comment opening,
|
|
412
|
+
// ate the rest of the file, and reported the tsconfig this very tool
|
|
413
|
+
// wrote as missing the entry it wrote.
|
|
317
414
|
const include = (() => {
|
|
318
415
|
try {
|
|
319
|
-
const parsed = JSON.parse(current
|
|
416
|
+
const parsed = JSON.parse(withoutComments(current));
|
|
320
417
|
return Array.isArray(parsed.include) ? parsed.include : null;
|
|
321
418
|
}
|
|
322
419
|
catch {
|
|
@@ -356,8 +453,12 @@ function gitignore(o, dir) {
|
|
|
356
453
|
// machine at a dev server that is not running there.
|
|
357
454
|
const all = ['.output', p.outDir, ...(p.hotFile ? [p.hotFile] : [])];
|
|
358
455
|
const outputs = all.filter((path) => !all.some((other) => other !== path && path.startsWith(other + '/')));
|
|
456
|
+
// A backend's secret lives in .env, which is the file whose commit is
|
|
457
|
+
// noticed late. Only when there is one: a project without a backend has
|
|
458
|
+
// its own policy and this leaves it alone.
|
|
459
|
+
const secrets = o.backend && o.host !== 'laravel' ? ['.env', '.env.*', '!.env.example'] : [];
|
|
359
460
|
const current = existsSync(path) ? readFileSync(path, 'utf-8') : '';
|
|
360
|
-
const missing = [...generated, ...outputs].filter((line) => !current.split('\n').some((existing) => existing.trim() === line));
|
|
461
|
+
const missing = [...generated, ...outputs, ...secrets].filter((line) => !current.split('\n').some((existing) => existing.trim() === line));
|
|
361
462
|
if (missing.length === 0)
|
|
362
463
|
return [{ kind: 'skipped', what: '.gitignore', detail: 'already covers the generated files' }];
|
|
363
464
|
// The blank line separates this block from whatever was above it, so there
|
|
@@ -367,18 +468,50 @@ function gitignore(o, dir) {
|
|
|
367
468
|
return [{ kind: 'merged', what: '.gitignore', detail: `added ${missing.length} generated paths` }];
|
|
368
469
|
}
|
|
369
470
|
/** Everything, in the order a reader would want to hear about it. */
|
|
471
|
+
/**
|
|
472
|
+
* The backend's two lines, for a project that has one and is not Laravel.
|
|
473
|
+
*
|
|
474
|
+
* .env is written once and never rewritten: the secret in it is the one the
|
|
475
|
+
* backend was given, and regenerating it on a second run would answer every
|
|
476
|
+
* host call with 403 - which reads as the application refusing its own data.
|
|
477
|
+
* The wiring on the other side is printed, never written: a package main in
|
|
478
|
+
* someone's module is not a file to add blind.
|
|
479
|
+
*/
|
|
480
|
+
function backend(o, found, dir) {
|
|
481
|
+
if (!o.backend || o.host === 'laravel')
|
|
482
|
+
return [];
|
|
483
|
+
const steps = [];
|
|
484
|
+
const env = join(dir, '.env');
|
|
485
|
+
if (existsSync(env)) {
|
|
486
|
+
steps.push({ kind: 'skipped', what: '.env', detail: 'already exists; RSC_BACKEND and RSC_HOST_CALL_SECRET must be in it' });
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
writeFileSync(env, t.backendEnv(o.backend, randomBytes(32).toString('base64url')));
|
|
490
|
+
steps.push({ kind: 'wrote', what: '.env', detail: 'RSC_BACKEND and a generated RSC_HOST_CALL_SECRET' });
|
|
491
|
+
}
|
|
492
|
+
const example = join(dir, '.env.example');
|
|
493
|
+
if (!existsSync(example)) {
|
|
494
|
+
writeFileSync(example, t.backendEnvExample(o.backend));
|
|
495
|
+
steps.push({ kind: 'wrote', what: '.env.example' });
|
|
496
|
+
}
|
|
497
|
+
steps.push({ kind: 'manual', what: 'backend', detail: t.backendStep(found.go) });
|
|
498
|
+
return steps;
|
|
499
|
+
}
|
|
370
500
|
export function initialise(o, found, dir) {
|
|
371
501
|
const steps = [
|
|
372
502
|
...routes(o, dir),
|
|
373
503
|
...viteConfig(o, found, dir),
|
|
374
504
|
...tsconfig(o, found, dir),
|
|
375
505
|
...gitignore(o, dir),
|
|
506
|
+
...backend(o, found, dir),
|
|
376
507
|
...mergeDependencies(o, found),
|
|
377
508
|
...mergeScripts(o, found),
|
|
378
509
|
];
|
|
379
510
|
writeFileSync(join(dir, 'package.json'), JSON.stringify(found.packageJson, null, 2) + '\n');
|
|
380
511
|
return steps;
|
|
381
512
|
}
|
|
513
|
+
/** Where a Go server listens, unless told otherwise. */
|
|
514
|
+
const DEFAULT_BACKEND = 'http://127.0.0.1:8080';
|
|
382
515
|
const INIT_HELP = `
|
|
383
516
|
rsc-kit init — add RSC to the project in this directory
|
|
384
517
|
|
|
@@ -390,7 +523,9 @@ const INIT_HELP = `
|
|
|
390
523
|
--source-dir <dir> where app/ should live (detected, usually src)
|
|
391
524
|
--host=… bun | hono | elysia | node (detected from your deps)
|
|
392
525
|
laravel is detected from artisan, never asked
|
|
393
|
-
--backend=<url>
|
|
526
|
+
--backend=<url> a backend answering host calls - a Go server, say, at
|
|
527
|
+
http://127.0.0.1:8080 (assumed when go.mod is here);
|
|
528
|
+
laravel reads APP_URL instead
|
|
394
529
|
--compiler=… none | oxc | babel
|
|
395
530
|
--tailwind add Tailwind as well
|
|
396
531
|
-y, --yes accept what was detected, ask nothing
|
|
@@ -423,9 +558,13 @@ export async function runInit(args) {
|
|
|
423
558
|
stdout.write(` ${dim('server')} ${found.host}${flags.host ? '' : dim(' (detected)')}\n`);
|
|
424
559
|
stdout.write(` ${dim('source')} ${flags.sourceDir ?? found.sourceDir ?? 'src'}\n`);
|
|
425
560
|
stdout.write(` ${dim('react')} ${found.hasReact ? 'already here' : 'will be added'}\n`);
|
|
561
|
+
const backendUrl = flags.backend ?? (found.go ? DEFAULT_BACKEND : undefined);
|
|
426
562
|
if (found.host === 'laravel') {
|
|
427
563
|
stdout.write(` ${dim('backend')} ${flags.backend ?? 'http://localhost'}\n`);
|
|
428
564
|
}
|
|
565
|
+
else if (backendUrl) {
|
|
566
|
+
stdout.write(` ${dim('backend')} ${backendUrl}${flags.backend ? '' : dim(' (go.mod is here)')}\n`);
|
|
567
|
+
}
|
|
429
568
|
stdout.write('\n');
|
|
430
569
|
let compiler = flags.compiler ?? 'none';
|
|
431
570
|
let tailwind = flags.tailwind ?? found.hasTailwind;
|
|
@@ -448,11 +587,14 @@ export async function runInit(args) {
|
|
|
448
587
|
compiler,
|
|
449
588
|
tailwind,
|
|
450
589
|
lint: false,
|
|
590
|
+
// An existing project has its own schema library and env handling; init adds neither.
|
|
591
|
+
validation: 'none',
|
|
592
|
+
env: false,
|
|
451
593
|
sourceDir: flags.sourceDir ?? found.sourceDir ?? 'src',
|
|
452
594
|
install: false,
|
|
453
595
|
git: false,
|
|
454
596
|
core: flags.core ?? publishedCore(),
|
|
455
|
-
backend:
|
|
597
|
+
backend: backendUrl,
|
|
456
598
|
};
|
|
457
599
|
const steps = initialise(options, found, dir);
|
|
458
600
|
const mark = { wrote: cyan('+'), merged: cyan('~'), manual: bold('!'), skipped: dim('·') };
|
|
@@ -462,7 +604,7 @@ export async function runInit(args) {
|
|
|
462
604
|
}
|
|
463
605
|
const manual = steps.filter((s) => s.kind === 'manual');
|
|
464
606
|
if (manual.length > 0) {
|
|
465
|
-
stdout.write(`\n${bold('Then, by hand:')} the
|
|
607
|
+
stdout.write(`\n${bold('Then, by hand:')} the steps marked ! above.\n\n`);
|
|
466
608
|
return;
|
|
467
609
|
}
|
|
468
610
|
stdout.write(options.host === 'laravel'
|
package/dist/options.d.ts
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export type Host = 'bun' | 'node' | 'worker' | 'laravel';
|
|
10
10
|
export type Compiler = 'none' | 'oxc' | 'babel';
|
|
11
|
+
/**
|
|
12
|
+
* The validation library, asked once because everything schema-shaped hangs
|
|
13
|
+
* off it: typed environment variables, `action.input(schema)`, a form's
|
|
14
|
+
* schema, a page's `searchParams`. All three speak Standard Schema, which is
|
|
15
|
+
* what the engine reads, so none is special-cased anywhere else.
|
|
16
|
+
*/
|
|
17
|
+
export type Validation = 'none' | 'zod' | 'valibot' | 'arktype';
|
|
11
18
|
export interface Options {
|
|
12
19
|
dir: string;
|
|
13
20
|
name: string;
|
|
@@ -22,6 +29,13 @@ export interface Options {
|
|
|
22
29
|
* component's rpc() reaches the application it is rendering for.
|
|
23
30
|
*/
|
|
24
31
|
backend?: string;
|
|
32
|
+
validation: Validation;
|
|
33
|
+
/**
|
|
34
|
+
* Typed environment variables through @t3-oss/env-core, read once at
|
|
35
|
+
* startup and refused with the variable named when one is missing or wrong.
|
|
36
|
+
* Needs a validation library; asked only when one was chosen.
|
|
37
|
+
*/
|
|
38
|
+
env: boolean;
|
|
25
39
|
install: boolean;
|
|
26
40
|
git: boolean;
|
|
27
41
|
/** What to depend on for the engine. A path makes a local checkout testable. */
|
|
@@ -35,6 +49,12 @@ export interface Options {
|
|
|
35
49
|
* question whose only right answer is "the framework I am already in" is a
|
|
36
50
|
* question with a wrong answer available.
|
|
37
51
|
*/
|
|
52
|
+
export declare const VALIDATIONS: {
|
|
53
|
+
value: Validation;
|
|
54
|
+
label: string;
|
|
55
|
+
hint: string;
|
|
56
|
+
}[];
|
|
57
|
+
export declare function assertValidation(value: string): Validation;
|
|
38
58
|
export declare const HOSTS: {
|
|
39
59
|
value: Host;
|
|
40
60
|
label: string;
|
|
@@ -93,4 +113,4 @@ export declare function assertUsableName(name: string): void;
|
|
|
93
113
|
* generator nobody can run without reading it first.
|
|
94
114
|
*/
|
|
95
115
|
export declare function assertInsideCwd(dir: string, cwd: string): void;
|
|
96
|
-
export declare const HELP = "\n create-rsc-kit \u2014 scaffold an RSC app\n\n Usage\n create-rsc-kit <dir> [options]\n bun create rsc-kit@latest <dir> [options]\n\n Options\n --host=bun|node|worker where it runs \u2014 picks the Nitro preset\n --compiler=none|oxc|babel React Compiler (prompt offers oxc; babel by flag)\n --tailwind / --no-tailwind include Tailwind\n --lint / --no-lint include oxlint\n --source-dir <dir> where app/ lives (default: src)\n --init add to the project here, rather than scaffold\n --core=<spec> engine dependency, e.g. file:../rsc-kit/packages/core\n --no-install skip installing dependencies\n --no-git skip git init\n -y, --yes accept every default, ask nothing\n -h, --help this\n";
|
|
116
|
+
export declare const HELP = "\n create-rsc-kit \u2014 scaffold an RSC app\n\n Usage\n create-rsc-kit <dir> [options]\n bun create rsc-kit@latest <dir> [options]\n\n Options\n --host=bun|node|worker where it runs \u2014 picks the Nitro preset\n --compiler=none|oxc|babel React Compiler (prompt offers oxc; babel by flag)\n --tailwind / --no-tailwind include Tailwind\n --lint / --no-lint include oxlint\n --validation=zod|valibot|arktype|none\n the schema library; forms, actions and env use it\n --env / --no-env typed environment variables (@t3-oss/env-core)\n --source-dir <dir> where app/ lives (default: src)\n --backend=<url> a backend answering host calls \u2014 a Go server, say,\n at http://127.0.0.1:8080; writes .env with a secret\n --init add to the project here, rather than scaffold\n --core=<spec> engine dependency, e.g. file:../rsc-kit/packages/core\n --no-install skip installing dependencies\n --no-git skip git init\n -y, --yes accept every default, ask nothing\n -h, --help this\n";
|
package/dist/options.js
CHANGED
|
@@ -9,6 +9,17 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
* question whose only right answer is "the framework I am already in" is a
|
|
10
10
|
* question with a wrong answer available.
|
|
11
11
|
*/
|
|
12
|
+
export const VALIDATIONS = [
|
|
13
|
+
{ value: 'zod', label: 'Zod', hint: 'the one most examples are written in' },
|
|
14
|
+
{ value: 'valibot', label: 'Valibot', hint: 'the same idea, a tenth of the bytes in the browser' },
|
|
15
|
+
{ value: 'arktype', label: 'ArkType', hint: 'TypeScript syntax as a string, fastest at runtime' },
|
|
16
|
+
{ value: 'none', label: 'None', hint: 'add one later; forms and actions take any Standard Schema' },
|
|
17
|
+
];
|
|
18
|
+
export function assertValidation(value) {
|
|
19
|
+
if (value === 'none' || value === 'zod' || value === 'valibot' || value === 'arktype')
|
|
20
|
+
return value;
|
|
21
|
+
throw new Error(`--validation must be one of zod, valibot, arktype or none; got ${JSON.stringify(value)}`);
|
|
22
|
+
}
|
|
12
23
|
export const HOSTS = [
|
|
13
24
|
{ value: 'bun', label: 'Bun', hint: 'and compiles to a single binary' },
|
|
14
25
|
{ value: 'node', label: 'Node', hint: 'the default everywhere else' },
|
|
@@ -132,6 +143,12 @@ export function parseArgs(argv) {
|
|
|
132
143
|
out.host = assertHost(arg.slice(7));
|
|
133
144
|
else if (arg.startsWith('--compiler='))
|
|
134
145
|
out.compiler = arg.slice(11);
|
|
146
|
+
else if (arg.startsWith('--validation='))
|
|
147
|
+
out.validation = assertValidation(arg.slice(13));
|
|
148
|
+
else if (arg === '--env')
|
|
149
|
+
out.env = true;
|
|
150
|
+
else if (arg === '--no-env')
|
|
151
|
+
out.env = false;
|
|
135
152
|
else if (arg.startsWith('--core='))
|
|
136
153
|
out.core = arg.slice(7);
|
|
137
154
|
else if (arg.startsWith('--backend='))
|
|
@@ -183,7 +200,12 @@ export const HELP = `
|
|
|
183
200
|
--compiler=none|oxc|babel React Compiler (prompt offers oxc; babel by flag)
|
|
184
201
|
--tailwind / --no-tailwind include Tailwind
|
|
185
202
|
--lint / --no-lint include oxlint
|
|
203
|
+
--validation=zod|valibot|arktype|none
|
|
204
|
+
the schema library; forms, actions and env use it
|
|
205
|
+
--env / --no-env typed environment variables (@t3-oss/env-core)
|
|
186
206
|
--source-dir <dir> where app/ lives (default: src)
|
|
207
|
+
--backend=<url> a backend answering host calls — a Go server, say,
|
|
208
|
+
at http://127.0.0.1:8080; writes .env with a secret
|
|
187
209
|
--init add to the project here, rather than scaffold
|
|
188
210
|
--core=<spec> engine dependency, e.g. file:../rsc-kit/packages/core
|
|
189
211
|
--no-install skip installing dependencies
|
package/dist/templates.d.ts
CHANGED
|
@@ -23,15 +23,17 @@ export interface Paths {
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function paths(o: Options): Paths;
|
|
25
25
|
/**
|
|
26
|
-
* The config the RSC build runs
|
|
26
|
+
* The config the RSC build runs: the app's own, on every host.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* Laravel used to get a second file, vite.rsc.config.ts, on the reasoning
|
|
29
|
+
* that laravel-vite-plugin and rscKit() cannot share one - which is true:
|
|
30
|
+
* laravel-vite-plugin sets base, publicDir, outDir, the input list and the
|
|
31
|
+
* server origin, and so does this build. But once the renderer owns the
|
|
32
|
+
* frontend there is nothing left for laravel-vite-plugin to do: no @vite
|
|
33
|
+
* directive, no public/hot, no Blade asset pipeline. The right answer is
|
|
34
|
+
* one config with rscKit() in it, which is what the Laravel docs app runs.
|
|
33
35
|
*/
|
|
34
|
-
export declare const configFile: (
|
|
36
|
+
export declare const configFile: (_o: Options) => string;
|
|
35
37
|
/**
|
|
36
38
|
* The commands, under the names someone would guess.
|
|
37
39
|
*
|
|
@@ -112,3 +114,39 @@ export declare function agents(o: Options): string;
|
|
|
112
114
|
*/
|
|
113
115
|
export declare function mcp(): string;
|
|
114
116
|
export declare function readme(o: Options): string;
|
|
117
|
+
/**
|
|
118
|
+
* Typed environment variables, read once at startup.
|
|
119
|
+
*
|
|
120
|
+
* A missing or malformed variable is refused here, with its name, before
|
|
121
|
+
* anything runs - not as an `undefined` three calls later. Server variables
|
|
122
|
+
* never reach the browser; a client one has to carry the prefix, and is read
|
|
123
|
+
* from import.meta.env, which is what Vite exposes there.
|
|
124
|
+
*/
|
|
125
|
+
export declare function env(o: Options): string;
|
|
126
|
+
/**
|
|
127
|
+
* The process bootstrap, written when the app validates its environment.
|
|
128
|
+
*
|
|
129
|
+
* env.ts refuses at import - and where that import happens decides who sees
|
|
130
|
+
* the refusal. Imported by the first page that needs a variable, a missing
|
|
131
|
+
* one is reported by that page, to that visitor, after the server said it
|
|
132
|
+
* was up. Imported here, the generated entry evaluates this file before any
|
|
133
|
+
* page module and awaits register() before the first request, so a server
|
|
134
|
+
* with a bad environment does not start. Next names the file the same.
|
|
135
|
+
*/
|
|
136
|
+
export declare function instrumentation(_o: Options): string;
|
|
137
|
+
/**
|
|
138
|
+
* The two lines that wire a backend: where host calls go, and the secret it
|
|
139
|
+
* checks. Written when the project has a backend answering them - a Go
|
|
140
|
+
* server, or anything speaking the contract - so the renderer needs no
|
|
141
|
+
* configuring in development or in production. A Laravel app has both in
|
|
142
|
+
* its .env already, under APP_URL.
|
|
143
|
+
*/
|
|
144
|
+
export declare function backendEnv(backend: string, secret: string): string;
|
|
145
|
+
export declare function backendEnvExample(backend: string): string;
|
|
146
|
+
/**
|
|
147
|
+
* What the backend has to do, said once at the end. Go gets its own, because
|
|
148
|
+
* the project said it was Go; anything else gets the contract.
|
|
149
|
+
*/
|
|
150
|
+
export declare function backendStep(go: boolean): string;
|
|
151
|
+
/** The example beside it - the one file that is committed. */
|
|
152
|
+
export declare const envExample = "# Copy to .env and fill in. Server variables never reach the browser;\n# a browser-readable one starts with PUBLIC_. The schema is src/env.ts.\n#\n# Not NODE_ENV. Vite sets it - development under `vite`, production under\n# `vite build` - and a value written here overrides that: NODE_ENV=development\n# in .env makes a production build emit React's development JSX runtime,\n# which the production server does not have, and every page fails to render.\n# DATABASE_URL=\n# SESSION_SECRET=\n# PUBLIC_SITE_URL=\n";
|
package/dist/templates.js
CHANGED
|
@@ -29,15 +29,17 @@ export function paths(o) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
* The config the RSC build runs
|
|
32
|
+
* The config the RSC build runs: the app's own, on every host.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* Laravel used to get a second file, vite.rsc.config.ts, on the reasoning
|
|
35
|
+
* that laravel-vite-plugin and rscKit() cannot share one - which is true:
|
|
36
|
+
* laravel-vite-plugin sets base, publicDir, outDir, the input list and the
|
|
37
|
+
* server origin, and so does this build. But once the renderer owns the
|
|
38
|
+
* frontend there is nothing left for laravel-vite-plugin to do: no @vite
|
|
39
|
+
* directive, no public/hot, no Blade asset pipeline. The right answer is
|
|
40
|
+
* one config with rscKit() in it, which is what the Laravel docs app runs.
|
|
39
41
|
*/
|
|
40
|
-
export const configFile = (
|
|
42
|
+
export const configFile = (_o) => 'vite.config.ts';
|
|
41
43
|
/**
|
|
42
44
|
* The commands, under the names someone would guess.
|
|
43
45
|
*
|
|
@@ -48,14 +50,13 @@ export const configFile = (o) => o.host === 'laravel' ? 'vite.rsc.config.ts' : '
|
|
|
48
50
|
*/
|
|
49
51
|
export function scripts(o) {
|
|
50
52
|
if (o.host === 'laravel') {
|
|
51
|
-
const config = `--config ${configFile(o)}`;
|
|
52
53
|
const actions = 'php artisan rsc:action-manifest';
|
|
53
54
|
return {
|
|
54
55
|
// The ordinary names. A Laravel application already has `dev` and
|
|
55
56
|
// `build`, and init combines rather than replaces — the stock ones run
|
|
56
57
|
// the asset pipeline, and both pipelines belong to `npm run dev`.
|
|
57
|
-
dev: `${actions} && vite
|
|
58
|
-
build: `${actions} && vite build
|
|
58
|
+
dev: `${actions} && vite`,
|
|
59
|
+
build: `${actions} && vite build`,
|
|
59
60
|
// What the build wrote. There is no server file to start any more.
|
|
60
61
|
start: 'bun .output/server/index.mjs',
|
|
61
62
|
};
|
|
@@ -154,6 +155,14 @@ export function packageJson(o) {
|
|
|
154
155
|
}
|
|
155
156
|
if (o.lint)
|
|
156
157
|
dev['oxlint'] = '^1.81.0';
|
|
158
|
+
if (o.validation === 'zod')
|
|
159
|
+
deps['zod'] = '^4.0.0';
|
|
160
|
+
if (o.validation === 'valibot')
|
|
161
|
+
deps['valibot'] = '^1.0.0';
|
|
162
|
+
if (o.validation === 'arktype')
|
|
163
|
+
deps['arktype'] = '^2.1.0';
|
|
164
|
+
if (o.env)
|
|
165
|
+
deps['@t3-oss/env-core'] = '^0.13.0';
|
|
157
166
|
return (JSON.stringify({
|
|
158
167
|
name: o.name,
|
|
159
168
|
type: 'module',
|
|
@@ -587,6 +596,16 @@ at build time. That is usually correct — just know that it is the trade.
|
|
|
587
596
|
\`await connection()\` says "render this per visitor" deliberately, when
|
|
588
597
|
nothing else in the page happens to say it.
|
|
589
598
|
|
|
599
|
+
## Startup
|
|
600
|
+
|
|
601
|
+
\`${o.sourceDir}/instrumentation.ts\` runs once before anything else — at
|
|
602
|
+
startup on a server, at the first request on a Worker — and the entry imports
|
|
603
|
+
it before any page. ${o.env
|
|
604
|
+
? "It imports \`./env\`, so a missing or malformed variable stops the server from starting rather than reaching a visitor. "
|
|
605
|
+
: ''}Put once-per-process setup there (\`register()\` may be async, and the first
|
|
606
|
+
render waits for it). Do not import a bootstrap module from pages to get the
|
|
607
|
+
same effect; it depends on nobody forgetting.
|
|
608
|
+
|
|
590
609
|
## Data
|
|
591
610
|
|
|
592
611
|
Fetch in a server component and await it. There is no loader and no
|
|
@@ -745,3 +764,144 @@ the ambient declarations. Rewritten every build, and gitignored.
|
|
|
745
764
|
Docs: https://docs.rsc-kit.dev
|
|
746
765
|
`;
|
|
747
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Typed environment variables, read once at startup.
|
|
769
|
+
*
|
|
770
|
+
* A missing or malformed variable is refused here, with its name, before
|
|
771
|
+
* anything runs - not as an `undefined` three calls later. Server variables
|
|
772
|
+
* never reach the browser; a client one has to carry the prefix, and is read
|
|
773
|
+
* from import.meta.env, which is what Vite exposes there.
|
|
774
|
+
*/
|
|
775
|
+
export function env(o) {
|
|
776
|
+
const lib = {
|
|
777
|
+
zod: {
|
|
778
|
+
imp: "import * as z from 'zod'",
|
|
779
|
+
url: 'z.url()',
|
|
780
|
+
str: 'z.string().min(1)',
|
|
781
|
+
opt: "z.enum(['development', 'production', 'test']).default('development')",
|
|
782
|
+
},
|
|
783
|
+
valibot: {
|
|
784
|
+
imp: "import * as v from 'valibot'",
|
|
785
|
+
url: 'v.pipe(v.string(), v.url())',
|
|
786
|
+
str: 'v.pipe(v.string(), v.minLength(1))',
|
|
787
|
+
opt: "v.optional(v.picklist(['development', 'production', 'test']), 'development')",
|
|
788
|
+
},
|
|
789
|
+
arktype: {
|
|
790
|
+
imp: "import { type } from 'arktype'",
|
|
791
|
+
url: "type('string.url')",
|
|
792
|
+
str: "type('string > 0')",
|
|
793
|
+
opt: "type(\"'development' | 'production' | 'test' | undefined\")",
|
|
794
|
+
},
|
|
795
|
+
}[o.validation === 'none' ? 'zod' : o.validation];
|
|
796
|
+
return `${lib.imp}
|
|
797
|
+
import { createEnv } from '@t3-oss/env-core'
|
|
798
|
+
|
|
799
|
+
// Read once, here, and refused with the variable named when one is missing
|
|
800
|
+
// or wrong - before anything runs, not as an undefined three calls later.
|
|
801
|
+
//
|
|
802
|
+
// Server variables stay on the server. A variable the browser may read has
|
|
803
|
+
// to start with PUBLIC_, and is read from import.meta.env, which is what Vite
|
|
804
|
+
// exposes there. Add a variable: one line in the schema, and every reader is
|
|
805
|
+
// typed.
|
|
806
|
+
export const env = createEnv({
|
|
807
|
+
server: {
|
|
808
|
+
NODE_ENV: ${lib.opt},
|
|
809
|
+
// DATABASE_URL: ${lib.url},
|
|
810
|
+
// SESSION_SECRET: ${lib.str},
|
|
811
|
+
},
|
|
812
|
+
clientPrefix: 'PUBLIC_',
|
|
813
|
+
client: {
|
|
814
|
+
// PUBLIC_SITE_URL: ${lib.url},
|
|
815
|
+
},
|
|
816
|
+
runtimeEnv: { ...process.env, ...import.meta.env },
|
|
817
|
+
emptyStringAsUndefined: true,
|
|
818
|
+
// A build machine without the production variables: SKIP_ENV_VALIDATION=1
|
|
819
|
+
// builds anyway, and the server that runs the build validates at startup.
|
|
820
|
+
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
|
821
|
+
})
|
|
822
|
+
`;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* The process bootstrap, written when the app validates its environment.
|
|
826
|
+
*
|
|
827
|
+
* env.ts refuses at import - and where that import happens decides who sees
|
|
828
|
+
* the refusal. Imported by the first page that needs a variable, a missing
|
|
829
|
+
* one is reported by that page, to that visitor, after the server said it
|
|
830
|
+
* was up. Imported here, the generated entry evaluates this file before any
|
|
831
|
+
* page module and awaits register() before the first request, so a server
|
|
832
|
+
* with a bad environment does not start. Next names the file the same.
|
|
833
|
+
*/
|
|
834
|
+
export function instrumentation(_o) {
|
|
835
|
+
return `// Runs once, before anything else: on a server at startup, on a Worker
|
|
836
|
+
// when its first request arrives. The entry imports this file first, so a
|
|
837
|
+
// package configured here is configured before any page module evaluates.
|
|
838
|
+
//
|
|
839
|
+
// env.ts validates on import. Importing it here means a missing variable
|
|
840
|
+
// stops the server from starting rather than reaching a visitor as a page
|
|
841
|
+
// that fails three calls later.
|
|
842
|
+
import './env'
|
|
843
|
+
|
|
844
|
+
// Anything asynchronous the app needs before its first request - warming a
|
|
845
|
+
// connection, checking a migration - goes here. The first render waits for
|
|
846
|
+
// it. Read environment inside this function, not at the top of the module,
|
|
847
|
+
// if the app deploys to a Worker: a binding is only readable once a request
|
|
848
|
+
// has arrived.
|
|
849
|
+
export async function register() {}
|
|
850
|
+
`;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* The two lines that wire a backend: where host calls go, and the secret it
|
|
854
|
+
* checks. Written when the project has a backend answering them - a Go
|
|
855
|
+
* server, or anything speaking the contract - so the renderer needs no
|
|
856
|
+
* configuring in development or in production. A Laravel app has both in
|
|
857
|
+
* its .env already, under APP_URL.
|
|
858
|
+
*/
|
|
859
|
+
export function backendEnv(backend, secret) {
|
|
860
|
+
return `# Where rpc() goes: a backend answering POST /__rsc/host-call. The renderer
|
|
861
|
+
# reads both of these, in development (vite) and in production (the built
|
|
862
|
+
# server). The backend checks the secret on every call; keep it out of git.
|
|
863
|
+
RSC_BACKEND=${backend}
|
|
864
|
+
RSC_HOST_CALL_SECRET=${secret}
|
|
865
|
+
`;
|
|
866
|
+
}
|
|
867
|
+
export function backendEnvExample(backend) {
|
|
868
|
+
return `# Copy to .env. The backend answering rpc(), and the secret it checks -
|
|
869
|
+
# generate one: openssl rand -base64 32
|
|
870
|
+
RSC_BACKEND=${backend}
|
|
871
|
+
RSC_HOST_CALL_SECRET=
|
|
872
|
+
`;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* What the backend has to do, said once at the end. Go gets its own, because
|
|
876
|
+
* the project said it was Go; anything else gets the contract.
|
|
877
|
+
*/
|
|
878
|
+
export function backendStep(go) {
|
|
879
|
+
if (go) {
|
|
880
|
+
return [
|
|
881
|
+
'answer host calls from your Go server - go get github.com/rsc-kit/go, then:',
|
|
882
|
+
'',
|
|
883
|
+
' reg := rsckit.NewRegistry()',
|
|
884
|
+
' reg.Register("Orders.recent", func(ctx context.Context, args rsckit.Args) (any, error) { … })',
|
|
885
|
+
' callback, _ := rsckit.NewCallbackHandler(reg, os.Getenv("RSC_HOST_CALL_SECRET"))',
|
|
886
|
+
' mux.Handle("POST /__rsc/host-call", callback)',
|
|
887
|
+
'',
|
|
888
|
+
' A page reads it with await rpc(\'Orders.recent\'). https://rsc-kit.dev/hosts/go',
|
|
889
|
+
].join('\n');
|
|
890
|
+
}
|
|
891
|
+
return [
|
|
892
|
+
'answer POST /__rsc/host-call from your backend, checking X-Rsc-Host-Secret',
|
|
893
|
+
' against RSC_HOST_CALL_SECRET in .env. The contract: https://rsc-kit.dev/hosts/your-own-backend',
|
|
894
|
+
].join('\n');
|
|
895
|
+
}
|
|
896
|
+
/** The example beside it - the one file that is committed. */
|
|
897
|
+
export const envExample = `# Copy to .env and fill in. Server variables never reach the browser;
|
|
898
|
+
# a browser-readable one starts with PUBLIC_. The schema is src/env.ts.
|
|
899
|
+
#
|
|
900
|
+
# Not NODE_ENV. Vite sets it - development under \`vite\`, production under
|
|
901
|
+
# \`vite build\` - and a value written here overrides that: NODE_ENV=development
|
|
902
|
+
# in .env makes a production build emit React's development JSX runtime,
|
|
903
|
+
# which the production server does not have, and every page fails to render.
|
|
904
|
+
# DATABASE_URL=
|
|
905
|
+
# SESSION_SECRET=
|
|
906
|
+
# PUBLIC_SITE_URL=
|
|
907
|
+
`;
|