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.
@@ -0,0 +1,133 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ export const HOSTS = [
4
+ { value: 'bun', label: 'Bun.serve', hint: 'no framework, fastest to start' },
5
+ { value: 'hono', label: 'Hono', hint: 'also what a Worker or Deno would use' },
6
+ { value: 'elysia', label: 'Elysia', hint: 'Bun-first, typed routes of its own' },
7
+ { value: 'node', label: 'node:http', hint: 'no Bun, no framework' },
8
+ ];
9
+ /**
10
+ * Not asked as a three-way.
11
+ *
12
+ * The compiler has a native implementation and a Babel one, and they produce
13
+ * the same transform — one is just faster to run. Asking which is asking the
14
+ * user to make a build-performance decision on our behalf, in a project whose
15
+ * whole argument is performance. So the prompt is yes/no and the answer is
16
+ * oxc; `--compiler=babel` is there for when the experimental one misbehaves.
17
+ */
18
+ export const DEFAULT_COMPILER = 'oxc';
19
+ /** The published version range, when there is no checkout to prefer. */
20
+ export const PUBLISHED_CORE = '^0.1.0';
21
+ /**
22
+ * What to depend on for the engine.
23
+ *
24
+ * Run from a checkout — linked, or straight out of the repo — the sibling
25
+ * package is what the author means, and pointing at npm instead fails the
26
+ * install on a version that may not exist yet. Installed from npm there is no
27
+ * sibling and the range is right.
28
+ */
29
+ export function defaultCore(fromDir) {
30
+ let dir = fromDir;
31
+ for (let up = 0; up < 6; up++) {
32
+ const core = join(dir, 'packages/core/package.json');
33
+ if (existsSync(core)) {
34
+ try {
35
+ const name = JSON.parse(readFileSync(core, 'utf-8')).name;
36
+ if (name === '@rsc-kit/core')
37
+ return 'file:' + join(dir, 'packages/core');
38
+ }
39
+ catch {
40
+ // Unreadable is the same as absent.
41
+ }
42
+ }
43
+ const parent = dirname(dir);
44
+ if (parent === dir)
45
+ break;
46
+ dir = parent;
47
+ }
48
+ return PUBLISHED_CORE;
49
+ }
50
+ export function parseArgs(argv) {
51
+ const out = {};
52
+ for (const arg of argv) {
53
+ if (arg === '--help' || arg === '-h')
54
+ out.help = true;
55
+ else if (arg === '--yes' || arg === '-y')
56
+ out.host ??= 'bun';
57
+ else if (arg === '--no-install')
58
+ out.install = false;
59
+ else if (arg === '--no-git')
60
+ out.git = false;
61
+ else if (arg.startsWith('--source-dir='))
62
+ out.sourceDir = arg.slice(13);
63
+ else if (arg === '--init')
64
+ out.init = true;
65
+ else if (arg === '--lint')
66
+ out.lint = true;
67
+ else if (arg === '--no-lint')
68
+ out.lint = false;
69
+ else if (arg === '--tailwind')
70
+ out.tailwind = true;
71
+ else if (arg === '--no-tailwind')
72
+ out.tailwind = false;
73
+ else if (arg.startsWith('--host='))
74
+ out.host = arg.slice(7);
75
+ else if (arg.startsWith('--compiler='))
76
+ out.compiler = arg.slice(11);
77
+ else if (arg.startsWith('--core='))
78
+ out.core = arg.slice(7);
79
+ else if (!arg.startsWith('-'))
80
+ out.dir ??= arg;
81
+ }
82
+ return out;
83
+ }
84
+ /**
85
+ * A directory name that is safe to be a package name and to interpolate.
86
+ *
87
+ * The name reaches a TypeScript string literal, JSX text and a package.json
88
+ * field. Escaping it differently at each of those is three chances to get one
89
+ * wrong — an apostrophe alone produced a generated file that would not parse,
90
+ * and a name containing a template expression produced one that executed. So
91
+ * it is checked once, against what npm already allows a package to be called.
92
+ */
93
+ const SAFE_NAME = /^[a-z0-9][a-z0-9._-]*$/i;
94
+ export function assertUsableName(name) {
95
+ if (SAFE_NAME.test(name) && name.length <= 214)
96
+ return;
97
+ throw new Error(`Not a usable project name: ${JSON.stringify(name)}. ` +
98
+ 'Use letters, digits, dots, dashes and underscores — it becomes the package name ' +
99
+ 'and is written into generated source.');
100
+ }
101
+ /**
102
+ * Where the app is written, refusing anywhere that is not below here.
103
+ *
104
+ * A generator that writes outside the directory it was pointed at is a
105
+ * generator nobody can run without reading it first.
106
+ */
107
+ export function assertInsideCwd(dir, cwd) {
108
+ const inside = dir === cwd || dir.startsWith(cwd.endsWith('/') ? cwd : cwd + '/');
109
+ if (inside)
110
+ return;
111
+ throw new Error(`Refusing to write outside the current directory: ${dir}. ` +
112
+ 'Change into the directory you want the app in and run it there.');
113
+ }
114
+ export const HELP = `
115
+ create-rsc-kit — scaffold an RSC app
116
+
117
+ Usage
118
+ create-rsc-kit <dir> [options]
119
+ bun create rsc-kit <dir> [options] (once published)
120
+
121
+ Options
122
+ --host=bun|hono|elysia|node which server to generate
123
+ --compiler=none|oxc|babel React Compiler (prompt offers oxc; babel by flag)
124
+ --tailwind / --no-tailwind include Tailwind
125
+ --lint / --no-lint include oxlint
126
+ --source-dir <dir> where app/ lives (default: src)
127
+ --init add to the project here, rather than scaffold
128
+ --core=<spec> engine dependency, e.g. file:../rsc-kit/packages/core
129
+ --no-install skip installing dependencies
130
+ --no-git skip git init
131
+ -y, --yes accept every default, ask nothing
132
+ -h, --help this
133
+ `;
@@ -0,0 +1,24 @@
1
+ export declare const dim: (t: string) => string;
2
+ export declare const bold: (t: string) => string;
3
+ export declare const cyan: (t: string) => string;
4
+ export interface Choice<T> {
5
+ value: T;
6
+ label: string;
7
+ hint: string;
8
+ }
9
+ export declare class Prompter {
10
+ /**
11
+ * Asked one at a time rather than over one long-lived interface.
12
+ *
13
+ * `select` takes stdin into raw mode to read arrow keys, and a readline
14
+ * interface left open would be consuming the same stream — the two fight
15
+ * over every keypress. Opening and closing per question keeps exactly one
16
+ * reader attached at a time.
17
+ */
18
+ private ask;
19
+ text(question: string, fallback: string): Promise<string>;
20
+ confirm(question: string, fallback: boolean): Promise<boolean>;
21
+ /** Arrow keys, j/k, or the number — whichever the hands reach for first. */
22
+ select<T>(question: string, choices: Choice<T>[]): Promise<T>;
23
+ close(): void;
24
+ }
package/dist/prompt.js ADDED
@@ -0,0 +1,129 @@
1
+ // Prompts, with no dependency.
2
+ //
3
+ // A create- package is run through `bunx`/`npm create`, which installs it
4
+ // first — so every dependency is latency the user waits through before being
5
+ // asked anything. readline is in both runtimes and costs nothing.
6
+ import { createInterface, emitKeypressEvents } from 'node:readline';
7
+ import { createInterface as createPromised } from 'node:readline/promises';
8
+ import { exit, stdin, stdout } from 'node:process';
9
+ const tty = stdout.isTTY === true;
10
+ const c = (code, text) => (tty ? code + text + '\x1b[0m' : text);
11
+ export const dim = (t) => c('\x1b[2m', t);
12
+ export const bold = (t) => c('\x1b[1m', t);
13
+ export const cyan = (t) => c('\x1b[36m', t);
14
+ const green = (t) => c('\x1b[32m', t);
15
+ const HIDE_CURSOR = '\x1b[?25l';
16
+ const SHOW_CURSOR = '\x1b[?25h';
17
+ /** Up n lines, then clear everything below. */
18
+ const rewind = (n) => `\x1b[${n}A\x1b[0J`;
19
+ export class Prompter {
20
+ /**
21
+ * Asked one at a time rather than over one long-lived interface.
22
+ *
23
+ * `select` takes stdin into raw mode to read arrow keys, and a readline
24
+ * interface left open would be consuming the same stream — the two fight
25
+ * over every keypress. Opening and closing per question keeps exactly one
26
+ * reader attached at a time.
27
+ */
28
+ async ask(question) {
29
+ const rl = createPromised({ input: stdin, output: stdout });
30
+ try {
31
+ return (await rl.question(question)).trim();
32
+ }
33
+ finally {
34
+ rl.close();
35
+ }
36
+ }
37
+ async text(question, fallback) {
38
+ const answer = await this.ask(`${bold(question)} ${dim(`(${fallback})`)} `);
39
+ return answer === '' ? fallback : answer;
40
+ }
41
+ async confirm(question, fallback) {
42
+ const answer = (await this.ask(`${bold(question)} ${dim(fallback ? '(Y/n)' : '(y/N)')} `)).toLowerCase();
43
+ if (answer === '')
44
+ return fallback;
45
+ return answer.startsWith('y');
46
+ }
47
+ /** Arrow keys, j/k, or the number — whichever the hands reach for first. */
48
+ select(question, choices) {
49
+ let index = 0;
50
+ const draw = (first) => {
51
+ const lines = choices.map((choice, i) => {
52
+ const on = i === index;
53
+ const marker = on ? green('●') : dim('○');
54
+ const label = on ? bold(choice.label) : choice.label;
55
+ return ` ${marker} ${label} ${dim(choice.hint)}`;
56
+ });
57
+ stdout.write((first ? '' : rewind(choices.length)) + lines.join('\n') + '\n');
58
+ };
59
+ stdout.write(`${bold(question)} ${dim('↑↓ then enter')}\n`);
60
+ stdout.write(HIDE_CURSOR);
61
+ draw(true);
62
+ /**
63
+ * Replace the whole list with the answer.
64
+ *
65
+ * Left on screen, every question's options stay in the transcript and the
66
+ * next question is pushed off the top — by the last one you cannot see
67
+ * what you picked for the first.
68
+ */
69
+ const collapse = (choice) => {
70
+ stdout.write(rewind(choices.length + 1) + `${green('✓')} ${bold(question)} ${cyan(choice.label)}\n`);
71
+ };
72
+ // Its own interface so keypress events are emitted at all; the promised
73
+ // one above is closed by the time this runs.
74
+ const rl = createInterface({ input: stdin, escapeCodeTimeout: 50 });
75
+ emitKeypressEvents(stdin, rl);
76
+ const wasRaw = stdin.isRaw === true;
77
+ if (stdin.isTTY)
78
+ stdin.setRawMode(true);
79
+ return new Promise((resolve) => {
80
+ const done = () => {
81
+ stdin.off('keypress', onKey);
82
+ if (stdin.isTTY)
83
+ stdin.setRawMode(wasRaw);
84
+ rl.close();
85
+ stdout.write(SHOW_CURSOR);
86
+ };
87
+ const onKey = (str, key) => {
88
+ const last = choices.length - 1;
89
+ // Ctrl-C has to be handled here: raw mode means no SIGINT, so without
90
+ // this the only way out of the prompt is closing the terminal.
91
+ if (key?.ctrl && key.name === 'c') {
92
+ done();
93
+ stdout.write('\n');
94
+ exit(130);
95
+ }
96
+ if (key?.name === 'up' || key?.name === 'k')
97
+ index = index === 0 ? last : index - 1;
98
+ else if (key?.name === 'down' || key?.name === 'j')
99
+ index = index === last ? 0 : index + 1;
100
+ else if (key?.name === 'home')
101
+ index = 0;
102
+ else if (key?.name === 'end')
103
+ index = last;
104
+ else if (key?.name === 'return' || key?.name === 'enter') {
105
+ done();
106
+ collapse(choices[index]);
107
+ resolve(choices[index].value);
108
+ return;
109
+ }
110
+ else if (/^[1-9]$/.test(str ?? '')) {
111
+ const picked = Number(str) - 1;
112
+ if (picked > last)
113
+ return;
114
+ done();
115
+ collapse(choices[picked]);
116
+ resolve(choices[picked].value);
117
+ return;
118
+ }
119
+ else
120
+ return;
121
+ draw(false);
122
+ };
123
+ stdin.on('keypress', onKey);
124
+ });
125
+ }
126
+ close() {
127
+ // Nothing is held open between questions.
128
+ }
129
+ }
@@ -0,0 +1,34 @@
1
+ import type { Host, Options } from './options.js';
2
+ export declare function packageJson(o: Options): string;
3
+ /** One server per app, so it needs no qualifier. */
4
+ export declare const serverFile: (_host: Host) => string;
5
+ export declare function viteConfig(o: Options): string;
6
+ export declare const tsconfig: (o: Options) => string;
7
+ export declare function server(host: Host): string;
8
+ export declare function layout(o: Options): string;
9
+ export declare function page(o: Options): string;
10
+ export declare function counter(o: Options): string;
11
+ /**
12
+ * Tailwind needs @source pointing at the RSC tree.
13
+ *
14
+ * Server components never enter the client module graph and Tailwind's
15
+ * detection roots at the Vite root, so without this the utilities layer holds
16
+ * only classes scraped from the generated entries. The build still succeeds
17
+ * and nothing warns — the page just arrives unstyled.
18
+ */
19
+ export declare const styles = "@import 'tailwindcss';\n\n/* The whole source tree, not just this directory. A client component is found\n automatically because it enters the browser bundle; a server component never\n does, so anything it uses has to be declared here or it is silently absent\n from the stylesheet. */\n@source '../';\n";
20
+ export declare const gitignore = "node_modules\nbuild\n.rsc\ndist\n*.log\n.DS_Store\n\n# Written by the build into the source dir, every run.\nsrc/rsc-env.d.ts\nsrc/rsc-types.d.ts\nsrc/rsc-routes.d.ts\nsrc/rsc-engine.d.ts\n";
21
+ /**
22
+ * oxlint, with the React Compiler's own rules turned on.
23
+ *
24
+ * Those `react/*` rules are the interesting half here: purity, immutability,
25
+ * set-state-in-render. They describe what the compiler needs in order to
26
+ * memoise safely, and they are worth running whether or not the compiler is
27
+ * enabled — a component that breaks them is a component with a bug the
28
+ * compiler would have made louder.
29
+ *
30
+ * `correctness: error` rather than the enumerated list an eslint migration
31
+ * leaves behind: a new project has nothing to grandfather in.
32
+ */
33
+ export declare function oxlintConfig(o: Options): string;
34
+ export declare function readme(o: Options): string;