create-rsc-kit 0.7.1 → 0.8.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/README.md +2 -2
- package/dist/index.js +5 -80
- package/dist/init.js +59 -11
- package/dist/stale.d.ts +19 -0
- package/dist/stale.js +81 -0
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +89 -26
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Scaffold a React Server Components app that builds and runs before you edit it.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
bun create rsc-kit my-app
|
|
6
|
+
bun create rsc-kit@latest my-app
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Asks which server (Bun, Hono, Elysia or `node:http`), whether you want the React
|
|
@@ -11,7 +11,7 @@ Compiler, and whether to include Tailwind. Every answer has a flag, so it runs
|
|
|
11
11
|
unattended too:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
bun create rsc-kit my-app --host=
|
|
14
|
+
bun create rsc-kit@latest my-app --host=bun --compiler=oxc --tailwind
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
The point is not the typing it saves. Several things in this setup fail by
|
package/dist/index.js
CHANGED
|
@@ -5,38 +5,18 @@
|
|
|
5
5
|
// several things in it that are invisible when wrong: NODE_ENV on the build,
|
|
6
6
|
// @source for Tailwind, the ambient declaration for the generated engine
|
|
7
7
|
// bundle. Each of those fails by producing a page that looks nearly right.
|
|
8
|
-
import { existsSync, mkdirSync,
|
|
8
|
+
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
12
|
import { argv, exit, stdout } from 'node:process';
|
|
13
13
|
import { DEFAULT_COMPILER, HELP, HOSTS, publishedCore, assertInsideCwd, assertUsableName, defaultCore, parseArgs, } from './options.js';
|
|
14
14
|
import { Prompter, bold, cyan, dim } from './prompt.js';
|
|
15
|
+
import { checkForNewer, notifyIfStale, selfVersion } from './stale.js';
|
|
15
16
|
import * as t from './templates.js';
|
|
16
17
|
// Same treatment as a refusal from write(): a bad flag is a decision this tool
|
|
17
18
|
// made, and it happens before the try/catch below because the flags are what
|
|
18
19
|
// everything else is built from.
|
|
19
|
-
/**
|
|
20
|
-
* This package's own version, printed so a stale one is visible.
|
|
21
|
-
*
|
|
22
|
-
* `bun create rsc-kit` is `bunx create-rsc-kit`, and bunx caches its
|
|
23
|
-
* resolution of `latest` under that key — so a machine that ran it once keeps
|
|
24
|
-
* scaffolding from whatever it downloaded then, however many releases ago.
|
|
25
|
-
* There is nothing this package can do about that, but there is something it
|
|
26
|
-
* can do about the symptom, which was a prompt offering hosts that had been
|
|
27
|
-
* removed two releases earlier and no way to tell why.
|
|
28
|
-
*
|
|
29
|
-
* Read rather than baked in: the version is only written at publish time.
|
|
30
|
-
*/
|
|
31
|
-
function selfVersion() {
|
|
32
|
-
try {
|
|
33
|
-
const path = join(dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
34
|
-
return `v${JSON.parse(readFileSync(path, 'utf-8')).version}`;
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return '';
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
20
|
const flags = (() => {
|
|
41
21
|
try {
|
|
42
22
|
return parseArgs(argv.slice(2));
|
|
@@ -54,7 +34,7 @@ const unattended = flags.yes === true || flags.host !== undefined;
|
|
|
54
34
|
// Resolved once: it walks up from this file looking for the sibling package.
|
|
55
35
|
const core = flags.core ?? defaultCore(dirname(fileURLToPath(import.meta.url)));
|
|
56
36
|
// Started before the first question and never awaited here — see notifyIfStale.
|
|
57
|
-
const staleCheck = checkForNewer();
|
|
37
|
+
const staleCheck = checkForNewer('create-rsc-kit', selfVersion(import.meta.url));
|
|
58
38
|
const options = await collect();
|
|
59
39
|
try {
|
|
60
40
|
write(options);
|
|
@@ -81,7 +61,7 @@ if (options.install) {
|
|
|
81
61
|
}
|
|
82
62
|
}
|
|
83
63
|
report(options);
|
|
84
|
-
await notifyIfStale();
|
|
64
|
+
await notifyIfStale(staleCheck, 'bun create rsc-kit@latest');
|
|
85
65
|
// ── ─────────────────────────────────────────────────────────────────────────
|
|
86
66
|
/** Declared, not assigned: collect() runs at module top level, above this. */
|
|
87
67
|
function basename(path) {
|
|
@@ -112,7 +92,7 @@ async function collect() {
|
|
|
112
92
|
HELP);
|
|
113
93
|
exit(1);
|
|
114
94
|
}
|
|
115
|
-
stdout.write(`\n${bold('Create an RSC app')} ${dim(selfVersion())}\n\n`);
|
|
95
|
+
stdout.write(`\n${bold('Create an RSC app')} ${dim(`v${selfVersion(import.meta.url)}`)}\n\n`);
|
|
116
96
|
const p = new Prompter();
|
|
117
97
|
try {
|
|
118
98
|
const dir = flags.dir ?? (await p.text('Directory', 'my-app'));
|
|
@@ -211,61 +191,6 @@ function report(o) {
|
|
|
211
191
|
stdout.write(` ${cyan(step)}\n`);
|
|
212
192
|
stdout.write(`\n${dim('Pages live in src/app. Where it deploys is the Nitro preset in vite.config.ts.')}\n\n`);
|
|
213
193
|
}
|
|
214
|
-
/**
|
|
215
|
-
* Ask the registry what `latest` is, so a stale copy can say so.
|
|
216
|
-
*
|
|
217
|
-
* Printing the version alone does not help: nobody knows 0.5.0 is two behind
|
|
218
|
-
* unless something tells them.
|
|
219
|
-
*
|
|
220
|
-
* `bun create rsc-kit` is `bunx create-rsc-kit`, and bunx reuses what it
|
|
221
|
-
* downloaded the first time — a machine that ran this once keeps scaffolding
|
|
222
|
-
* from that copy however many releases later. Writing the tag out is what
|
|
223
|
-
* makes it revalidate. Measured against a 0.6.0 planted in the cache:
|
|
224
|
-
*
|
|
225
|
-
* bun create rsc-kit stale
|
|
226
|
-
* bunx create-rsc-kit stale
|
|
227
|
-
* bun create rsc-kit@latest fresh
|
|
228
|
-
* bunx create-rsc-kit@latest fresh
|
|
229
|
-
* npx create-rsc-kit@latest fresh
|
|
230
|
-
*
|
|
231
|
-
* So the escape is one word, and this is what tells someone to use it — the
|
|
232
|
-
* form without it is the one every set of install instructions writes.
|
|
233
|
-
*
|
|
234
|
-
* create-next-app does this with the `update-check` package. Not here: this
|
|
235
|
-
* package is installed by bunx before it can ask anything, so a dependency is
|
|
236
|
-
* latency every user waits through — the same reason the prompts are written
|
|
237
|
-
* against readline. It is one request.
|
|
238
|
-
*
|
|
239
|
-
* Started before the first prompt and read after the last, so it costs nothing
|
|
240
|
-
* — a person answering questions is slower than a registry. Failure is
|
|
241
|
-
* silence: offline, behind a proxy, or a registry that is down are all just no
|
|
242
|
-
* notice rather than a scaffolder that will not run.
|
|
243
|
-
*/
|
|
244
|
-
function checkForNewer() {
|
|
245
|
-
// Unknown is not stale. selfVersion() answers '' when it cannot read the
|
|
246
|
-
// manifest, and comparing against that would report every run as behind.
|
|
247
|
-
const mine = selfVersion().slice(1);
|
|
248
|
-
if (!mine)
|
|
249
|
-
return Promise.resolve(null);
|
|
250
|
-
return (async () => {
|
|
251
|
-
const response = await fetch('https://registry.npmjs.org/create-rsc-kit/latest', {
|
|
252
|
-
signal: AbortSignal.timeout(3000),
|
|
253
|
-
headers: { accept: 'application/json' },
|
|
254
|
-
});
|
|
255
|
-
if (!response.ok)
|
|
256
|
-
return null;
|
|
257
|
-
const latest = (await response.json()).version;
|
|
258
|
-
return latest && latest !== mine ? latest : null;
|
|
259
|
-
})().catch(() => null);
|
|
260
|
-
}
|
|
261
|
-
async function notifyIfStale() {
|
|
262
|
-
const latest = await staleCheck;
|
|
263
|
-
if (!latest)
|
|
264
|
-
return;
|
|
265
|
-
stdout.write(`${dim('A newer create-rsc-kit is out:')} ${bold(`v${latest}`)}${dim(`, and this is ${selfVersion()}.`)}\n` +
|
|
266
|
-
`${dim('bunx reuses the copy it downloaded first. Name the tag and it fetches:')}\n\n` +
|
|
267
|
-
` ${cyan('bun create rsc-kit@latest')}\n\n`);
|
|
268
|
-
}
|
|
269
194
|
/** A path the user can paste, when it is under where they are. */
|
|
270
195
|
function relativeish(dir) {
|
|
271
196
|
const cwd = process.cwd();
|
package/dist/init.js
CHANGED
|
@@ -277,25 +277,72 @@ function routes(o, dir) {
|
|
|
277
277
|
return steps;
|
|
278
278
|
}
|
|
279
279
|
/**
|
|
280
|
-
* A tsconfig, for a project that has none.
|
|
280
|
+
* A tsconfig, for a project that has none — and one line for a project that has.
|
|
281
281
|
*
|
|
282
282
|
* Laravel ships without one, and the route tree is .tsx — so with nothing here
|
|
283
283
|
* an editor reports an error on every generated file and the `typecheck`
|
|
284
284
|
* script has no configuration to read. Written only when absent, like
|
|
285
285
|
* everything else.
|
|
286
|
+
*
|
|
287
|
+
* Where one exists it is not replaced, but `include` still has to name
|
|
288
|
+
* `.rsc-kit`, because that is where the build writes its ambient declarations
|
|
289
|
+
* and TypeScript will not find them otherwise. Not a style preference: a
|
|
290
|
+
* directory whose name begins with a dot is outside the default `**\/*`, so a
|
|
291
|
+
* project with no `include` at all misses them exactly like one that lists
|
|
292
|
+
* only `src`. Measured both ways.
|
|
293
|
+
*
|
|
294
|
+
* Missing it is invisible — every file is written, the build passes, and Link
|
|
295
|
+
* takes `string` again instead of the route union, so a link to a page that
|
|
296
|
+
* does not exist compiles and 404s in the browser.
|
|
286
297
|
*/
|
|
287
298
|
function tsconfig(o, found, dir) {
|
|
288
|
-
const
|
|
289
|
-
if (found.hasTypeScript) {
|
|
290
|
-
|
|
299
|
+
const path = join(dir, 'tsconfig.json');
|
|
300
|
+
if (!found.hasTypeScript) {
|
|
301
|
+
writeFileSync(path, t.tsconfig(o));
|
|
302
|
+
return [{ kind: 'wrote', what: 'tsconfig.json' }];
|
|
303
|
+
}
|
|
304
|
+
// Reported, never rewritten. Adding the entry means parsing and reprinting
|
|
305
|
+
// the file, which loses the comments a tsconfig is allowed to have and the
|
|
306
|
+
// formatting someone chose — a worse trade than one line of output, for a
|
|
307
|
+
// file this does not own.
|
|
308
|
+
const current = readFileSync(path, 'utf-8');
|
|
309
|
+
// Comments are legal here and JSON.parse does not take them.
|
|
310
|
+
const include = (() => {
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(current.replace(/\/\*[\s\S]*?\*\/|(^|\s)\/\/.*$/gm, '$1'));
|
|
313
|
+
return Array.isArray(parsed.include) ? parsed.include : null;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
if (include?.some((entry) => typeof entry === 'string' && entry.includes('.rsc-kit'))) {
|
|
320
|
+
return [{ kind: 'skipped', what: 'tsconfig.json', detail: 'already includes .rsc-kit' }];
|
|
291
321
|
}
|
|
292
|
-
|
|
293
|
-
|
|
322
|
+
// An absent `include` is not an empty one — TypeScript's default covers the
|
|
323
|
+
// project — but the default is `**\/*`, and a directory whose name begins
|
|
324
|
+
// with a dot is outside it. So both cases need the entry, and a project with
|
|
325
|
+
// no `include` needs `**\/*` written alongside it or it loses everything
|
|
326
|
+
// else. Measured both ways.
|
|
327
|
+
return [
|
|
328
|
+
{
|
|
329
|
+
kind: 'manual',
|
|
330
|
+
what: 'tsconfig.json',
|
|
331
|
+
detail: include
|
|
332
|
+
? `add "${TYPES_GLOB}" to "include", or typed routes fall back to string`
|
|
333
|
+
: `add "include": ["**/*", "${TYPES_GLOB}"], or typed routes fall back to string`,
|
|
334
|
+
},
|
|
335
|
+
];
|
|
294
336
|
}
|
|
337
|
+
/** Where the build writes its ambient declarations, as a tsconfig include. */
|
|
338
|
+
const TYPES_GLOB = '.rsc-kit/**/*';
|
|
295
339
|
/** Ignore the files the build rewrites into the source dir on every run. */
|
|
296
340
|
function gitignore(o, dir) {
|
|
297
341
|
const path = join(dir, '.gitignore');
|
|
298
|
-
|
|
342
|
+
// The declarations moved out of the source directory into .rsc-kit, so this
|
|
343
|
+
// is one line where it used to be four. The stub stays put — the app imports
|
|
344
|
+
// it by relative path.
|
|
345
|
+
const generated = ['.rsc-kit/', `${o.sourceDir}/server-actions.generated.ts`];
|
|
299
346
|
const p = t.paths(o);
|
|
300
347
|
// Everything the build writes: the bundles, Nitro's output, and the hot
|
|
301
348
|
// file. The hot file is the worst of them to commit — it points every other
|
|
@@ -306,9 +353,10 @@ function gitignore(o, dir) {
|
|
|
306
353
|
const missing = [...generated, ...outputs].filter((line) => !current.split('\n').some((existing) => existing.trim() === line));
|
|
307
354
|
if (missing.length === 0)
|
|
308
355
|
return [{ kind: 'skipped', what: '.gitignore', detail: 'already covers the generated files' }];
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
356
|
+
// The blank line separates this block from whatever was above it, so there
|
|
357
|
+
// is nothing to separate from when the file is new.
|
|
358
|
+
const head = current === '' ? '' : current.endsWith('\n') ? current + '\n' : current + '\n\n';
|
|
359
|
+
writeFileSync(path, head + '# The RSC build: rewritten every run, and what it builds.\n' + missing.join('\n') + '\n');
|
|
312
360
|
return [{ kind: 'merged', what: '.gitignore', detail: `added ${missing.length} generated paths` }];
|
|
313
361
|
}
|
|
314
362
|
/** Everything, in the order a reader would want to hear about it. */
|
|
@@ -358,7 +406,7 @@ export async function runInit(args) {
|
|
|
358
406
|
if (!existsSync(join(dir, 'package.json'))) {
|
|
359
407
|
stdout.write(`\n${bold('No package.json here.')}\n` +
|
|
360
408
|
` init adds RSC to a project that already exists. To start a new one:\n` +
|
|
361
|
-
` ${cyan('bun create rsc-kit my-app')}\n\n`);
|
|
409
|
+
` ${cyan('bun create rsc-kit@latest my-app')}\n\n`);
|
|
362
410
|
exit(1);
|
|
363
411
|
}
|
|
364
412
|
const flags = parseArgs(args);
|
package/dist/stale.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The calling package's version, read from the manifest above it.
|
|
3
|
+
*
|
|
4
|
+
* Read rather than baked in, because the version is only written at publish
|
|
5
|
+
* time. The caller passes its own `import.meta.url` so this answers for
|
|
6
|
+
* whichever package asked — `create-rsc-kit` and `rsc-kit` both do.
|
|
7
|
+
*/
|
|
8
|
+
export declare function selfVersion(moduleUrl: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Ask the registry what `latest` is. Null when there is nothing to say.
|
|
11
|
+
*
|
|
12
|
+
* Start it before the first prompt and read it after the last: a person
|
|
13
|
+
* answering questions is slower than a registry, so it costs nothing. Failure
|
|
14
|
+
* is silence — offline, behind a proxy, or a registry that is down are all no
|
|
15
|
+
* notice rather than a tool that hangs or will not run.
|
|
16
|
+
*/
|
|
17
|
+
export declare function checkForNewer(pkg: string, mine: string): Promise<string | null>;
|
|
18
|
+
/** Say it, and say the way out — which is the tag, not the version. */
|
|
19
|
+
export declare function notifyIfStale(check: Promise<string | null>, command: string): Promise<void>;
|
package/dist/stale.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Telling someone their scaffolder is old, because nothing else will.
|
|
2
|
+
//
|
|
3
|
+
// `bun create rsc-kit` is `bunx create-rsc-kit`, and `bunx rsc-kit init` is the
|
|
4
|
+
// same shape — bunx reuses the copy it downloaded the first time. A machine
|
|
5
|
+
// that ran either once keeps using that version however many releases later,
|
|
6
|
+
// which here meant scaffolding from one two releases old: offering hosts that
|
|
7
|
+
// had been removed, and writing a config the current plugin refuses.
|
|
8
|
+
//
|
|
9
|
+
// Naming the tag is what makes bunx revalidate. Measured against an older
|
|
10
|
+
// build planted in the cache slot:
|
|
11
|
+
//
|
|
12
|
+
// bun create rsc-kit stale
|
|
13
|
+
// bunx create-rsc-kit stale
|
|
14
|
+
// bunx rsc-kit init stale
|
|
15
|
+
// bun create rsc-kit@latest fresh
|
|
16
|
+
// bunx create-rsc-kit@latest fresh
|
|
17
|
+
// npx create-rsc-kit@latest fresh
|
|
18
|
+
//
|
|
19
|
+
// So every instruction this project publishes names it. That is the fix, and
|
|
20
|
+
// this is for the copies already cached, which no wording can reach. Printing
|
|
21
|
+
// a version alone would not do it either — nobody knows v0.5.0 is two behind
|
|
22
|
+
// unless something says so.
|
|
23
|
+
//
|
|
24
|
+
// create-next-app does this with the `update-check` package. Not here: both
|
|
25
|
+
// packages are installed by bunx before they can do anything, so a dependency
|
|
26
|
+
// is latency every user waits through, which is the same reason the prompts
|
|
27
|
+
// are written against readline. It is one request to one url.
|
|
28
|
+
import { readFileSync } from 'node:fs';
|
|
29
|
+
import { dirname, join } from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { stdout } from 'node:process';
|
|
32
|
+
import { bold, cyan, dim } from './prompt.js';
|
|
33
|
+
/**
|
|
34
|
+
* The calling package's version, read from the manifest above it.
|
|
35
|
+
*
|
|
36
|
+
* Read rather than baked in, because the version is only written at publish
|
|
37
|
+
* time. The caller passes its own `import.meta.url` so this answers for
|
|
38
|
+
* whichever package asked — `create-rsc-kit` and `rsc-kit` both do.
|
|
39
|
+
*/
|
|
40
|
+
export function selfVersion(moduleUrl) {
|
|
41
|
+
try {
|
|
42
|
+
const path = join(dirname(fileURLToPath(moduleUrl)), '../package.json');
|
|
43
|
+
return JSON.parse(readFileSync(path, 'utf-8')).version;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Ask the registry what `latest` is. Null when there is nothing to say.
|
|
51
|
+
*
|
|
52
|
+
* Start it before the first prompt and read it after the last: a person
|
|
53
|
+
* answering questions is slower than a registry, so it costs nothing. Failure
|
|
54
|
+
* is silence — offline, behind a proxy, or a registry that is down are all no
|
|
55
|
+
* notice rather than a tool that hangs or will not run.
|
|
56
|
+
*/
|
|
57
|
+
export function checkForNewer(pkg, mine) {
|
|
58
|
+
// Unknown is not stale. An unreadable manifest answers '', and comparing
|
|
59
|
+
// against that would report every run as behind.
|
|
60
|
+
if (!mine)
|
|
61
|
+
return Promise.resolve(null);
|
|
62
|
+
return (async () => {
|
|
63
|
+
const response = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
|
|
64
|
+
signal: AbortSignal.timeout(3000),
|
|
65
|
+
headers: { accept: 'application/json' },
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok)
|
|
68
|
+
return null;
|
|
69
|
+
const latest = (await response.json()).version;
|
|
70
|
+
return latest && latest !== mine ? latest : null;
|
|
71
|
+
})().catch(() => null);
|
|
72
|
+
}
|
|
73
|
+
/** Say it, and say the way out — which is the tag, not the version. */
|
|
74
|
+
export async function notifyIfStale(check, command) {
|
|
75
|
+
const latest = await check;
|
|
76
|
+
if (!latest)
|
|
77
|
+
return;
|
|
78
|
+
stdout.write(`${dim('A newer release is out:')} ${bold(`v${latest}`)}${dim('.')}\n` +
|
|
79
|
+
`${dim('bunx reuses the copy it downloaded first. Name the tag and it fetches:')}\n\n` +
|
|
80
|
+
` ${cyan(command)}\n\n`);
|
|
81
|
+
}
|
package/dist/templates.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export declare function counter(o: Options): string;
|
|
|
68
68
|
* and nothing warns — the page just arrives unstyled.
|
|
69
69
|
*/
|
|
70
70
|
export declare const styles = "@import 'tailwindcss';\n\n/* The whole source tree, not just this directory. A client component is found\n automatically because it enters the browser bundle; a server component never\n does, so anything it uses has to be declared here or it is silently absent\n from the stylesheet. */\n@source '../';\n";
|
|
71
|
-
export declare const gitignore = "node_modules\nbuild\n.rsc\ndist\n*.log\n.DS_Store\n\n#
|
|
71
|
+
export declare const gitignore = "node_modules\nbuild\n.output\n.rsc\ndist\n*.log\n.DS_Store\n\n# Rewritten by the build every run: the ambient declarations, and the stub\n# module the app imports its server actions from.\n.rsc-kit/\nsrc/server-actions.generated.ts\n";
|
|
72
72
|
/**
|
|
73
73
|
* oxlint, with the React Compiler's own rules turned on.
|
|
74
74
|
*
|
package/dist/templates.js
CHANGED
|
@@ -63,16 +63,34 @@ export function scripts(o) {
|
|
|
63
63
|
return {
|
|
64
64
|
dev: 'vite',
|
|
65
65
|
build: 'vite build',
|
|
66
|
+
// The two that produce something you ship build first, rather than reading
|
|
67
|
+
// whatever .output happens to hold. Run on a project that has never been
|
|
68
|
+
// built, they failed with `ENOENT opening root directory ".output/server"`
|
|
69
|
+
// — a path the app did not write and has no reason to recognise. Run on one
|
|
70
|
+
// built a while ago, which is worse, they silently packaged the old code.
|
|
71
|
+
//
|
|
72
|
+
// start and preview are left alone: they are the inner loop, they follow a
|
|
73
|
+
// build in every set of instructions, and re-running one on each restart
|
|
74
|
+
// costs more than it saves.
|
|
66
75
|
...(o.host === 'worker'
|
|
67
|
-
? {
|
|
76
|
+
? {
|
|
77
|
+
preview: 'wrangler dev .output/server/index.mjs',
|
|
78
|
+
deploy: 'vite build && nitro deploy --prebuilt',
|
|
79
|
+
}
|
|
68
80
|
: {
|
|
69
81
|
start: `${o.host === 'node' ? 'node' : 'bun'} .output/server/index.mjs`,
|
|
70
82
|
// Bun only, and only because serveStatic: 'inline' is set in the vite
|
|
71
83
|
// config. Without that the binary compiles, serves pages, and 404s
|
|
72
84
|
// every asset — the static path resolves into Bun's virtual
|
|
73
85
|
// filesystem, where the files on disk are not.
|
|
86
|
+
//
|
|
87
|
+
// Into dist/, which is already ignored. Named after the project it
|
|
88
|
+
// landed a 63MB binary in the root of, next to the source, with
|
|
89
|
+
// nothing in .gitignore covering it.
|
|
74
90
|
...(o.host === 'bun'
|
|
75
|
-
? {
|
|
91
|
+
? {
|
|
92
|
+
compile: 'vite build && bun build --compile .output/server/index.mjs --outfile dist/app',
|
|
93
|
+
}
|
|
76
94
|
: {}),
|
|
77
95
|
}),
|
|
78
96
|
typecheck: 'tsc --noEmit',
|
|
@@ -228,7 +246,10 @@ export const tsconfig = (o) => JSON.stringify({
|
|
|
228
246
|
? ['node', 'vite/client']
|
|
229
247
|
: ['@types/bun', 'vite/client'],
|
|
230
248
|
},
|
|
231
|
-
|
|
249
|
+
// .rsc-kit holds the generated ambient declarations. Ambient means
|
|
250
|
+
// inside the project, and `include` is what decides that — leave it out
|
|
251
|
+
// and typed routes silently fall back to string.
|
|
252
|
+
include: [`${o.sourceDir}/**/*`, '.rsc-kit/**/*'],
|
|
232
253
|
}, null, 2) + '\n';
|
|
233
254
|
export function layout(o) {
|
|
234
255
|
return `${o.tailwind ? "import './styles.css'\n" : ''}import type { ReactNode } from 'react'
|
|
@@ -257,23 +278,39 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|
|
257
278
|
export function page(o) {
|
|
258
279
|
const h1 = o.tailwind ? ' className="text-3xl font-bold"' : '';
|
|
259
280
|
const p = o.tailwind ? ' className="mt-4 text-slate-600"' : '';
|
|
281
|
+
const code = o.tailwind ? ' className="rounded bg-slate-100 px-1"' : '';
|
|
282
|
+
// Deliberately nothing that changes between renders.
|
|
283
|
+
//
|
|
284
|
+
// This page said "Rendered on the server at {new Date()}" — and every route
|
|
285
|
+
// it can is frozen at build time, so that timestamp was the build's and
|
|
286
|
+
// never moved again. Reload and the same instant is still there, under a
|
|
287
|
+
// sentence claiming it was rendered just now. The first thing the starter
|
|
288
|
+
// did was look broken, and the fix someone reaches for is to stop
|
|
289
|
+
// prerendering the page that is teaching them about prerendering.
|
|
260
290
|
return `import { Counter } from '../components/Counter'
|
|
261
291
|
|
|
262
292
|
export const metadata = { title: 'Home' }
|
|
263
293
|
|
|
264
|
-
// A server component
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
294
|
+
// A server component. It runs on the server and ships no JavaScript of its
|
|
295
|
+
// own — this file is not in the browser bundle. It can be \`async\` and await
|
|
296
|
+
// whatever it needs; nothing here does yet.
|
|
297
|
+
export default function HomePage() {
|
|
268
298
|
return (
|
|
269
299
|
<>
|
|
270
300
|
<h1${h1}>${o.name}</h1>
|
|
271
301
|
<p${p}>
|
|
272
|
-
|
|
273
|
-
|
|
302
|
+
The only JavaScript on this page is the counter below, because
|
|
303
|
+
<code${code}>Counter.tsx</code> is the only file that opts into the
|
|
304
|
+
client. Everything else rendered on the server and stayed there.
|
|
274
305
|
</p>
|
|
275
306
|
|
|
276
307
|
<Counter />
|
|
308
|
+
|
|
309
|
+
<p${p}>
|
|
310
|
+
Edit <code${code}>src/app/page.tsx</code> and the change arrives without
|
|
311
|
+
a reload. Add <code${code}>src/app/about/page.tsx</code> and{' '}
|
|
312
|
+
<code${code}>/about</code> exists — there is no route table to update.
|
|
313
|
+
</p>
|
|
277
314
|
</>
|
|
278
315
|
)
|
|
279
316
|
}
|
|
@@ -318,16 +355,16 @@ export const styles = `@import 'tailwindcss';
|
|
|
318
355
|
`;
|
|
319
356
|
export const gitignore = `node_modules
|
|
320
357
|
build
|
|
358
|
+
.output
|
|
321
359
|
.rsc
|
|
322
360
|
dist
|
|
323
361
|
*.log
|
|
324
362
|
.DS_Store
|
|
325
363
|
|
|
326
|
-
#
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
src/
|
|
330
|
-
src/rsc-engine.d.ts
|
|
364
|
+
# Rewritten by the build every run: the ambient declarations, and the stub
|
|
365
|
+
# module the app imports its server actions from.
|
|
366
|
+
.rsc-kit/
|
|
367
|
+
src/server-actions.generated.ts
|
|
331
368
|
`;
|
|
332
369
|
/**
|
|
333
370
|
* oxlint, with the React Compiler's own rules turned on.
|
|
@@ -375,31 +412,57 @@ export function oxlintConfig(o) {
|
|
|
375
412
|
}
|
|
376
413
|
export function readme(o) {
|
|
377
414
|
const pm = o.host === 'node' ? 'npm run' : 'bun run';
|
|
415
|
+
const runtime = o.host === 'worker' ? 'Cloudflare Workers' : o.host === 'node' ? 'Node' : 'Bun';
|
|
416
|
+
// A Worker is deployed rather than started, and only Bun compiles.
|
|
417
|
+
const serve = o.host === 'worker'
|
|
418
|
+
? `${pm} preview # wrangler dev, on workerd\n${pm} deploy # nitro deploy --prebuilt`
|
|
419
|
+
: `${pm} start # serve on http://localhost:${PORT}`;
|
|
420
|
+
const compile = o.host === 'bun'
|
|
421
|
+
? `\n\n\`${pm} compile\` builds and then puts the whole application into
|
|
422
|
+
\`dist/app\` — engine, pages and assets, with Bun's runtime inside it. Run it
|
|
423
|
+
directly:
|
|
424
|
+
|
|
425
|
+
\`\`\`sh
|
|
426
|
+
./dist/app
|
|
427
|
+
\`\`\`
|
|
428
|
+
|
|
429
|
+
It builds first, so the binary is never a version behind your source. Frozen
|
|
430
|
+
pages stay outside it: a binary has no filesystem to read them from, so it
|
|
431
|
+
renders those live.`
|
|
432
|
+
: '';
|
|
378
433
|
return `# ${o.name}
|
|
379
434
|
|
|
380
|
-
React Server Components
|
|
435
|
+
React Server Components, served by ${runtime}.
|
|
381
436
|
|
|
382
437
|
\`\`\`sh
|
|
383
438
|
${pm} dev # vite — serves from source, no build step
|
|
384
439
|
${pm} build # bundles, then freezes every page it can
|
|
385
|
-
${
|
|
440
|
+
${serve}
|
|
386
441
|
\`\`\`
|
|
387
442
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
443
|
+
There is no server file here. \`vite.config.ts\` names a Nitro preset and the
|
|
444
|
+
server is built around your route tree, into \`.output/\` — changing where this
|
|
445
|
+
deploys is changing that one string.${compile}
|
|
446
|
+
|
|
447
|
+
Freezing is part of \`build\`: it renders every page it can and stores the
|
|
448
|
+
result, so those pages are read off disk instead of rendered per visitor.
|
|
449
|
+
Turn it off with \`rscKit({ prerender: false })\` when the build machine
|
|
450
|
+
cannot do what the pages need.
|
|
392
451
|
|
|
393
452
|
## Where things go
|
|
394
453
|
|
|
395
|
-
src/app/layout.tsx
|
|
396
|
-
src/app/page.tsx
|
|
397
|
-
src/app/
|
|
398
|
-
src/components/
|
|
454
|
+
src/app/layout.tsx the root layout; owns <html>
|
|
455
|
+
src/app/page.tsx /
|
|
456
|
+
src/app/styles.css imported by the layout
|
|
457
|
+
src/components/ client components ("use client")
|
|
399
458
|
|
|
400
|
-
A directory with a \`page.tsx\` is a route
|
|
459
|
+
A directory with a \`page.tsx\` is a route, so \`src/app/about/page.tsx\` is
|
|
460
|
+
\`/about\` with nothing to register. \`[slug]\` is a parameter, and
|
|
401
461
|
\`middleware.ts\` runs before anything at or below it renders.
|
|
402
462
|
|
|
403
|
-
|
|
463
|
+
\`.rsc-kit/\` is the build's: the route types that make \`href\` checkable, and
|
|
464
|
+
the ambient declarations. Rewritten every build, and gitignored.
|
|
465
|
+
|
|
466
|
+
Docs: https://rsc-kit.dev
|
|
404
467
|
`;
|
|
405
468
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-rsc-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Scaffold an RSC app on Bun, Hono, Elysia or Node.",
|
|
6
6
|
"bin": {
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
".": {
|
|
35
35
|
"types": "./dist/index.d.ts",
|
|
36
36
|
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./stale": {
|
|
39
|
+
"types": "./dist/stale.d.ts",
|
|
40
|
+
"default": "./dist/stale.js"
|
|
37
41
|
}
|
|
38
42
|
},
|
|
39
43
|
"repository": {
|