create-rsc-kit 0.7.0 → 0.7.2

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 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=hono --compiler=oxc --tailwind
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
@@ -12,6 +12,7 @@ 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
@@ -32,6 +33,8 @@ if (flags.help) {
32
33
  const unattended = flags.yes === true || flags.host !== undefined;
33
34
  // Resolved once: it walks up from this file looking for the sibling package.
34
35
  const core = flags.core ?? defaultCore(dirname(fileURLToPath(import.meta.url)));
36
+ // Started before the first question and never awaited here — see notifyIfStale.
37
+ const staleCheck = checkForNewer('create-rsc-kit', selfVersion(import.meta.url));
35
38
  const options = await collect();
36
39
  try {
37
40
  write(options);
@@ -58,6 +61,7 @@ if (options.install) {
58
61
  }
59
62
  }
60
63
  report(options);
64
+ await notifyIfStale(staleCheck, 'bun create rsc-kit@latest');
61
65
  // ── ─────────────────────────────────────────────────────────────────────────
62
66
  /** Declared, not assigned: collect() runs at module top level, above this. */
63
67
  function basename(path) {
@@ -88,7 +92,7 @@ async function collect() {
88
92
  HELP);
89
93
  exit(1);
90
94
  }
91
- stdout.write(`\n${bold('Create an RSC app')}\n\n`);
95
+ stdout.write(`\n${bold('Create an RSC app')} ${dim(`v${selfVersion(import.meta.url)}`)}\n\n`);
92
96
  const p = new Prompter();
93
97
  try {
94
98
  const dir = flags.dir ?? (await p.text('Directory', 'my-app'));
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 steps = [];
289
- if (found.hasTypeScript) {
290
- return [...steps, { kind: 'skipped', what: 'tsconfig.json', detail: 'already here' }];
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
- writeFileSync(join(dir, 'tsconfig.json'), t.tsconfig(o));
293
- return [...steps, { kind: 'wrote', what: 'tsconfig.json' }];
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
- const generated = ['rsc-env.d.ts', 'rsc-types.d.ts', 'rsc-routes.d.ts', 'rsc-engine.d.ts'].map((f) => `${o.sourceDir}/${f}`);
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
- writeFileSync(path, current + (current.endsWith('\n') || current === '' ? '' : '\n') +
310
- '\n# The RSC build: rewritten into the source dir every run, and written out.\n' +
311
- missing.join('\n') + '\n');
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/options.d.ts CHANGED
@@ -93,4 +93,4 @@ export declare function assertUsableName(name: string): void;
93
93
  * generator nobody can run without reading it first.
94
94
  */
95
95
  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 <dir> [options] (once published)\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";
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";
package/dist/options.js CHANGED
@@ -176,7 +176,7 @@ export const HELP = `
176
176
 
177
177
  Usage
178
178
  create-rsc-kit <dir> [options]
179
- bun create rsc-kit <dir> [options] (once published)
179
+ bun create rsc-kit@latest <dir> [options]
180
180
 
181
181
  Options
182
182
  --host=bun|node|worker where it runs — picks the Nitro preset
@@ -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
+ }
@@ -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# Written by the build into the source dir, every run.\nsrc/rsc-env.d.ts\nsrc/rsc-types.d.ts\nsrc/rsc-routes.d.ts\nsrc/rsc-engine.d.ts\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
@@ -228,7 +228,10 @@ export const tsconfig = (o) => JSON.stringify({
228
228
  ? ['node', 'vite/client']
229
229
  : ['@types/bun', 'vite/client'],
230
230
  },
231
- include: [`${o.sourceDir}/**/*`],
231
+ // .rsc-kit holds the generated ambient declarations. Ambient means
232
+ // inside the project, and `include` is what decides that — leave it out
233
+ // and typed routes silently fall back to string.
234
+ include: [`${o.sourceDir}/**/*`, '.rsc-kit/**/*'],
232
235
  }, null, 2) + '\n';
233
236
  export function layout(o) {
234
237
  return `${o.tailwind ? "import './styles.css'\n" : ''}import type { ReactNode } from 'react'
@@ -318,16 +321,16 @@ export const styles = `@import 'tailwindcss';
318
321
  `;
319
322
  export const gitignore = `node_modules
320
323
  build
324
+ .output
321
325
  .rsc
322
326
  dist
323
327
  *.log
324
328
  .DS_Store
325
329
 
326
- # Written by the build into the source dir, every run.
327
- src/rsc-env.d.ts
328
- src/rsc-types.d.ts
329
- src/rsc-routes.d.ts
330
- src/rsc-engine.d.ts
330
+ # Rewritten by the build every run: the ambient declarations, and the stub
331
+ # module the app imports its server actions from.
332
+ .rsc-kit/
333
+ src/server-actions.generated.ts
331
334
  `;
332
335
  /**
333
336
  * oxlint, with the React Compiler's own rules turned on.
@@ -375,31 +378,49 @@ export function oxlintConfig(o) {
375
378
  }
376
379
  export function readme(o) {
377
380
  const pm = o.host === 'node' ? 'npm run' : 'bun run';
381
+ const runtime = o.host === 'worker' ? 'Cloudflare Workers' : o.host === 'node' ? 'Node' : 'Bun';
382
+ // A Worker is deployed rather than started, and only Bun compiles.
383
+ const serve = o.host === 'worker'
384
+ ? `${pm} preview # wrangler dev, on workerd\n${pm} deploy # nitro deploy --prebuilt`
385
+ : `${pm} start # serve on http://localhost:${PORT}`;
386
+ const compile = o.host === 'bun'
387
+ ? `\n\n\`${pm} compile\` puts the whole application in one file — engine, pages and
388
+ assets — with Bun's runtime inside it. Frozen pages stay outside: a binary
389
+ has no filesystem to read them from, so it renders those live.`
390
+ : '';
378
391
  return `# ${o.name}
379
392
 
380
- React Server Components on ${o.host === 'node' ? 'node:http' : o.host === 'bun' ? 'Bun.serve' : o.host}.
393
+ React Server Components, served by ${runtime}.
381
394
 
382
395
  \`\`\`sh
383
396
  ${pm} dev # vite — serves from source, no build step
384
397
  ${pm} build # bundles, then freezes every page it can
385
- ${pm} start # serve on http://localhost:${PORT}
398
+ ${serve}
386
399
  \`\`\`
387
400
 
388
- Freezing is part of \`build\`. To redo it against fresh data without
389
- rebuilding — or after turning it off in \`vite.config.ts\` — run
390
- \`bunx rsc-kit prerender --out ${paths(o).outDir}\`, keeping that package's version in
391
- step with \`@rsc-kit/core\`.
401
+ There is no server file here. \`vite.config.ts\` names a Nitro preset and the
402
+ server is built around your route tree, into \`.output/\` — changing where this
403
+ deploys is changing that one string.${compile}
404
+
405
+ Freezing is part of \`build\`: it renders every page it can and stores the
406
+ result, so those pages are read off disk instead of rendered per visitor.
407
+ Turn it off with \`rscKit({ prerender: false })\` when the build machine
408
+ cannot do what the pages need.
392
409
 
393
410
  ## Where things go
394
411
 
395
- src/app/layout.tsx the root layout; owns <html>
396
- src/app/page.tsx /
397
- src/app/about/page.tsx /about
398
- src/components/ client components ("use client")
412
+ src/app/layout.tsx the root layout; owns <html>
413
+ src/app/page.tsx /
414
+ src/app/styles.css imported by the layout
415
+ src/components/ client components ("use client")
399
416
 
400
- A directory with a \`page.tsx\` is a route. \`[slug]\` is a parameter,
417
+ A directory with a \`page.tsx\` is a route, so \`src/app/about/page.tsx\` is
418
+ \`/about\` with nothing to register. \`[slug]\` is a parameter, and
401
419
  \`middleware.ts\` runs before anything at or below it renders.
402
420
 
403
- Docs: https://github.com/ramonmalcolm/rsc-kit
421
+ \`.rsc-kit/\` is the build's: the route types that make \`href\` checkable, and
422
+ the ambient declarations. Rewritten every build, and gitignored.
423
+
424
+ Docs: https://rsc-kit.dev
404
425
  `;
405
426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-rsc-kit",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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": {