create-rsc-kit 0.9.0 → 0.11.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 +1 -0
- package/dist/init.js +4 -0
- package/dist/templates.d.ts +14 -0
- package/dist/templates.js +167 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -139,6 +139,7 @@ function write(o) {
|
|
|
139
139
|
['vite.config.ts', t.viteConfig(o)],
|
|
140
140
|
['.gitignore', t.gitignore],
|
|
141
141
|
['README.md', t.readme(o)],
|
|
142
|
+
['AGENTS.md', t.agents(o)],
|
|
142
143
|
['src/app/layout.tsx', t.layout(o)],
|
|
143
144
|
['src/app/page.tsx', t.page(o)],
|
|
144
145
|
['src/components/Counter.tsx', t.counter(o)],
|
package/dist/init.js
CHANGED
|
@@ -261,6 +261,10 @@ function routes(o, dir) {
|
|
|
261
261
|
[join(o.sourceDir, 'app/layout.tsx'), t.layout(o)],
|
|
262
262
|
[join(o.sourceDir, 'app/page.tsx'), t.page(o)],
|
|
263
263
|
[join(o.sourceDir, 'components/Counter.tsx'), t.counter(o)],
|
|
264
|
+
// Beside the route tree rather than merged into an existing AGENTS.md: a
|
|
265
|
+
// file of someone else's instructions is not one to append to blind, and
|
|
266
|
+
// the loop below skips it if it is already there.
|
|
267
|
+
['AGENTS.md', t.agents(o)],
|
|
264
268
|
];
|
|
265
269
|
if (o.tailwind)
|
|
266
270
|
files.push([join(o.sourceDir, 'app/styles.css'), t.styles]);
|
package/dist/templates.d.ts
CHANGED
|
@@ -82,4 +82,18 @@ export declare const gitignore = "node_modules\nbuild\n.output\n.rsc\ndist\n*.lo
|
|
|
82
82
|
* leaves behind: a new project has nothing to grandfather in.
|
|
83
83
|
*/
|
|
84
84
|
export declare function oxlintConfig(o: Options): string;
|
|
85
|
+
/**
|
|
86
|
+
* What an agent working in this project needs to know and would otherwise guess.
|
|
87
|
+
*
|
|
88
|
+
* Not a summary of the documentation. Every line here is a thing that is easy
|
|
89
|
+
* to get wrong from React or Next habits and produces code that looks right:
|
|
90
|
+
* the wrong directive, a client component reaching for the request, a check
|
|
91
|
+
* written in the component instead of the action. An agent that knows the
|
|
92
|
+
* documentation exists will still write those, because they are what the
|
|
93
|
+
* neighbouring frameworks taught it.
|
|
94
|
+
*
|
|
95
|
+
* AGENTS.md rather than CLAUDE.md: it is the cross-tool name, and Claude Code
|
|
96
|
+
* reads it too.
|
|
97
|
+
*/
|
|
98
|
+
export declare function agents(o: Options): string;
|
|
85
99
|
export declare function readme(o: Options): string;
|
package/dist/templates.js
CHANGED
|
@@ -196,11 +196,7 @@ export function viteConfig(o) {
|
|
|
196
196
|
//
|
|
197
197
|
// Not optional, and not a flag on rscKit() either — the plugin builds for
|
|
198
198
|
// Nitro and nothing else, so a config without this line has no server.
|
|
199
|
-
|
|
200
|
-
// scanDirs is empty without it, so a handler written there is never scanned,
|
|
201
|
-
// the request falls through to the rsc entry, and the 404 it answers with
|
|
202
|
-
// reads as a routing bug in this package rather than a missing option.
|
|
203
|
-
plugins.push(`nitro({ preset: ${JSON.stringify(preset(o.host))}, serveStatic: 'inline', serverDir: 'server' })`);
|
|
199
|
+
plugins.push(`nitro({ preset: ${JSON.stringify(preset(o.host))}, serveStatic: 'inline' })`);
|
|
204
200
|
plugins.push(`rscKit({
|
|
205
201
|
${options.join(',\n ')},
|
|
206
202
|
})`);
|
|
@@ -273,11 +269,7 @@ export const tsconfig = (o) => JSON.stringify({
|
|
|
273
269
|
// .rsc-kit holds the generated ambient declarations. Ambient means
|
|
274
270
|
// inside the project, and `include` is what decides that — leave it out
|
|
275
271
|
// and typed routes silently fall back to string.
|
|
276
|
-
|
|
277
|
-
// server/ is the same mistake one directory over: an api handler outside
|
|
278
|
-
// the program is not type-checked at all, so one returning the wrong
|
|
279
|
-
// shape builds, deploys and fails at runtime with nothing having said so.
|
|
280
|
-
include: [`${o.sourceDir}/**/*`, 'server/**/*', '.rsc-kit/**/*'],
|
|
272
|
+
include: [`${o.sourceDir}/**/*`, '.rsc-kit/**/*'],
|
|
281
273
|
}, null, 2) + '\n';
|
|
282
274
|
export function layout(o) {
|
|
283
275
|
return `${o.tailwind ? "import './styles.css'\n" : ''}import type { ReactNode } from 'react'
|
|
@@ -440,6 +432,171 @@ export function oxlintConfig(o) {
|
|
|
440
432
|
ignorePatterns: ['build', 'dist', `${o.sourceDir}/rsc-*.d.ts`],
|
|
441
433
|
}, null, 2) + '\n');
|
|
442
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* What an agent working in this project needs to know and would otherwise guess.
|
|
437
|
+
*
|
|
438
|
+
* Not a summary of the documentation. Every line here is a thing that is easy
|
|
439
|
+
* to get wrong from React or Next habits and produces code that looks right:
|
|
440
|
+
* the wrong directive, a client component reaching for the request, a check
|
|
441
|
+
* written in the component instead of the action. An agent that knows the
|
|
442
|
+
* documentation exists will still write those, because they are what the
|
|
443
|
+
* neighbouring frameworks taught it.
|
|
444
|
+
*
|
|
445
|
+
* AGENTS.md rather than CLAUDE.md: it is the cross-tool name, and Claude Code
|
|
446
|
+
* reads it too.
|
|
447
|
+
*/
|
|
448
|
+
export function agents(o) {
|
|
449
|
+
const pm = o.host === 'node' ? 'npm run' : 'bun run';
|
|
450
|
+
return `# Working in this project
|
|
451
|
+
|
|
452
|
+
React Server Components through \`@rsc-kit/core\`, a Vite plugin. Routes are
|
|
453
|
+
files under \`${o.sourceDir}/app\`. There is no server file to edit — the build
|
|
454
|
+
generates it.
|
|
455
|
+
|
|
456
|
+
Read the guides at https://rsc-kit.dev before reaching for a pattern from
|
|
457
|
+
another framework. The notes below are only the things most often got wrong.
|
|
458
|
+
|
|
459
|
+
## Commands
|
|
460
|
+
|
|
461
|
+
\`\`\`sh
|
|
462
|
+
${pm} dev # vite, with the engine in it
|
|
463
|
+
${pm} build # builds and prerenders; prints what it froze
|
|
464
|
+
${pm} typecheck
|
|
465
|
+
\`\`\`
|
|
466
|
+
|
|
467
|
+
**Read the build output.** It is not decoration — it says which routes were
|
|
468
|
+
stored, which render per request, and why:
|
|
469
|
+
|
|
470
|
+
\`\`\`
|
|
471
|
+
○ /account 85 kB
|
|
472
|
+
◐ /locale 85 kB
|
|
473
|
+
dynamic — called cookies(), headers()
|
|
474
|
+
\`\`\`
|
|
475
|
+
|
|
476
|
+
If a page you expected to be static is not, the reason is on that line. Do not
|
|
477
|
+
guess at it.
|
|
478
|
+
|
|
479
|
+
## Server and client
|
|
480
|
+
|
|
481
|
+
Every component is a **server** component unless its file starts with
|
|
482
|
+
\`"use client"\`. Server components can be \`async\` and read the database
|
|
483
|
+
directly. They do not ship to the browser.
|
|
484
|
+
|
|
485
|
+
Add \`"use client"\` only when the file needs state, an effect, an event
|
|
486
|
+
handler or a browser API. It is a boundary, not a label: everything that file
|
|
487
|
+
imports goes to the browser too.
|
|
488
|
+
|
|
489
|
+
\`"use server"\` is a different thing and not the opposite. It marks a module
|
|
490
|
+
whose exports may be **called from** the browser — a server action.
|
|
491
|
+
|
|
492
|
+
\`\`\`ts
|
|
493
|
+
'use server'
|
|
494
|
+
export async function createPost(input) { … } // callable from a client component
|
|
495
|
+
\`\`\`
|
|
496
|
+
|
|
497
|
+
Do not put \`"use server"\` at the top of a page to make it a server component.
|
|
498
|
+
It already is one.
|
|
499
|
+
|
|
500
|
+
## Reading the request
|
|
501
|
+
|
|
502
|
+
\`cookies()\`, \`headers()\` and \`searchParams()\` come from
|
|
503
|
+
\`@rsc-kit/core/request\` and are **async**. So are a page's \`params\` and
|
|
504
|
+
\`searchParams\` props, and an api route's.
|
|
505
|
+
|
|
506
|
+
Reading any of them makes the page render per request instead of being frozen
|
|
507
|
+
at build time. That is usually correct — just know that it is the trade.
|
|
508
|
+
|
|
509
|
+
\`await connection()\` says "render this per visitor" deliberately, when
|
|
510
|
+
nothing else in the page happens to say it.
|
|
511
|
+
|
|
512
|
+
## Data
|
|
513
|
+
|
|
514
|
+
Fetch in a server component and await it. There is no loader and no
|
|
515
|
+
\`getServerSideProps\`.
|
|
516
|
+
|
|
517
|
+
Better still, do not await it — pass the promise to a client component and let
|
|
518
|
+
it \`use()\` the value. The shell paints immediately and the data streams into
|
|
519
|
+
the same response, with no request from the browser:
|
|
520
|
+
|
|
521
|
+
\`\`\`tsx
|
|
522
|
+
export default function Page() {
|
|
523
|
+
const posts = getPosts() // not awaited
|
|
524
|
+
|
|
525
|
+
return (
|
|
526
|
+
<Suspense fallback={<Skeleton />}>
|
|
527
|
+
<List posts={posts} /> {/* "use client": use(posts) */}
|
|
528
|
+
</Suspense>
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
\`\`\`
|
|
532
|
+
|
|
533
|
+
For data the **browser** decides to fetch — a filter, a refresh — use TanStack
|
|
534
|
+
Query or SWR. This project does not ship a cache and should not grow one.
|
|
535
|
+
|
|
536
|
+
## Actions, and where the check goes
|
|
537
|
+
|
|
538
|
+
An action is a public endpoint. Anyone can call it directly, so the
|
|
539
|
+
authorisation check belongs **inside the action**, never in the component that
|
|
540
|
+
renders the button.
|
|
541
|
+
|
|
542
|
+
Build actions from a client so the check cannot be forgotten:
|
|
543
|
+
|
|
544
|
+
\`\`\`ts
|
|
545
|
+
'use server'
|
|
546
|
+
import { createActionClient } from '@rsc-kit/core/action'
|
|
547
|
+
|
|
548
|
+
export const client = createActionClient().use(async ({ next }) => {
|
|
549
|
+
const user = await currentUser()
|
|
550
|
+
|
|
551
|
+
if (!user) throw new ServerAuthenticationError()
|
|
552
|
+
|
|
553
|
+
return next({ ctx: { user } })
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
export const createPost = client.input(schema).handler(async ({ input, ctx }) => …)
|
|
557
|
+
export const listPosts = client.query(async ({ ctx }) => …)
|
|
558
|
+
\`\`\`
|
|
559
|
+
|
|
560
|
+
\`.handler()\` is a mutation, \`.query()\` is a read sent as a GET. Both run
|
|
561
|
+
the middleware, so \`ctx.user\` is typed and non-null inside them.
|
|
562
|
+
|
|
563
|
+
Authorise on **identity, not arguments**. \`deletePost(id)\` that trusts the id
|
|
564
|
+
is an IDOR — the caller chooses the id.
|
|
565
|
+
|
|
566
|
+
Middleware in \`middleware.ts\` guards a route tree. It does **not** run for
|
|
567
|
+
actions, because an action renders no route.
|
|
568
|
+
|
|
569
|
+
## Urls are input
|
|
570
|
+
|
|
571
|
+
Export a schema beside the page or route and the values arrive parsed and typed:
|
|
572
|
+
|
|
573
|
+
\`\`\`ts
|
|
574
|
+
export const params = z.object({ slug: z.string().min(1) })
|
|
575
|
+
export const searchParams = z.object({ page: z.coerce.number().int().min(1).default(1) })
|
|
576
|
+
\`\`\`
|
|
577
|
+
|
|
578
|
+
Bad \`params\` answer 404, bad \`searchParams\` reach the error boundary. Do
|
|
579
|
+
not hand-parse \`Number(searchParams.get('page'))\`.
|
|
580
|
+
|
|
581
|
+
## Api routes
|
|
582
|
+
|
|
583
|
+
\`${o.sourceDir}/app/**/route.ts\`, exporting \`GET\`, \`POST\` and so on.
|
|
584
|
+
A real \`Request\` in, a real \`Response\` out. Await \`params\`,
|
|
585
|
+
\`searchParams\` and \`body\` from the second argument.
|
|
586
|
+
|
|
587
|
+
They run their directory's \`middleware.ts\`, and a \`GET\` that reads nothing
|
|
588
|
+
from the request is answered from disk.
|
|
589
|
+
|
|
590
|
+
## Things that are not this project
|
|
591
|
+
|
|
592
|
+
- No \`pages/\` directory, no \`_app\`, no \`getStaticProps\`.
|
|
593
|
+
- No \`next/link\`, \`next/image\` or \`next/navigation\` — use
|
|
594
|
+
\`@rsc-kit/core/Link\` and \`@rsc-kit/core/navigate\`.
|
|
595
|
+
- No \`express\`/\`fastify\` server to write. Do not add one.
|
|
596
|
+
- Do not install a state manager to move data from server to client. Props and
|
|
597
|
+
promises already cross that boundary.
|
|
598
|
+
`;
|
|
599
|
+
}
|
|
443
600
|
export function readme(o) {
|
|
444
601
|
const pm = o.host === 'node' ? 'npm run' : 'bun run';
|
|
445
602
|
const runtime = o.host === 'worker' ? 'Cloudflare Workers' : o.host === 'node' ? 'Node' : 'Bun';
|