create-rsc-kit 0.14.0 → 0.15.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 CHANGED
@@ -140,9 +140,11 @@ function write(o) {
140
140
  ['.gitignore', t.gitignore],
141
141
  ['README.md', t.readme(o)],
142
142
  ['AGENTS.md', t.agents(o)],
143
+ ['.mcp.json', t.mcp()],
143
144
  ['src/app/layout.tsx', t.layout(o)],
144
145
  ['src/app/page.tsx', t.page(o)],
145
146
  ['src/components/Counter.tsx', t.counter(o)],
147
+ ['tests/app.test.ts', t.smokeTest(o)],
146
148
  ];
147
149
  if (o.tailwind)
148
150
  files.push(['src/app/styles.css', t.styles]);
package/dist/init.js CHANGED
@@ -265,9 +265,12 @@ function routes(o, dir) {
265
265
  // file of someone else's instructions is not one to append to blind, and
266
266
  // the loop below skips it if it is already there.
267
267
  ['AGENTS.md', t.agents(o)],
268
+ ['.mcp.json', t.mcp()],
268
269
  ];
269
270
  if (o.tailwind)
270
271
  files.push([join(o.sourceDir, 'app/styles.css'), t.styles]);
272
+ if (o.host !== 'laravel')
273
+ files.push(['tests/app.test.ts', t.smokeTest(o)]);
271
274
  for (const [path, contents] of files) {
272
275
  const full = join(dir, path);
273
276
  if (existsSync(full)) {
@@ -57,6 +57,14 @@ export declare function viteConfig(o: Options): string;
57
57
  export declare const WRANGLER_FILE = "wrangler.toml";
58
58
  export declare const tsconfig: (o: Options) => string;
59
59
  export declare function layout(o: Options): string;
60
+ /**
61
+ * A test that goes through the real build, so there is one to extend.
62
+ *
63
+ * Without it, "add a test" starts with choosing a runner and a way to reach
64
+ * the app, and what gets chosen is a port, a spawned server, and a sleep. This
65
+ * is the shape instead: the deployed handler, a Request in, a Response out.
66
+ */
67
+ export declare function smokeTest(o: Options): string;
60
68
  export declare function page(o: Options): string;
61
69
  export declare function counter(o: Options): string;
62
70
  /**
@@ -96,4 +104,11 @@ export declare function oxlintConfig(o: Options): string;
96
104
  * reads it too.
97
105
  */
98
106
  export declare function agents(o: Options): string;
107
+ /**
108
+ * Project-scoped MCP config, which Claude Code reads from the project root
109
+ * and asks the user to approve on first use. Nothing is installed: npx fetches
110
+ * the server the first time an agent starts it. Other clients want the same
111
+ * four lines in their own file.
112
+ */
113
+ export declare function mcp(): string;
99
114
  export declare function readme(o: Options): string;
package/dist/templates.js CHANGED
@@ -95,6 +95,18 @@ export function scripts(o) {
95
95
  }),
96
96
  typecheck: 'tsc --noEmit',
97
97
  ...(o.lint ? { lint: 'oxlint src --fix', 'lint:check': 'oxlint src --deny-warnings' } : {}),
98
+ // Laravel returned above: its pages call into PHP that createTestApp does
99
+ // not run, and its tests are Pest, on the other side.
100
+ ...testScripts(o),
101
+ };
102
+ }
103
+ function testScripts(o) {
104
+ const test = o.host === 'node' ? 'node --test "tests/**/*.test.ts"' : 'bun test tests';
105
+ return {
106
+ test,
107
+ // The one command an agent runs before saying it is done. Each part exists
108
+ // on its own; this is so nothing has to remember the list.
109
+ check: ['tsc --noEmit', ...(o.lint ? ['oxlint src --deny-warnings'] : []), test].join(' && '),
98
110
  };
99
111
  }
100
112
  export function packageJson(o) {
@@ -146,13 +158,7 @@ export function packageJson(o) {
146
158
  name: o.name,
147
159
  type: 'module',
148
160
  private: true,
149
- scripts: {
150
- ...scripts(o),
151
- typecheck: 'tsc --noEmit',
152
- ...(o.lint
153
- ? { lint: 'oxlint src --fix', 'lint:check': 'oxlint src --deny-warnings' }
154
- : {}),
155
- },
161
+ scripts: scripts(o),
156
162
  dependencies: sorted(deps),
157
163
  devDependencies: sorted(dev),
158
164
  }, null, 2) + '\n');
@@ -269,7 +275,7 @@ export const tsconfig = (o) => JSON.stringify({
269
275
  // .rsc-kit holds the generated ambient declarations. Ambient means
270
276
  // inside the project, and `include` is what decides that — leave it out
271
277
  // and typed routes silently fall back to string.
272
- include: [`${o.sourceDir}/**/*`, '.rsc-kit/**/*'],
278
+ include: [`${o.sourceDir}/**/*`, 'tests/**/*', '.rsc-kit/**/*'],
273
279
  }, null, 2) + '\n';
274
280
  export function layout(o) {
275
281
  return `${o.tailwind ? "import './styles.css'\n" : ''}import type { ReactNode } from 'react'
@@ -296,6 +302,54 @@ export default function RootLayout({ children }: { children: ReactNode }) {
296
302
  }
297
303
  `;
298
304
  }
305
+ /**
306
+ * A test that goes through the real build, so there is one to extend.
307
+ *
308
+ * Without it, "add a test" starts with choosing a runner and a way to reach
309
+ * the app, and what gets chosen is a port, a spawned server, and a sleep. This
310
+ * is the shape instead: the deployed handler, a Request in, a Response out.
311
+ */
312
+ export function smokeTest(o) {
313
+ const runner = o.host === 'node'
314
+ ? `import { before, describe, test } from 'node:test'
315
+ import assert from 'node:assert/strict'`
316
+ : `import { beforeAll, describe, expect, test } from 'bun:test'`;
317
+ const setup = o.host === 'node' ? 'before' : 'beforeAll';
318
+ const ok = o.host === 'node'
319
+ ? ` assert.equal(res.status, 200)
320
+ assert.match(await res.text(), /${o.name}/)`
321
+ : ` expect(res.status).toBe(200)
322
+ expect(await res.text()).toContain('${o.name}')`;
323
+ const missing = o.host === 'node'
324
+ ? ` assert.equal((await app.fetch('/no-such-page')).status, 404)`
325
+ : ` expect((await app.fetch('/no-such-page')).status).toBe(404)`;
326
+ return `// The whole app as it is deployed, without a port or a browser.
327
+ //
328
+ // createTestApp builds when the source is newer than the last build and hands
329
+ // back the same Request → Response handler the server runs: the real router,
330
+ // the real middleware, the real api routes, the pages the build stored.
331
+ ${runner}
332
+ import { createTestApp } from '@rsc-kit/core/testing'
333
+
334
+ let app: Awaited<ReturnType<typeof createTestApp>>
335
+
336
+ ${setup}(async () => {
337
+ app = await createTestApp()
338
+ }${o.host === 'node' ? '' : ', 120_000'})
339
+
340
+ describe('the app', () => {
341
+ test('serves the home page', async () => {
342
+ const res = await app.fetch('/')
343
+
344
+ ${ok}
345
+ })
346
+
347
+ test('answers a url that matches nothing with a 404', async () => {
348
+ ${missing}
349
+ })
350
+ })
351
+ `;
352
+ }
299
353
  export function page(o) {
300
354
  const h1 = o.tailwind ? ' className="text-3xl font-bold"' : '';
301
355
  const p = o.tailwind ? ' className="mt-4 text-slate-600"' : '';
@@ -456,14 +510,32 @@ generates it.
456
510
  Read the guides at https://rsc-kit.dev before reaching for a pattern from
457
511
  another framework. The notes below are only the things most often got wrong.
458
512
 
513
+ The \`rsc-kit\` MCP server in \`.mcp.json\` answers from this project's last
514
+ build — which routes froze and why, what is heaviest — and has the long-form
515
+ recipe for anything here (\`how_to\`). Ask it before guessing.
516
+
459
517
  ## Commands
460
518
 
461
519
  \`\`\`sh
462
520
  ${pm} dev # vite, with the engine in it
463
521
  ${pm} build # builds and prerenders; prints what it froze
464
- ${pm} typecheck
522
+ ${o.host === 'laravel' ? `${pm} typecheck` : `${pm} check # typecheck, lint and tests — run before saying it is done`}
465
523
  \`\`\`
466
-
524
+ ${o.host === 'laravel'
525
+ ? ''
526
+ : `
527
+ Tests live in \`tests/\` and go through the real build: \`createTestApp()\` from
528
+ \`@rsc-kit/core/testing\` hands back the deployed Request → Response handler,
529
+ so a test fetches a url and reads the response. Extend \`tests/app.test.ts\`;
530
+ do not add a runner, a port or a spawned server. Actions, queries and api
531
+ routes are plain functions and can also be called directly.
532
+
533
+ **A change is not done without its test.** A guarded route gets a test that a
534
+ stranger is turned away; an action gets a test of its refusal, and one that
535
+ someone else's id is refused; an api route gets its 4xx. The exact shapes are
536
+ in \`how_to({ topic: 'testing' })\`. Then \`${pm} check\`. Nothing here needs
537
+ the app running - the build and the tests are the verification.
538
+ `}
467
539
  **Read the build output.** It is not decoration — it says which routes were
468
540
  stored, which render per request, and why:
469
541
 
@@ -597,6 +669,19 @@ from the request is answered from disk.
597
669
  promises already cross that boundary.
598
670
  `;
599
671
  }
672
+ /**
673
+ * Project-scoped MCP config, which Claude Code reads from the project root
674
+ * and asks the user to approve on first use. Nothing is installed: npx fetches
675
+ * the server the first time an agent starts it. Other clients want the same
676
+ * four lines in their own file.
677
+ */
678
+ export function mcp() {
679
+ return (JSON.stringify({
680
+ mcpServers: {
681
+ 'rsc-kit': { command: 'npx', args: ['-y', '@rsc-kit/mcp'] },
682
+ },
683
+ }, null, 2) + '\n');
684
+ }
600
685
  export function readme(o) {
601
686
  const pm = o.host === 'node' ? 'npm run' : 'bun run';
602
687
  const runtime = o.host === 'worker' ? 'Cloudflare Workers' : o.host === 'node' ? 'Node' : 'Bun';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-rsc-kit",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "Scaffold an RSC app on Bun, Hono, Elysia or Node.",
6
6
  "bin": {