@rsc-kit/core 0.30.15 → 0.30.17
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 +3 -1
- package/dist/commandFiles.d.ts +97 -0
- package/dist/commandFiles.js +297 -0
- package/dist/commandFiles.js.map +1 -0
- package/dist/commands.d.ts +95 -0
- package/dist/commands.js +207 -0
- package/dist/commands.js.map +1 -0
- package/dist/vite.js +101 -6
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,9 @@ is underneath:
|
|
|
28
28
|
`await rpc('Orders.recent')`, typed from the backend's own signatures. See
|
|
29
29
|
[Backend-Answered Pages](https://docs.rsc-kit.dev/hosts/backend-answered-pages).
|
|
30
30
|
- **It compiles to a single binary** with `bun build --compile`, assets and
|
|
31
|
-
frozen pages inside it.
|
|
31
|
+
frozen pages inside it. A file in `src/commands` is a release step or a
|
|
32
|
+
worker in the same binary: `./app migrate`
|
|
33
|
+
([guide](https://docs.rsc-kit.dev/guides/commands)).
|
|
32
34
|
|
|
33
35
|
## Setup
|
|
34
36
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The build's half of app commands: which files in `src/commands` are
|
|
3
|
+
* commands, what the generated entry says about them, and the small entries
|
|
4
|
+
* that run one. The runner they call is `commands.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export interface CommandFile {
|
|
7
|
+
/** What is typed after the binary: the file's name without its extension. */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Absolute path, imported lazily by the entry. */
|
|
10
|
+
file: string;
|
|
11
|
+
/** A plain string literal exported as `description`, for `--help`. */
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CommandFiles {
|
|
15
|
+
commands: CommandFile[];
|
|
16
|
+
/** Paths under src/commands that are not commands, and why, for the build to say. */
|
|
17
|
+
ignored: {
|
|
18
|
+
path: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The commands under `<sourceDir>/commands`.
|
|
24
|
+
*
|
|
25
|
+
* Flat on purpose: a name is what a person types, and `db/seed.ts` raises
|
|
26
|
+
* whether that is `db:seed`, `db/seed` or `seed`. Declaration files, tests and
|
|
27
|
+
* anything starting with `_` or `.` are not commands - the last is how a
|
|
28
|
+
* helper lives beside the commands that share it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function commandFiles(sourceDir: string): CommandFiles;
|
|
31
|
+
/** The module the generated entry reads the table from, resolved when the bundle is built. */
|
|
32
|
+
export declare const COMMANDS_MODULE_ID = "virtual:rsc-kit/commands";
|
|
33
|
+
/**
|
|
34
|
+
* The table: every command as a dynamic import, so each is a chunk of its own
|
|
35
|
+
* and the server never evaluates one. An empty object when there are none -
|
|
36
|
+
* a Worker's build, say, which does not carry them.
|
|
37
|
+
*
|
|
38
|
+
* A module of its own rather than text in the entry because the entry is
|
|
39
|
+
* written while the config is still being read, before anything knows the
|
|
40
|
+
* build's mode or which Nitro preset it is for.
|
|
41
|
+
*/
|
|
42
|
+
export declare function commandsModule(commands: readonly CommandFile[]): string;
|
|
43
|
+
/**
|
|
44
|
+
* What the generated server entry adds for an app with a `src/commands`:
|
|
45
|
+
* `runCommand` over the table, with the entry's own `register()` and
|
|
46
|
+
* `shutdown()`. `runner` is the engine's `commands` module, named without an
|
|
47
|
+
* extension like every other engine module the entry imports.
|
|
48
|
+
*/
|
|
49
|
+
export declare function commandsEntry(runner: string): string;
|
|
50
|
+
/** What the entries need to know that only the build does. */
|
|
51
|
+
export interface CommandEntryOptions {
|
|
52
|
+
commands: readonly CommandFile[];
|
|
53
|
+
/** Polyfills the app's graph has (`reflect-metadata`), by package name. */
|
|
54
|
+
polyfills: readonly string[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What the entries can answer without loading the app: `--help`, no command,
|
|
58
|
+
* a name that is not one.
|
|
59
|
+
*
|
|
60
|
+
* Loading the app imports instrumentation.ts, and so validates the
|
|
61
|
+
* environment - and `./app --help` on a build machine, or in a CI step that
|
|
62
|
+
* has no database url, is not a reason to refuse. The names and descriptions
|
|
63
|
+
* are in the entry itself because the binary carries nothing else: a
|
|
64
|
+
* Dockerfile copies `dist/app` and `.output/rsc-kit.json` is not in it.
|
|
65
|
+
*
|
|
66
|
+
* The help text is written by the same function the runner uses, with the
|
|
67
|
+
* program's name left to fill in, so the two cannot word it differently.
|
|
68
|
+
* `answer` is the whole of the logic; the runner repeats it as a backstop.
|
|
69
|
+
*/
|
|
70
|
+
export declare function commandAnswerSource({ commands }: CommandEntryOptions): string;
|
|
71
|
+
/**
|
|
72
|
+
* The entry a plain build runs a command from, written beside the server:
|
|
73
|
+
*
|
|
74
|
+
* node .output/server/commands.mjs migrate
|
|
75
|
+
* bun .output/server/commands.mjs migrate
|
|
76
|
+
*
|
|
77
|
+
* The server is not started - nothing here imports it - and the flag is set
|
|
78
|
+
* before the entry is imported, since importing it is what would otherwise
|
|
79
|
+
* call register().
|
|
80
|
+
*/
|
|
81
|
+
export declare function commandsEntrySource(options: CommandEntryOptions): string;
|
|
82
|
+
/**
|
|
83
|
+
* The entry a binary is compiled from, for an app that has `src/commands`:
|
|
84
|
+
* the server with no arguments, and a command with one.
|
|
85
|
+
*
|
|
86
|
+
* The server's import is dynamic here, and only here. A static import starts
|
|
87
|
+
* the server as it evaluates, before any line of this file could look at
|
|
88
|
+
* `argv`, and a release step run as `./app migrate` would bind the port the
|
|
89
|
+
* running version holds. It is not awaited - a top-level await is what stopped
|
|
90
|
+
* `--bytecode` from applying to this entry - and a literal specifier is one
|
|
91
|
+
* Bun embeds, as it does the static one. The command path chains its imports
|
|
92
|
+
* with `then` for the same reason.
|
|
93
|
+
*
|
|
94
|
+
* Any argument is a command, and an unknown one is refused (exit 2) rather
|
|
95
|
+
* than ignored by a server that starts.
|
|
96
|
+
*/
|
|
97
|
+
export declare function compileEntryWithCommands(dirName: string, options: CommandEntryOptions): string;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The build's half of app commands: which files in `src/commands` are
|
|
3
|
+
* commands, what the generated entry says about them, and the small entries
|
|
4
|
+
* that run one. The runner they call is `commands.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { commandHelp, EARLY_SIGNALS } from './commands.js';
|
|
9
|
+
import { inlineModuleName } from './files.js';
|
|
10
|
+
const SOURCE = /\.(?:[cm]?[jt]sx?)$/;
|
|
11
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/;
|
|
12
|
+
/** Earlier wins when two files share a name: migrate.ts over migrate.js. */
|
|
13
|
+
const EXTENSION_ORDER = ['ts', 'mts', 'cts', 'tsx', 'js', 'mjs', 'cjs', 'jsx'];
|
|
14
|
+
/**
|
|
15
|
+
* `export const description = 'Apply the database migrations'`, read off the
|
|
16
|
+
* source so `--help` can list a command without loading it - a command's
|
|
17
|
+
* imports are the database and the logger, and listing should not open either.
|
|
18
|
+
* A plain string literal only; anything computed is simply not listed.
|
|
19
|
+
*/
|
|
20
|
+
function describe(source) {
|
|
21
|
+
const match = /^\s*export\s+const\s+description\s*(?::\s*string\s*)?=\s*(["'`])([^"'`\\\n$]*)\1/m.exec(source);
|
|
22
|
+
return match?.[2]?.trim() || undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The commands under `<sourceDir>/commands`.
|
|
26
|
+
*
|
|
27
|
+
* Flat on purpose: a name is what a person types, and `db/seed.ts` raises
|
|
28
|
+
* whether that is `db:seed`, `db/seed` or `seed`. Declaration files, tests and
|
|
29
|
+
* anything starting with `_` or `.` are not commands - the last is how a
|
|
30
|
+
* helper lives beside the commands that share it.
|
|
31
|
+
*/
|
|
32
|
+
export function commandFiles(sourceDir) {
|
|
33
|
+
const dir = join(sourceDir, 'commands');
|
|
34
|
+
const found = { commands: [], ignored: [] };
|
|
35
|
+
if (!existsSync(dir))
|
|
36
|
+
return found;
|
|
37
|
+
const taken = new Map();
|
|
38
|
+
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const path = join(dir, entry.name);
|
|
41
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.'))
|
|
42
|
+
continue;
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
found.ignored.push({ path, reason: 'commands are the files directly in src/commands, not a folder of them' });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!SOURCE.test(entry.name) || /\.d\.[cm]?ts$/.test(entry.name) || /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(entry.name))
|
|
48
|
+
continue;
|
|
49
|
+
const extension = entry.name.slice(entry.name.lastIndexOf('.') + 1);
|
|
50
|
+
const name = entry.name.slice(0, -(extension.length + 1));
|
|
51
|
+
if (!NAME.test(name)) {
|
|
52
|
+
found.ignored.push({ path, reason: `"${name}" cannot be typed as a command: letters, digits, "-", "_", "." and ":"` });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const existing = taken.get(name);
|
|
56
|
+
if (existing) {
|
|
57
|
+
const keep = EXTENSION_ORDER.indexOf(existing.file.split('.').pop() ?? '') <= EXTENSION_ORDER.indexOf(extension);
|
|
58
|
+
found.ignored.push({
|
|
59
|
+
path: keep ? path : existing.file,
|
|
60
|
+
reason: `two files are called "${name}"; ${keep ? existing.file : path} is the command`,
|
|
61
|
+
});
|
|
62
|
+
if (keep)
|
|
63
|
+
continue;
|
|
64
|
+
found.commands.splice(found.commands.indexOf(existing), 1);
|
|
65
|
+
}
|
|
66
|
+
const command = { name, file: path, description: describe(readFileSync(path, 'utf-8')) };
|
|
67
|
+
taken.set(name, command);
|
|
68
|
+
found.commands.push(command);
|
|
69
|
+
}
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
/** The module the generated entry reads the table from, resolved when the bundle is built. */
|
|
73
|
+
export const COMMANDS_MODULE_ID = 'virtual:rsc-kit/commands';
|
|
74
|
+
/**
|
|
75
|
+
* The table: every command as a dynamic import, so each is a chunk of its own
|
|
76
|
+
* and the server never evaluates one. An empty object when there are none -
|
|
77
|
+
* a Worker's build, say, which does not carry them.
|
|
78
|
+
*
|
|
79
|
+
* A module of its own rather than text in the entry because the entry is
|
|
80
|
+
* written while the config is still being read, before anything knows the
|
|
81
|
+
* build's mode or which Nitro preset it is for.
|
|
82
|
+
*/
|
|
83
|
+
export function commandsModule(commands) {
|
|
84
|
+
const rows = commands.map(({ name, file, description }) => ` ${JSON.stringify(name)}: { load: () => import(${JSON.stringify(file)})${description ? `, description: ${JSON.stringify(description)}` : ''} },`);
|
|
85
|
+
return `export const commands = {\n${rows.join('\n')}\n}\n`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* What the generated server entry adds for an app with a `src/commands`:
|
|
89
|
+
* `runCommand` over the table, with the entry's own `register()` and
|
|
90
|
+
* `shutdown()`. `runner` is the engine's `commands` module, named without an
|
|
91
|
+
* extension like every other engine module the entry imports.
|
|
92
|
+
*/
|
|
93
|
+
export function commandsEntry(runner) {
|
|
94
|
+
return `
|
|
95
|
+
import { commands as __commands } from ${JSON.stringify(COMMANDS_MODULE_ID)}
|
|
96
|
+
import { runCommand as __runCommand } from ${JSON.stringify(runner)}
|
|
97
|
+
|
|
98
|
+
// src/commands, each loaded only when it is named.
|
|
99
|
+
export function runCommand(argv: string[], program?: string) {
|
|
100
|
+
return __runCommand(argv, { commands: __commands, program, ready: instrumented, shutdown: __instrumentation.shutdown })
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* What the entries can answer without loading the app: `--help`, no command,
|
|
106
|
+
* a name that is not one.
|
|
107
|
+
*
|
|
108
|
+
* Loading the app imports instrumentation.ts, and so validates the
|
|
109
|
+
* environment - and `./app --help` on a build machine, or in a CI step that
|
|
110
|
+
* has no database url, is not a reason to refuse. The names and descriptions
|
|
111
|
+
* are in the entry itself because the binary carries nothing else: a
|
|
112
|
+
* Dockerfile copies `dist/app` and `.output/rsc-kit.json` is not in it.
|
|
113
|
+
*
|
|
114
|
+
* The help text is written by the same function the runner uses, with the
|
|
115
|
+
* program's name left to fill in, so the two cannot word it differently.
|
|
116
|
+
* `answer` is the whole of the logic; the runner repeats it as a backstop.
|
|
117
|
+
*/
|
|
118
|
+
export function commandAnswerSource({ commands }) {
|
|
119
|
+
const table = Object.fromEntries(commands.map(({ name, description }) => [name, { description }]));
|
|
120
|
+
const help = commandHelp(table, '%PROGRAM%');
|
|
121
|
+
return (`const COMMAND_NAMES = ${JSON.stringify(commands.map((c) => c.name))};\n` +
|
|
122
|
+
`const COMMAND_HELP = ${JSON.stringify(help)};\n` +
|
|
123
|
+
'\n' +
|
|
124
|
+
'const answer = (argv, program) => {\n' +
|
|
125
|
+
' const [name] = argv;\n' +
|
|
126
|
+
' const help = COMMAND_HELP.split("%PROGRAM%").join(program);\n' +
|
|
127
|
+
'\n' +
|
|
128
|
+
' if (name === "--help" || name === "-h") return { code: 0, text: help };\n' +
|
|
129
|
+
' if (name === undefined) return { code: 2, text: help };\n' +
|
|
130
|
+
' if (!COMMAND_NAMES.includes(name)) return { code: 2, text: "Not a command: " + name + "\\n\\n" + help };\n' +
|
|
131
|
+
'\n' +
|
|
132
|
+
' return null;\n' +
|
|
133
|
+
'};\n');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The process's SIGTERM and SIGINT, held from before the app loads.
|
|
137
|
+
*
|
|
138
|
+
* The runner takes over once the app has loaded, and the app can take a while
|
|
139
|
+
* - a signal before that finds only the default action, which ends the
|
|
140
|
+
* process with 143 and neither stops a worker the way it asks nor lets it
|
|
141
|
+
* say it stopped. These listeners count what comes first and the runner is
|
|
142
|
+
* handed it (see EARLY_SIGNALS in commands.ts), and from then on each signal
|
|
143
|
+
* is passed to the runner. They are never taken off: a signal is delivered on
|
|
144
|
+
* the event loop's next turn, and one that arrived during a synchronous load
|
|
145
|
+
* is lost to a listener removed before that turn.
|
|
146
|
+
*
|
|
147
|
+
* If the load never finishes the process still ends: RSC_SHUTDOWN_TIMEOUT
|
|
148
|
+
* after the first, or at once on the second.
|
|
149
|
+
*/
|
|
150
|
+
function earlySignalsSource() {
|
|
151
|
+
return ('const early = { count: 0, timer: undefined, listener: undefined };\n' +
|
|
152
|
+
'const onSignal = () => {\n' +
|
|
153
|
+
' if (early.listener) return early.listener();\n' +
|
|
154
|
+
'\n' +
|
|
155
|
+
' early.count += 1;\n' +
|
|
156
|
+
'\n' +
|
|
157
|
+
' if (early.count > 1) process.exit(1);\n' +
|
|
158
|
+
'\n' +
|
|
159
|
+
' early.timer = setTimeout(() => process.exit(1), (Number(process.env.RSC_SHUTDOWN_TIMEOUT) || 10) * 1000);\n' +
|
|
160
|
+
'};\n' +
|
|
161
|
+
'early.handoff = (listener) => {\n' +
|
|
162
|
+
' clearTimeout(early.timer);\n' +
|
|
163
|
+
' early.listener = listener;\n' +
|
|
164
|
+
'};\n' +
|
|
165
|
+
'process.on("SIGTERM", onSignal);\n' +
|
|
166
|
+
'process.on("SIGINT", onSignal);\n' +
|
|
167
|
+
`globalThis[Symbol.for(${JSON.stringify(EARLY_SIGNALS.description)})] = early;\n`);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Loading the app to run a command: the polyfills first, then the entry.
|
|
171
|
+
*
|
|
172
|
+
* A polyfill a dependency checks for at import (`reflect-metadata`, which
|
|
173
|
+
* tsyringe wants and an auth library brings) is loaded for the server by a
|
|
174
|
+
* Nitro plugin, and a command does not go through Nitro. The bundler decides
|
|
175
|
+
* where an external import lands among the chunks, so the app cannot be left
|
|
176
|
+
* to import it in time - this does, by name, from the copy the build traced
|
|
177
|
+
* beside the server. Only in the command path: the server has its own.
|
|
178
|
+
*/
|
|
179
|
+
function loadAppSource(polyfills) {
|
|
180
|
+
const loads = polyfills.map((name) => `await import(${JSON.stringify(name)});\n`).join('');
|
|
181
|
+
return loads + 'const { runCommand } = await import("./_ssr/rsc.mjs");\n';
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The entry a plain build runs a command from, written beside the server:
|
|
185
|
+
*
|
|
186
|
+
* node .output/server/commands.mjs migrate
|
|
187
|
+
* bun .output/server/commands.mjs migrate
|
|
188
|
+
*
|
|
189
|
+
* The server is not started - nothing here imports it - and the flag is set
|
|
190
|
+
* before the entry is imported, since importing it is what would otherwise
|
|
191
|
+
* call register().
|
|
192
|
+
*/
|
|
193
|
+
export function commandsEntrySource(options) {
|
|
194
|
+
return ("// Run one of the app's commands without starting the server:\n" +
|
|
195
|
+
'// node .output/server/commands.mjs <command> [args]\n' +
|
|
196
|
+
'// bun .output/server/commands.mjs <command> [args]\n' +
|
|
197
|
+
'import { relative } from "node:path";\n' +
|
|
198
|
+
'import { fileURLToPath } from "node:url";\n' +
|
|
199
|
+
'\n' +
|
|
200
|
+
commandAnswerSource(options) +
|
|
201
|
+
'\n' +
|
|
202
|
+
'const argv = process.argv.slice(2);\n' +
|
|
203
|
+
'const program = (process.versions.bun ? "bun" : "node") + " " + relative(process.cwd(), fileURLToPath(import.meta.url));\n' +
|
|
204
|
+
'const answered = answer(argv, program);\n' +
|
|
205
|
+
'\n' +
|
|
206
|
+
'if (answered) {\n' +
|
|
207
|
+
' (answered.code === 0 ? process.stdout : process.stderr).write(answered.text);\n' +
|
|
208
|
+
' process.exitCode = answered.code;\n' +
|
|
209
|
+
'} else {\n' +
|
|
210
|
+
' globalThis[Symbol.for("rsc-kit.command")] = true;\n' +
|
|
211
|
+
'\n' +
|
|
212
|
+
indent(earlySignalsSource()) +
|
|
213
|
+
'\n' +
|
|
214
|
+
' let code = 1;\n' +
|
|
215
|
+
'\n' +
|
|
216
|
+
' try {\n' +
|
|
217
|
+
indent(indent(loadAppSource(options.polyfills))) +
|
|
218
|
+
' code = await runCommand(argv, program);\n' +
|
|
219
|
+
' } catch (error) {\n' +
|
|
220
|
+
' // The app failed to load: instrumentation.ts refusing a bad environment,\n' +
|
|
221
|
+
' // as it would stop the server.\n' +
|
|
222
|
+
' console.error("[rsc-kit] the app failed to load:", error);\n' +
|
|
223
|
+
' }\n' +
|
|
224
|
+
'\n' +
|
|
225
|
+
' process.exit(code);\n' +
|
|
226
|
+
'}\n');
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The entry a binary is compiled from, for an app that has `src/commands`:
|
|
230
|
+
* the server with no arguments, and a command with one.
|
|
231
|
+
*
|
|
232
|
+
* The server's import is dynamic here, and only here. A static import starts
|
|
233
|
+
* the server as it evaluates, before any line of this file could look at
|
|
234
|
+
* `argv`, and a release step run as `./app migrate` would bind the port the
|
|
235
|
+
* running version holds. It is not awaited - a top-level await is what stopped
|
|
236
|
+
* `--bytecode` from applying to this entry - and a literal specifier is one
|
|
237
|
+
* Bun embeds, as it does the static one. The command path chains its imports
|
|
238
|
+
* with `then` for the same reason.
|
|
239
|
+
*
|
|
240
|
+
* Any argument is a command, and an unknown one is refused (exit 2) rather
|
|
241
|
+
* than ignored by a server that starts.
|
|
242
|
+
*/
|
|
243
|
+
export function compileEntryWithCommands(dirName, options) {
|
|
244
|
+
// One after another, the polyfills before the entry that needs them.
|
|
245
|
+
const chain = [...options.polyfills.map((name) => `import(${JSON.stringify(name)})`), 'import("./_ssr/rsc.mjs")']
|
|
246
|
+
.map((load) => `.then(() => ${load})`)
|
|
247
|
+
.join('\n ');
|
|
248
|
+
return ('// The entry to compile into one binary:\n' +
|
|
249
|
+
'// bun build --compile .output/server/compile.mjs --outfile dist/app\n' +
|
|
250
|
+
'//\n' +
|
|
251
|
+
'// ./app starts the server. ./app <command> runs src/commands/<command>.\n' +
|
|
252
|
+
'//\n' +
|
|
253
|
+
'// A binary carries what is imported by name. The server reads its frozen pages\n' +
|
|
254
|
+
'// through a computed import, which a compile cannot see - imported here, they\n' +
|
|
255
|
+
'// travel inside the binary and are handed to the server before it starts.\n' +
|
|
256
|
+
'// The server itself is imported only when no command was named: a static\n' +
|
|
257
|
+
'// import would start it, and bind its port, before argv could be read. Not\n' +
|
|
258
|
+
'// awaited, since a top-level await is what stopped --bytecode from applying.\n' +
|
|
259
|
+
'import { basename } from "node:path";\n' +
|
|
260
|
+
`import "./${inlineModuleName(dirName)}";\n` +
|
|
261
|
+
'\n' +
|
|
262
|
+
commandAnswerSource(options) +
|
|
263
|
+
'\n' +
|
|
264
|
+
'const failed = (what) => (error) => {\n' +
|
|
265
|
+
' console.error(what, error);\n' +
|
|
266
|
+
' process.exit(1);\n' +
|
|
267
|
+
'};\n' +
|
|
268
|
+
'\n' +
|
|
269
|
+
'if (process.argv.length <= 2) {\n' +
|
|
270
|
+
' import("./index.mjs").catch(failed("[rsc-kit] the server failed to start:"));\n' +
|
|
271
|
+
'} else {\n' +
|
|
272
|
+
' const argv = process.argv.slice(2);\n' +
|
|
273
|
+
' const program = "./" + basename(process.execPath);\n' +
|
|
274
|
+
' const answered = answer(argv, program);\n' +
|
|
275
|
+
'\n' +
|
|
276
|
+
' if (answered) {\n' +
|
|
277
|
+
' (answered.code === 0 ? process.stdout : process.stderr).write(answered.text);\n' +
|
|
278
|
+
' process.exitCode = answered.code;\n' +
|
|
279
|
+
' } else {\n' +
|
|
280
|
+
' globalThis[Symbol.for("rsc-kit.command")] = true;\n' +
|
|
281
|
+
'\n' +
|
|
282
|
+
indent(indent(earlySignalsSource())) +
|
|
283
|
+
'\n' +
|
|
284
|
+
' Promise.resolve()\n' +
|
|
285
|
+
` ${chain}\n` +
|
|
286
|
+
' .then(({ runCommand }) => runCommand(argv, program))\n' +
|
|
287
|
+
' .then((code) => process.exit(code), failed("[rsc-kit] the app failed to load:"));\n' +
|
|
288
|
+
' }\n' +
|
|
289
|
+
'}\n');
|
|
290
|
+
}
|
|
291
|
+
function indent(text) {
|
|
292
|
+
return text
|
|
293
|
+
.split('\n')
|
|
294
|
+
.map((line) => (line ? ' ' + line : line))
|
|
295
|
+
.join('\n');
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=commandFiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commandFiles.js","sourceRoot":"","sources":["../src/commandFiles.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The build's half of app commands: which files in `src/commands` are\n * commands, what the generated entry says about them, and the small entries\n * that run one. The runner they call is `commands.ts`.\n */\n\nimport { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { commandHelp, EARLY_SIGNALS } from './commands.js'\nimport { inlineModuleName } from './files.js'\n\nexport interface CommandFile {\n /** What is typed after the binary: the file's name without its extension. */\n name: string\n /** Absolute path, imported lazily by the entry. */\n file: string\n /** A plain string literal exported as `description`, for `--help`. */\n description?: string\n}\n\nexport interface CommandFiles {\n commands: CommandFile[]\n /** Paths under src/commands that are not commands, and why, for the build to say. */\n ignored: { path: string; reason: string }[]\n}\n\nconst SOURCE = /\\.(?:[cm]?[jt]sx?)$/\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/\n\n/** Earlier wins when two files share a name: migrate.ts over migrate.js. */\nconst EXTENSION_ORDER = ['ts', 'mts', 'cts', 'tsx', 'js', 'mjs', 'cjs', 'jsx']\n\n/**\n * `export const description = 'Apply the database migrations'`, read off the\n * source so `--help` can list a command without loading it - a command's\n * imports are the database and the logger, and listing should not open either.\n * A plain string literal only; anything computed is simply not listed.\n */\nfunction describe(source: string): string | undefined {\n const match = /^\\s*export\\s+const\\s+description\\s*(?::\\s*string\\s*)?=\\s*([\"'`])([^\"'`\\\\\\n$]*)\\1/m.exec(source)\n\n return match?.[2]?.trim() || undefined\n}\n\n/**\n * The commands under `<sourceDir>/commands`.\n *\n * Flat on purpose: a name is what a person types, and `db/seed.ts` raises\n * whether that is `db:seed`, `db/seed` or `seed`. Declaration files, tests and\n * anything starting with `_` or `.` are not commands - the last is how a\n * helper lives beside the commands that share it.\n */\nexport function commandFiles(sourceDir: string): CommandFiles {\n const dir = join(sourceDir, 'commands')\n const found: CommandFiles = { commands: [], ignored: [] }\n\n if (!existsSync(dir)) return found\n\n const taken = new Map<string, CommandFile>()\n const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))\n\n for (const entry of entries) {\n const path = join(dir, entry.name)\n\n if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue\n\n if (entry.isDirectory()) {\n found.ignored.push({ path, reason: 'commands are the files directly in src/commands, not a folder of them' })\n\n continue\n }\n\n if (!SOURCE.test(entry.name) || /\\.d\\.[cm]?ts$/.test(entry.name) || /\\.(?:test|spec)\\.[cm]?[jt]sx?$/.test(entry.name)) continue\n\n const extension = entry.name.slice(entry.name.lastIndexOf('.') + 1)\n const name = entry.name.slice(0, -(extension.length + 1))\n\n if (!NAME.test(name)) {\n found.ignored.push({ path, reason: `\"${name}\" cannot be typed as a command: letters, digits, \"-\", \"_\", \".\" and \":\"` })\n\n continue\n }\n\n const existing = taken.get(name)\n\n if (existing) {\n const keep = EXTENSION_ORDER.indexOf(existing.file.split('.').pop() ?? '') <= EXTENSION_ORDER.indexOf(extension)\n\n found.ignored.push({\n path: keep ? path : existing.file,\n reason: `two files are called \"${name}\"; ${keep ? existing.file : path} is the command`,\n })\n\n if (keep) continue\n\n found.commands.splice(found.commands.indexOf(existing), 1)\n }\n\n const command: CommandFile = { name, file: path, description: describe(readFileSync(path, 'utf-8')) }\n\n taken.set(name, command)\n found.commands.push(command)\n }\n\n return found\n}\n\n/** The module the generated entry reads the table from, resolved when the bundle is built. */\nexport const COMMANDS_MODULE_ID = 'virtual:rsc-kit/commands'\n\n/**\n * The table: every command as a dynamic import, so each is a chunk of its own\n * and the server never evaluates one. An empty object when there are none -\n * a Worker's build, say, which does not carry them.\n *\n * A module of its own rather than text in the entry because the entry is\n * written while the config is still being read, before anything knows the\n * build's mode or which Nitro preset it is for.\n */\nexport function commandsModule(commands: readonly CommandFile[]): string {\n const rows = commands.map(\n ({ name, file, description }) =>\n ` ${JSON.stringify(name)}: { load: () => import(${JSON.stringify(file)})${description ? `, description: ${JSON.stringify(description)}` : ''} },`,\n )\n\n return `export const commands = {\\n${rows.join('\\n')}\\n}\\n`\n}\n\n/**\n * What the generated server entry adds for an app with a `src/commands`:\n * `runCommand` over the table, with the entry's own `register()` and\n * `shutdown()`. `runner` is the engine's `commands` module, named without an\n * extension like every other engine module the entry imports.\n */\nexport function commandsEntry(runner: string): string {\n return `\nimport { commands as __commands } from ${JSON.stringify(COMMANDS_MODULE_ID)}\nimport { runCommand as __runCommand } from ${JSON.stringify(runner)}\n\n// src/commands, each loaded only when it is named.\nexport function runCommand(argv: string[], program?: string) {\n return __runCommand(argv, { commands: __commands, program, ready: instrumented, shutdown: __instrumentation.shutdown })\n}\n`\n}\n\n/** What the entries need to know that only the build does. */\nexport interface CommandEntryOptions {\n commands: readonly CommandFile[]\n /** Polyfills the app's graph has (`reflect-metadata`), by package name. */\n polyfills: readonly string[]\n}\n\n/**\n * What the entries can answer without loading the app: `--help`, no command,\n * a name that is not one.\n *\n * Loading the app imports instrumentation.ts, and so validates the\n * environment - and `./app --help` on a build machine, or in a CI step that\n * has no database url, is not a reason to refuse. The names and descriptions\n * are in the entry itself because the binary carries nothing else: a\n * Dockerfile copies `dist/app` and `.output/rsc-kit.json` is not in it.\n *\n * The help text is written by the same function the runner uses, with the\n * program's name left to fill in, so the two cannot word it differently.\n * `answer` is the whole of the logic; the runner repeats it as a backstop.\n */\nexport function commandAnswerSource({ commands }: CommandEntryOptions): string {\n const table = Object.fromEntries(commands.map(({ name, description }) => [name, { description }]))\n const help = commandHelp(table, '%PROGRAM%')\n\n return (\n `const COMMAND_NAMES = ${JSON.stringify(commands.map((c) => c.name))};\\n` +\n `const COMMAND_HELP = ${JSON.stringify(help)};\\n` +\n '\\n' +\n 'const answer = (argv, program) => {\\n' +\n ' const [name] = argv;\\n' +\n ' const help = COMMAND_HELP.split(\"%PROGRAM%\").join(program);\\n' +\n '\\n' +\n ' if (name === \"--help\" || name === \"-h\") return { code: 0, text: help };\\n' +\n ' if (name === undefined) return { code: 2, text: help };\\n' +\n ' if (!COMMAND_NAMES.includes(name)) return { code: 2, text: \"Not a command: \" + name + \"\\\\n\\\\n\" + help };\\n' +\n '\\n' +\n ' return null;\\n' +\n '};\\n'\n )\n}\n\n/**\n * The process's SIGTERM and SIGINT, held from before the app loads.\n *\n * The runner takes over once the app has loaded, and the app can take a while\n * - a signal before that finds only the default action, which ends the\n * process with 143 and neither stops a worker the way it asks nor lets it\n * say it stopped. These listeners count what comes first and the runner is\n * handed it (see EARLY_SIGNALS in commands.ts), and from then on each signal\n * is passed to the runner. They are never taken off: a signal is delivered on\n * the event loop's next turn, and one that arrived during a synchronous load\n * is lost to a listener removed before that turn.\n *\n * If the load never finishes the process still ends: RSC_SHUTDOWN_TIMEOUT\n * after the first, or at once on the second.\n */\nfunction earlySignalsSource(): string {\n return (\n 'const early = { count: 0, timer: undefined, listener: undefined };\\n' +\n 'const onSignal = () => {\\n' +\n ' if (early.listener) return early.listener();\\n' +\n '\\n' +\n ' early.count += 1;\\n' +\n '\\n' +\n ' if (early.count > 1) process.exit(1);\\n' +\n '\\n' +\n ' early.timer = setTimeout(() => process.exit(1), (Number(process.env.RSC_SHUTDOWN_TIMEOUT) || 10) * 1000);\\n' +\n '};\\n' +\n 'early.handoff = (listener) => {\\n' +\n ' clearTimeout(early.timer);\\n' +\n ' early.listener = listener;\\n' +\n '};\\n' +\n 'process.on(\"SIGTERM\", onSignal);\\n' +\n 'process.on(\"SIGINT\", onSignal);\\n' +\n `globalThis[Symbol.for(${JSON.stringify(EARLY_SIGNALS.description)})] = early;\\n`\n )\n}\n\n/**\n * Loading the app to run a command: the polyfills first, then the entry.\n *\n * A polyfill a dependency checks for at import (`reflect-metadata`, which\n * tsyringe wants and an auth library brings) is loaded for the server by a\n * Nitro plugin, and a command does not go through Nitro. The bundler decides\n * where an external import lands among the chunks, so the app cannot be left\n * to import it in time - this does, by name, from the copy the build traced\n * beside the server. Only in the command path: the server has its own.\n */\nfunction loadAppSource(polyfills: readonly string[]): string {\n const loads = polyfills.map((name) => `await import(${JSON.stringify(name)});\\n`).join('')\n\n return loads + 'const { runCommand } = await import(\"./_ssr/rsc.mjs\");\\n'\n}\n\n/**\n * The entry a plain build runs a command from, written beside the server:\n *\n * node .output/server/commands.mjs migrate\n * bun .output/server/commands.mjs migrate\n *\n * The server is not started - nothing here imports it - and the flag is set\n * before the entry is imported, since importing it is what would otherwise\n * call register().\n */\nexport function commandsEntrySource(options: CommandEntryOptions): string {\n return (\n \"// Run one of the app's commands without starting the server:\\n\" +\n '// node .output/server/commands.mjs <command> [args]\\n' +\n '// bun .output/server/commands.mjs <command> [args]\\n' +\n 'import { relative } from \"node:path\";\\n' +\n 'import { fileURLToPath } from \"node:url\";\\n' +\n '\\n' +\n commandAnswerSource(options) +\n '\\n' +\n 'const argv = process.argv.slice(2);\\n' +\n 'const program = (process.versions.bun ? \"bun\" : \"node\") + \" \" + relative(process.cwd(), fileURLToPath(import.meta.url));\\n' +\n 'const answered = answer(argv, program);\\n' +\n '\\n' +\n 'if (answered) {\\n' +\n ' (answered.code === 0 ? process.stdout : process.stderr).write(answered.text);\\n' +\n ' process.exitCode = answered.code;\\n' +\n '} else {\\n' +\n ' globalThis[Symbol.for(\"rsc-kit.command\")] = true;\\n' +\n '\\n' +\n indent(earlySignalsSource()) +\n '\\n' +\n ' let code = 1;\\n' +\n '\\n' +\n ' try {\\n' +\n indent(indent(loadAppSource(options.polyfills))) +\n ' code = await runCommand(argv, program);\\n' +\n ' } catch (error) {\\n' +\n ' // The app failed to load: instrumentation.ts refusing a bad environment,\\n' +\n ' // as it would stop the server.\\n' +\n ' console.error(\"[rsc-kit] the app failed to load:\", error);\\n' +\n ' }\\n' +\n '\\n' +\n ' process.exit(code);\\n' +\n '}\\n'\n )\n}\n\n/**\n * The entry a binary is compiled from, for an app that has `src/commands`:\n * the server with no arguments, and a command with one.\n *\n * The server's import is dynamic here, and only here. A static import starts\n * the server as it evaluates, before any line of this file could look at\n * `argv`, and a release step run as `./app migrate` would bind the port the\n * running version holds. It is not awaited - a top-level await is what stopped\n * `--bytecode` from applying to this entry - and a literal specifier is one\n * Bun embeds, as it does the static one. The command path chains its imports\n * with `then` for the same reason.\n *\n * Any argument is a command, and an unknown one is refused (exit 2) rather\n * than ignored by a server that starts.\n */\nexport function compileEntryWithCommands(dirName: string, options: CommandEntryOptions): string {\n // One after another, the polyfills before the entry that needs them.\n const chain = [...options.polyfills.map((name) => `import(${JSON.stringify(name)})`), 'import(\"./_ssr/rsc.mjs\")']\n .map((load) => `.then(() => ${load})`)\n .join('\\n ')\n\n return (\n '// The entry to compile into one binary:\\n' +\n '// bun build --compile .output/server/compile.mjs --outfile dist/app\\n' +\n '//\\n' +\n '// ./app starts the server. ./app <command> runs src/commands/<command>.\\n' +\n '//\\n' +\n '// A binary carries what is imported by name. The server reads its frozen pages\\n' +\n '// through a computed import, which a compile cannot see - imported here, they\\n' +\n '// travel inside the binary and are handed to the server before it starts.\\n' +\n '// The server itself is imported only when no command was named: a static\\n' +\n '// import would start it, and bind its port, before argv could be read. Not\\n' +\n '// awaited, since a top-level await is what stopped --bytecode from applying.\\n' +\n 'import { basename } from \"node:path\";\\n' +\n `import \"./${inlineModuleName(dirName)}\";\\n` +\n '\\n' +\n commandAnswerSource(options) +\n '\\n' +\n 'const failed = (what) => (error) => {\\n' +\n ' console.error(what, error);\\n' +\n ' process.exit(1);\\n' +\n '};\\n' +\n '\\n' +\n 'if (process.argv.length <= 2) {\\n' +\n ' import(\"./index.mjs\").catch(failed(\"[rsc-kit] the server failed to start:\"));\\n' +\n '} else {\\n' +\n ' const argv = process.argv.slice(2);\\n' +\n ' const program = \"./\" + basename(process.execPath);\\n' +\n ' const answered = answer(argv, program);\\n' +\n '\\n' +\n ' if (answered) {\\n' +\n ' (answered.code === 0 ? process.stdout : process.stderr).write(answered.text);\\n' +\n ' process.exitCode = answered.code;\\n' +\n ' } else {\\n' +\n ' globalThis[Symbol.for(\"rsc-kit.command\")] = true;\\n' +\n '\\n' +\n indent(indent(earlySignalsSource())) +\n '\\n' +\n ' Promise.resolve()\\n' +\n ` ${chain}\\n` +\n ' .then(({ runCommand }) => runCommand(argv, program))\\n' +\n ' .then((code) => process.exit(code), failed(\"[rsc-kit] the app failed to load:\"));\\n' +\n ' }\\n' +\n '}\\n'\n )\n}\n\nfunction indent(text: string): string {\n return text\n .split('\\n')\n .map((line) => (line ? ' ' + line : line))\n .join('\\n')\n}\n"]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App commands: the files in `src/commands`, run as `./app <name>`.
|
|
3
|
+
*
|
|
4
|
+
* A compiled app is one binary that starts a server, and a release step or a
|
|
5
|
+
* queue worker is the same code started differently - `./app migrate`,
|
|
6
|
+
* `./app work`. The same entry also runs from a plain build, as
|
|
7
|
+
* `node .output/server/commands.mjs <name>`, so nothing about a command
|
|
8
|
+
* depends on Bun, on a platform, or on the binary.
|
|
9
|
+
*
|
|
10
|
+
* Each command is a file with a default export, `(args, { signal }) => number
|
|
11
|
+
* | void`. It shares the build's code and dependencies, so the database
|
|
12
|
+
* package and the logger the pages use are the ones it uses, in the same
|
|
13
|
+
* bundle. It is loaded only when it is named: the server never evaluates one.
|
|
14
|
+
*
|
|
15
|
+
* `register()` from instrumentation.ts is skipped unless the command says
|
|
16
|
+
* `export const register = true`. Importing instrumentation.ts is not
|
|
17
|
+
* skipped - a bad environment fails there, at import, for a command as for a
|
|
18
|
+
* server - but a command that only needs the schema should not pay for, or
|
|
19
|
+
* fail on, a connection it never uses.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here imports `node:` anything: the runner is bundled into the
|
|
22
|
+
* server's entry for every host, and a host without those modules must still
|
|
23
|
+
* load it. `process` is reached lazily, inside the functions that need it.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Set on `globalThis` before the server's entry is imported, by whatever runs
|
|
27
|
+
* a command. The entry would otherwise call register() the moment it loads, as
|
|
28
|
+
* a server does at startup, which a command has not asked for.
|
|
29
|
+
*/
|
|
30
|
+
export declare const COMMAND_MODE: unique symbol;
|
|
31
|
+
/** What a command receives beside its arguments. */
|
|
32
|
+
export interface CommandContext {
|
|
33
|
+
/** Aborted by SIGTERM or SIGINT: a long-running command stops when it is. */
|
|
34
|
+
signal: AbortSignal;
|
|
35
|
+
/**
|
|
36
|
+
* When the runner stops waiting for the command, as a time in milliseconds
|
|
37
|
+
* (`Date.now()`'s), once it has been told to stop; undefined until then.
|
|
38
|
+
* A worker finishing its job reads `deadline - Date.now()` rather than
|
|
39
|
+
* RSC_SHUTDOWN_TIMEOUT and its default.
|
|
40
|
+
*/
|
|
41
|
+
readonly deadline: number | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The entry that loads the app owns the process's SIGTERM and SIGINT, and
|
|
45
|
+
* leaves this on `globalThis` for the runner.
|
|
46
|
+
*
|
|
47
|
+
* Loading can take a while, and a signal before the runner exists finds only
|
|
48
|
+
* the default action, which ends the process with 143. So the entry holds
|
|
49
|
+
* them, and the runner is handed over to: the count of those that came first,
|
|
50
|
+
* and every later one. The entry's listener is never taken off. A signal is
|
|
51
|
+
* delivered on the event loop's next turn, not when it arrives, and a listener
|
|
52
|
+
* removed in between has the signal taken from it without anyone getting it.
|
|
53
|
+
*/
|
|
54
|
+
export declare const EARLY_SIGNALS: unique symbol;
|
|
55
|
+
/** What the build knows of one command file: how to load it, and a line to list it by. */
|
|
56
|
+
export interface CommandEntry {
|
|
57
|
+
load: () => Promise<{
|
|
58
|
+
default?: unknown;
|
|
59
|
+
register?: unknown;
|
|
60
|
+
}>;
|
|
61
|
+
description?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface RunCommandOptions {
|
|
64
|
+
commands: Record<string, CommandEntry>;
|
|
65
|
+
/** instrumentation.ts's register(), once. Awaited only for a command that exports `register = true`. */
|
|
66
|
+
ready: () => Promise<unknown>;
|
|
67
|
+
/** instrumentation.ts's shutdown(), called after a command that was given `register()`. */
|
|
68
|
+
shutdown?: () => unknown;
|
|
69
|
+
/** How the usage line names this program: `./app`, `node .output/server/commands.mjs`. */
|
|
70
|
+
program?: string;
|
|
71
|
+
out?: (text: string) => void;
|
|
72
|
+
err?: (text: string) => void;
|
|
73
|
+
/** SIGTERM and SIGINT. The process, unless a test brings its own. */
|
|
74
|
+
signals?: {
|
|
75
|
+
on: (signal: 'SIGTERM' | 'SIGINT', listener: () => void) => unknown;
|
|
76
|
+
off: (signal: 'SIGTERM' | 'SIGINT', listener: () => void) => unknown;
|
|
77
|
+
};
|
|
78
|
+
/** Milliseconds a command gets to stop once told to, and shutdown() after it. RSC_SHUTDOWN_TIMEOUT seconds (10) by default. */
|
|
79
|
+
limit?: number;
|
|
80
|
+
}
|
|
81
|
+
/** Whether this process was started to run a command rather than to serve. */
|
|
82
|
+
export declare function inCommandMode(): boolean;
|
|
83
|
+
/** The list `--help` prints, and what an unknown name is answered with. */
|
|
84
|
+
export declare function commandHelp(commands: Record<string, {
|
|
85
|
+
description?: string;
|
|
86
|
+
}>, program?: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* Run the command `argv` names and say how it ended.
|
|
89
|
+
*
|
|
90
|
+
* Resolves to the exit code and leaves exiting to the caller, so a test sees
|
|
91
|
+
* the code and an entry decides when the process stops. 0 is success and 1 is
|
|
92
|
+
* a command that threw or would not stop; 2 is a name that is not a command,
|
|
93
|
+
* as it is for a tool that was misused.
|
|
94
|
+
*/
|
|
95
|
+
export declare function runCommand(argv: readonly string[], options: RunCommandOptions): Promise<number>;
|