create-rsc-kit 0.18.1 → 0.19.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 +7 -0
- package/dist/init.js +98 -17
- package/dist/templates.d.ts +12 -0
- package/dist/templates.js +41 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -158,6 +158,13 @@ function write(o) {
|
|
|
158
158
|
['src/components/Counter.tsx', t.counter(o)],
|
|
159
159
|
['tests/app.test.ts', t.smokeTest(o)],
|
|
160
160
|
];
|
|
161
|
+
// Bun's test runner reads the real server-only package, which throws on
|
|
162
|
+
// import; a preload stubs it so an action file that carries the line is
|
|
163
|
+
// still a function a test can call.
|
|
164
|
+
if (o.host !== 'node') {
|
|
165
|
+
files.push(['bunfig.toml', t.bunfig]);
|
|
166
|
+
files.push(['tests/preload.ts', t.testPreload]);
|
|
167
|
+
}
|
|
161
168
|
if (o.tailwind)
|
|
162
169
|
files.push(['src/app/styles.css', t.styles]);
|
|
163
170
|
if (o.env) {
|
package/dist/init.js
CHANGED
|
@@ -182,6 +182,59 @@ function combine(name, theirs, ours) {
|
|
|
182
182
|
* that quietly replaces a working build script loses someone's trust
|
|
183
183
|
* permanently, and it only has to be wrong once.
|
|
184
184
|
*/
|
|
185
|
+
/** Where this tool's section of an AGENTS.md begins and ends, so a second run finds it. */
|
|
186
|
+
const AGENTS_START = '<!-- rsc-kit:start -->';
|
|
187
|
+
const AGENTS_END = '<!-- rsc-kit:end -->';
|
|
188
|
+
/**
|
|
189
|
+
* This tool's instructions, added to an existing AGENTS.md or written as one.
|
|
190
|
+
*
|
|
191
|
+
* Someone else's instructions are not a file to rewrite, but a delimited
|
|
192
|
+
* section at the end is not a rewrite: theirs stay exactly as written above
|
|
193
|
+
* it, the markers say what is ours, and a second run finds the markers and
|
|
194
|
+
* leaves it be. A project that already has an AGENTS.md is the project whose
|
|
195
|
+
* agents most need to hear how this works.
|
|
196
|
+
*/
|
|
197
|
+
function mergeAgents(dir, o) {
|
|
198
|
+
const path = join(dir, 'AGENTS.md');
|
|
199
|
+
if (!existsSync(path)) {
|
|
200
|
+
writeFileSync(path, t.agents(o));
|
|
201
|
+
return { kind: 'wrote', what: 'AGENTS.md' };
|
|
202
|
+
}
|
|
203
|
+
const existing = readFileSync(path, 'utf8');
|
|
204
|
+
if (existing.includes(AGENTS_START) || /rsc-kit/i.test(existing)) {
|
|
205
|
+
return { kind: 'skipped', what: 'AGENTS.md', detail: 'already covers rsc-kit' };
|
|
206
|
+
}
|
|
207
|
+
writeFileSync(path, existing.replace(/\s*$/, '\n\n') + `${AGENTS_START}\n${t.agents(o).trim()}\n${AGENTS_END}\n`);
|
|
208
|
+
return { kind: 'merged', what: 'AGENTS.md', detail: 'added a section at the end; yours is untouched above it' };
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The rsc-kit server, added to an existing .mcp.json or written as a new one.
|
|
212
|
+
*
|
|
213
|
+
* The file is a map of servers under `mcpServers`; a project with other
|
|
214
|
+
* servers keeps them, and one that already lists rsc-kit is left alone. A
|
|
215
|
+
* file that does not parse is not one to rewrite.
|
|
216
|
+
*/
|
|
217
|
+
function mergeMcp(dir, o) {
|
|
218
|
+
const path = join(dir, '.mcp.json');
|
|
219
|
+
if (!existsSync(path)) {
|
|
220
|
+
writeFileSync(path, t.mcp(o));
|
|
221
|
+
return { kind: 'wrote', what: '.mcp.json' };
|
|
222
|
+
}
|
|
223
|
+
let existing;
|
|
224
|
+
try {
|
|
225
|
+
existing = JSON.parse(readFileSync(path, 'utf8'));
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return { kind: 'skipped', what: '.mcp.json', detail: 'already exists and is not JSON; add the rsc-kit server by hand' };
|
|
229
|
+
}
|
|
230
|
+
if (existing.mcpServers?.['rsc-kit']) {
|
|
231
|
+
return { kind: 'skipped', what: '.mcp.json', detail: 'already lists rsc-kit' };
|
|
232
|
+
}
|
|
233
|
+
const ours = JSON.parse(t.mcp(o)).mcpServers['rsc-kit'];
|
|
234
|
+
existing.mcpServers = { ...(existing.mcpServers ?? {}), 'rsc-kit': ours };
|
|
235
|
+
writeFileSync(path, JSON.stringify(existing, null, 2) + '\n');
|
|
236
|
+
return { kind: 'merged', what: '.mcp.json', detail: 'added the rsc-kit server beside the others' };
|
|
237
|
+
}
|
|
185
238
|
function mergeScripts(o, found) {
|
|
186
239
|
const pkg = found.packageJson;
|
|
187
240
|
const scripts = pkg.scripts ?? {};
|
|
@@ -315,18 +368,17 @@ function manualPluginStep(o, existing) {
|
|
|
315
368
|
function routes(o, dir) {
|
|
316
369
|
const appDir = join(dir, o.sourceDir, 'app');
|
|
317
370
|
const steps = [];
|
|
371
|
+
// The two files beside the tree are looked at whether or not the tree is
|
|
372
|
+
// here: a project that already has pages is the one whose agents and
|
|
373
|
+
// editor most need to know about this.
|
|
374
|
+
steps.push(mergeAgents(dir, o), mergeMcp(dir, o));
|
|
318
375
|
if (existsSync(join(appDir, 'layout.tsx')) || existsSync(join(appDir, 'page.tsx'))) {
|
|
319
|
-
return [{ kind: 'skipped', what: `${o.sourceDir}/app`, detail: 'a route tree is already here' }];
|
|
376
|
+
return [...steps, { kind: 'skipped', what: `${o.sourceDir}/app`, detail: 'a route tree is already here' }];
|
|
320
377
|
}
|
|
321
378
|
const files = [
|
|
322
379
|
[join(o.sourceDir, 'app/layout.tsx'), t.layout(o)],
|
|
323
380
|
[join(o.sourceDir, 'app/page.tsx'), t.page(o)],
|
|
324
381
|
[join(o.sourceDir, 'components/Counter.tsx'), t.counter(o)],
|
|
325
|
-
// Beside the route tree rather than merged into an existing AGENTS.md: a
|
|
326
|
-
// file of someone else's instructions is not one to append to blind, and
|
|
327
|
-
// the loop below skips it if it is already there.
|
|
328
|
-
['AGENTS.md', t.agents(o)],
|
|
329
|
-
['.mcp.json', t.mcp(o)],
|
|
330
382
|
];
|
|
331
383
|
if (o.tailwind)
|
|
332
384
|
files.push([join(o.sourceDir, 'app/styles.css'), t.styles]);
|
|
@@ -401,10 +453,10 @@ function tsconfig(o, found, dir) {
|
|
|
401
453
|
writeFileSync(path, t.tsconfig(o));
|
|
402
454
|
return [{ kind: 'wrote', what: 'tsconfig.json' }];
|
|
403
455
|
}
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
456
|
+
// Edited in place, never reprinted: parsing and printing the file would
|
|
457
|
+
// lose the comments a tsconfig is allowed to have and the formatting
|
|
458
|
+
// someone chose. The entry goes in as text, at the front of the include
|
|
459
|
+
// list that is there, or as a list of its own after the opening brace.
|
|
408
460
|
const current = readFileSync(path, 'utf-8');
|
|
409
461
|
// Comments are legal here and JSON.parse does not take them. Stripped
|
|
410
462
|
// outside strings only: a glob like ".rsc-kit/**/*" holds "/*", and a
|
|
@@ -428,13 +480,25 @@ function tsconfig(o, found, dir) {
|
|
|
428
480
|
// with a dot is outside it. So both cases need the entry, and a project with
|
|
429
481
|
// no `include` needs `**\/*` written alongside it or it loses everything
|
|
430
482
|
// else. Measured both ways.
|
|
483
|
+
const opened = /"include"\s*:\s*\[/.exec(current);
|
|
484
|
+
if (include && opened) {
|
|
485
|
+
const at = opened.index + opened[0].length;
|
|
486
|
+
const rest = current.slice(at);
|
|
487
|
+
// The list's own style: one entry per line, or all on one.
|
|
488
|
+
const separator = /^\s*\n/.test(rest) ? rest.match(/^\s*\n(\s*)/)[0] : ' ';
|
|
489
|
+
writeFileSync(path, current.slice(0, at) + `${separator}"${TYPES_GLOB}",` + (separator === ' ' ? ' ' : '') + rest.replace(/^\s*\n/, ''));
|
|
490
|
+
return [{ kind: 'merged', what: 'tsconfig.json', detail: `added "${TYPES_GLOB}" to "include"` }];
|
|
491
|
+
}
|
|
492
|
+
const brace = current.indexOf('{');
|
|
493
|
+
if (!include && brace !== -1) {
|
|
494
|
+
writeFileSync(path, current.slice(0, brace + 1) + `\n "include": ["**/*", "${TYPES_GLOB}"],` + current.slice(brace + 1));
|
|
495
|
+
return [{ kind: 'merged', what: 'tsconfig.json', detail: `added "include": ["**/*", "${TYPES_GLOB}"]` }];
|
|
496
|
+
}
|
|
431
497
|
return [
|
|
432
498
|
{
|
|
433
499
|
kind: 'manual',
|
|
434
500
|
what: 'tsconfig.json',
|
|
435
|
-
detail: include
|
|
436
|
-
? `add "${TYPES_GLOB}" to "include", or typed routes fall back to string`
|
|
437
|
-
: `add "include": ["**/*", "${TYPES_GLOB}"], or typed routes fall back to string`,
|
|
501
|
+
detail: `add "${TYPES_GLOB}" to "include", or typed routes fall back to string`,
|
|
438
502
|
},
|
|
439
503
|
];
|
|
440
504
|
}
|
|
@@ -483,7 +547,23 @@ function backend(o, found, dir) {
|
|
|
483
547
|
const steps = [];
|
|
484
548
|
const env = join(dir, '.env');
|
|
485
549
|
if (existsSync(env)) {
|
|
486
|
-
|
|
550
|
+
// The two lines, added to what is there. A secret already present is
|
|
551
|
+
// the one the backend was given and is never regenerated; a backend
|
|
552
|
+
// already named is left as named.
|
|
553
|
+
const current = readFileSync(env, 'utf8');
|
|
554
|
+
const missing = [];
|
|
555
|
+
if (!/^\s*RSC_BACKEND=/m.test(current))
|
|
556
|
+
missing.push(`RSC_BACKEND=${o.backend}`);
|
|
557
|
+
if (!/^\s*RSC_HOST_CALL_SECRET=/m.test(current)) {
|
|
558
|
+
missing.push(`RSC_HOST_CALL_SECRET=${randomBytes(32).toString('base64url')}`);
|
|
559
|
+
}
|
|
560
|
+
if (missing.length === 0) {
|
|
561
|
+
steps.push({ kind: 'skipped', what: '.env', detail: 'already has RSC_BACKEND and RSC_HOST_CALL_SECRET' });
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
writeFileSync(env, current.replace(/\s*$/, '\n\n') + missing.join('\n') + '\n');
|
|
565
|
+
steps.push({ kind: 'merged', what: '.env', detail: `added ${missing.map((line) => line.split('=')[0]).join(' and ')}` });
|
|
566
|
+
}
|
|
487
567
|
}
|
|
488
568
|
else {
|
|
489
569
|
writeFileSync(env, t.backendEnv(o.backend, randomBytes(32).toString('base64url')));
|
|
@@ -515,9 +595,10 @@ const DEFAULT_BACKEND = 'http://127.0.0.1:8080';
|
|
|
515
595
|
const INIT_HELP = `
|
|
516
596
|
rsc-kit init — add RSC to the project in this directory
|
|
517
597
|
|
|
518
|
-
Nothing existing is
|
|
519
|
-
|
|
520
|
-
|
|
598
|
+
Nothing existing is rewritten. New files are written; a file that is a list
|
|
599
|
+
gets our entry added to it - .mcp.json, AGENTS.md, tsconfig.json's include,
|
|
600
|
+
.env - with yours left as written; and for anything else already there the
|
|
601
|
+
exact edit is printed for you to make.
|
|
521
602
|
|
|
522
603
|
Options
|
|
523
604
|
--source-dir <dir> where app/ should live (detected, usually src)
|
package/dist/templates.d.ts
CHANGED
|
@@ -66,6 +66,18 @@ export declare function layout(o: Options): string;
|
|
|
66
66
|
* the app, and what gets chosen is a port, a spawned server, and a sleep. This
|
|
67
67
|
* is the shape instead: the deployed handler, a Request in, a Response out.
|
|
68
68
|
*/
|
|
69
|
+
/** Bun's test settings: the preload below, before every test file. */
|
|
70
|
+
export declare const bunfig = "[test]\npreload = [\"./tests/preload.ts\"]\n";
|
|
71
|
+
/**
|
|
72
|
+
* What every test file sees first.
|
|
73
|
+
*
|
|
74
|
+
* `import 'server-only'` is honoured by the build - a client file importing
|
|
75
|
+
* the module fails to build rather than shipping a secret - and resolves to
|
|
76
|
+
* nothing on the server. Under bun test it is the real package, which throws
|
|
77
|
+
* on import, so an action or a route that carries the line would not be a
|
|
78
|
+
* function a test can call. Stubbed here, once.
|
|
79
|
+
*/
|
|
80
|
+
export declare const testPreload = "import { mock } from 'bun:test'\n\n// The build honours this import and resolves it to nothing on the server;\n// the real package throws when imported, which is what a test would hit.\nmock.module('server-only', () => ({}))\nmock.module('client-only', () => ({}))\n";
|
|
69
81
|
export declare function smokeTest(o: Options): string;
|
|
70
82
|
export declare function page(o: Options): string;
|
|
71
83
|
export declare function counter(o: Options): string;
|
package/dist/templates.js
CHANGED
|
@@ -92,12 +92,17 @@ export function scripts(o) {
|
|
|
92
92
|
// every asset — the static path resolves into Bun's virtual
|
|
93
93
|
// filesystem, where the files on disk are not.
|
|
94
94
|
//
|
|
95
|
+
// compile.mjs rather than index.mjs: the build writes it beside the
|
|
96
|
+
// server, and it is what puts the frozen pages inside the binary -
|
|
97
|
+
// imported by name, which a compile embeds, where the server's own
|
|
98
|
+
// computed import is invisible to it.
|
|
99
|
+
//
|
|
95
100
|
// Into dist/, which is already ignored. Named after the project it
|
|
96
101
|
// landed a 63MB binary in the root of, next to the source, with
|
|
97
102
|
// nothing in .gitignore covering it.
|
|
98
103
|
...(o.host === 'bun'
|
|
99
104
|
? {
|
|
100
|
-
compile: `${vite} build && bun build --compile .output/server/
|
|
105
|
+
compile: `${vite} build && bun build --compile .output/server/compile.mjs --outfile dist/app`,
|
|
101
106
|
}
|
|
102
107
|
: {}),
|
|
103
108
|
}),
|
|
@@ -326,6 +331,26 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|
|
326
331
|
* the app, and what gets chosen is a port, a spawned server, and a sleep. This
|
|
327
332
|
* is the shape instead: the deployed handler, a Request in, a Response out.
|
|
328
333
|
*/
|
|
334
|
+
/** Bun's test settings: the preload below, before every test file. */
|
|
335
|
+
export const bunfig = `[test]
|
|
336
|
+
preload = ["./tests/preload.ts"]
|
|
337
|
+
`;
|
|
338
|
+
/**
|
|
339
|
+
* What every test file sees first.
|
|
340
|
+
*
|
|
341
|
+
* `import 'server-only'` is honoured by the build - a client file importing
|
|
342
|
+
* the module fails to build rather than shipping a secret - and resolves to
|
|
343
|
+
* nothing on the server. Under bun test it is the real package, which throws
|
|
344
|
+
* on import, so an action or a route that carries the line would not be a
|
|
345
|
+
* function a test can call. Stubbed here, once.
|
|
346
|
+
*/
|
|
347
|
+
export const testPreload = `import { mock } from 'bun:test'
|
|
348
|
+
|
|
349
|
+
// The build honours this import and resolves it to nothing on the server;
|
|
350
|
+
// the real package throws when imported, which is what a test would hit.
|
|
351
|
+
mock.module('server-only', () => ({}))
|
|
352
|
+
mock.module('client-only', () => ({}))
|
|
353
|
+
`;
|
|
329
354
|
export function smokeTest(o) {
|
|
330
355
|
const runner = o.host === 'node'
|
|
331
356
|
? `import { before, describe, test } from 'node:test'
|
|
@@ -618,6 +643,13 @@ it before any page. ${o.env
|
|
|
618
643
|
render waits for it). Do not import a bootstrap module from pages to get the
|
|
619
644
|
same effect; it depends on nobody forgetting.
|
|
620
645
|
|
|
646
|
+
## Forms
|
|
647
|
+
|
|
648
|
+
Uncontrolled. Inputs keep their value in the DOM, an initial value is
|
|
649
|
+
\`defaultValue\`, and the action reads \`FormData\`. Do not write \`useState\` +
|
|
650
|
+
\`value\`/\`onChange\` per input. Control one field only when the UI must react
|
|
651
|
+
as the user types, and bind that one with \`useField\`.
|
|
652
|
+
|
|
621
653
|
## Data
|
|
622
654
|
|
|
623
655
|
Fetch in a server component and await it. There is no loader and no
|
|
@@ -691,7 +723,8 @@ not hand-parse \`Number(searchParams.get('page'))\`.
|
|
|
691
723
|
|
|
692
724
|
\`${o.sourceDir}/app/**/route.ts\`, exporting \`GET\`, \`POST\` and so on.
|
|
693
725
|
A real \`Request\` in, a real \`Response\` out. Await \`params\`,
|
|
694
|
-
\`searchParams\` and \`body\` from the second argument
|
|
726
|
+
\`searchParams\` and \`body\` from the second argument - never
|
|
727
|
+
\`new URL(request.url).searchParams\`, which the build cannot see.
|
|
695
728
|
|
|
696
729
|
They run their directory's \`middleware.ts\`, and a \`GET\` that reads nothing
|
|
697
730
|
from the request is answered from disk.
|
|
@@ -820,6 +853,8 @@ import { createEnv } from '@t3-oss/env-core'
|
|
|
820
853
|
// to start with PUBLIC_, and is read from import.meta.env, which is what Vite
|
|
821
854
|
// exposes there. Add a variable: one line in the schema, and every reader is
|
|
822
855
|
// typed.
|
|
856
|
+
const processEnv: Record<string, string | undefined> = typeof process === 'undefined' ? {} : process.env
|
|
857
|
+
|
|
823
858
|
export const env = createEnv({
|
|
824
859
|
server: {
|
|
825
860
|
NODE_ENV: ${lib.opt},
|
|
@@ -830,11 +865,13 @@ export const env = createEnv({
|
|
|
830
865
|
client: {
|
|
831
866
|
// PUBLIC_SITE_URL: ${lib.url},
|
|
832
867
|
},
|
|
833
|
-
|
|
868
|
+
// process is the server's; a "use client" file importing this for a
|
|
869
|
+
// PUBLIC_ value has only import.meta.env, and Vite fills the PUBLIC_ ones.
|
|
870
|
+
runtimeEnv: { ...processEnv, ...import.meta.env },
|
|
834
871
|
emptyStringAsUndefined: true,
|
|
835
872
|
// A build machine without the production variables: SKIP_ENV_VALIDATION=1
|
|
836
873
|
// builds anyway, and the server that runs the build validates at startup.
|
|
837
|
-
skipValidation: !!
|
|
874
|
+
skipValidation: !!processEnv.SKIP_ENV_VALIDATION,
|
|
838
875
|
})
|
|
839
876
|
`;
|
|
840
877
|
}
|