@rsc-kit/core 0.30.16 → 0.30.18

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.
@@ -47,6 +47,27 @@ export declare function commandsModule(commands: readonly CommandFile[]): string
47
47
  * extension like every other engine module the entry imports.
48
48
  */
49
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;
50
71
  /**
51
72
  * The entry a plain build runs a command from, written beside the server:
52
73
  *
@@ -57,4 +78,20 @@ export declare function commandsEntry(runner: string): string;
57
78
  * before the entry is imported, since importing it is what would otherwise
58
79
  * call register().
59
80
  */
60
- export declare function commandsEntrySource(): string;
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;
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
+ import { commandHelp, EARLY_SIGNALS } from './commands.js';
9
+ import { inlineModuleName } from './files.js';
8
10
  const SOURCE = /\.(?:[cm]?[jt]sx?)$/;
9
11
  const NAME = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/;
10
12
  /** Earlier wins when two files share a name: migrate.ts over migrate.js. */
@@ -99,6 +101,85 @@ export function runCommand(argv: string[], program?: string) {
99
101
  }
100
102
  `;
101
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
+ }
102
183
  /**
103
184
  * The entry a plain build runs a command from, written beside the server:
104
185
  *
@@ -109,28 +190,113 @@ export function runCommand(argv: string[], program?: string) {
109
190
  * before the entry is imported, since importing it is what would otherwise
110
191
  * call register().
111
192
  */
112
- export function commandsEntrySource() {
113
- return ('// Run one of the app\'s commands without starting the server:\n' +
193
+ export function commandsEntrySource(options) {
194
+ return ("// Run one of the app's commands without starting the server:\n" +
114
195
  '// node .output/server/commands.mjs <command> [args]\n' +
115
196
  '// bun .output/server/commands.mjs <command> [args]\n' +
116
197
  'import { relative } from "node:path";\n' +
117
198
  'import { fileURLToPath } from "node:url";\n' +
118
199
  '\n' +
119
- 'globalThis[Symbol.for("rsc-kit.command")] = true;\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' +
120
211
  '\n' +
121
- 'let code = 1;\n' +
212
+ indent(earlySignalsSource()) +
122
213
  '\n' +
123
- 'try {\n' +
124
- ' const { runCommand } = await import("./_ssr/rsc.mjs");\n' +
125
- ' const runtime = process.versions.bun ? "bun" : "node";\n' +
214
+ ' let code = 1;\n' +
126
215
  '\n' +
127
- ' code = await runCommand(process.argv.slice(2), runtime + " " + relative(process.cwd(), fileURLToPath(import.meta.url)));\n' +
128
- '} catch (error) {\n' +
129
- ' // The app failed to load: instrumentation.ts refusing a bad environment,\n' +
130
- ' // as it would stop the server.\n' +
131
- ' console.error("[rsc-kit] the app failed to load:", error);\n' +
132
- '}\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' +
133
224
  '\n' +
134
- 'process.exit(code);\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 --splitting .output/server/compile.mjs --outfile dist/app\n' +
250
+ '//\n' +
251
+ '// --splitting is what lets a worker hold a stop. Bun parses every module in a\n' +
252
+ '// binary before it runs the first line of any, and a stop that arrives in that\n' +
253
+ '// time is the default action (exit 143): splitting makes this file its own\n' +
254
+ '// chunk, which holds the stop for the command from its first line.\n' +
255
+ '//\n' +
256
+ '// ./app starts the server. ./app <command> runs src/commands/<command>.\n' +
257
+ '//\n' +
258
+ '// A binary carries what is imported by name. The server reads its frozen pages\n' +
259
+ '// through a computed import, which a compile cannot see - imported here, they\n' +
260
+ '// travel inside the binary and are handed to the server before it starts.\n' +
261
+ '// The server itself is imported only when no command was named: a static\n' +
262
+ '// import would start it, and bind its port, before argv could be read. Not\n' +
263
+ '// awaited, since a top-level await is what stopped --bytecode from applying.\n' +
264
+ 'import { basename } from "node:path";\n' +
265
+ `import "./${inlineModuleName(dirName)}";\n` +
266
+ '\n' +
267
+ commandAnswerSource(options) +
268
+ '\n' +
269
+ 'const failed = (what) => (error) => {\n' +
270
+ ' console.error(what, error);\n' +
271
+ ' process.exit(1);\n' +
272
+ '};\n' +
273
+ '\n' +
274
+ 'if (process.argv.length <= 2) {\n' +
275
+ ' import("./index.mjs").catch(failed("[rsc-kit] the server failed to start:"));\n' +
276
+ '} else {\n' +
277
+ ' const argv = process.argv.slice(2);\n' +
278
+ ' const program = "./" + basename(process.execPath);\n' +
279
+ ' const answered = answer(argv, program);\n' +
280
+ '\n' +
281
+ ' if (answered) {\n' +
282
+ ' (answered.code === 0 ? process.stdout : process.stderr).write(answered.text);\n' +
283
+ ' process.exitCode = answered.code;\n' +
284
+ ' } else {\n' +
285
+ ' globalThis[Symbol.for("rsc-kit.command")] = true;\n' +
286
+ '\n' +
287
+ indent(indent(earlySignalsSource())) +
288
+ '\n' +
289
+ ' Promise.resolve()\n' +
290
+ ` ${chain}\n` +
291
+ ' .then(({ runCommand }) => runCommand(argv, program))\n' +
292
+ ' .then((code) => process.exit(code), failed("[rsc-kit] the app failed to load:"));\n' +
293
+ ' }\n' +
294
+ '}\n');
295
+ }
296
+ function indent(text) {
297
+ return text
298
+ .split('\n')
299
+ .map((line) => (line ? ' ' + line : line))
300
+ .join('\n');
135
301
  }
136
302
  //# sourceMappingURL=commandFiles.js.map
@@ -1 +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'\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/**\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(): 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 'globalThis[Symbol.for(\"rsc-kit.command\")] = true;\\n' +\n '\\n' +\n 'let code = 1;\\n' +\n '\\n' +\n 'try {\\n' +\n ' const { runCommand } = await import(\"./_ssr/rsc.mjs\");\\n' +\n ' const runtime = process.versions.bun ? \"bun\" : \"node\";\\n' +\n '\\n' +\n ' code = await runCommand(process.argv.slice(2), runtime + \" \" + relative(process.cwd(), fileURLToPath(import.meta.url)));\\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"]}
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 --splitting .output/server/compile.mjs --outfile dist/app\\n' +\n '//\\n' +\n '// --splitting is what lets a worker hold a stop. Bun parses every module in a\\n' +\n '// binary before it runs the first line of any, and a stop that arrives in that\\n' +\n '// time is the default action (exit 143): splitting makes this file its own\\n' +\n '// chunk, which holds the stop for the command from its first line.\\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"]}
@@ -7,8 +7,8 @@
7
7
  * `node .output/server/commands.mjs <name>`, so nothing about a command
8
8
  * depends on Bun, on a platform, or on the binary.
9
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
10
+ * Each command is a file with a default export, `(args, context) => number
11
+ * | void`, where `context` is `{ signal, deadline }`. It shares the build's code and dependencies, so the database
12
12
  * package and the logger the pages use are the ones it uses, in the same
13
13
  * bundle. It is loaded only when it is named: the server never evaluates one.
14
14
  *
@@ -32,7 +32,38 @@ export declare const COMMAND_MODE: unique symbol;
32
32
  export interface CommandContext {
33
33
  /** Aborted by SIGTERM or SIGINT: a long-running command stops when it is. */
34
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
+ * A getter, because what it answers changes: read `context.deadline` when
42
+ * you need it. `({ deadline }) => ...` reads it once, as the command
43
+ * starts, when it is always undefined, and keeps that.
44
+ */
45
+ readonly deadline: number | undefined;
35
46
  }
47
+ /** A command's default export. */
48
+ export type CommandFunction = (args: string[], context: CommandContext) => number | void | Promise<number | void>;
49
+ /**
50
+ * Types a command's default export, so its arguments and context are known
51
+ * without annotating them: `export default defineCommand(async (args, context) => {})`.
52
+ * It returns what it is given.
53
+ */
54
+ export declare function defineCommand(command: CommandFunction): CommandFunction;
55
+ /**
56
+ * The entry that loads the app owns the process's SIGTERM and SIGINT, and
57
+ * leaves this on `globalThis` for the runner.
58
+ *
59
+ * Loading can take a while, and a signal before the runner exists finds only
60
+ * the default action, which ends the process with 143. So the entry holds
61
+ * them, and the runner is handed over to: the count of those that came first,
62
+ * and every later one. The entry's listener is never taken off. A signal is
63
+ * delivered on the event loop's next turn, not when it arrives, and a listener
64
+ * removed in between has the signal taken from it without anyone getting it.
65
+ */
66
+ export declare const EARLY_SIGNALS: unique symbol;
36
67
  /** What the build knows of one command file: how to load it, and a line to list it by. */
37
68
  export interface CommandEntry {
38
69
  load: () => Promise<{
@@ -62,7 +93,9 @@ export interface RunCommandOptions {
62
93
  /** Whether this process was started to run a command rather than to serve. */
63
94
  export declare function inCommandMode(): boolean;
64
95
  /** The list `--help` prints, and what an unknown name is answered with. */
65
- export declare function commandHelp(commands: Record<string, CommandEntry>, program?: string): string;
96
+ export declare function commandHelp(commands: Record<string, {
97
+ description?: string;
98
+ }>, program?: string): string;
66
99
  /**
67
100
  * Run the command `argv` names and say how it ended.
68
101
  *
package/dist/commands.js CHANGED
@@ -7,8 +7,8 @@
7
7
  * `node .output/server/commands.mjs <name>`, so nothing about a command
8
8
  * depends on Bun, on a platform, or on the binary.
9
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
10
+ * Each command is a file with a default export, `(args, context) => number
11
+ * | void`, where `context` is `{ signal, deadline }`. It shares the build's code and dependencies, so the database
12
12
  * package and the logger the pages use are the ones it uses, in the same
13
13
  * bundle. It is loaded only when it is named: the server never evaluates one.
14
14
  *
@@ -28,6 +28,26 @@
28
28
  * a server does at startup, which a command has not asked for.
29
29
  */
30
30
  export const COMMAND_MODE = Symbol.for('rsc-kit.command');
31
+ /**
32
+ * Types a command's default export, so its arguments and context are known
33
+ * without annotating them: `export default defineCommand(async (args, context) => {})`.
34
+ * It returns what it is given.
35
+ */
36
+ export function defineCommand(command) {
37
+ return command;
38
+ }
39
+ /**
40
+ * The entry that loads the app owns the process's SIGTERM and SIGINT, and
41
+ * leaves this on `globalThis` for the runner.
42
+ *
43
+ * Loading can take a while, and a signal before the runner exists finds only
44
+ * the default action, which ends the process with 143. So the entry holds
45
+ * them, and the runner is handed over to: the count of those that came first,
46
+ * and every later one. The entry's listener is never taken off. A signal is
47
+ * delivered on the event loop's next turn, not when it arrives, and a listener
48
+ * removed in between has the signal taken from it without anyone getting it.
49
+ */
50
+ export const EARLY_SIGNALS = Symbol.for('rsc-kit.command.signals');
31
51
  /** Whether this process was started to run a command rather than to serve. */
32
52
  export function inCommandMode() {
33
53
  return Boolean(globalThis[COMMAND_MODE]);
@@ -96,13 +116,21 @@ export async function runCommand(argv, options) {
96
116
  let registered = false;
97
117
  let stop;
98
118
  let stopTimer;
119
+ let deadline;
99
120
  let signalled = 0;
121
+ const context = {
122
+ signal: controller.signal,
123
+ get deadline() {
124
+ return deadline;
125
+ },
126
+ };
100
127
  // The first signal asks the command to stop and gives it `limit` to do so,
101
128
  // as the server gives in-flight requests theirs. The second is a person who
102
129
  // meant it, and nothing waits.
103
130
  const onSignal = () => {
104
131
  signalled += 1;
105
132
  if (signalled === 1) {
133
+ deadline = Date.now() + limit;
106
134
  controller.abort();
107
135
  stopTimer = setTimeout(() => {
108
136
  err(`[rsc-kit] the command "${name}" did not stop within ${limit / 1000} seconds.\n`);
@@ -116,8 +144,19 @@ export async function runCommand(argv, options) {
116
144
  const stopped = new Promise((resolve) => {
117
145
  stop = resolve;
118
146
  });
119
- signals?.on('SIGTERM', onSignal);
120
- signals?.on('SIGINT', onSignal);
147
+ // Behind an entry that loaded the app, the process's signals are the
148
+ // entry's: it hands each one over, and what came while the app was loading
149
+ // is counted here. Told to stop while loading is told to stop.
150
+ const early = globalThis[EARLY_SIGNALS];
151
+ if (early) {
152
+ early.handoff(onSignal);
153
+ for (let caught = 0; caught < early.count; caught++)
154
+ onSignal();
155
+ }
156
+ else {
157
+ signals?.on('SIGTERM', onSignal);
158
+ signals?.on('SIGINT', onSignal);
159
+ }
121
160
  // A command that is only waiting - for its signal, for a message that has
122
161
  // not come - holds nothing open, and Node answers an event loop with nothing
123
162
  // in it by exiting 13 with a warning about an unsettled await, where Bun
@@ -135,7 +174,7 @@ export async function runCommand(argv, options) {
135
174
  await options.ready();
136
175
  registered = true;
137
176
  }
138
- return exitCodeOf(await module.default(args, { signal: controller.signal }), name);
177
+ return exitCodeOf(await module.default(args, context), name);
139
178
  }
140
179
  catch (error) {
141
180
  err(`[rsc-kit] the command "${name}" failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
@@ -149,8 +188,12 @@ export async function runCommand(argv, options) {
149
188
  finally {
150
189
  clearInterval(alive);
151
190
  clearTimeout(stopTimer);
152
- signals?.off('SIGTERM', onSignal);
153
- signals?.off('SIGINT', onSignal);
191
+ if (early)
192
+ early.handoff(undefined);
193
+ else {
194
+ signals?.off('SIGTERM', onSignal);
195
+ signals?.off('SIGINT', onSignal);
196
+ }
154
197
  }
155
198
  // What register() opened is closed here, bounded like the server's: a
156
199
  // close that hangs must not hold a release step open.
@@ -1 +1 @@
1
- {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * App commands: the files in `src/commands`, run as `./app <name>`.\n *\n * A compiled app is one binary that starts a server, and a release step or a\n * queue worker is the same code started differently - `./app migrate`,\n * `./app work`. The same entry also runs from a plain build, as\n * `node .output/server/commands.mjs <name>`, so nothing about a command\n * depends on Bun, on a platform, or on the binary.\n *\n * Each command is a file with a default export, `(args, { signal }) => number\n * | void`. It shares the build's code and dependencies, so the database\n * package and the logger the pages use are the ones it uses, in the same\n * bundle. It is loaded only when it is named: the server never evaluates one.\n *\n * `register()` from instrumentation.ts is skipped unless the command says\n * `export const register = true`. Importing instrumentation.ts is not\n * skipped - a bad environment fails there, at import, for a command as for a\n * server - but a command that only needs the schema should not pay for, or\n * fail on, a connection it never uses.\n *\n * Nothing here imports `node:` anything: the runner is bundled into the\n * server's entry for every host, and a host without those modules must still\n * load it. `process` is reached lazily, inside the functions that need it.\n */\n\n/**\n * Set on `globalThis` before the server's entry is imported, by whatever runs\n * a command. The entry would otherwise call register() the moment it loads, as\n * a server does at startup, which a command has not asked for.\n */\nexport const COMMAND_MODE = Symbol.for('rsc-kit.command')\n\n/** What a command receives beside its arguments. */\nexport interface CommandContext {\n /** Aborted by SIGTERM or SIGINT: a long-running command stops when it is. */\n signal: AbortSignal\n}\n\n/** What the build knows of one command file: how to load it, and a line to list it by. */\nexport interface CommandEntry {\n load: () => Promise<{ default?: unknown; register?: unknown }>\n description?: string\n}\n\nexport interface RunCommandOptions {\n commands: Record<string, CommandEntry>\n /** instrumentation.ts's register(), once. Awaited only for a command that exports `register = true`. */\n ready: () => Promise<unknown>\n /** instrumentation.ts's shutdown(), called after a command that was given `register()`. */\n shutdown?: () => unknown\n /** How the usage line names this program: `./app`, `node .output/server/commands.mjs`. */\n program?: string\n out?: (text: string) => void\n err?: (text: string) => void\n /** SIGTERM and SIGINT. The process, unless a test brings its own. */\n signals?: {\n on: (signal: 'SIGTERM' | 'SIGINT', listener: () => void) => unknown\n off: (signal: 'SIGTERM' | 'SIGINT', listener: () => void) => unknown\n }\n /** Milliseconds a command gets to stop once told to, and shutdown() after it. RSC_SHUTDOWN_TIMEOUT seconds (10) by default. */\n limit?: number\n}\n\n/** Whether this process was started to run a command rather than to serve. */\nexport function inCommandMode(): boolean {\n return Boolean((globalThis as Record<symbol, unknown>)[COMMAND_MODE])\n}\n\n/** The list `--help` prints, and what an unknown name is answered with. */\nexport function commandHelp(commands: Record<string, CommandEntry>, program = 'app'): string {\n const names = Object.keys(commands).sort()\n const width = Math.max(0, ...names.map((name) => name.length))\n const rows = names.map((name) => {\n const description = commands[name]?.description\n\n return description ? ` ${name.padEnd(width)} ${description}` : ` ${name}`\n })\n\n return (\n `Usage: ${program} <command> [args]\\n\\n` +\n (rows.length ? `Commands:\\n${rows.join('\\n')}\\n` : 'There are no commands: add a file to src/commands.\\n')\n )\n}\n\n/**\n * An exit code from what a command returned. Nothing is success; a number is\n * the code. Anything else is the command's mistake, and said so, because the\n * alternative is a process that exits 0 after returning a string.\n */\nfunction exitCodeOf(result: unknown, name: string): number {\n if (result === undefined) return 0\n\n if (typeof result === 'number' && Number.isInteger(result) && result >= 0 && result <= 255) return result\n\n throw new TypeError(\n `the command \"${name}\" returned ${JSON.stringify(result)}. A command returns an exit code from 0 to 255, or nothing.`,\n )\n}\n\nfunction unref(timer: unknown): void {\n const handle = timer as { unref?: () => void } | undefined\n\n if (handle && typeof handle.unref === 'function') handle.unref()\n}\n\n/** The seconds a command and its shutdown() get after a signal, in milliseconds. */\nfunction defaultLimit(): number {\n const seconds = Number(typeof process !== 'undefined' ? process.env.RSC_SHUTDOWN_TIMEOUT : undefined)\n\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 10) * 1000\n}\n\n/**\n * Run the command `argv` names and say how it ended.\n *\n * Resolves to the exit code and leaves exiting to the caller, so a test sees\n * the code and an entry decides when the process stops. 0 is success and 1 is\n * a command that threw or would not stop; 2 is a name that is not a command,\n * as it is for a tool that was misused.\n */\nexport async function runCommand(argv: readonly string[], options: RunCommandOptions): Promise<number> {\n const out = options.out ?? ((text: string) => void process.stdout.write(text))\n const err = options.err ?? ((text: string) => void process.stderr.write(text))\n const [name, ...args] = argv\n\n if (name === '--help' || name === '-h') {\n out(commandHelp(options.commands, options.program))\n\n return 0\n }\n\n if (name === undefined) {\n err(commandHelp(options.commands, options.program))\n\n return 2\n }\n\n const command = Object.hasOwn(options.commands, name) ? options.commands[name] : undefined\n\n if (!command) {\n err(`Not a command: ${name}\\n\\n${commandHelp(options.commands, options.program)}`)\n\n return 2\n }\n\n const limit = options.limit ?? defaultLimit()\n const signals = options.signals ?? (typeof process !== 'undefined' ? process : undefined)\n const controller = new AbortController()\n\n let registered = false\n let stop: ((code: number) => void) | undefined\n let stopTimer: unknown\n let signalled = 0\n\n // The first signal asks the command to stop and gives it `limit` to do so,\n // as the server gives in-flight requests theirs. The second is a person who\n // meant it, and nothing waits.\n const onSignal = () => {\n signalled += 1\n\n if (signalled === 1) {\n controller.abort()\n\n stopTimer = setTimeout(() => {\n err(`[rsc-kit] the command \"${name}\" did not stop within ${limit / 1000} seconds.\\n`)\n stop?.(1)\n }, limit)\n\n unref(stopTimer)\n\n return\n }\n\n stop?.(1)\n }\n\n const stopped = new Promise<number>((resolve) => {\n stop = resolve\n })\n\n signals?.on('SIGTERM', onSignal)\n signals?.on('SIGINT', onSignal)\n\n // A command that is only waiting - for its signal, for a message that has\n // not come - holds nothing open, and Node answers an event loop with nothing\n // in it by exiting 13 with a warning about an unsettled await, where Bun\n // simply waits. A command that is running is running until it returns.\n const alive = setInterval(() => {}, 2 ** 30)\n\n const ran = (async (): Promise<number> => {\n try {\n const module = await command.load()\n\n if (typeof module.default !== 'function') {\n throw new TypeError(`src/commands/${name} has no default export. Export a function: export default async (args) => {}`)\n }\n\n // Opted in, and only then: a migration that needs the schema should not\n // fail because the queue it never touches is not up.\n if (module.register === true) {\n await options.ready()\n registered = true\n }\n\n return exitCodeOf(await (module.default as (...a: unknown[]) => unknown)(args, { signal: controller.signal }), name)\n } catch (error) {\n err(`[rsc-kit] the command \"${name}\" failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`)\n\n return 1\n }\n })()\n\n let code: number\n\n try {\n code = await Promise.race([ran, stopped])\n } finally {\n clearInterval(alive)\n clearTimeout(stopTimer as Parameters<typeof clearTimeout>[0])\n signals?.off('SIGTERM', onSignal)\n signals?.off('SIGINT', onSignal)\n }\n\n // What register() opened is closed here, bounded like the server's: a\n // close that hangs must not hold a release step open.\n if (registered && options.shutdown) {\n let timer: unknown\n\n await Promise.race([\n Promise.resolve()\n .then(() => options.shutdown?.())\n .catch((error) => err(`[rsc-kit] instrumentation.ts shutdown() failed: ${error instanceof Error ? error.message : String(error)}\\n`)),\n new Promise((resolve) => {\n timer = setTimeout(resolve, limit)\n unref(timer)\n }),\n ])\n\n clearTimeout(timer as Parameters<typeof clearTimeout>[0])\n }\n\n return code\n}\n"]}
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * App commands: the files in `src/commands`, run as `./app <name>`.\n *\n * A compiled app is one binary that starts a server, and a release step or a\n * queue worker is the same code started differently - `./app migrate`,\n * `./app work`. The same entry also runs from a plain build, as\n * `node .output/server/commands.mjs <name>`, so nothing about a command\n * depends on Bun, on a platform, or on the binary.\n *\n * Each command is a file with a default export, `(args, context) => number\n * | void`, where `context` is `{ signal, deadline }`. It shares the build's code and dependencies, so the database\n * package and the logger the pages use are the ones it uses, in the same\n * bundle. It is loaded only when it is named: the server never evaluates one.\n *\n * `register()` from instrumentation.ts is skipped unless the command says\n * `export const register = true`. Importing instrumentation.ts is not\n * skipped - a bad environment fails there, at import, for a command as for a\n * server - but a command that only needs the schema should not pay for, or\n * fail on, a connection it never uses.\n *\n * Nothing here imports `node:` anything: the runner is bundled into the\n * server's entry for every host, and a host without those modules must still\n * load it. `process` is reached lazily, inside the functions that need it.\n */\n\n/**\n * Set on `globalThis` before the server's entry is imported, by whatever runs\n * a command. The entry would otherwise call register() the moment it loads, as\n * a server does at startup, which a command has not asked for.\n */\nexport const COMMAND_MODE = Symbol.for('rsc-kit.command')\n\n/** What a command receives beside its arguments. */\nexport interface CommandContext {\n /** Aborted by SIGTERM or SIGINT: a long-running command stops when it is. */\n signal: AbortSignal\n /**\n * When the runner stops waiting for the command, as a time in milliseconds\n * (`Date.now()`'s), once it has been told to stop; undefined until then.\n * A worker finishing its job reads `deadline - Date.now()` rather than\n * RSC_SHUTDOWN_TIMEOUT and its default.\n *\n * A getter, because what it answers changes: read `context.deadline` when\n * you need it. `({ deadline }) => ...` reads it once, as the command\n * starts, when it is always undefined, and keeps that.\n */\n readonly deadline: number | undefined\n}\n\n/** A command's default export. */\nexport type CommandFunction = (args: string[], context: CommandContext) => number | void | Promise<number | void>\n\n/**\n * Types a command's default export, so its arguments and context are known\n * without annotating them: `export default defineCommand(async (args, context) => {})`.\n * It returns what it is given.\n */\nexport function defineCommand(command: CommandFunction): CommandFunction {\n return command\n}\n\n/**\n * The entry that loads the app owns the process's SIGTERM and SIGINT, and\n * leaves this on `globalThis` for the runner.\n *\n * Loading can take a while, and a signal before the runner exists finds only\n * the default action, which ends the process with 143. So the entry holds\n * them, and the runner is handed over to: the count of those that came first,\n * and every later one. The entry's listener is never taken off. A signal is\n * delivered on the event loop's next turn, not when it arrives, and a listener\n * removed in between has the signal taken from it without anyone getting it.\n */\nexport const EARLY_SIGNALS = Symbol.for('rsc-kit.command.signals')\n\ninterface EarlySignals {\n /** Stops that arrived before there was a runner to give them to. */\n count: number\n /** From now on, the entry gives each signal to this, and stops holding the process to its own timeout. */\n handoff: (listener: (() => void) | undefined) => void\n}\n\n/** What the build knows of one command file: how to load it, and a line to list it by. */\nexport interface CommandEntry {\n load: () => Promise<{ default?: unknown; register?: unknown }>\n description?: string\n}\n\nexport interface RunCommandOptions {\n commands: Record<string, CommandEntry>\n /** instrumentation.ts's register(), once. Awaited only for a command that exports `register = true`. */\n ready: () => Promise<unknown>\n /** instrumentation.ts's shutdown(), called after a command that was given `register()`. */\n shutdown?: () => unknown\n /** How the usage line names this program: `./app`, `node .output/server/commands.mjs`. */\n program?: string\n out?: (text: string) => void\n err?: (text: string) => void\n /** SIGTERM and SIGINT. The process, unless a test brings its own. */\n signals?: {\n on: (signal: 'SIGTERM' | 'SIGINT', listener: () => void) => unknown\n off: (signal: 'SIGTERM' | 'SIGINT', listener: () => void) => unknown\n }\n /** Milliseconds a command gets to stop once told to, and shutdown() after it. RSC_SHUTDOWN_TIMEOUT seconds (10) by default. */\n limit?: number\n}\n\n/** Whether this process was started to run a command rather than to serve. */\nexport function inCommandMode(): boolean {\n return Boolean((globalThis as Record<symbol, unknown>)[COMMAND_MODE])\n}\n\n/** The list `--help` prints, and what an unknown name is answered with. */\nexport function commandHelp(commands: Record<string, { description?: string }>, program = 'app'): string {\n const names = Object.keys(commands).sort()\n const width = Math.max(0, ...names.map((name) => name.length))\n const rows = names.map((name) => {\n const description = commands[name]?.description\n\n return description ? ` ${name.padEnd(width)} ${description}` : ` ${name}`\n })\n\n return (\n `Usage: ${program} <command> [args]\\n\\n` +\n (rows.length ? `Commands:\\n${rows.join('\\n')}\\n` : 'There are no commands: add a file to src/commands.\\n')\n )\n}\n\n/**\n * An exit code from what a command returned. Nothing is success; a number is\n * the code. Anything else is the command's mistake, and said so, because the\n * alternative is a process that exits 0 after returning a string.\n */\nfunction exitCodeOf(result: unknown, name: string): number {\n if (result === undefined) return 0\n\n if (typeof result === 'number' && Number.isInteger(result) && result >= 0 && result <= 255) return result\n\n throw new TypeError(\n `the command \"${name}\" returned ${JSON.stringify(result)}. A command returns an exit code from 0 to 255, or nothing.`,\n )\n}\n\nfunction unref(timer: unknown): void {\n const handle = timer as { unref?: () => void } | undefined\n\n if (handle && typeof handle.unref === 'function') handle.unref()\n}\n\n/** The seconds a command and its shutdown() get after a signal, in milliseconds. */\nfunction defaultLimit(): number {\n const seconds = Number(typeof process !== 'undefined' ? process.env.RSC_SHUTDOWN_TIMEOUT : undefined)\n\n return (Number.isFinite(seconds) && seconds > 0 ? seconds : 10) * 1000\n}\n\n/**\n * Run the command `argv` names and say how it ended.\n *\n * Resolves to the exit code and leaves exiting to the caller, so a test sees\n * the code and an entry decides when the process stops. 0 is success and 1 is\n * a command that threw or would not stop; 2 is a name that is not a command,\n * as it is for a tool that was misused.\n */\nexport async function runCommand(argv: readonly string[], options: RunCommandOptions): Promise<number> {\n const out = options.out ?? ((text: string) => void process.stdout.write(text))\n const err = options.err ?? ((text: string) => void process.stderr.write(text))\n const [name, ...args] = argv\n\n if (name === '--help' || name === '-h') {\n out(commandHelp(options.commands, options.program))\n\n return 0\n }\n\n if (name === undefined) {\n err(commandHelp(options.commands, options.program))\n\n return 2\n }\n\n const command = Object.hasOwn(options.commands, name) ? options.commands[name] : undefined\n\n if (!command) {\n err(`Not a command: ${name}\\n\\n${commandHelp(options.commands, options.program)}`)\n\n return 2\n }\n\n const limit = options.limit ?? defaultLimit()\n const signals = options.signals ?? (typeof process !== 'undefined' ? process : undefined)\n const controller = new AbortController()\n\n let registered = false\n let stop: ((code: number) => void) | undefined\n let stopTimer: unknown\n let deadline: number | undefined\n let signalled = 0\n const context: CommandContext = {\n signal: controller.signal,\n get deadline() {\n return deadline\n },\n }\n\n // The first signal asks the command to stop and gives it `limit` to do so,\n // as the server gives in-flight requests theirs. The second is a person who\n // meant it, and nothing waits.\n const onSignal = () => {\n signalled += 1\n\n if (signalled === 1) {\n deadline = Date.now() + limit\n controller.abort()\n\n stopTimer = setTimeout(() => {\n err(`[rsc-kit] the command \"${name}\" did not stop within ${limit / 1000} seconds.\\n`)\n stop?.(1)\n }, limit)\n\n unref(stopTimer)\n\n return\n }\n\n stop?.(1)\n }\n\n const stopped = new Promise<number>((resolve) => {\n stop = resolve\n })\n\n // Behind an entry that loaded the app, the process's signals are the\n // entry's: it hands each one over, and what came while the app was loading\n // is counted here. Told to stop while loading is told to stop.\n const early = (globalThis as Record<symbol, EarlySignals | undefined>)[EARLY_SIGNALS]\n\n if (early) {\n early.handoff(onSignal)\n\n for (let caught = 0; caught < early.count; caught++) onSignal()\n } else {\n signals?.on('SIGTERM', onSignal)\n signals?.on('SIGINT', onSignal)\n }\n\n // A command that is only waiting - for its signal, for a message that has\n // not come - holds nothing open, and Node answers an event loop with nothing\n // in it by exiting 13 with a warning about an unsettled await, where Bun\n // simply waits. A command that is running is running until it returns.\n const alive = setInterval(() => {}, 2 ** 30)\n\n const ran = (async (): Promise<number> => {\n try {\n const module = await command.load()\n\n if (typeof module.default !== 'function') {\n throw new TypeError(`src/commands/${name} has no default export. Export a function: export default async (args) => {}`)\n }\n\n // Opted in, and only then: a migration that needs the schema should not\n // fail because the queue it never touches is not up.\n if (module.register === true) {\n await options.ready()\n registered = true\n }\n\n return exitCodeOf(await (module.default as (...a: unknown[]) => unknown)(args, context), name)\n } catch (error) {\n err(`[rsc-kit] the command \"${name}\" failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`)\n\n return 1\n }\n })()\n\n let code: number\n\n try {\n code = await Promise.race([ran, stopped])\n } finally {\n clearInterval(alive)\n clearTimeout(stopTimer as Parameters<typeof clearTimeout>[0])\n\n if (early) early.handoff(undefined)\n else {\n signals?.off('SIGTERM', onSignal)\n signals?.off('SIGINT', onSignal)\n }\n }\n\n // What register() opened is closed here, bounded like the server's: a\n // close that hangs must not hold a release step open.\n if (registered && options.shutdown) {\n let timer: unknown\n\n await Promise.race([\n Promise.resolve()\n .then(() => options.shutdown?.())\n .catch((error) => err(`[rsc-kit] instrumentation.ts shutdown() failed: ${error instanceof Error ? error.message : String(error)}\\n`)),\n new Promise((resolve) => {\n timer = setTimeout(resolve, limit)\n unref(timer)\n }),\n ])\n\n clearTimeout(timer as Parameters<typeof clearTimeout>[0])\n }\n\n return code\n}\n"]}
package/dist/files.d.ts CHANGED
@@ -67,9 +67,7 @@ export declare function assetsReader(assets: AssetsBinding, prefix: string): (na
67
67
  * whose other Nitro app compiled with it asked why this one could not.
68
68
  * `bun build --compile --bytecode .output/server/compile.mjs` works now.
69
69
  */
70
- export declare function compileEntrySource(dirName: string, options?: {
71
- commands?: boolean;
72
- }): string;
70
+ export declare function compileEntrySource(dirName: string): string;
73
71
  /** The name the build writes the inline module under, beside the stored pages' directory. */
74
72
  export declare function inlineModuleName(dirName: string): string;
75
73
  /**