create-rsc-kit 0.28.0 → 0.29.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.
@@ -0,0 +1,47 @@
1
+ import type { Options } from './options.js';
2
+ /** What the rules depend on: the app's host, where its source is, and two choices made at scaffold. */
3
+ export interface AgentsOptions {
4
+ host: Options['host'];
5
+ sourceDir: string;
6
+ env: boolean;
7
+ pwa: boolean;
8
+ /** Whether actions are a backend's (Go, another server) rather than this app's JavaScript. */
9
+ backend?: boolean;
10
+ }
11
+ /** What a start marker records. */
12
+ export interface AgentsStamp extends AgentsOptions {
13
+ version: string;
14
+ }
15
+ /** What the rules need from a scaffold's options. */
16
+ export declare function agentsOptions(o: Options): AgentsOptions;
17
+ /** The rules alone, for the options given: what the MCP server's `rules` tool answers. */
18
+ export declare function rules(o: AgentsOptions): string;
19
+ /** The section, between its markers, stamped with this version and the options. */
20
+ export declare function agentsSection(o: AgentsOptions): string;
21
+ /** The stamp in an AGENTS.md's section; `{}` for a section from before stamps; null with no section. */
22
+ export declare function readStamp(text: string): Partial<AgentsStamp> | null;
23
+ /** What `updateAgents` did, said once. */
24
+ export type AgentsResult = {
25
+ kind: 'wrote' | 'updated' | 'current';
26
+ path: string;
27
+ version: string;
28
+ } | {
29
+ kind: 'refused';
30
+ reason: string;
31
+ };
32
+ /**
33
+ * Write the rsc-kit section of `dir`/AGENTS.md as this version writes it.
34
+ *
35
+ * The options come from the section's own stamp; `given` overrides them, and
36
+ * is what an unstamped section from an older rsc-kit needs - the rules depend
37
+ * on the host and the source directory, and they are not guessed. A file with
38
+ * no section is only rewritten whole with `replace`, since then nothing
39
+ * marks what is ours.
40
+ */
41
+ export declare function updateAgents(dir: string, given?: Partial<AgentsOptions> & {
42
+ replace?: boolean;
43
+ }): AgentsResult;
44
+ /** Parse `rsc-kit agents` flags. */
45
+ export declare function agentsFlags(args: string[]): Partial<AgentsOptions> & {
46
+ replace?: boolean;
47
+ };
package/dist/agents.js ADDED
@@ -0,0 +1,120 @@
1
+ // The rsc-kit section of an AGENTS.md: the rules an agent follows in an
2
+ // rsc-kit app, kept current the way Laravel Boost keeps its guidelines.
3
+ //
4
+ // The package owns the section and regenerates it on request - `rsc-kit
5
+ // agents` - and git shows the change for review. Everything outside the
6
+ // markers is the app's and is never touched. The start marker records which
7
+ // version wrote the section and the options it was written for, so it can be
8
+ // written again exactly, and the dev server can say when it is behind.
9
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { selfVersion } from './stale.js';
12
+ import * as t from './templates.js';
13
+ const START = '<!-- rsc-kit:start';
14
+ const END = '<!-- rsc-kit:end -->';
15
+ /** What the rules need from a scaffold's options. */
16
+ export function agentsOptions(o) {
17
+ return { host: o.host, sourceDir: o.sourceDir, env: Boolean(o.env), pwa: Boolean(o.pwa), backend: Boolean(o.backend) };
18
+ }
19
+ /** The rules alone, for the options given: what the MCP server's `rules` tool answers. */
20
+ export function rules(o) {
21
+ return t.agents({ ...o, backend: o.backend ? 'yes' : undefined }).trim();
22
+ }
23
+ /** The section, between its markers, stamped with this version and the options. */
24
+ export function agentsSection(o) {
25
+ const stamp = {
26
+ version: selfVersion(import.meta.url),
27
+ host: o.host,
28
+ sourceDir: o.sourceDir,
29
+ env: Boolean(o.env),
30
+ pwa: Boolean(o.pwa),
31
+ backend: Boolean(o.backend),
32
+ };
33
+ return `${START} ${JSON.stringify(stamp)} -->\n${rules(o)}\n${END}\n`;
34
+ }
35
+ /** The stamp in an AGENTS.md's section; `{}` for a section from before stamps; null with no section. */
36
+ export function readStamp(text) {
37
+ const at = text.indexOf(START);
38
+ if (at === -1)
39
+ return null;
40
+ const line = text.slice(at, text.indexOf('-->', at));
41
+ const json = line.slice(START.length).trim();
42
+ try {
43
+ return json ? JSON.parse(json) : {};
44
+ }
45
+ catch {
46
+ return {};
47
+ }
48
+ }
49
+ /**
50
+ * Write the rsc-kit section of `dir`/AGENTS.md as this version writes it.
51
+ *
52
+ * The options come from the section's own stamp; `given` overrides them, and
53
+ * is what an unstamped section from an older rsc-kit needs - the rules depend
54
+ * on the host and the source directory, and they are not guessed. A file with
55
+ * no section is only rewritten whole with `replace`, since then nothing
56
+ * marks what is ours.
57
+ */
58
+ export function updateAgents(dir, given = {}) {
59
+ const path = join(dir, 'AGENTS.md');
60
+ const version = selfVersion(import.meta.url);
61
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
62
+ const stamp = existing === null ? null : readStamp(existing);
63
+ if (existing !== null && stamp === null && !given.replace) {
64
+ return {
65
+ kind: 'refused',
66
+ reason: 'AGENTS.md has no rsc-kit section to update. If it is the one rsc-kit wrote, run again with --replace ' +
67
+ '(and --host and --source-dir) to rewrite it whole; git shows the change.',
68
+ };
69
+ }
70
+ const o = { ...stamp, ...given };
71
+ if (!o.host || !o.sourceDir) {
72
+ return {
73
+ kind: 'refused',
74
+ reason: 'This AGENTS.md was written before rsc-kit recorded the options it was written for. ' +
75
+ 'Say them once: --host bun|node|worker|laravel --source-dir <dir> [--env] [--pwa].',
76
+ };
77
+ }
78
+ const section = agentsSection({
79
+ host: o.host,
80
+ sourceDir: o.sourceDir,
81
+ env: Boolean(o.env),
82
+ pwa: Boolean(o.pwa),
83
+ backend: Boolean(o.backend),
84
+ });
85
+ if (existing === null || (stamp === null && given.replace)) {
86
+ writeFileSync(path, section);
87
+ return { kind: 'wrote', path, version };
88
+ }
89
+ const start = existing.indexOf(START);
90
+ const endAt = existing.indexOf(END, start);
91
+ const before = existing.slice(0, start);
92
+ const after = endAt === -1 ? '' : existing.slice(endAt + END.length).replace(/^\n/, '');
93
+ const next = before + section + after;
94
+ if (next === existing)
95
+ return { kind: 'current', path, version };
96
+ writeFileSync(path, next);
97
+ return { kind: 'updated', path, version };
98
+ }
99
+ /** Parse `rsc-kit agents` flags. */
100
+ export function agentsFlags(args) {
101
+ const out = {};
102
+ for (let i = 0; i < args.length; i++) {
103
+ const arg = args[i];
104
+ const [flag, inline] = arg.split('=', 2);
105
+ const value = () => inline ?? args[++i];
106
+ if (flag === '--host')
107
+ out.host = value();
108
+ else if (flag === '--source-dir')
109
+ out.sourceDir = value();
110
+ else if (flag === '--env')
111
+ out.env = true;
112
+ else if (flag === '--pwa')
113
+ out.pwa = true;
114
+ else if (flag === '--backend')
115
+ out.backend = true;
116
+ else if (flag === '--replace')
117
+ out.replace = true;
118
+ }
119
+ return out;
120
+ }
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { argv, exit, stdout } from 'node:process';
15
15
  import { DEFAULT_COMPILER, HELP, HOSTS, publishedCore, assertInsideCwd, assertUsableName, defaultCore, parseArgs, VALIDATIONS } from './options.js';
16
16
  import { Prompter, bold, cyan, dim } from './prompt.js';
17
17
  import { checkForNewer, notifyIfStale, selfVersion } from './stale.js';
18
+ import { agentsOptions, agentsSection } from './agents.js';
18
19
  import * as t from './templates.js';
19
20
  import { setUpShadcn } from './shadcn.js';
20
21
  import { STARTER_ICONS } from './icons.js';
@@ -182,7 +183,13 @@ function write(o) {
182
183
  ['vite.config.ts', t.viteConfig(o)],
183
184
  ['.gitignore', t.gitignore],
184
185
  ['README.md', t.readme(o)],
185
- ['AGENTS.md', t.agents(o)],
186
+ // Between markers, stamped: `rsc-kit agents` can bring it up to date.
187
+ ['AGENTS.md', agentsSection(agentsOptions(o))],
188
+ // The client every action and query is built from, for an app whose
189
+ // actions are its own JavaScript - so the check is there before the first.
190
+ ...(o.host !== 'laravel' && !o.backend
191
+ ? [[`${o.sourceDir}/server/client.ts`, t.actionClient()]]
192
+ : []),
186
193
  ['.mcp.json', t.mcp(o)],
187
194
  ['src/app/layout.tsx', t.layout(o)],
188
195
  ['src/app/page.tsx', t.page(o)],
package/dist/init.js CHANGED
@@ -17,6 +17,7 @@ import { DEFAULT_COMPILER, parseArgs, publishedCore } from './options.js';
17
17
  import { setUpShadcn } from './shadcn.js';
18
18
  import { Prompter, bold, cyan, dim } from './prompt.js';
19
19
  import * as t from './templates.js';
20
+ import { agentsOptions, agentsSection, readStamp } from './agents.js';
20
21
  // Nothing here detects a host any more: a host is a Nitro preset, and Nitro
21
22
  // is added to whatever project this runs in.
22
23
  const HOST_PACKAGES = {};
@@ -228,9 +229,6 @@ function combine(name, theirs, ours) {
228
229
  * that quietly replaces a working build script loses someone's trust
229
230
  * permanently, and it only has to be wrong once.
230
231
  */
231
- /** Where this tool's section of an AGENTS.md begins and ends, so a second run finds it. */
232
- const AGENTS_START = '<!-- rsc-kit:start -->';
233
- const AGENTS_END = '<!-- rsc-kit:end -->';
234
232
  /**
235
233
  * This tool's instructions, added to an existing AGENTS.md or written as one.
236
234
  *
@@ -243,14 +241,14 @@ const AGENTS_END = '<!-- rsc-kit:end -->';
243
241
  function mergeAgents(dir, o) {
244
242
  const path = join(dir, 'AGENTS.md');
245
243
  if (!existsSync(path)) {
246
- writeFileSync(path, t.agents(o));
244
+ writeFileSync(path, agentsSection(agentsOptions(o)));
247
245
  return { kind: 'wrote', what: 'AGENTS.md' };
248
246
  }
249
247
  const existing = readFileSync(path, 'utf8');
250
- if (existing.includes(AGENTS_START) || /rsc-kit/i.test(existing)) {
251
- return { kind: 'skipped', what: 'AGENTS.md', detail: 'already covers rsc-kit' };
248
+ if (readStamp(existing) !== null || /rsc-kit/i.test(existing)) {
249
+ return { kind: 'skipped', what: 'AGENTS.md', detail: 'already covers rsc-kit - rsc-kit agents brings it up to date' };
252
250
  }
253
- writeFileSync(path, existing.replace(/\s*$/, '\n\n') + `${AGENTS_START}\n${t.agents(o).trim()}\n${AGENTS_END}\n`);
251
+ writeFileSync(path, existing.replace(/\s*$/, '\n\n') + agentsSection(agentsOptions(o)));
254
252
  return { kind: 'merged', what: 'AGENTS.md', detail: 'added a section at the end; yours is untouched above it' };
255
253
  }
256
254
  /**
@@ -169,3 +169,9 @@ export declare function backendEnvExample(backend: string): string;
169
169
  export declare function backendStep(go: boolean): string;
170
170
  /** The example beside it - the one file that is committed. */
171
171
  export declare const envExample = "# Copy to .env and fill in. Server variables never reach the browser;\n# a browser-readable one starts with VITE_. 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# VITE_SITE_URL=\n";
172
+ /**
173
+ * The action client every action and query is built from - for an app whose
174
+ * actions are JavaScript. One place for the check, so a new action cannot be
175
+ * written without it.
176
+ */
177
+ export declare function actionClient(): string;
package/dist/templates.js CHANGED
@@ -624,6 +624,46 @@ The \`rsc-kit\` MCP server in \`.mcp.json\` answers from this project's last
624
624
  build — which routes froze and why, what is heaviest — and has the long-form
625
625
  recipe for anything here (\`how_to\`). Ask it before guessing.
626
626
 
627
+ ## Rules
628
+
629
+ Follow these on every change. Each is explained further down.
630
+
631
+ ${o.host === 'laravel'
632
+ ? `- **Run \`${pm} typecheck\` before saying it is done**, and test the PHP side with Pest.`
633
+ : `- **A change is not done without its test**, and not done until \`${pm} check\` passes.
634
+ A route, action, query or api route that is added or changed gets one.`}
635
+ ${o.host === 'laravel'
636
+ ? `- **Configuration is Laravel's**: a new setting goes in \`config/\` and \`.env.example\`.`
637
+ : `- **Every environment variable is declared in \`${o.sourceDir}/env.ts\`** - server or
638
+ \`VITE_\` client schema - and in \`.env.example\`, rsc-kit's own included
639
+ (\`RSC_SIGNING_SECRET\`, once a page uses \`refreshOn\`). Server code reads
640
+ \`env\` from it, never \`process.env\`; a client component reads its \`VITE_\` value
641
+ from \`import.meta.env\`, already checked there.${o.env ? '' : ` No \`env.ts\` yet? \`how_to({ topic: 'env' })\` sets it up.`}`}
642
+ - **Pages paint at once.** A page is synchronous; each read is an async
643
+ component under its own \`<Suspense>\`. Reach for \`loading.tsx\` only when a
644
+ route's whole body is one read.
645
+ - **Server components call the data function directly** - never \`fetch\` this
646
+ app's own api routes from the server.
647
+ - **Live data**: \`refreshOn\` on the section and \`changed()\` where the data
648
+ changes. \`usePolling\` only for data nothing can announce. Never
649
+ \`setInterval\` with \`refresh()\`.
650
+ ${o.host === 'laravel' || o.backend
651
+ ? `- **Authorisation lives in the backend**, in the function the call reaches${o.host === 'laravel' ? ' - \`#[Authenticated]\`, \`#[Can]\`, a policy' : ' - its guard or the handler'} - never only in the component that shows the button.`
652
+ : `- **Every action and query is built from the client in \`${o.sourceDir}/server/client.ts\`**,
653
+ and authorisation is its \`.use()\` middleware - never a check written in a
654
+ component, and never a bare \`'use server'\` function for anything that writes.`}
655
+ - **Links and redirects are typed**: \`\`<Link href={\`/posts/\${slug}\`}>\`\`, a template
656
+ literal, so a renamed route fails the typecheck. Never build urls by
657
+ concatenation.
658
+ - **Metadata is \`export const metadata\`** (or \`generateMetadata\`), never tags in
659
+ a page's markup.
660
+ - **Never edit generated files**: \`.rsc-kit/\`, \`${o.sourceDir}/server-actions.generated.ts\`,
661
+ \`rsc-host.json\`. Change what they are generated from.
662
+ - **Secrets are generated, never invented or committed**: \`openssl rand -base64 32\`,
663
+ kept in \`.env\` (ignored) and the platform's secret settings.
664
+ - **Ask before guessing**: \`how_to\` has the pattern for anything here; a
665
+ pattern from another framework is usually wrong here.
666
+
627
667
  ## Commands
628
668
 
629
669
  \`\`\`sh
@@ -803,6 +843,25 @@ A real \`Request\` in, a real \`Response\` out. Await \`params\`,
803
843
  They run their directory's \`middleware.ts\`, and a \`GET\` that reads nothing
804
844
  from the request is answered from disk.
805
845
 
846
+ ## Live data
847
+
848
+ A section that names what it depends on refreshes in every open tab when
849
+ something says that changed - a webhook, a job, another visitor:
850
+
851
+ \`\`\`tsx
852
+ export default section('repos', Repos, { refreshOn: ({ params }) => [\`team:\${params.team}:repos\`] })
853
+ \`\`\`
854
+
855
+ \`\`\`ts
856
+ await changed(\`team:\${team}:repos\`) // after the write commits
857
+ \`\`\`
858
+
859
+ Name the data with ids, as narrowly as it changes. More than one process
860
+ needs a shared store, and production needs \`RSC_SIGNING_SECRET\` - ${o.host === 'laravel'
861
+ ? 'in the renderer\'s environment, beside the host-call secret, and in `.env.example`:'
862
+ : 'declared in `env.ts`, required in production, so a deploy without it fails at startup:'}
863
+ \`how_to({ topic: 'refresh-on' })\`, or the Live data guide.
864
+
806
865
  ## Things that are not this project
807
866
 
808
867
  - No \`pages/\` directory, no \`_app\`, no \`getStaticProps\`.
@@ -1047,3 +1106,45 @@ export const envExample = `# Copy to .env and fill in. Server variables never re
1047
1106
  # SESSION_SECRET=
1048
1107
  # VITE_SITE_URL=
1049
1108
  `;
1109
+ /**
1110
+ * The action client every action and query is built from - for an app whose
1111
+ * actions are JavaScript. One place for the check, so a new action cannot be
1112
+ * written without it.
1113
+ */
1114
+ export function actionClient() {
1115
+ return `'use server'
1116
+
1117
+ import { createActionClient } from '@rsc-kit/core/action'
1118
+ import { ServerAuthenticationError } from '@rsc-kit/core/errors'
1119
+
1120
+ // Every action and query in this app is built from one of these clients, so
1121
+ // the check runs before the body and cannot be forgotten:
1122
+ //
1123
+ // export const createPost = authedClient.input(schema).handler(async ({ input, ctx }) => …)
1124
+ // export const getPosts = publicClient.query(async () => …)
1125
+ //
1126
+ // An action is a public endpoint - anyone can call it, with any arguments -
1127
+ // so the check belongs here, never in the component that shows the button.
1128
+
1129
+ /** Anyone may call it: public reads, a contact form. */
1130
+ export const publicClient = createActionClient()
1131
+
1132
+ /** Only someone signed in; ctx.user is theirs inside the action. */
1133
+ export const authedClient = publicClient.use(async ({ next }) => {
1134
+ const user = await currentUser()
1135
+
1136
+ if (!user) throw new ServerAuthenticationError()
1137
+
1138
+ return next({ ctx: { user } })
1139
+ })
1140
+
1141
+ /**
1142
+ * Who is calling - connect this to your session: read its cookie with
1143
+ * cookies() from '@rsc-kit/core/request', and look the user up. Until then
1144
+ * nobody is signed in, and authedClient refuses every call.
1145
+ */
1146
+ async function currentUser(): Promise<{ id: string } | null> {
1147
+ return null
1148
+ }
1149
+ `;
1150
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-rsc-kit",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "description": "Scaffold an RSC app on Bun, Hono, Elysia or Node.",
6
6
  "bin": {
@@ -38,6 +38,10 @@
38
38
  "./stale": {
39
39
  "types": "./dist/stale.d.ts",
40
40
  "default": "./dist/stale.js"
41
+ },
42
+ "./agents": {
43
+ "types": "./dist/agents.d.ts",
44
+ "default": "./dist/agents.js"
41
45
  }
42
46
  },
43
47
  "repository": {