create-rsc-kit 0.1.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/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # create-rsc-kit
2
+
3
+ Scaffold a React Server Components app that builds and runs before you edit it.
4
+
5
+ ```sh
6
+ bun create rsc-kit my-app
7
+ ```
8
+
9
+ Asks which server (Bun, Hono, Elysia or `node:http`), whether you want the React
10
+ Compiler, and whether to include Tailwind. Every answer has a flag, so it runs
11
+ unattended too:
12
+
13
+ ```sh
14
+ bun create rsc-kit my-app --host=hono --compiler=oxc --tailwind
15
+ ```
16
+
17
+ The point is not the typing it saves. Several things in this setup fail by
18
+ producing an app that looks nearly right — a server that serves a development
19
+ payload to a production client, a stylesheet with no server-component classes
20
+ in it, an engine declaration that typechecks the server and fails the prerender.
21
+ What comes out has those right.
22
+
23
+ Docs: https://rsc-kit.dev · Licence: MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ // create-rsc-kit — scaffold an app that builds and runs before it is edited.
3
+ //
4
+ // The point is not saving typing. It is that the combination of choices has
5
+ // several things in it that are invisible when wrong: NODE_ENV on the build,
6
+ // @source for Tailwind, the ambient declaration for the generated engine
7
+ // bundle. Each of those fails by producing a page that looks nearly right.
8
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { argv, exit, stdout } from 'node:process';
13
+ import { DEFAULT_COMPILER, HELP, HOSTS, PUBLISHED_CORE, assertInsideCwd, assertUsableName, defaultCore, parseArgs, } from './options.js';
14
+ import { Prompter, bold, cyan, dim } from './prompt.js';
15
+ import * as t from './templates.js';
16
+ const flags = parseArgs(argv.slice(2));
17
+ if (flags.help) {
18
+ stdout.write(HELP);
19
+ exit(0);
20
+ }
21
+ const unattended = flags.host !== undefined;
22
+ // Resolved once: it walks up from this file looking for the sibling package.
23
+ const core = flags.core ?? defaultCore(dirname(fileURLToPath(import.meta.url)));
24
+ const options = await collect();
25
+ try {
26
+ write(options);
27
+ }
28
+ catch (error) {
29
+ // A refusal is a message, not a crash dump. Everything thrown from write()
30
+ // is a decision this tool made deliberately — an unusable name, a directory
31
+ // outside where it was run — and a stack trace buries the sentence that
32
+ // says which.
33
+ stdout.write(`\n${bold('Cannot scaffold here.')}\n ${error.message}\n\n`);
34
+ exit(1);
35
+ }
36
+ if (options.git)
37
+ run('git', ['init', '--quiet'], options.dir);
38
+ if (options.install) {
39
+ stdout.write(`\n${dim('Installing dependencies…')}\n`);
40
+ const pm = options.host === 'node' ? 'npm' : 'bun';
41
+ const ok = run(pm, ['install'], options.dir);
42
+ if (!ok) {
43
+ stdout.write(`\n${bold('Dependencies did not install.')} The files are written; run the install yourself.\n` +
44
+ (options.core === PUBLISHED_CORE
45
+ ? dim(' @rsc-kit/core may not be published yet — pass --core=file:<path> to use a local checkout.\n')
46
+ : ''));
47
+ }
48
+ }
49
+ report(options);
50
+ // ── ─────────────────────────────────────────────────────────────────────────
51
+ /** Declared, not assigned: collect() runs at module top level, above this. */
52
+ function basename(path) {
53
+ return path.replace(/\/+$/, '').split('/').pop() || 'app';
54
+ }
55
+ async function collect() {
56
+ if (unattended) {
57
+ const dir = flags.dir ?? 'my-app';
58
+ return {
59
+ dir: resolve(dir),
60
+ name: basename(dir),
61
+ host: flags.host,
62
+ compiler: flags.compiler ?? 'none',
63
+ tailwind: flags.tailwind ?? true,
64
+ lint: flags.lint ?? true,
65
+ sourceDir: flags.sourceDir ?? 'src',
66
+ install: flags.install ?? true,
67
+ git: flags.git ?? true,
68
+ core,
69
+ };
70
+ }
71
+ // Nothing is attached to answer. readline would simply never resolve, and a
72
+ // scaffolder that hangs with no output is indistinguishable from a slow one.
73
+ if (!process.stdin.isTTY) {
74
+ stdout.write(`\n${bold('No terminal to ask questions on.')}\n` +
75
+ dim(' Pass the answers as flags instead, e.g.\n') +
76
+ ` ${cyan('create-rsc-kit my-app --host=bun --compiler=none --no-tailwind')}\n` +
77
+ HELP);
78
+ exit(1);
79
+ }
80
+ stdout.write(`\n${bold('Create an RSC app')}\n\n`);
81
+ const p = new Prompter();
82
+ try {
83
+ const dir = flags.dir ?? (await p.text('Directory', 'my-app'));
84
+ const host = await p.select('Server', HOSTS);
85
+ // Yes/no, not which: see DEFAULT_COMPILER.
86
+ const compiler = flags.compiler ?? ((await p.confirm('React Compiler', true)) ? DEFAULT_COMPILER : 'none');
87
+ const tailwind = flags.tailwind ?? (await p.confirm('Tailwind CSS', true));
88
+ const lint = flags.lint ?? (await p.confirm('oxlint', true));
89
+ return {
90
+ dir: resolve(dir),
91
+ name: basename(dir),
92
+ host,
93
+ compiler,
94
+ tailwind,
95
+ lint,
96
+ sourceDir: flags.sourceDir ?? 'src',
97
+ install: flags.install ?? (await p.confirm('Install dependencies now', true)),
98
+ git: flags.git ?? true,
99
+ core,
100
+ };
101
+ }
102
+ finally {
103
+ p.close();
104
+ }
105
+ }
106
+ function write(o) {
107
+ // Checked before anything is written: the name is interpolated into
108
+ // generated source, and the directory decides where that source lands.
109
+ assertUsableName(o.name);
110
+ assertInsideCwd(o.dir, process.cwd());
111
+ // An existing directory is fine; an existing *app* is not. Overwriting
112
+ // someone's package.json to scaffold over it is not recoverable.
113
+ if (existsSync(join(o.dir, 'package.json'))) {
114
+ stdout.write(`\n${bold('There is already a package.json in ' + o.dir + '.')}\n`);
115
+ stdout.write(dim(' Choose an empty directory, or delete it first.\n'));
116
+ exit(1);
117
+ }
118
+ if (existsSync(o.dir) && readdirSync(o.dir).some((f) => !f.startsWith('.'))) {
119
+ stdout.write(`\n${dim(o.dir + ' is not empty; adding to it.')}\n`);
120
+ }
121
+ const files = [
122
+ ['package.json', t.packageJson(o)],
123
+ ['tsconfig.json', t.tsconfig(o)],
124
+ ['vite.config.ts', t.viteConfig(o)],
125
+ [t.serverFile(o.host), t.server(o.host)],
126
+ ['.gitignore', t.gitignore],
127
+ ['README.md', t.readme(o)],
128
+ ['src/app/layout.tsx', t.layout(o)],
129
+ ['src/app/page.tsx', t.page(o)],
130
+ ['src/components/Counter.tsx', t.counter(o)],
131
+ ];
132
+ if (o.tailwind)
133
+ files.push(['src/app/styles.css', t.styles]);
134
+ if (o.lint)
135
+ files.push(['.oxlintrc.json', t.oxlintConfig(o)]);
136
+ const replaced = [];
137
+ for (const [path, contents] of files) {
138
+ const full = join(o.dir, path);
139
+ // wx: create, or fail. Scaffolding into a directory that already holds a
140
+ // README, a .gitignore or a vite config used to replace them silently —
141
+ // and a .gitignore is exactly the file whose loss is noticed late. The
142
+ // package.json guard above does not cover them, and a directory holding
143
+ // only dotfiles did not even produce the "not empty" notice.
144
+ mkdirSync(dirname(full), { recursive: true });
145
+ try {
146
+ writeFileSync(full, contents, { flag: 'wx' });
147
+ }
148
+ catch (error) {
149
+ if (error.code !== 'EEXIST')
150
+ throw error;
151
+ replaced.push(path);
152
+ }
153
+ }
154
+ if (replaced.length > 0) {
155
+ stdout.write(`\n${bold('Left alone, because they already exist:')}\n`);
156
+ for (const path of replaced)
157
+ stdout.write(` ${dim(path)}\n`);
158
+ }
159
+ }
160
+ function run(command, args, cwd) {
161
+ const result = spawnSync(command, args, { cwd, stdio: 'inherit' });
162
+ return result.status === 0;
163
+ }
164
+ function report(o) {
165
+ const pm = o.host === 'node' ? 'npm run' : 'bun run';
166
+ const steps = [
167
+ `cd ${relativeish(o.dir)}`,
168
+ ...(o.install ? [] : [o.host === 'node' ? 'npm install' : 'bun install']),
169
+ `${pm} build`,
170
+ `${pm} start`,
171
+ ];
172
+ stdout.write(`\n${bold('Done.')} ${dim(o.dir)}\n\n`);
173
+ for (const step of steps)
174
+ stdout.write(` ${cyan(step)}\n`);
175
+ stdout.write(`\n${dim('The route tree is read at build time — rebuild after adding a page under src/app.')}\n\n`);
176
+ }
177
+ /** A path the user can paste, when it is under where they are. */
178
+ function relativeish(dir) {
179
+ const cwd = process.cwd();
180
+ return dir.startsWith(cwd + '/') ? dir.slice(cwd.length + 1) : dir;
181
+ }
package/dist/init.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { Host, Options } from './options.js';
2
+ export interface Detected {
3
+ /** What the project's package.json says it already depends on. */
4
+ deps: Record<string, string>;
5
+ host: Host | null;
6
+ sourceDir: string | null;
7
+ viteConfig: string | null;
8
+ hasReact: boolean;
9
+ hasTailwind: boolean;
10
+ hasTypeScript: boolean;
11
+ packageJson: Record<string, unknown>;
12
+ }
13
+ /** What a step did, for the report at the end. */
14
+ export interface Step {
15
+ kind: 'wrote' | 'merged' | 'manual' | 'skipped';
16
+ what: string;
17
+ detail?: string;
18
+ }
19
+ /**
20
+ * What is already here.
21
+ *
22
+ * Every answer is a guess the caller can override — the point is to not ask
23
+ * about things the project has already decided.
24
+ */
25
+ export declare function detect(dir: string): Detected;
26
+ /** Everything, in the order a reader would want to hear about it. */
27
+ export declare function initialise(o: Options, found: Detected, dir: string): Step[];
28
+ /**
29
+ * Add RSC to a project that already exists.
30
+ *
31
+ * Almost everything is detected rather than asked: which server the project
32
+ * already uses, where its source lives, whether React and Tailwind are already
33
+ * there. A question about something the project has already decided is a
34
+ * question with a wrong answer available.
35
+ */
36
+ export declare function runInit(args: string[]): Promise<void>;
package/dist/init.js ADDED
@@ -0,0 +1,286 @@
1
+ // Adding RSC to a project that already exists.
2
+ //
3
+ // Scaffolding writes whatever it likes into an empty directory. This cannot:
4
+ // the vite config, the server and package.json are already someone's, and they
5
+ // are the three files most likely to hold work that took a while to get right.
6
+ //
7
+ // So the rule here is that nothing existing is ever rewritten. New files are
8
+ // written, missing dependencies are added, and for anything already present
9
+ // the exact edit is printed for the reader to make. A tool that silently
10
+ // reformats a working server has to be right about more than it can know.
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { cwd, exit, stdout } from 'node:process';
14
+ import { DEFAULT_COMPILER, parseArgs } from './options.js';
15
+ import { Prompter, bold, cyan, dim } from './prompt.js';
16
+ import * as t from './templates.js';
17
+ const HOST_PACKAGES = { hono: 'hono', elysia: 'elysia' };
18
+ /**
19
+ * What is already here.
20
+ *
21
+ * Every answer is a guess the caller can override — the point is to not ask
22
+ * about things the project has already decided.
23
+ */
24
+ export function detect(dir) {
25
+ const pkgPath = join(dir, 'package.json');
26
+ const packageJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
27
+ const deps = {
28
+ ...(packageJson.dependencies ?? {}),
29
+ ...(packageJson.devDependencies ?? {}),
30
+ };
31
+ let host = null;
32
+ for (const [pkg, value] of Object.entries(HOST_PACKAGES)) {
33
+ if (deps[pkg])
34
+ host = value;
35
+ }
36
+ // No framework named, so it comes down to which runtime's types are here.
37
+ // Bun is the default because a project with neither is more likely to be
38
+ // reaching for this from Bun than from bare node:http.
39
+ if (!host)
40
+ host = deps['@types/node'] && !deps['@types/bun'] ? 'node' : 'bun';
41
+ return {
42
+ deps,
43
+ host,
44
+ sourceDir: ['src', 'app', 'resources/js'].find((d) => existsSync(join(dir, d))) ?? null,
45
+ viteConfig: ['vite.config.ts', 'vite.config.js', 'vite.config.mts'].find((f) => existsSync(join(dir, f))) ?? null,
46
+ hasReact: Boolean(deps.react),
47
+ hasTailwind: Boolean(deps.tailwindcss),
48
+ hasTypeScript: existsSync(join(dir, 'tsconfig.json')),
49
+ packageJson,
50
+ };
51
+ }
52
+ /**
53
+ * Add the dependencies the engine needs, without touching versions already
54
+ * chosen. A project on React 19.3 does not want to be pinned back to ours.
55
+ */
56
+ function mergeDependencies(o, found) {
57
+ const pkg = found.packageJson;
58
+ const wanted = JSON.parse(t.packageJson(o));
59
+ const steps = [];
60
+ const added = [];
61
+ for (const [field, incoming] of [
62
+ ['dependencies', wanted.dependencies],
63
+ ['devDependencies', wanted.devDependencies],
64
+ ]) {
65
+ const current = pkg[field] ?? {};
66
+ for (const [name, range] of Object.entries(incoming)) {
67
+ // Already declared anywhere: leave it exactly as it is.
68
+ if (found.deps[name])
69
+ continue;
70
+ current[name] = range;
71
+ added.push(name);
72
+ }
73
+ if (Object.keys(current).length > 0) {
74
+ pkg[field] = Object.fromEntries(Object.entries(current).sort(([a], [b]) => a.localeCompare(b)));
75
+ }
76
+ }
77
+ steps.push(added.length > 0
78
+ ? { kind: 'merged', what: 'package.json', detail: `added ${added.join(', ')}` }
79
+ : { kind: 'skipped', what: 'package.json dependencies', detail: 'everything needed is already here' });
80
+ return steps;
81
+ }
82
+ /**
83
+ * Scripts, but never over one that exists.
84
+ *
85
+ * `dev` and `build` are the two most likely to already mean something, and
86
+ * quietly replacing either is how a tool loses someone's trust permanently.
87
+ */
88
+ function mergeScripts(o, found) {
89
+ const pkg = found.packageJson;
90
+ const scripts = pkg.scripts ?? {};
91
+ const wanted = JSON.parse(t.packageJson(o)).scripts;
92
+ const added = [];
93
+ const conflicts = [];
94
+ for (const [name, command] of Object.entries(wanted)) {
95
+ if (scripts[name] === undefined) {
96
+ scripts[name] = command;
97
+ added.push(name);
98
+ }
99
+ else if (scripts[name] !== command) {
100
+ conflicts.push(`${name}: ${command}`);
101
+ }
102
+ }
103
+ pkg.scripts = scripts;
104
+ const steps = [];
105
+ if (added.length > 0)
106
+ steps.push({ kind: 'merged', what: 'scripts', detail: added.join(', ') });
107
+ if (conflicts.length > 0) {
108
+ steps.push({
109
+ kind: 'manual',
110
+ what: 'scripts you already have',
111
+ detail: `left alone — add these yourself if you want them:\n ${conflicts.join('\n ')}`,
112
+ });
113
+ }
114
+ return steps;
115
+ }
116
+ /** The plugin entry, written if there is no config and printed if there is. */
117
+ function viteConfig(o, found, dir) {
118
+ if (found.viteConfig === null) {
119
+ writeFileSync(join(dir, 'vite.config.ts'), t.viteConfig(o));
120
+ return [{ kind: 'wrote', what: 'vite.config.ts' }];
121
+ }
122
+ return [
123
+ {
124
+ kind: 'manual',
125
+ what: found.viteConfig,
126
+ detail: `add the plugin — it must come before any react() layer:\n` +
127
+ ` import { rscRoutes } from '@rsc-kit/core/vite'\n\n` +
128
+ ` plugins: [\n` +
129
+ ` rscRoutes({ sourceDir: '${o.sourceDir}', outDir: 'build', assetsDir: 'build/public' }),\n` +
130
+ ` …whatever you already have\n` +
131
+ ` ]`,
132
+ },
133
+ ];
134
+ }
135
+ /** The server: written only when there is nothing there to break. */
136
+ function server(o, dir) {
137
+ const file = t.serverFile(o.host);
138
+ if (existsSync(join(dir, file))) {
139
+ return [
140
+ {
141
+ kind: 'manual',
142
+ what: file,
143
+ detail: 'left alone. Mount the handler in it — anything the route table does not\n' +
144
+ ' claim comes back null, so your own routes still win:\n\n' +
145
+ t
146
+ .server(o.host)
147
+ .split('\n')
148
+ .map((line) => ' ' + line)
149
+ .join('\n'),
150
+ },
151
+ ];
152
+ }
153
+ writeFileSync(join(dir, file), t.server(o.host));
154
+ return [{ kind: 'wrote', what: file }];
155
+ }
156
+ /** The route tree, only where there is not one already. */
157
+ function routes(o, dir) {
158
+ const appDir = join(dir, o.sourceDir, 'app');
159
+ const steps = [];
160
+ if (existsSync(join(appDir, 'layout.tsx')) || existsSync(join(appDir, 'page.tsx'))) {
161
+ return [{ kind: 'skipped', what: `${o.sourceDir}/app`, detail: 'a route tree is already here' }];
162
+ }
163
+ const files = [
164
+ [join(o.sourceDir, 'app/layout.tsx'), t.layout(o)],
165
+ [join(o.sourceDir, 'app/page.tsx'), t.page(o)],
166
+ [join(o.sourceDir, 'components/Counter.tsx'), t.counter(o)],
167
+ ];
168
+ if (o.tailwind)
169
+ files.push([join(o.sourceDir, 'app/styles.css'), t.styles]);
170
+ for (const [path, contents] of files) {
171
+ const full = join(dir, path);
172
+ if (existsSync(full)) {
173
+ steps.push({ kind: 'skipped', what: path, detail: 'already exists' });
174
+ continue;
175
+ }
176
+ mkdirSync(dirname(full), { recursive: true });
177
+ writeFileSync(full, contents);
178
+ steps.push({ kind: 'wrote', what: path });
179
+ }
180
+ return steps;
181
+ }
182
+ /** Ignore the files the build rewrites into the source dir on every run. */
183
+ function gitignore(o, dir) {
184
+ const path = join(dir, '.gitignore');
185
+ const generated = ['rsc-env.d.ts', 'rsc-types.d.ts', 'rsc-routes.d.ts', 'rsc-engine.d.ts'].map((f) => `${o.sourceDir}/${f}`);
186
+ const current = existsSync(path) ? readFileSync(path, 'utf-8') : '';
187
+ const missing = generated.filter((line) => !current.includes(line));
188
+ if (missing.length === 0)
189
+ return [{ kind: 'skipped', what: '.gitignore', detail: 'already covers the generated files' }];
190
+ writeFileSync(path, current + (current.endsWith('\n') || current === '' ? '' : '\n') +
191
+ '\n# Written into the source dir by the RSC build, every run.\n' +
192
+ missing.join('\n') + '\nbuild\n');
193
+ return [{ kind: 'merged', what: '.gitignore', detail: `added ${missing.length} generated paths` }];
194
+ }
195
+ /** Everything, in the order a reader would want to hear about it. */
196
+ export function initialise(o, found, dir) {
197
+ const steps = [
198
+ ...routes(o, dir),
199
+ ...viteConfig(o, found, dir),
200
+ ...server(o, dir),
201
+ ...gitignore(o, dir),
202
+ ...mergeDependencies(o, found),
203
+ ...mergeScripts(o, found),
204
+ ];
205
+ writeFileSync(join(dir, 'package.json'), JSON.stringify(found.packageJson, null, 2) + '\n');
206
+ return steps;
207
+ }
208
+ const INIT_HELP = `
209
+ rsc-kit init — add RSC to the project in this directory
210
+
211
+ Nothing existing is ever rewritten. New files are written, missing
212
+ dependencies are added, and for anything already there the exact edit is
213
+ printed for you to make.
214
+
215
+ Options
216
+ --source-dir <dir> where app/ should live (detected, usually src)
217
+ --host=… bun | hono | elysia | node (detected from your deps)
218
+ --compiler=… none | oxc | babel
219
+ --tailwind add Tailwind as well
220
+ -y, --yes accept what was detected, ask nothing
221
+ -h, --help this
222
+ `;
223
+ /**
224
+ * Add RSC to a project that already exists.
225
+ *
226
+ * Almost everything is detected rather than asked: which server the project
227
+ * already uses, where its source lives, whether React and Tailwind are already
228
+ * there. A question about something the project has already decided is a
229
+ * question with a wrong answer available.
230
+ */
231
+ export async function runInit(args) {
232
+ if (args.includes('--help') || args.includes('-h')) {
233
+ stdout.write(INIT_HELP);
234
+ return;
235
+ }
236
+ const dir = cwd();
237
+ if (!existsSync(join(dir, 'package.json'))) {
238
+ stdout.write(`\n${bold('No package.json here.')}\n` +
239
+ ` init adds RSC to a project that already exists. To start a new one:\n` +
240
+ ` ${cyan('bun create rsc-kit my-app')}\n\n`);
241
+ exit(1);
242
+ }
243
+ const flags = parseArgs(args);
244
+ const found = detect(dir);
245
+ const unattended = args.includes('-y') || args.includes('--yes') || flags.host !== undefined;
246
+ stdout.write(`\n${bold('Adding rsc-kit')} ${dim(dir)}\n\n`);
247
+ stdout.write(` ${dim('server')} ${found.host}${flags.host ? '' : dim(' (detected)')}\n`);
248
+ stdout.write(` ${dim('source')} ${flags.sourceDir ?? found.sourceDir ?? 'src'}\n`);
249
+ stdout.write(` ${dim('react')} ${found.hasReact ? 'already here' : 'will be added'}\n\n`);
250
+ let compiler = flags.compiler ?? 'none';
251
+ let tailwind = flags.tailwind ?? found.hasTailwind;
252
+ if (!unattended) {
253
+ const p = new Prompter();
254
+ try {
255
+ compiler = flags.compiler ?? ((await p.confirm('React Compiler', true)) ? DEFAULT_COMPILER : 'none');
256
+ if (flags.tailwind === undefined && !found.hasTailwind) {
257
+ tailwind = await p.confirm('Tailwind CSS', false);
258
+ }
259
+ }
260
+ finally {
261
+ p.close();
262
+ }
263
+ }
264
+ const options = {
265
+ dir,
266
+ name: 'app',
267
+ host: flags.host ?? found.host ?? 'bun',
268
+ compiler,
269
+ tailwind,
270
+ lint: false,
271
+ sourceDir: flags.sourceDir ?? found.sourceDir ?? 'src',
272
+ install: false,
273
+ git: false,
274
+ core: flags.core ?? '^0.1.0',
275
+ };
276
+ const steps = initialise(options, found, dir);
277
+ const mark = { wrote: cyan('+'), merged: cyan('~'), manual: bold('!'), skipped: dim('·') };
278
+ stdout.write(`${bold('Done.')}\n\n`);
279
+ for (const step of steps) {
280
+ stdout.write(` ${mark[step.kind]} ${step.what}${step.detail ? dim(' — ' + step.detail) : ''}\n`);
281
+ }
282
+ const manual = steps.filter((s) => s.kind === 'manual');
283
+ stdout.write(manual.length > 0
284
+ ? `\n${bold('Then, by hand:')} the edits marked ! above are in files you already had.\n\n`
285
+ : `\n ${cyan('bun install')} and you are ready.\n\n`);
286
+ }
@@ -0,0 +1,55 @@
1
+ export type Host = 'bun' | 'hono' | 'elysia' | 'node';
2
+ export type Compiler = 'none' | 'oxc' | 'babel';
3
+ export interface Options {
4
+ dir: string;
5
+ name: string;
6
+ host: Host;
7
+ compiler: Compiler;
8
+ tailwind: boolean;
9
+ lint: boolean;
10
+ /** Where the app/ route tree lives, relative to the project. */
11
+ sourceDir: string;
12
+ install: boolean;
13
+ git: boolean;
14
+ /** What to depend on for the engine. A path makes a local checkout testable. */
15
+ core: string;
16
+ }
17
+ export declare const HOSTS: {
18
+ value: Host;
19
+ label: string;
20
+ hint: string;
21
+ }[];
22
+ /**
23
+ * Not asked as a three-way.
24
+ *
25
+ * The compiler has a native implementation and a Babel one, and they produce
26
+ * the same transform — one is just faster to run. Asking which is asking the
27
+ * user to make a build-performance decision on our behalf, in a project whose
28
+ * whole argument is performance. So the prompt is yes/no and the answer is
29
+ * oxc; `--compiler=babel` is there for when the experimental one misbehaves.
30
+ */
31
+ export declare const DEFAULT_COMPILER: Compiler;
32
+ /** The published version range, when there is no checkout to prefer. */
33
+ export declare const PUBLISHED_CORE = "^0.1.0";
34
+ /**
35
+ * What to depend on for the engine.
36
+ *
37
+ * Run from a checkout — linked, or straight out of the repo — the sibling
38
+ * package is what the author means, and pointing at npm instead fails the
39
+ * install on a version that may not exist yet. Installed from npm there is no
40
+ * sibling and the range is right.
41
+ */
42
+ export declare function defaultCore(fromDir: string): string;
43
+ export declare function parseArgs(argv: string[]): Partial<Options> & {
44
+ help?: boolean;
45
+ init?: boolean;
46
+ };
47
+ export declare function assertUsableName(name: string): void;
48
+ /**
49
+ * Where the app is written, refusing anywhere that is not below here.
50
+ *
51
+ * A generator that writes outside the directory it was pointed at is a
52
+ * generator nobody can run without reading it first.
53
+ */
54
+ export declare function assertInsideCwd(dir: string, cwd: string): void;
55
+ 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|hono|elysia|node which server to generate\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";