@luckystack/server 0.10.1 → 0.11.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/AGENTS.md +7 -0
- package/CHANGELOG.md +11 -1
- package/README.md +2 -200
- package/dist/afterListen.d.ts +2 -0
- package/dist/argv.d.ts +8 -0
- package/dist/bootstrap.d.ts +36 -0
- package/dist/capabilities.d.ts +10 -0
- package/dist/createServer.d.ts +4 -0
- package/dist/customRoutesRegistry.d.ts +9 -0
- package/dist/devServerInfo.d.ts +7 -0
- package/dist/devToolsStatus.d.ts +3 -0
- package/dist/errorFormatterRegistry.d.ts +2 -0
- package/dist/hookPayloads.d.ts +62 -0
- package/dist/httpHandler.d.ts +3 -0
- package/dist/httpRoutes/apiRoute.d.ts +2 -0
- package/dist/httpRoutes/authApiRoute.d.ts +2 -0
- package/dist/httpRoutes/authCallbackRoute.d.ts +2 -0
- package/dist/httpRoutes/authLogoutRoute.d.ts +2 -0
- package/dist/httpRoutes/authProvidersRoute.d.ts +2 -0
- package/dist/httpRoutes/authSecondFactorRoutes.d.ts +3 -0
- package/dist/httpRoutes/csrfMiddleware.d.ts +8 -0
- package/dist/httpRoutes/csrfRoute.d.ts +2 -0
- package/dist/httpRoutes/customRoutes.d.ts +3 -0
- package/dist/httpRoutes/faviconRoute.d.ts +2 -0
- package/dist/httpRoutes/healthRoutes.d.ts +4 -0
- package/dist/httpRoutes/resolveRequesterIp.d.ts +27 -0
- package/dist/httpRoutes/sessionCookie.d.ts +1 -0
- package/dist/httpRoutes/staticRoutes.d.ts +2 -0
- package/dist/httpRoutes/syncRoute.d.ts +2 -0
- package/dist/httpRoutes/testResetRoute.d.ts +2 -0
- package/dist/httpRoutes/timingSafeEqual.d.ts +1 -0
- package/dist/httpRoutes/types.d.ts +15 -0
- package/dist/httpRoutes/uploadsRoute.d.ts +2 -0
- package/dist/index.d.ts +21 -352
- package/dist/index.js +32 -7
- package/dist/index.js.map +1 -1
- package/dist/loadSocket.d.ts +21 -0
- package/dist/logSanitize.d.ts +1 -0
- package/dist/originExemptRegistry.d.ts +13 -0
- package/dist/parseArgv.d.ts +1 -2
- package/dist/portResolution.d.ts +7 -0
- package/dist/runtimeMapsLoader.d.ts +46 -0
- package/dist/securityHeadersRegistry.d.ts +13 -0
- package/dist/sse.d.ts +11 -0
- package/dist/stopServer.d.ts +28 -0
- package/dist/types.d.ts +138 -0
- package/dist/verifyBootstrap.d.ts +18 -0
- package/docs/argv-parsing.md +3 -244
- package/docs/create-server.md +3 -327
- package/docs/http-routes.md +3 -319
- package/docs/request-pipeline.md +3 -196
- package/docs/runtime-maps.md +3 -207
- package/docs/security-defaults.md +7 -236
- package/package.json +12 -12
- package/CLAUDE.md +0 -89
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import type { Redis as RedisClient } from 'ioredis';
|
|
3
|
+
import { Server as SocketIOServer } from 'socket.io';
|
|
4
|
+
export interface LoadSocketOptions {
|
|
5
|
+
maxHttpBufferSize?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface LoadSocketResult {
|
|
8
|
+
io: SocketIOServer;
|
|
9
|
+
/**
|
|
10
|
+
* The duplicated Redis pub/sub clients backing the Socket.io adapter. The
|
|
11
|
+
* server bootstrap owns them so graceful shutdown can `quit()` them — they are
|
|
12
|
+
* NOT closed by `io.close()` (which only tears down the engine + connections).
|
|
13
|
+
* Created here (instead of letting `attachSocketRedisAdapter` duplicate
|
|
14
|
+
* internally) precisely so the server holds a handle to disconnect them.
|
|
15
|
+
*/
|
|
16
|
+
adapterClients: {
|
|
17
|
+
pubClient: RedisClient;
|
|
18
|
+
subClient: RedisClient;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare const loadSocket: (httpServer: HttpServer, options?: LoadSocketOptions) => LoadSocketResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sanitizeForLog: (value: unknown) => unknown;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface OriginExemptMatcher {
|
|
2
|
+
/**
|
|
3
|
+
* A route is exempt when its path equals this prefix OR continues past it on a
|
|
4
|
+
* path-SEGMENT boundary (i.e. `<prefix>/...`). Matching is boundary-aware so a
|
|
5
|
+
* prefix can't bleed into a sibling route — `/webhooks` exempts `/webhooks` and
|
|
6
|
+
* `/webhooks/stripe` but NOT `/webhooksadmin`.
|
|
7
|
+
*/
|
|
8
|
+
pathPrefix: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const registerOriginExemptPath: (matcher: OriginExemptMatcher) => void;
|
|
11
|
+
export declare const getOriginExemptPaths: () => readonly OriginExemptMatcher[];
|
|
12
|
+
export declare const clearOriginExemptPaths: () => void;
|
|
13
|
+
export declare const isOriginExemptPath: (routePath: string) => boolean;
|
package/dist/parseArgv.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { }
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ResolveServerPortInput {
|
|
2
|
+
optionsPort?: number | string;
|
|
3
|
+
parsedPort?: number | null;
|
|
4
|
+
defaultPort?: number | string;
|
|
5
|
+
}
|
|
6
|
+
export declare const normalizeServerPort: (value: number | string, source?: string) => number;
|
|
7
|
+
export declare const resolveServerPort: ({ optionsPort, parsedPort, defaultPort, }: ResolveServerPortInput) => number;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type RuntimeMapsProvider } from '@luckystack/core';
|
|
2
|
+
type RuntimeMapRecord = Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* The shape that the generated maps module must export. `scripts/generateServerRequests.ts`
|
|
5
|
+
* emits exactly this shape: `{ apis, syncs, functions }` where each value is a
|
|
6
|
+
* `Record<string, Handler>`. Consumers can import this type to validate their
|
|
7
|
+
* generated file in CI or type-check the `loadGenerated` callback return.
|
|
8
|
+
*/
|
|
9
|
+
export interface GeneratedRuntimeMapsModule {
|
|
10
|
+
apis: RuntimeMapRecord;
|
|
11
|
+
syncs: RuntimeMapRecord;
|
|
12
|
+
functions: RuntimeMapRecord;
|
|
13
|
+
}
|
|
14
|
+
export interface ProdRuntimeMapsLoaderOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Dynamic-import callback for the generated maps module of a given preset.
|
|
17
|
+
* Called once per preset per process lifetime; the result is cached.
|
|
18
|
+
*
|
|
19
|
+
* The resolved module must have shape `{ apis, syncs, functions }` (the
|
|
20
|
+
* shape `scripts/generateServerRequests.ts` emits — see
|
|
21
|
+
* {@link GeneratedRuntimeMapsModule}). A mis-shaped module logs a warning
|
|
22
|
+
* and results in every route for that preset returning `notFound`.
|
|
23
|
+
*
|
|
24
|
+
* Pass a function that calls `import()` with a path relative to YOUR
|
|
25
|
+
* server-side module — the framework cannot resolve a relative path on
|
|
26
|
+
* your behalf because dynamic-import resolution is module-scoped.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* loadGenerated: (preset) => import(`./prod/generatedApis.${preset}`)
|
|
30
|
+
*/
|
|
31
|
+
loadGenerated: (preset: string) => Promise<unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Override the preset(s) to load. Skips the argv lookup. Accepts a single
|
|
34
|
+
* preset name or an array. Useful in tests or when the preset list comes
|
|
35
|
+
* from a non-argv source.
|
|
36
|
+
*/
|
|
37
|
+
preset?: string | string[];
|
|
38
|
+
}
|
|
39
|
+
export declare const createProdRuntimeMapsProvider: (options: ProdRuntimeMapsLoaderOptions) => RuntimeMapsProvider;
|
|
40
|
+
/**
|
|
41
|
+
* Convenience wrapper around `createProdRuntimeMapsProvider` +
|
|
42
|
+
* `registerRuntimeMapsProvider`. Most consumers want this — pass the
|
|
43
|
+
* loader callback once and the runtime is wired.
|
|
44
|
+
*/
|
|
45
|
+
export declare const registerProdRuntimeMapsProvider: (options: ProdRuntimeMapsLoaderOptions) => RuntimeMapsProvider;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
export type SecurityHeadersBuilder = (req: IncomingMessage) => Record<string, string> | null | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Register a custom security-headers builder. Called for every HTTP
|
|
5
|
+
* request; return a plain object that gets merged on top of the framework
|
|
6
|
+
* defaults. Use for Content-Security-Policy, HSTS, Permissions-Policy.
|
|
7
|
+
*
|
|
8
|
+
* Last-write-wins: subsequent calls replace the previous builder. Pass
|
|
9
|
+
* `null` to unregister.
|
|
10
|
+
*/
|
|
11
|
+
export declare const registerSecurityHeaders: (builder: SecurityHeadersBuilder | null) => void;
|
|
12
|
+
/** Read the active builder (or null). */
|
|
13
|
+
export declare const getSecurityHeadersBuilder: () => SecurityHeadersBuilder | null;
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ServerResponse } from 'node:http';
|
|
2
|
+
export declare const shouldUseHttpStream: ({ acceptHeader, queryString, }: {
|
|
3
|
+
acceptHeader: string | string[] | undefined;
|
|
4
|
+
queryString: string | undefined;
|
|
5
|
+
}) => boolean;
|
|
6
|
+
export declare const initSseResponse: (res: ServerResponse) => void;
|
|
7
|
+
export declare const sendSseEvent: ({ res, event, data, }: {
|
|
8
|
+
res: ServerResponse;
|
|
9
|
+
event: string;
|
|
10
|
+
data: unknown;
|
|
11
|
+
}) => void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import type { Server as SocketIOServer } from 'socket.io';
|
|
3
|
+
import type { Redis as RedisClient } from 'ioredis';
|
|
4
|
+
import type { StopLuckyStackServerOptions } from './types.js';
|
|
5
|
+
export interface GracefulShutdownDeps {
|
|
6
|
+
httpServer: HttpServer;
|
|
7
|
+
ioServer: SocketIOServer;
|
|
8
|
+
adapterClients: {
|
|
9
|
+
pubClient: RedisClient;
|
|
10
|
+
subClient: RedisClient;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Run the graceful-shutdown sequence (MIS-016). Ordered + isolated:
|
|
15
|
+
* 1. Stop accepting NEW HTTP connections (`httpServer.close()` is kicked off
|
|
16
|
+
* immediately; we await it last so in-flight requests can drain).
|
|
17
|
+
* 2. Dispatch the core `preServerStop` hook so consumers can flush queues /
|
|
18
|
+
* release leases / close pools.
|
|
19
|
+
* 3. `flushErrorTrackers()` so buffered events aren't lost — bounded by the
|
|
20
|
+
* timeout race.
|
|
21
|
+
* 4. Close the Socket.io server, then the HTTP server, then `quit()` the
|
|
22
|
+
* Redis-adapter pub/sub clients.
|
|
23
|
+
*
|
|
24
|
+
* Every step is wrapped in {@link withTimeout} so a single failing/hanging step
|
|
25
|
+
* cannot hang the whole shutdown — the sequence always settles within
|
|
26
|
+
* `timeoutMs` per step. Never rejects.
|
|
27
|
+
*/
|
|
28
|
+
export declare const runGracefulShutdown: (deps: GracefulShutdownDeps, options?: StopLuckyStackServerOptions) => Promise<void>;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse, Server as HttpServer } from 'node:http';
|
|
2
|
+
import type { Server as SocketIOServer } from 'socket.io';
|
|
3
|
+
export interface RouteContext {
|
|
4
|
+
routePath: string;
|
|
5
|
+
method: string;
|
|
6
|
+
queryString: string | undefined;
|
|
7
|
+
token: string | null;
|
|
8
|
+
}
|
|
9
|
+
export type StaticFileHandler = (req: IncomingMessage, res: ServerResponse) => unknown;
|
|
10
|
+
export type FaviconHandler = (res: ServerResponse) => unknown;
|
|
11
|
+
export type CustomRouteHandler = (req: IncomingMessage, res: ServerResponse, ctx: RouteContext) => Promise<boolean> | boolean;
|
|
12
|
+
export type CustomRoutePhase = 'pre-params' | 'post-params';
|
|
13
|
+
export interface CreateLuckyStackServerOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Port the HTTP server listens on. Resolution order:
|
|
16
|
+
* 1. This option (`options.port`)
|
|
17
|
+
* 2. Second positional argv (`npm run server -- <bundles> <port>`), parsed
|
|
18
|
+
* by `@luckystack/server/parseArgv`
|
|
19
|
+
* 3. `options.defaultPort` (the project's single-instance default, e.g. the
|
|
20
|
+
* `backend` port from `config.ports.ts`)
|
|
21
|
+
* 4. `80` for generic consumers that supplied no port source
|
|
22
|
+
*/
|
|
23
|
+
port?: number | string;
|
|
24
|
+
/**
|
|
25
|
+
* Project's single-instance default backend port. Sits BELOW positional argv
|
|
26
|
+
* so a multi-instance boot (`npm run server -- <preset> <port>`) still wins.
|
|
27
|
+
* The scaffold passes the `backend` value from `config.ports.ts` here, so the
|
|
28
|
+
* listen port has one static source of truth.
|
|
29
|
+
*/
|
|
30
|
+
defaultPort?: number;
|
|
31
|
+
/** Bind address. Defaults to process.env.SERVER_IP or '127.0.0.1'. */
|
|
32
|
+
ip?: string;
|
|
33
|
+
/** Project-side static file handler (Vite output, etc.). Used as the catch-all. */
|
|
34
|
+
serveFile?: StaticFileHandler;
|
|
35
|
+
/** Project-side favicon handler. */
|
|
36
|
+
serveFavicon?: FaviconHandler;
|
|
37
|
+
/**
|
|
38
|
+
* Optional pre-fallback hook for project-specific HTTP routes. Return `true`
|
|
39
|
+
* if the route was handled (response ended). Return `false` (or omit return)
|
|
40
|
+
* to fall through to the framework's static file serving.
|
|
41
|
+
*/
|
|
42
|
+
customRoutes?: CustomRouteHandler;
|
|
43
|
+
/**
|
|
44
|
+
* Enable dev-mode tooling (devkit hot reload, REPL, console init).
|
|
45
|
+
* Defaults to `resolveRuntimeMode() !== 'production'` (NODE_ENV-based).
|
|
46
|
+
*/
|
|
47
|
+
enableDevTools?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Maximum HTTP buffer size for socket.io. Defaults to 5 MB. Adjust if you
|
|
50
|
+
* stream large payloads through sockets.
|
|
51
|
+
*/
|
|
52
|
+
maxHttpBufferSize?: number;
|
|
53
|
+
/** Fail boot if no DeployConfig has been registered. Default: false. */
|
|
54
|
+
requireDeployConfig?: boolean;
|
|
55
|
+
/** Fail boot if no ServicesConfig has been registered. Default: false. */
|
|
56
|
+
requireServicesConfig?: boolean;
|
|
57
|
+
/** Fail boot if no OAuth providers have been registered. Default: false. */
|
|
58
|
+
requireOAuthProviders?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Dynamic-import callback for production runtime maps. When provided, the
|
|
61
|
+
* framework registers its built-in `RuntimeMapsProvider` and the consumer
|
|
62
|
+
* no longer needs a hand-rolled `server/prod/runtimeMaps.ts`. Pass a
|
|
63
|
+
* function that calls `import()` with a path relative to the consumer's
|
|
64
|
+
* server module — the framework cannot resolve that path on the
|
|
65
|
+
* consumer's behalf because dynamic-import resolution is module-scoped.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`)
|
|
69
|
+
*/
|
|
70
|
+
loadGeneratedMaps?: (preset: string) => Promise<unknown>;
|
|
71
|
+
/**
|
|
72
|
+
* Override the preset(s) to load (skips argv lookup). Pass a single preset
|
|
73
|
+
* name or an array for multi-preset boots. Default: the first positional
|
|
74
|
+
* argv arg parsed by `@luckystack/server/parseArgv`, falling back to
|
|
75
|
+
* `'default'`.
|
|
76
|
+
*/
|
|
77
|
+
runtimeMapsPreset?: string | string[];
|
|
78
|
+
}
|
|
79
|
+
export interface StopLuckyStackServerOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Why the server is stopping — threaded into the `preServerStop` hook payload.
|
|
82
|
+
* Defaults to `'manual'` for a programmatic `stop()`; the signal handlers pass
|
|
83
|
+
* the received signal name.
|
|
84
|
+
*/
|
|
85
|
+
reason?: 'SIGTERM' | 'SIGINT' | 'SIGHUP' | 'manual';
|
|
86
|
+
/**
|
|
87
|
+
* Soft budget (ms) for the whole shutdown sequence — the `preServerStop` hook
|
|
88
|
+
* fan-out, the bounded `flushErrorTrackers()` race, and closing the
|
|
89
|
+
* http/io/redis-adapter handles. Each step is wrapped so a single hanging step
|
|
90
|
+
* cannot stall the others past this deadline. Defaults to `10000`.
|
|
91
|
+
*/
|
|
92
|
+
timeoutMs?: number;
|
|
93
|
+
}
|
|
94
|
+
export interface AfterListenOptions {
|
|
95
|
+
/**
|
|
96
|
+
* Re-throw instead of swallowing. Use only when the process is genuinely
|
|
97
|
+
* useless without this task. Default `false` — a listening server survives a
|
|
98
|
+
* failing post-listen task.
|
|
99
|
+
*/
|
|
100
|
+
fatal?: boolean;
|
|
101
|
+
/** Label used in the failure log. Defaults to `post-listen task`. */
|
|
102
|
+
label?: string;
|
|
103
|
+
}
|
|
104
|
+
export interface RunningLuckyStackServer {
|
|
105
|
+
httpServer: HttpServer;
|
|
106
|
+
ioServer: SocketIOServer;
|
|
107
|
+
/**
|
|
108
|
+
* Start listening. Resolves when the HTTP server is ready. Logs the bound
|
|
109
|
+
* URL on success.
|
|
110
|
+
*/
|
|
111
|
+
listen: (callback?: () => void) => Promise<HttpServer>;
|
|
112
|
+
/**
|
|
113
|
+
* Run work that belongs AFTER the server is accepting traffic — durable queue
|
|
114
|
+
* workers, warm-up passes, reconciliation jobs.
|
|
115
|
+
*
|
|
116
|
+
* A rejection here is logged loudly and swallowed by default, because the
|
|
117
|
+
* server is already up: killing a healthy process over one broken dependency
|
|
118
|
+
* turns an outage into a crash-loop (supervisor restarts, dependency is still
|
|
119
|
+
* down, repeat). Pass `{ fatal: true }` when the process genuinely has no
|
|
120
|
+
* purpose without the task — the rejection then propagates to the caller.
|
|
121
|
+
*
|
|
122
|
+
* Never resolves to the task's value; it is fire-and-report, not a pipeline
|
|
123
|
+
* stage. Read what you need inside the callback.
|
|
124
|
+
*/
|
|
125
|
+
afterListen: (task: () => void | Promise<void>, options?: AfterListenOptions) => Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Graceful shutdown (MIS-016). Stops accepting new connections, dispatches the
|
|
128
|
+
* core `preServerStop` hook, flushes error-trackers (bounded by a timeout
|
|
129
|
+
* race), and closes the Socket.io server, the HTTP server, and the
|
|
130
|
+
* Redis-adapter pub/sub clients. Every step is isolated + time-bounded so one
|
|
131
|
+
* failing/hanging step cannot hang the whole shutdown. Idempotent — a second
|
|
132
|
+
* call returns the in-flight shutdown promise. Resolves once shutdown
|
|
133
|
+
* completes (or the timeout forces it to finish).
|
|
134
|
+
*/
|
|
135
|
+
stop: (options?: StopLuckyStackServerOptions) => Promise<void>;
|
|
136
|
+
/** Alias of {@link stop} matching the `net.Server`-style `close({ timeoutMs })` API. */
|
|
137
|
+
close: (options?: StopLuckyStackServerOptions) => Promise<void>;
|
|
138
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface BootstrapRequirements {
|
|
2
|
+
/**
|
|
3
|
+
* If true, require a deploy config to have been registered. Defaults to
|
|
4
|
+
* false because single-instance deployments don't need one.
|
|
5
|
+
*/
|
|
6
|
+
requireDeployConfig?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* If true, require a services config to have been registered. Defaults to
|
|
9
|
+
* false; only needed when running the router.
|
|
10
|
+
*/
|
|
11
|
+
requireServicesConfig?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* If true, require an OAuth provider list to have been registered.
|
|
14
|
+
* Defaults to false (an app may use credentials only).
|
|
15
|
+
*/
|
|
16
|
+
requireOAuthProviders?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare const verifyBootstrap: (requirements?: BootstrapRequirements) => Promise<void>;
|
package/docs/argv-parsing.md
CHANGED
|
@@ -1,246 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Runtime arguments
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Explicit runtime arguments let deployment choose bundles and bind port without editing generated source. Validate them before boot rather than continuing with an ambiguous or partial map; fail loudly on invalid input. Source: `../src/argv.ts`, `../src/parseArgv.ts`, `../src/portResolution.ts`.
|
|
4
4
|
|
|
5
|
-
|
|
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 provides one shape consumed by `createProdRuntimeMapsProvider` (preset), `createLuckyStackServer` (port), and the browser-safe `@luckystack/core/config` port-override registry.
|
|
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 registers the result: `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 it registers the parsed port in `@luckystack/core/config`, and the consumer's config reads that override at top-level evaluation time. When no CLI port was supplied, the registry stays empty and `config.ts` uses the consumer-owned `config.ports.ts` backend value.
|
|
26
|
-
|
|
27
|
-
Importing the consumer config before `parseArgv` runs can lock the static default into its OAuth callback base. The server listen path still reads the same registry through `getParsedPort()`, so both surfaces share one override without an environment writeback.
|
|
28
|
-
|
|
29
|
-
## API Reference
|
|
30
|
-
|
|
31
|
-
### `parseServerArgv(argv: string[]): ParsedServerArgv`
|
|
32
|
-
|
|
33
|
-
**Signature:**
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
export interface ParsedServerArgv {
|
|
37
|
-
bundles: string[];
|
|
38
|
-
port: number | null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const parseServerArgv = (argv: string[]): ParsedServerArgv;
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**Parameters:**
|
|
45
|
-
|
|
46
|
-
| Field | Type | Purpose |
|
|
47
|
-
| --- | --- | --- |
|
|
48
|
-
| `argv` | `string[]` | Positional args (typically `process.argv.slice(2)`). |
|
|
49
|
-
|
|
50
|
-
**Returns:** `{ bundles, port }`:
|
|
51
|
-
|
|
52
|
-
- `bundles: string[]` — deduplicated, trimmed, non-empty entries from arg 0. Empty array when arg 0 is missing or empty.
|
|
53
|
-
- `port: number | null` — `parseInt(argv[1], 10)` when arg 1 is supplied; `null` otherwise.
|
|
54
|
-
|
|
55
|
-
**Behavior:**
|
|
56
|
-
|
|
57
|
-
- Reject more than 2 positional arguments by throwing `Error('[luckystack:argv] unexpected positional argument(s): "<rest>". Usage: npm run server -- <bundle[,bundle...]> [port]')`.
|
|
58
|
-
- For arg 0: split on `,`, `trim()` each piece, drop falsy, collapse via `Array.from(new Set(...))`.
|
|
59
|
-
- 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]')`.
|
|
60
|
-
|
|
61
|
-
**Errors / Edge cases:**
|
|
62
|
-
|
|
63
|
-
- Whitespace in arg 0 (`"billing, vehicles"`) is supported — trimmed.
|
|
64
|
-
- A trailing comma (`"billing,"`) is silently dropped.
|
|
65
|
-
- A leading `0` in the port string is accepted (`/^\d+$/`) and parsed normally.
|
|
66
|
-
- An empty arg 0 (`""`) yields `bundles: []`; the downstream resolver falls back to `['default']`.
|
|
67
|
-
|
|
68
|
-
**Example:**
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
parseServerArgv(['billing,vehicles', '4001']);
|
|
72
|
-
// => { bundles: ['billing', 'vehicles'], port: 4001 }
|
|
73
|
-
|
|
74
|
-
parseServerArgv([]);
|
|
75
|
-
// => { bundles: [], port: null }
|
|
76
|
-
|
|
77
|
-
parseServerArgv(['billing', '4001', 'oops']);
|
|
78
|
-
// => throws (too many positionals)
|
|
79
|
-
|
|
80
|
-
parseServerArgv(['billing', 'PORT']);
|
|
81
|
-
// => throws (non-numeric port)
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
### `applyServerArgv(): void`
|
|
87
|
-
|
|
88
|
-
**Signature:**
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
export const applyServerArgv = (): void;
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**Parameters:** none. Reads `process.argv.slice(2)`.
|
|
95
|
-
|
|
96
|
-
**Returns:** `void`.
|
|
97
|
-
|
|
98
|
-
**Behavior:**
|
|
99
|
-
|
|
100
|
-
- Idempotent. Subsequent calls return immediately via the module-level `hasRun` latch.
|
|
101
|
-
- First call:
|
|
102
|
-
1. `parseServerArgv(process.argv.slice(2))` (throws on malformed input).
|
|
103
|
-
2. Caches `bundles` in module state.
|
|
104
|
-
3. Calls `registerPortOverride(port)`. A numeric port becomes visible through the browser-safe core/config entry; `null` clears the override.
|
|
105
|
-
|
|
106
|
-
**Errors / Edge cases:**
|
|
107
|
-
|
|
108
|
-
- Throwing during this call aborts boot before consumer config evaluates.
|
|
109
|
-
- Calling `applyServerArgv` after consumer config has evaluated is too late — that module has already captured its callback base.
|
|
110
|
-
|
|
111
|
-
**Example:**
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
// server.ts — first line
|
|
115
|
-
import '@luckystack/server/parseArgv';
|
|
116
|
-
// rest of bootstrap...
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Or call it explicitly when you control the boot timing:
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
import { applyServerArgv } from '@luckystack/server';
|
|
123
|
-
applyServerArgv();
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
---
|
|
127
|
-
|
|
128
|
-
### `getParsedBundles(): string[]`
|
|
129
|
-
|
|
130
|
-
**Signature:**
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
export const getParsedBundles = (): string[];
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Returns:** the cached `bundles` array. Empty until `applyServerArgv()` has run.
|
|
137
|
-
|
|
138
|
-
**Behavior:**
|
|
139
|
-
|
|
140
|
-
- Read-only; never throws.
|
|
141
|
-
- Used by `createProdRuntimeMapsProvider` to resolve which preset(s) to load when neither `options.preset` nor a literal string is supplied.
|
|
142
|
-
|
|
143
|
-
**Example:**
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
import { applyServerArgv, getParsedBundles } from '@luckystack/server';
|
|
147
|
-
|
|
148
|
-
applyServerArgv();
|
|
149
|
-
console.log(getParsedBundles()); // e.g. ['billing', 'vehicles']
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
### `getParsedPort(): number | null`
|
|
155
|
-
|
|
156
|
-
**Signature:**
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
export const getParsedPort = (): number | null;
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
**Returns:** the port currently held in the core/config override registry. `null` when no positional port was registered.
|
|
163
|
-
|
|
164
|
-
**Behavior:**
|
|
165
|
-
|
|
166
|
-
- Read-only; never throws.
|
|
167
|
-
- Consumed by `createLuckyStackServer` as one of the port-resolution fallbacks:
|
|
168
|
-
1. `options.port`
|
|
169
|
-
2. `getParsedPort()`
|
|
170
|
-
3. `options.defaultPort` (the scaffold's `config.ports.ts` backend)
|
|
171
|
-
4. `80` (generic final fallback)
|
|
172
|
-
|
|
173
|
-
**Example:**
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
import { getParsedPort } from '@luckystack/server';
|
|
177
|
-
|
|
178
|
-
const port = getParsedPort();
|
|
179
|
-
if (port !== null) {
|
|
180
|
-
console.log(`argv supplied port ${port}`);
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
### Side-effect entrypoint: `@luckystack/server/parseArgv`
|
|
187
|
-
|
|
188
|
-
**Module body (verbatim):**
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
import { applyServerArgv } from './argv';
|
|
192
|
-
|
|
193
|
-
applyServerArgv();
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
**Usage:** import as the FIRST line of your `server.ts`:
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
import '@luckystack/server/parseArgv';
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
Consumer `config.ts` MUST be imported below this line because it reads `getPortOverride()` at module evaluation time. Importing core itself first is safe: the registry is mutable and browser-safe. With no CLI port, scaffold config intentionally falls back to its pure-data `config.ports.ts` value.
|
|
203
|
-
|
|
204
|
-
## Resolution order summary
|
|
205
|
-
|
|
206
|
-
| Consumer | Source of port |
|
|
207
|
-
| --- | --- |
|
|
208
|
-
| `createLuckyStackServer` | `options.port` -> `getParsedPort()` -> `options.defaultPort` -> `80` |
|
|
209
|
-
| `createLuckyStackServer` IP | `options.ip` -> `SERVER_IP` -> `127.0.0.1` |
|
|
210
|
-
| `createProdRuntimeMapsProvider` | `options.preset` (string -> single-entry array; non-empty array -> dedup) -> `getParsedBundles()` -> `['default']` |
|
|
211
|
-
|
|
212
|
-
## CLI examples
|
|
213
|
-
|
|
214
|
-
```bash
|
|
215
|
-
# Default preset, config.ports.ts backend
|
|
216
|
-
npm run server
|
|
217
|
-
|
|
218
|
-
# Single bundle, config.ports.ts backend
|
|
219
|
-
npm run server -- billing
|
|
220
|
-
|
|
221
|
-
# Two bundles merged, port 4001
|
|
222
|
-
npm run server -- billing,vehicles 4001
|
|
223
|
-
|
|
224
|
-
# Whitespace in bundle list is fine
|
|
225
|
-
npm run server -- "billing , vehicles" 4001
|
|
226
|
-
|
|
227
|
-
# Invalid port — boot aborts with descriptive error
|
|
228
|
-
npm run server -- billing PORT
|
|
229
|
-
# Error: [luckystack:argv] port argument must be numeric, got: "PORT".
|
|
230
|
-
|
|
231
|
-
# Extra positional — boot aborts
|
|
232
|
-
npm run server -- billing 4001 oops
|
|
233
|
-
# Error: [luckystack:argv] unexpected positional argument(s): "oops".
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
## Interaction with runtime maps
|
|
237
|
-
|
|
238
|
-
`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.
|
|
239
|
-
|
|
240
|
-
## Related
|
|
241
|
-
|
|
242
|
-
- Function INDEX: `packages/server/CLAUDE.md`
|
|
243
|
-
- Runtime maps: `packages/server/docs/runtime-maps.md`
|
|
244
|
-
- Create server: `packages/server/docs/create-server.md`
|
|
245
|
-
- Architecture: `docs/ARCHITECTURE_PACKAGING.md` (preset bundles, multi-service builds)
|
|
246
|
-
- README: `packages/server/README.md`
|
|
5
|
+
A selected bundle controls which generated routes load, not who may invoke them. Keep runtime map isolation and application authorization independent. See `./runtime-maps.md`, `../src/verifyBootstrap.ts`; consult source for current argument spelling and precedence.
|