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 +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +181 -0
- package/dist/init.d.ts +36 -0
- package/dist/init.js +286 -0
- package/dist/options.d.ts +55 -0
- package/dist/options.js +133 -0
- package/dist/prompt.d.ts +24 -0
- package/dist/prompt.js +129 -0
- package/dist/templates.d.ts +34 -0
- package/dist/templates.js +405 -0
- package/package.json +45 -0
package/dist/options.js
ADDED
|
@@ -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
|
+
`;
|
package/dist/prompt.d.ts
ADDED
|
@@ -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;
|