@luckystack/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +83 -0
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/index.d.ts +292 -0
- package/dist/index.js +1665 -0
- package/dist/index.js.map +1 -0
- package/dist/parseArgv.d.ts +2 -0
- package/dist/parseArgv.js +38 -0
- package/dist/parseArgv.js.map +1 -0
- package/docs/argv-parsing.md +256 -0
- package/docs/create-server.md +295 -0
- package/docs/http-routes.md +309 -0
- package/docs/request-pipeline.md +198 -0
- package/docs/runtime-maps.md +206 -0
- package/docs/security-defaults.md +236 -0
- package/package.json +79 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Argv Parsing (`parseServerArgv` + `applyServerArgv`)
|
|
2
|
+
|
|
3
|
+
> Deep specs. Bron: `packages/server/src/argv.ts`, `packages/server/src/parseArgv.ts`. Bijgewerkt: 2026-05-20.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`@luckystack/server` accepts two positional CLI arguments on boot:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm run server -- <bundle[,bundle...]> [port]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Arg 0 — preset list. Comma-separated; duplicates collapsed; runtime maps from each preset are shallow-merged at boot.
|
|
14
|
+
- Arg 1 — listen port. Numeric. Optional.
|
|
15
|
+
|
|
16
|
+
Argv replaces the legacy `LUCKYSTACK_BUNDLE` + `SERVER_PORT` environment toggles with one shape consumed by `createProdRuntimeMapsProvider` (preset) and `createLuckyStackServer` (port).
|
|
17
|
+
|
|
18
|
+
The module exposes:
|
|
19
|
+
|
|
20
|
+
- A pure parser: `parseServerArgv(argv)`.
|
|
21
|
+
- A side-effect runner that reads `process.argv.slice(2)` once and caches: `applyServerArgv()`.
|
|
22
|
+
- Read accessors: `getParsedBundles()`, `getParsedPort()`.
|
|
23
|
+
- A side-effect-only entrypoint `@luckystack/server/parseArgv` that simply imports `applyServerArgv` and runs it.
|
|
24
|
+
|
|
25
|
+
The side-effect entry MUST be the FIRST import in the consumer's `server.ts` because the parsed port is written back to `process.env.SERVER_PORT`, and downstream modules read that variable at top-level evaluation time:
|
|
26
|
+
|
|
27
|
+
- `@luckystack/core` env Zod schema
|
|
28
|
+
- `@luckystack/core` `bindAddress.ts` fallback
|
|
29
|
+
- The consumer's `config.ts` `backendUrl` constant
|
|
30
|
+
- `@luckystack/login` `oauthProviders.ts` callback URL builder
|
|
31
|
+
|
|
32
|
+
Importing anything that pulls one of those four before `parseArgv` runs will lock in the wrong port.
|
|
33
|
+
|
|
34
|
+
## API Reference
|
|
35
|
+
|
|
36
|
+
### `parseServerArgv(argv: string[]): ParsedServerArgv`
|
|
37
|
+
|
|
38
|
+
**Signature:**
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
export interface ParsedServerArgv {
|
|
42
|
+
bundles: string[];
|
|
43
|
+
port: number | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const parseServerArgv = (argv: string[]): ParsedServerArgv;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Parameters:**
|
|
50
|
+
|
|
51
|
+
| Field | Type | Purpose |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `argv` | `string[]` | Positional args (typically `process.argv.slice(2)`). |
|
|
54
|
+
|
|
55
|
+
**Returns:** `{ bundles, port }`:
|
|
56
|
+
|
|
57
|
+
- `bundles: string[]` — deduplicated, trimmed, non-empty entries from arg 0. Empty array when arg 0 is missing or empty.
|
|
58
|
+
- `port: number | null` — `parseInt(argv[1], 10)` when arg 1 is supplied; `null` otherwise.
|
|
59
|
+
|
|
60
|
+
**Behavior:**
|
|
61
|
+
|
|
62
|
+
- Reject more than 2 positional arguments by throwing `Error('[luckystack:argv] unexpected positional argument(s): "<rest>". Usage: npm run server -- <bundle[,bundle...]> [port]')`.
|
|
63
|
+
- For arg 0: split on `,`, `trim()` each piece, drop falsy, collapse via `Array.from(new Set(...))`.
|
|
64
|
+
- For arg 1: must match `/^\d+$/`. Otherwise throws `Error('[luckystack:argv] port argument must be numeric, got: "<value>". Usage: npm run server -- <bundle[,bundle...]> [port]')`.
|
|
65
|
+
|
|
66
|
+
**Errors / Edge cases:**
|
|
67
|
+
|
|
68
|
+
- Whitespace in arg 0 (`"billing, vehicles"`) is supported — trimmed.
|
|
69
|
+
- A trailing comma (`"billing,"`) is silently dropped.
|
|
70
|
+
- A leading `0` in the port string is accepted (`/^\d+$/`) and parsed normally.
|
|
71
|
+
- An empty arg 0 (`""`) yields `bundles: []`; the downstream resolver falls back to `['default']`.
|
|
72
|
+
|
|
73
|
+
**Example:**
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
parseServerArgv(['billing,vehicles', '4001']);
|
|
77
|
+
// => { bundles: ['billing', 'vehicles'], port: 4001 }
|
|
78
|
+
|
|
79
|
+
parseServerArgv([]);
|
|
80
|
+
// => { bundles: [], port: null }
|
|
81
|
+
|
|
82
|
+
parseServerArgv(['billing', '4001', 'oops']);
|
|
83
|
+
// => throws (too many positionals)
|
|
84
|
+
|
|
85
|
+
parseServerArgv(['billing', 'PORT']);
|
|
86
|
+
// => throws (non-numeric port)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### `applyServerArgv(): void`
|
|
92
|
+
|
|
93
|
+
**Signature:**
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
export const applyServerArgv = (): void;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Parameters:** none. Reads `process.argv.slice(2)`.
|
|
100
|
+
|
|
101
|
+
**Returns:** `void`.
|
|
102
|
+
|
|
103
|
+
**Behavior:**
|
|
104
|
+
|
|
105
|
+
- Idempotent. Subsequent calls return immediately via the module-level `hasRun` latch.
|
|
106
|
+
- First call:
|
|
107
|
+
1. `parseServerArgv(process.argv.slice(2))` (throws on malformed input).
|
|
108
|
+
2. Caches `bundles` + `port` in module state.
|
|
109
|
+
3. When `port !== null`, writes `process.env.SERVER_PORT = String(port)`. This is the writeback that lets the four downstream env-readers (listed in Overview) see the resolved port without per-call refactoring.
|
|
110
|
+
|
|
111
|
+
**Errors / Edge cases:**
|
|
112
|
+
|
|
113
|
+
- Throwing during this call aborts boot before any other module has a chance to read `SERVER_PORT`.
|
|
114
|
+
- Calling `applyServerArgv` after another module has already read `SERVER_PORT` is too late — that consumer has already captured the old value.
|
|
115
|
+
|
|
116
|
+
**Example:**
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// server.ts — first line
|
|
120
|
+
import '@luckystack/server/parseArgv';
|
|
121
|
+
// rest of bootstrap...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Or call it explicitly when you control the boot timing:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { applyServerArgv } from '@luckystack/server';
|
|
128
|
+
applyServerArgv();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### `getParsedBundles(): string[]`
|
|
134
|
+
|
|
135
|
+
**Signature:**
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
export const getParsedBundles = (): string[];
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Returns:** the cached `bundles` array. Empty until `applyServerArgv()` has run.
|
|
142
|
+
|
|
143
|
+
**Behavior:**
|
|
144
|
+
|
|
145
|
+
- Read-only; never throws.
|
|
146
|
+
- Used by `createProdRuntimeMapsProvider` to resolve which preset(s) to load when neither `options.preset` nor a literal string is supplied.
|
|
147
|
+
|
|
148
|
+
**Example:**
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { applyServerArgv, getParsedBundles } from '@luckystack/server';
|
|
152
|
+
|
|
153
|
+
applyServerArgv();
|
|
154
|
+
console.log(getParsedBundles()); // e.g. ['billing', 'vehicles']
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### `getParsedPort(): number | null`
|
|
160
|
+
|
|
161
|
+
**Signature:**
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
export const getParsedPort = (): number | null;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Returns:** the cached `port`. `null` until `applyServerArgv()` has run with a numeric arg 1.
|
|
168
|
+
|
|
169
|
+
**Behavior:**
|
|
170
|
+
|
|
171
|
+
- Read-only; never throws.
|
|
172
|
+
- Consumed by `createLuckyStackServer` as one of the port-resolution fallbacks:
|
|
173
|
+
1. `options.port`
|
|
174
|
+
2. `getParsedPort()`
|
|
175
|
+
3. `process.env.SERVER_PORT`
|
|
176
|
+
4. `80`
|
|
177
|
+
|
|
178
|
+
**Example:**
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { getParsedPort } from '@luckystack/server';
|
|
182
|
+
|
|
183
|
+
const port = getParsedPort();
|
|
184
|
+
if (port !== null) {
|
|
185
|
+
console.log(`argv supplied port ${port}`);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### Side-effect entrypoint: `@luckystack/server/parseArgv`
|
|
192
|
+
|
|
193
|
+
**Module body (verbatim):**
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { applyServerArgv } from './argv';
|
|
197
|
+
|
|
198
|
+
applyServerArgv();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Usage:** import as the FIRST line of your `server.ts`:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import '@luckystack/server/parseArgv';
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Anything that depends on `process.env.SERVER_PORT` MUST be imported below this line. Common pitfalls:
|
|
208
|
+
|
|
209
|
+
- Importing `'./config'` (which evaluates `backendUrl` at top level) before `parseArgv`.
|
|
210
|
+
- Importing `@luckystack/core` modules that pull the env Zod schema before `parseArgv`.
|
|
211
|
+
|
|
212
|
+
Both will freeze the wrong port into module-level constants.
|
|
213
|
+
|
|
214
|
+
## Resolution order summary
|
|
215
|
+
|
|
216
|
+
| Consumer | Source of port |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| `createLuckyStackServer` | `options.port` -> `getParsedPort()` -> `SERVER_PORT` -> `80` |
|
|
219
|
+
| `createLuckyStackServer` IP | `options.ip` -> `SERVER_IP` -> `127.0.0.1` |
|
|
220
|
+
| `createProdRuntimeMapsProvider` | `options.preset` (string -> single-entry array; non-empty array -> dedup) -> `getParsedBundles()` -> `['default']` |
|
|
221
|
+
|
|
222
|
+
## CLI examples
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# Default preset, port 80
|
|
226
|
+
npm run server
|
|
227
|
+
|
|
228
|
+
# Single bundle, port 80
|
|
229
|
+
npm run server -- billing
|
|
230
|
+
|
|
231
|
+
# Two bundles merged, port 4001
|
|
232
|
+
npm run server -- billing,vehicles 4001
|
|
233
|
+
|
|
234
|
+
# Whitespace in bundle list is fine
|
|
235
|
+
npm run server -- "billing , vehicles" 4001
|
|
236
|
+
|
|
237
|
+
# Invalid port — boot aborts with descriptive error
|
|
238
|
+
npm run server -- billing PORT
|
|
239
|
+
# Error: [luckystack:argv] port argument must be numeric, got: "PORT".
|
|
240
|
+
|
|
241
|
+
# Extra positional — boot aborts
|
|
242
|
+
npm run server -- billing 4001 oops
|
|
243
|
+
# Error: [luckystack:argv] unexpected positional argument(s): "oops".
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Interaction with runtime maps
|
|
247
|
+
|
|
248
|
+
`createProdRuntimeMapsProvider({ loadGenerated, preset? })` calls `getParsedBundles()` when `preset` is omitted or empty. Each resolved preset is dynamically imported via the consumer-supplied `loadGenerated` callback, then shallow-merged into one runtime view. Key collisions across presets throw at boot. See `runtime-maps.md` for the merge semantics.
|
|
249
|
+
|
|
250
|
+
## Related
|
|
251
|
+
|
|
252
|
+
- Function INDEX: `packages/server/CLAUDE.md`
|
|
253
|
+
- Runtime maps: `packages/server/docs/runtime-maps.md`
|
|
254
|
+
- Create server: `packages/server/docs/create-server.md`
|
|
255
|
+
- Architecture: `docs/ARCHITECTURE_PACKAGING.md` (preset bundles, multi-service builds)
|
|
256
|
+
- README: `packages/server/README.md`
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Create Server (`createLuckyStackServer` + `bootstrapLuckyStack`)
|
|
2
|
+
|
|
3
|
+
> Deep specs. Bron: `packages/server/src/createServer.ts`, `packages/server/src/bootstrap.ts`, `packages/server/src/verifyBootstrap.ts`, `packages/server/src/hookPayloads.ts`, `packages/server/src/types.ts`. Bijgewerkt: 2026-05-20.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`@luckystack/server` exposes two boot entries:
|
|
8
|
+
|
|
9
|
+
- `bootstrapLuckyStack(options)` — high-level, auto-imports the project's `luckystack/<package>/*.ts` overlay folder in topological order, then delegates to `createLuckyStackServer`. This is what `create-luckystack-app` scaffolds and what most apps should call.
|
|
10
|
+
- `createLuckyStackServer(options)` — low-level factory. Skips overlay loading; the caller is responsible for getting every framework registry populated (project config, deploy config, runtime maps, OAuth providers, etc.) before it runs. Returns `{ httpServer, ioServer, listen }`.
|
|
11
|
+
|
|
12
|
+
Both honor an explicit pre-flight check via `verifyBootstrap(requirements?)`, which fails fast when a critical registration was forgotten. `createLuckyStackServer` invokes it internally so direct callers don't have to.
|
|
13
|
+
|
|
14
|
+
Boot order (effective for both entries):
|
|
15
|
+
|
|
16
|
+
1. `bootstrapLuckyStack` only: load overlay files (`core` -> `deploy` -> `login` -> `sentry` -> `presence` -> `docs-ui` -> `server`, each folder topologically followed by alphabetical `*.ts`).
|
|
17
|
+
2. If `options.loadGeneratedMaps` was supplied, register the framework-shipped runtime-maps provider before `verifyBootstrap` so the boot check sees it.
|
|
18
|
+
3. `verifyBootstrap` runs (using the per-call `requireDeployConfig` / `requireServicesConfig` / `requireOAuthProviders` flags). Throws a single descriptive `Error` if anything is missing.
|
|
19
|
+
4. Resolve `port` (`options.port` -> argv-parsed -> `SERVER_PORT` -> `80`) and `ip` (`options.ip` -> `SERVER_IP` -> `127.0.0.1`); register them with `registerBindAddress` so framework consumers see the resolved values.
|
|
20
|
+
5. In dev mode (`enableDevTools !== false` and `NODE_ENV !== 'production'`): `initConsolelog()` + dynamic-import `@luckystack/devkit` -> `initializeAll()` + `setupWatchers()`. Install `SIGINT` / `SIGTERM` handlers that force-exit.
|
|
21
|
+
6. `writeBootUuid()` writes a fresh boot UUID to Redis so `/_health` becomes truthful and the router can detect rolling restarts.
|
|
22
|
+
7. Construct `http.createServer(handleHttpRequest)` and `loadSocket(httpServer, { maxHttpBufferSize })`.
|
|
23
|
+
8. Return `{ httpServer, ioServer, listen }`. The HTTP server has NOT started listening yet; the caller invokes `listen()` to bind.
|
|
24
|
+
|
|
25
|
+
## API Reference
|
|
26
|
+
|
|
27
|
+
### `createLuckyStackServer(options?: CreateLuckyStackServerOptions): Promise<RunningLuckyStackServer>`
|
|
28
|
+
|
|
29
|
+
**Signature:**
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
export const createLuckyStackServer = async (
|
|
33
|
+
options: CreateLuckyStackServerOptions = {}
|
|
34
|
+
): Promise<RunningLuckyStackServer>;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Parameters:**
|
|
38
|
+
|
|
39
|
+
| Field | Type | Default | Purpose |
|
|
40
|
+
| --- | --- | --- | --- |
|
|
41
|
+
| `port` | `number \| string` | `getParsedPort()` -> `SERVER_PORT` -> `80` | HTTP listen port. String coerced via `parseInt(_, 10)`. |
|
|
42
|
+
| `ip` | `string` | `process.env.SERVER_IP ?? '127.0.0.1'` | Bind address. Registered with `registerBindAddress` so `checkOrigin` and other framework code see the resolved value. |
|
|
43
|
+
| `serveFile` | `StaticFileHandler` | none | Catch-all GET handler (Vite output, SPA `index.html`). Called for `/assets/*`, known static extensions, and the final SPA fallback. Without it the static fallback returns `404`. |
|
|
44
|
+
| `serveFavicon` | `FaviconHandler` | none | Handler for `/favicon.ico`. Without it the route returns `404`. |
|
|
45
|
+
| `customRoutes` | `CustomRouteHandler` | none | Inline custom-route hook. Composed after the registry handlers from `registerCustomRoute(...)`; first one to return `true` wins. |
|
|
46
|
+
| `enableDevTools` | `boolean` | `NODE_ENV !== 'production'` | Toggles `initConsolelog()` + devkit hot reload + REPL. Pass `false` to opt out. |
|
|
47
|
+
| `maxHttpBufferSize` | `number` | `5 * 1024 * 1024` | Forwarded to Socket.io. Raise for large payloads. |
|
|
48
|
+
| `requireDeployConfig` | `boolean` | `false` | When `true`, `verifyBootstrap` fails if no `DeployConfig` was registered. |
|
|
49
|
+
| `requireServicesConfig` | `boolean` | `false` | When `true`, `verifyBootstrap` fails if no `ServicesConfig` was registered (router topology). |
|
|
50
|
+
| `requireOAuthProviders` | `boolean` | `false` | When `true`, `verifyBootstrap` fails if only the default `credentials` provider is registered. |
|
|
51
|
+
| `loadGeneratedMaps` | `(preset: string) => Promise<unknown>` | none | Callback that resolves a generated runtime-maps module per preset. Triggers `registerProdRuntimeMapsProvider` internally. Required because dynamic-import resolution is module-scoped — the framework cannot resolve a relative path on the consumer's behalf. |
|
|
52
|
+
| `runtimeMapsPreset` | `string \| string[]` | argv -> `'default'` | Overrides the argv-derived preset list. |
|
|
53
|
+
|
|
54
|
+
**Returns:** `RunningLuckyStackServer`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
interface RunningLuckyStackServer {
|
|
58
|
+
httpServer: http.Server;
|
|
59
|
+
ioServer: socket.io.Server;
|
|
60
|
+
listen: (callback?: () => void) => Promise<http.Server>;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`listen()` resolves when the HTTP server is bound. Logs `Server is running on http://<ip>:<port>/` when `projectConfig.logging.socketStartup` or `projectConfig.logging.devLogs` is enabled. It is safe to call once; calling twice yields a Node `ERR_SERVER_ALREADY_LISTEN` from the underlying `httpServer.listen`.
|
|
65
|
+
|
|
66
|
+
**Behavior (execution order):**
|
|
67
|
+
|
|
68
|
+
- Auto-register prod runtime-maps provider if `loadGeneratedMaps` is set.
|
|
69
|
+
- Run `verifyBootstrap` (may throw).
|
|
70
|
+
- Resolve port + ip; call `registerBindAddress({ ip, port })`.
|
|
71
|
+
- If dev-tools enabled: load console initializer + devkit (`initializeAll`, `setupWatchers`); attach SIGINT / SIGTERM force-exit handlers.
|
|
72
|
+
- `await writeBootUuid()`.
|
|
73
|
+
- Create `http.Server` whose request handler is `handleHttpRequest(req, res, options)` (see `request-pipeline.md`).
|
|
74
|
+
- `loadSocket(httpServer, { maxHttpBufferSize })` attaches the Socket.io server.
|
|
75
|
+
- Return `{ httpServer, ioServer, listen }` without listening.
|
|
76
|
+
|
|
77
|
+
**Errors / Edge cases:**
|
|
78
|
+
|
|
79
|
+
- Throws (from `verifyBootstrap`) when a required registry is missing — single multi-line `Error`.
|
|
80
|
+
- Dev-only branch dynamically imports `@luckystack/devkit`; production bundles exclude it so the import is never reached when `NODE_ENV === 'production'`.
|
|
81
|
+
- `enableDevTools: true` in production loads devkit; this is supported but unusual.
|
|
82
|
+
- SIGINT / SIGTERM handlers are installed only when dev-tools branch runs.
|
|
83
|
+
- `writeBootUuid` failures bubble — boot will abort if Redis is unreachable at start.
|
|
84
|
+
|
|
85
|
+
**Example — direct boot (skip overlay):**
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import 'dotenv/config';
|
|
89
|
+
import './config';
|
|
90
|
+
import './deploy.config';
|
|
91
|
+
import { initializeSentry } from '@luckystack/error-tracking';
|
|
92
|
+
import { registerPresenceHooks } from '@luckystack/presence';
|
|
93
|
+
import { createLuckyStackServer } from '@luckystack/server';
|
|
94
|
+
import { serveFile, serveFavicon } from './prod/serveFile';
|
|
95
|
+
|
|
96
|
+
initializeSentry();
|
|
97
|
+
registerPresenceHooks();
|
|
98
|
+
|
|
99
|
+
const server = await createLuckyStackServer({
|
|
100
|
+
serveFile,
|
|
101
|
+
serveFavicon,
|
|
102
|
+
loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`),
|
|
103
|
+
requireDeployConfig: true,
|
|
104
|
+
});
|
|
105
|
+
await server.listen();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### `bootstrapLuckyStack(options?: BootstrapLuckyStackOptions): Promise<RunningLuckyStackServer>`
|
|
111
|
+
|
|
112
|
+
**Signature:**
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
export const bootstrapLuckyStack = async (
|
|
116
|
+
options: BootstrapLuckyStackOptions = {}
|
|
117
|
+
): Promise<RunningLuckyStackServer>;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Parameters:** extends `CreateLuckyStackServerOptions` with:
|
|
121
|
+
|
|
122
|
+
| Field | Type | Default | Purpose |
|
|
123
|
+
| --- | --- | --- | --- |
|
|
124
|
+
| `overlayRoot` | `string` | `'luckystack'` | Folder name (relative to `ROOT_DIR` from `@luckystack/core`) that holds the per-package overlay files. Absolute paths are honored as-is. |
|
|
125
|
+
| `skipOverlayLoad` | `boolean` | `false` | Skip auto-loading the overlay folder. Useful for tests or hand-built registries. |
|
|
126
|
+
|
|
127
|
+
**Returns:** identical to `createLuckyStackServer`.
|
|
128
|
+
|
|
129
|
+
**Behavior:**
|
|
130
|
+
|
|
131
|
+
- If `skipOverlayLoad !== true` and `<ROOT_DIR>/<overlayRoot>` exists, walk the canonical package order (`core`, `deploy`, `login`, `sentry`, `presence`, `docs-ui`, `server`).
|
|
132
|
+
- Inside each package folder: import `index.ts`/`index.js` first if present, then every remaining `*.ts` / `*.js` file in alphabetical order. Each file is responsible for its own side-effect registration.
|
|
133
|
+
- Then delegate to `createLuckyStackServer(options)`.
|
|
134
|
+
|
|
135
|
+
**Errors / Edge cases:**
|
|
136
|
+
|
|
137
|
+
- Missing `<overlayRoot>` folder is non-fatal — the function returns silently so projects can use the legacy single-file `config.ts` layout.
|
|
138
|
+
- Missing per-package subfolders (e.g. no `luckystack/docs-ui/`) are silently skipped.
|
|
139
|
+
- Import errors from overlay files bubble up unchanged; fix the failing module.
|
|
140
|
+
|
|
141
|
+
**Example — recommended scaffolded boot:**
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import '@luckystack/server/parseArgv';
|
|
145
|
+
import 'dotenv/config';
|
|
146
|
+
import './config';
|
|
147
|
+
import './deploy.config';
|
|
148
|
+
import { bootstrapLuckyStack, verifyBootstrap } from '@luckystack/server';
|
|
149
|
+
import { serveFile, serveFavicon } from './prod/serveFile';
|
|
150
|
+
|
|
151
|
+
const server = await bootstrapLuckyStack({
|
|
152
|
+
serveFile,
|
|
153
|
+
serveFavicon,
|
|
154
|
+
loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await verifyBootstrap({
|
|
158
|
+
requireDeployConfig: true,
|
|
159
|
+
requireOAuthProviders: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await server.listen();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### `verifyBootstrap(requirements?: BootstrapRequirements): Promise<void>`
|
|
168
|
+
|
|
169
|
+
**Signature:**
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
export const verifyBootstrap = async (
|
|
173
|
+
requirements: BootstrapRequirements = {},
|
|
174
|
+
): Promise<void>;
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Parameters:**
|
|
178
|
+
|
|
179
|
+
| Field | Type | Default | Purpose |
|
|
180
|
+
| --- | --- | --- | --- |
|
|
181
|
+
| `requireDeployConfig` | `boolean` | `false` | Fail unless `registerDeployConfig` has been called. |
|
|
182
|
+
| `requireServicesConfig` | `boolean` | `false` | Fail unless `registerServicesConfig` has been called. |
|
|
183
|
+
| `requireOAuthProviders` | `boolean` | `false` | Fail unless an OAuth provider beyond the default `credentials` entry has been registered. |
|
|
184
|
+
|
|
185
|
+
**Returns:** `Promise<void>` — resolves silently when every required registry is in place.
|
|
186
|
+
|
|
187
|
+
**Behavior:**
|
|
188
|
+
|
|
189
|
+
- Builds a `missing[]` array. Checks in this order:
|
|
190
|
+
1. `isProjectConfigRegistered()` — always required.
|
|
191
|
+
2. `isDeployConfigRegistered()` — only when `requireDeployConfig`.
|
|
192
|
+
3. `isServicesConfigRegistered()` (lazy import) — only when `requireServicesConfig`.
|
|
193
|
+
4. `getOAuthProviders().length > 1` (lazy import from `@luckystack/login`) — only when `requireOAuthProviders`. Default registry length is `1` (`credentials`).
|
|
194
|
+
5. `isRuntimeMapsProviderRegistered()` — hard-fail in production, loud `getLogger().warn(...)` in dev/test.
|
|
195
|
+
6. `isLocalizedNormalizerRegistered()` — hard-fail in production, warn in dev/test.
|
|
196
|
+
- If `missing.length === 0`: return. Otherwise throw a single `Error` whose message lists every missing registration with a one-line remediation hint and points at `docs/ARCHITECTURE_PACKAGING.md`.
|
|
197
|
+
|
|
198
|
+
**Errors / Edge cases:**
|
|
199
|
+
|
|
200
|
+
- Throws synchronously after building the missing list. No partial-state recovery.
|
|
201
|
+
- The `RuntimeMapsProvider` warning in dev is intentional: devkit hot-reload normally registers one, so missing it during a bare boot is informational, not fatal.
|
|
202
|
+
- The `LocalizedNormalizer` warning in dev means error responses will surface the raw `errorCode` string instead of localized copy.
|
|
203
|
+
|
|
204
|
+
**Example:**
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { verifyBootstrap } from '@luckystack/server';
|
|
208
|
+
|
|
209
|
+
await verifyBootstrap({
|
|
210
|
+
requireDeployConfig: true,
|
|
211
|
+
requireServicesConfig: false,
|
|
212
|
+
requireOAuthProviders: true,
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Hook payload types
|
|
217
|
+
|
|
218
|
+
Module augmentation in `hookPayloads.ts` extends `@luckystack/core`'s `HookPayloads` interface, so `dispatchHook` / `registerHook` accept these names with the correct payload typing.
|
|
219
|
+
|
|
220
|
+
| Hook name | Payload type | When it fires |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `onSocketConnect` | `OnSocketConnectPayload` (`{ socketId, token, ip }`) | Notification: every successful Socket.io connection. |
|
|
223
|
+
| `onSocketDisconnect` | `OnSocketDisconnectPayload` (`{ socketId, token, reason }`) | Notification: every Socket.io disconnect (with reason string). |
|
|
224
|
+
| `preRoomJoin` | `PreRoomJoinPayload` (`{ token, room }`) | Before the user joins a room. May stop with `HookStopSignal`. |
|
|
225
|
+
| `postRoomJoin` | `PostRoomJoinPayload` (`{ token, room, allRooms }`) | After a successful join. |
|
|
226
|
+
| `preRoomLeave` | `PreRoomLeavePayload` (`{ token, room }`) | Before the user leaves a room. May stop. |
|
|
227
|
+
| `postRoomLeave` | `PostRoomLeavePayload` (`{ token, room, allRooms }`) | After a successful leave. |
|
|
228
|
+
| `onLocationUpdate` | `OnLocationUpdatePayload` (`{ token, oldLocation?, newLocation }`) | When the presence layer reports a location change. |
|
|
229
|
+
|
|
230
|
+
Naming convention (also documented in `hookPayloads.ts`):
|
|
231
|
+
|
|
232
|
+
- `on*` — pure notifications; return values are ignored.
|
|
233
|
+
- `pre*` — may return a `HookStopSignal` (`{ errorCode: string; httpStatus?: number }`) to abort the main flow.
|
|
234
|
+
- `post*` — fire after the side-effect succeeds.
|
|
235
|
+
|
|
236
|
+
## Types
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
export interface CreateLuckyStackServerOptions { /* see table above */ }
|
|
240
|
+
|
|
241
|
+
export interface BootstrapLuckyStackOptions extends CreateLuckyStackServerOptions {
|
|
242
|
+
overlayRoot?: string;
|
|
243
|
+
skipOverlayLoad?: boolean;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface BootstrapRequirements {
|
|
247
|
+
requireDeployConfig?: boolean;
|
|
248
|
+
requireServicesConfig?: boolean;
|
|
249
|
+
requireOAuthProviders?: boolean;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface RunningLuckyStackServer {
|
|
253
|
+
httpServer: http.Server;
|
|
254
|
+
ioServer: socket.io.Server;
|
|
255
|
+
listen: (callback?: () => void) => Promise<http.Server>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface RouteContext {
|
|
259
|
+
routePath: string;
|
|
260
|
+
method: string;
|
|
261
|
+
queryString: string | undefined;
|
|
262
|
+
token: string | null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export type StaticFileHandler = (req: IncomingMessage, res: ServerResponse) => unknown;
|
|
266
|
+
export type FaviconHandler = (res: ServerResponse) => unknown;
|
|
267
|
+
export type CustomRouteHandler = (
|
|
268
|
+
req: IncomingMessage,
|
|
269
|
+
res: ServerResponse,
|
|
270
|
+
ctx: RouteContext,
|
|
271
|
+
) => Promise<boolean> | boolean;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Config keys consumed at boot
|
|
275
|
+
|
|
276
|
+
| Source | Key | Effect |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| env | `SERVER_PORT` | Port fallback when neither `options.port` nor argv supplies one. |
|
|
279
|
+
| env | `SERVER_IP` | IP fallback. |
|
|
280
|
+
| env | `NODE_ENV` | Switches `enableDevTools` default, dynamic-import branch for devkit, and `verifyBootstrap` warn-vs-throw for runtime-maps / localized normalizer. |
|
|
281
|
+
| env | `SECURE` | When `'true'`, session cookies are emitted with the `Secure;` flag (see `request-pipeline.md`). |
|
|
282
|
+
| config | `projectConfig.logging.socketStartup` / `projectConfig.logging.devLogs` | Gates the "Server is running on ..." log line emitted by `listen()`. |
|
|
283
|
+
| config | `projectConfig.http.*` | Route paths + cookie + CORS + stream settings consumed at request time (not at boot). |
|
|
284
|
+
| argv | `<bundles> [port]` | Parsed by `applyServerArgv()` and surfaced via `getParsedBundles()` / `getParsedPort()`. |
|
|
285
|
+
|
|
286
|
+
## Related
|
|
287
|
+
|
|
288
|
+
- Function INDEX: `packages/server/CLAUDE.md`
|
|
289
|
+
- README (consumer quickstart): `packages/server/README.md`
|
|
290
|
+
- Argv parsing: `packages/server/docs/argv-parsing.md`
|
|
291
|
+
- Runtime maps: `packages/server/docs/runtime-maps.md`
|
|
292
|
+
- Request pipeline: `packages/server/docs/request-pipeline.md`
|
|
293
|
+
- HTTP routes: `packages/server/docs/http-routes.md`
|
|
294
|
+
- Security: `packages/server/docs/security-defaults.md`
|
|
295
|
+
- Architecture: `docs/ARCHITECTURE_PACKAGING.md`, `docs/ARCHITECTURE_SOCKET.md`, `docs/HOSTING.md`
|