@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 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>;