@luckystack/server 0.8.3 → 0.8.5
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 +31 -0
- package/CLAUDE.md +7 -6
- package/README.md +20 -15
- package/dist/{chunk-QLVQ75QL.js → chunk-HJXOI6LY.js} +9 -12
- package/dist/chunk-HJXOI6LY.js.map +1 -0
- package/dist/index.d.ts +22 -7
- package/dist/index.js +33 -16
- package/dist/index.js.map +1 -1
- package/dist/parseArgv.js +1 -1
- package/dist/parseArgv.js.map +1 -1
- package/docs/argv-parsing.md +16 -26
- package/docs/create-server.md +6 -6
- package/docs/http-routes.md +7 -5
- package/docs/request-pipeline.md +1 -1
- package/docs/runtime-maps.md +1 -1
- package/docs/security-defaults.md +20 -14
- package/package.json +14 -10
- package/dist/chunk-QLVQ75QL.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `collectOverlayEntries(packageDir)` — the single owner of what a `luckystack/<pkg>/` folder contributes at boot, exported so `scripts/bundleServer.mjs` decides identically instead of reimplementing the walk (ADR 0047).
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Test files in a `luckystack/<pkg>/` overlay folder are no longer imported at boot or bundled into the production server. A test file that only performs side effects (registering a stub adapter, say) previously ran in production without failing loudly. Covers `.tests.` / `.test.` / `.spec.` plus `__tests__` / `__mocks__` folders.
|
|
17
|
+
- A subdirectory inside an overlay package folder is now reported instead of skipped in silence. The walk is flat by design, so overlay code parked one level down never ran and nothing pointed at it.
|
|
18
|
+
|
|
19
|
+
- Composed presets accept a repeated NESTED function namespace. Equivalence is now checked recursively, so a namespace object rebuilt per preset (any function module living in a subdirectory of a `serverFunctionDirs` root) no longer reads as a differing implementation and rejects the compose at boot. A genuinely differing leaf implementation still fails closed.
|
|
20
|
+
|
|
21
|
+
## [0.8.4] - 2026-08-17
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Port resolution is now `options.port > positional argv registry override >
|
|
26
|
+
options.defaultPort > 80`. The legacy `SERVER_PORT` environment fallback and
|
|
27
|
+
argv-to-environment writeback are removed. Migrate by editing
|
|
28
|
+
`config.ports.ts`, passing `port` / `defaultPort`, or supplying the second
|
|
29
|
+
positional argv.
|
|
30
|
+
- `@luckystack/server/parseArgv` now registers its port through the browser-safe
|
|
31
|
+
`@luckystack/core/config` registry, keeping consumer OAuth config and server
|
|
32
|
+
listen resolution aligned without environment mutation.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Declare `@luckystack/cron` as an optional peer so the auto-detected cron overlay is represented in the server package contract.
|
|
37
|
+
- Programmatic `options.port` can now rewrite an OAuth callback that still names
|
|
38
|
+
`options.defaultPort`, while explicit local router/reverse-proxy ingress ports
|
|
39
|
+
remain authoritative.
|
|
40
|
+
|
|
10
41
|
## [0.8.2] - 2026-07-27
|
|
11
42
|
|
|
12
43
|
### Fixed
|
package/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## What this package does
|
|
6
6
|
|
|
7
|
-
One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js HTTP server, Socket.io (with optional Redis adapter), framework routes (`/api/*`, `/sync/*`, `/_health`, `/livez`, `/readyz`, `/_test/reset`, `/auth/*`, `/uploads/*`,
|
|
7
|
+
One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js HTTP server, Socket.io (with optional Redis adapter), framework routes (`/api/*`, `/sync/*`, `/_health`, `/livez`, `/readyz`, `/_test/reset`, `/auth/*`, `/uploads/*`, including `GET /auth/csrf`), CSRF middleware, CORS + security headers, presence broadcasting, and dev-only hot reload. Consumer's `server.ts` shrinks to roughly twenty lines. Boots are gated by `verifyBootstrap` so missing registrations surface a single readable error instead of mid-request crashes. If devkit initialization fails, API/sync dispatch and `/readyz` all fail closed with 503.
|
|
8
8
|
|
|
9
9
|
## When to USE this package
|
|
10
10
|
|
|
@@ -29,13 +29,14 @@ One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js
|
|
|
29
29
|
| `RunningLuckyStackServer.stop(options?)` / `.close(options?)` | Graceful shutdown (MIS-016). Stops accepting new connections, dispatches the core `preServerStop` hook, flushes error-trackers (bounded by a per-step timeout race), then closes io + http + the Redis-adapter pub/sub clients. Each step isolated + time-bounded so one hanging step can't stall shutdown. Idempotent. `options`: `{ reason?, timeoutMs? }` (default `timeoutMs` 10000). | -> docs/create-server.md |
|
|
30
30
|
| `bootstrapLuckyStack(options)` | High-level entry: auto-imports `luckystack/<pkg>/*.ts` overlay in topological order, then delegates to `createLuckyStackServer`. An overlay file that fails to import (classic: it imports a package removed via `luckystack remove`) aborts boot with an ACTIONABLE error naming the file, instead of a raw ERR_MODULE_NOT_FOUND. | -> docs/create-server.md |
|
|
31
31
|
| `OVERLAY_ORDER` (exported const) | The canonical overlay walk order (`core, deploy, login, email, sentry, presence, cron, docs-ui, server`). Exported so the consumer's `scripts/bundleServer.mjs` imports it at build time — a hardcoded bundler copy once silently dropped the cron slot from prod; `overlayOrderParity.test.ts` pins the bundlers' fallback lists to this. | -> docs/create-server.md |
|
|
32
|
+
| `collectOverlayEntries(packageDir)` | Single owner of what one `luckystack/<pkg>/` folder contributes: `{ fileNames, ignoredDirectoryNames }`. `index.*` first, then the rest alphabetically; `.ts`/`.js` only; **test files excluded** (`.tests.`/`.test.`/`.spec.`, plus `__tests__`/`__mocks__` folders) so they never boot or reach the production bundle. The walk is FLAT — any other subdirectory comes back in `ignoredDirectoryNames` and the loader warns, instead of dropping overlay code in silence. Exported so `scripts/bundleServer.mjs` decides identically. ADR 0047. | -> docs/create-server.md |
|
|
32
33
|
| `registerOverlayLoader(loader)` | Production-bundle seam: the server bundler (`scripts/bundleServer.mjs`) generates an entry that statically imports the overlay files and registers this loader; `bootstrapLuckyStack` then runs it INSTEAD of the runtime `.ts` folder walk (plain `node` cannot import `.ts` — ERR_UNKNOWN_FILE_EXTENSION). | -> docs/create-server.md |
|
|
33
34
|
| `verifyBootstrap(requirements?)` | Pre-flight check for ProjectConfig / DeployConfig / ServicesConfig / OAuth / RuntimeMapsProvider / LocalizedNormalizer. Throws one descriptive `Error`. | -> docs/create-server.md |
|
|
34
35
|
| `parseServerArgv(argv)` | Pure parser: validates positional `<bundles> [port]` and returns `{ bundles, port }`. Throws on malformed input. | -> docs/argv-parsing.md |
|
|
35
|
-
| `applyServerArgv()` | Side-effect runner: parses `process.argv.slice(2)`, stores bundles
|
|
36
|
+
| `applyServerArgv()` | Side-effect runner: parses `process.argv.slice(2)`, stores bundles, and registers the port through browser-safe `@luckystack/core/config`. Idempotent. | -> docs/argv-parsing.md |
|
|
36
37
|
| `getParsedBundles()` | Returns the preset list parsed by `applyServerArgv()` (empty array before first call). | -> docs/argv-parsing.md |
|
|
37
38
|
| `getParsedPort()` | Returns the port parsed by `applyServerArgv()` (`null` if argv omitted it). | -> docs/argv-parsing.md |
|
|
38
|
-
| `@luckystack/server/parseArgv` (side-effect import) | First-line import that runs `applyServerArgv()` before
|
|
39
|
+
| `@luckystack/server/parseArgv` (side-effect import) | First-line import that runs `applyServerArgv()` before consumer `config.ts` reads the typed override registry. | -> docs/argv-parsing.md |
|
|
39
40
|
| `createProdRuntimeMapsProvider(options)` | Build a `RuntimeMapsProvider` that loads generated maps in prod and delegates to devkit discovery in dev. Composed presets reject duplicate API/sync ownership, deduplicate equivalent full-registry function entries, and reject differing function implementations. Returns the provider without registering. | -> docs/runtime-maps.md |
|
|
40
41
|
| `registerProdRuntimeMapsProvider(options)` | Convenience wrapper: builds the provider AND calls `registerRuntimeMapsProvider`. Most consumers want this. | -> docs/runtime-maps.md |
|
|
41
42
|
| `registerCustomRoute(handler, options?)` | Append a custom HTTP route handler. `options.phase`: `'post-params'` (default, runs after body parse) or `'pre-params'` (runs before `getParams`, raw `req` stream intact — webhooks + streaming uploads). | -> /docs/ARCHITECTURE_HTTP.md |
|
|
@@ -63,7 +64,7 @@ One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js
|
|
|
63
64
|
|
|
64
65
|
## Config keys (env vars + registerProjectConfig slots)
|
|
65
66
|
|
|
66
|
-
- Port precedence — `options.port` → positional argv → `options.defaultPort` (the scaffold passes `config.ports.backend`) →
|
|
67
|
+
- Port precedence — `options.port` → positional argv registry override → `options.defaultPort` (the scaffold passes `config.ports.backend`) → `80`. Inputs must be integer ports in `0..65535`. After bind, `httpServer.address().port` is authoritative (including `listen(0)`). In dev, that port is advertised to `node_modules/.luckystack/dev-server.json`; cleanup removes it only when the exiting PID still owns it. The Vite proxy follows the advertisement for HTTP + WebSocket requests.
|
|
67
68
|
- `SERVER_IP` (env, optional, default `127.0.0.1`) — bind address fallback when `options.ip` is omitted.
|
|
68
69
|
- `NODE_ENV` (env, required for security-sensitive branches) — `development` / `test` toggle devkit hot reload + REPL and gate `/_test/reset`.
|
|
69
70
|
- `TEST_RESET_TOKEN` (env, required for `/_test/reset` to be reachable at all) — must match the `x-test-reset-token` request header. No fallback "no auth" mode.
|
|
@@ -77,8 +78,8 @@ One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js
|
|
|
77
78
|
## Peer dependencies
|
|
78
79
|
|
|
79
80
|
- **Required (runtime deps)**: `@luckystack/api`, `@luckystack/core`.
|
|
80
|
-
- **
|
|
81
|
-
- **Optional (all auto-detected by `bootstrapLuckyStack`; the server degrades gracefully when absent — `auth.disabled` / `sync.disabled`, presence skipped)**: `@luckystack/login`, `@luckystack/presence`, `@luckystack/sync`, `@luckystack/error-tracking`, `@luckystack/email`, `@luckystack/cron`, `@luckystack/docs-ui`, `@luckystack/devkit` (dev-only, dynamically imported by the `enableDevTools` branch).
|
|
81
|
+
- **Required peer**: `socket.io@^4.8.0`.
|
|
82
|
+
- **Optional (all auto-detected by `bootstrapLuckyStack`; the server degrades gracefully when absent — `auth.disabled` / `sync.disabled`, presence skipped)**: `@prisma/client@^6.19.0`, `@luckystack/login`, `@luckystack/presence`, `@luckystack/sync`, `@luckystack/error-tracking`, `@luckystack/email`, `@luckystack/cron`, `@luckystack/docs-ui`, `@luckystack/devkit` (dev-only, dynamically imported by the `enableDevTools` branch).
|
|
82
83
|
- **0.2.0 install-anything-anytime**: `bootstrapLuckyStack` runs an auto-detect phase BEFORE the consumer overlay that imports each installed optional package's `@luckystack/<pkg>/register` side-effect subpath (`OPTIONAL_PACKAGES` in `capabilities.ts` = `login, email, error-tracking, presence, cron, docs-ui`). So `npm i @luckystack/<pkg>` + env + restart self-wires the BACKEND with zero code edits. Detection uses `import.meta.resolve` (the `@luckystack/*` exports maps are import-only — a CJS `require.resolve` reports them absent). Client-side mounts (presence JSX, login pages) need `npx luckystack add <feature>` (`@luckystack/cli`) because Vite can't statically import an uninstalled package and file-routing only scans `src/`.
|
|
83
84
|
|
|
84
85
|
## Related
|
package/README.md
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @luckystack/server
|
|
8
|
+
npm install @luckystack/server socket.io
|
|
9
|
+
|
|
10
|
+
# Add the features your project uses; the server auto-detects these optional peers.
|
|
11
|
+
# For the standard API + realtime stack, for example:
|
|
12
|
+
npm install @luckystack/api @luckystack/sync
|
|
9
13
|
```
|
|
10
14
|
|
|
11
15
|
## Quickstart — recommended (`bootstrapLuckyStack`)
|
|
@@ -31,7 +35,7 @@ await server.listen();
|
|
|
31
35
|
|
|
32
36
|
The bootstrap call runs (in order):
|
|
33
37
|
|
|
34
|
-
1. Auto-imports `luckystack/core/*.ts`, `luckystack/deploy/*.ts`, `luckystack/login/*.ts`, `luckystack/sentry/*.ts`, `luckystack/presence/*.ts`, `luckystack/docs-ui/*.ts`, `luckystack/server/*.ts` (topologically sorted, then alphabetically inside each folder).
|
|
38
|
+
1. Auto-imports `luckystack/core/*.ts`, `luckystack/deploy/*.ts`, `luckystack/login/*.ts`, `luckystack/email/*.ts`, `luckystack/sentry/*.ts`, `luckystack/presence/*.ts`, `luckystack/cron/*.ts`, `luckystack/docs-ui/*.ts`, `luckystack/server/*.ts` (topologically sorted, then alphabetically inside each folder).
|
|
35
39
|
2. Hands off to `createLuckyStackServer(options)`.
|
|
36
40
|
|
|
37
41
|
Set `skipOverlayLoad: true` to handle every registration yourself, or `overlayRoot: 'custom-folder'` to load from somewhere other than `./luckystack`.
|
|
@@ -88,7 +92,8 @@ Both entries handle devkit hot reload + console init in dev mode automatically
|
|
|
88
92
|
|
|
89
93
|
```ts
|
|
90
94
|
interface CreateLuckyStackServerOptions {
|
|
91
|
-
port?: number | string; //
|
|
95
|
+
port?: number | string; // options.port > argv > defaultPort > 80
|
|
96
|
+
defaultPort?: number; // scaffold's config.ports.ts backend default
|
|
92
97
|
ip?: string; // default: process.env.SERVER_IP ?? '127.0.0.1'
|
|
93
98
|
serveFile?: StaticFileHandler; // catch-all for non-framework GETs (Vite output, SPA fallback)
|
|
94
99
|
serveFavicon?: FaviconHandler; // /favicon.ico
|
|
@@ -117,7 +122,7 @@ registerCustomRoute(async (req, res) => {
|
|
|
117
122
|
|
|
118
123
|
- **HTTP server** with CORS + security headers, OPTIONS preflight, method validation, cookie sliding, CSRF middleware.
|
|
119
124
|
- **Socket.io server** attached to the HTTP server, with optional Redis adapter when configured in deploy config.
|
|
120
|
-
- **Framework routes:** `/_health`, `/livez`, `/readyz`, `/_test/reset` (dev/test only), `/api/*` and `/sync/*` (with SSE streaming), `/auth/api`, `/auth/callback/*`, `/uploads/*`, `/assets
|
|
125
|
+
- **Framework routes:** `/_health`, `/livez`, `/readyz`, `/_test/reset` (dev/test only), `/api/*` and `/sync/*` (with SSE streaming), `/auth/api`, `/auth/callback/*`, `GET /auth/csrf`, `/uploads/*`, and `/assets/*`.
|
|
121
126
|
- **Presence broadcasting** (when enabled in project config): connect / disconnect / reconnect, location updates, peer notifications.
|
|
122
127
|
- **Boot UUID** written on startup so the load-balancer (`@luckystack/router`) can detect rolling restarts.
|
|
123
128
|
- **Dev tools** in non-production: devkit hot reload, console initializer.
|
|
@@ -128,7 +133,7 @@ registerCustomRoute(async (req, res) => {
|
|
|
128
133
|
|
|
129
134
|
| File | Routes |
|
|
130
135
|
| --- | --- |
|
|
131
|
-
| `csrfMiddleware.ts` + `csrfRoute.ts` | CSRF guard on writes + `GET /csrf
|
|
136
|
+
| `csrfMiddleware.ts` + `csrfRoute.ts` | CSRF guard on cookie-mode writes + `GET /auth/csrf` token issue |
|
|
132
137
|
| `healthRoutes.ts` | `/livez`, `/readyz`, `/_health` |
|
|
133
138
|
| `testResetRoute.ts` | `/_test/reset` (dev/test only — see Security below) |
|
|
134
139
|
| `faviconRoute.ts` | `/favicon.ico` |
|
|
@@ -146,7 +151,7 @@ Top-level `handleHttpRequest` + `dispatchRoutes(handlers, ctx)` are the only orc
|
|
|
146
151
|
|
|
147
152
|
- **CORS fail-closed.** If neither `Origin` nor `Referer` is present, only read-only methods (GET, HEAD, OPTIONS) are allowed; state-changing methods are rejected with 403. Earlier builds fell back to `Host`, which silently bypassed CORS for non-browser callers (`curl`, server-to-server). When you `curl` a write endpoint, set `-H 'Origin: https://your-allowed-origin'`.
|
|
148
153
|
- **`/_test/reset` is fail-closed.** It requires both `NODE_ENV` to be exactly `development` or `test` AND a non-empty `TEST_RESET_TOKEN` env var. Any other state returns 403 (no silent allow-list). Wire `TEST_RESET_TOKEN` in your dev/test `.env.local` and pass it as the `x-test-reset-token: ${TOKEN}` header on the reset call. `@luckystack/test-runner`'s `resetServerState` reads the same env var.
|
|
149
|
-
- **CSRF middleware.**
|
|
154
|
+
- **CSRF middleware.** In cookie mode, state-changing framework routes and eligible custom routes require the configured CSRF header (default `x-csrf-token`). With login installed it must match the session-bound token; without login it must match the stateless double-submit cookie issued by `GET /auth/csrf`. The `apiRequest` helper in `@luckystack/core/client` fetches and attaches it automatically. Auth bootstrap/callback routes and explicitly origin-exempt paths are excluded. Rejections dispatch `csrfMismatch` before returning 403; the hook receives token presence only, never the value. Configure the header name, token length, and cookie name/options through `registerCsrfConfig(...)` — see `packages/core/docs/csrf-config.md` and `packages/server/docs/security-defaults.md`.
|
|
150
155
|
|
|
151
156
|
## Public API
|
|
152
157
|
|
|
@@ -161,15 +166,13 @@ Top-level `handleHttpRequest` + `dispatchRoutes(handlers, ctx)` are the only orc
|
|
|
161
166
|
|
|
162
167
|
## Dependencies
|
|
163
168
|
|
|
164
|
-
- Runtime: `@luckystack/api`, `@luckystack/core
|
|
165
|
-
-
|
|
166
|
-
|
|
167
|
-
- `socket.io@^4.8.0`
|
|
168
|
-
- Optional peer: `@luckystack/error-tracking`, `@luckystack/email`, `@luckystack/docs-ui` — bootstrap auto-detects them but does not require them.
|
|
169
|
+
- Runtime: `@luckystack/api`, `@luckystack/core`
|
|
170
|
+
- Required peer: `socket.io@^4.8.0`
|
|
171
|
+
- Optional peers: `@prisma/client@^6.19.0`, `@luckystack/login`, `@luckystack/presence`, `@luckystack/sync`, `@luckystack/error-tracking`, `@luckystack/email`, `@luckystack/cron`, `@luckystack/docs-ui`, and `@luckystack/devkit` (dev-only). Bootstrap auto-detects installed optional peers and degrades when they are absent.
|
|
169
172
|
|
|
170
173
|
## Selecting bundles + port at runtime
|
|
171
174
|
|
|
172
|
-
`@luckystack/server/parseArgv` is a side-effect-only entrypoint. Import it as the **first line** of your `server.ts` so positional CLI
|
|
175
|
+
`@luckystack/server/parseArgv` is a side-effect-only entrypoint. Import it as the **first line** of your `server.ts` so a positional CLI port reaches the browser-safe `@luckystack/core/config` registry before your project's `config.ts` evaluates.
|
|
173
176
|
|
|
174
177
|
```ts
|
|
175
178
|
// server.ts
|
|
@@ -177,11 +180,13 @@ import '@luckystack/server/parseArgv';
|
|
|
177
180
|
// ...rest of your bootstrap
|
|
178
181
|
```
|
|
179
182
|
|
|
180
|
-
Argv shape: `<bundle[,bundle...]> [port]
|
|
183
|
+
Argv shape: `<bundle[,bundle...]> [port]`. The positional port overrides the
|
|
184
|
+
scaffold's `defaultPort: ports.backend`. Generic boots with no explicit,
|
|
185
|
+
positional, or default port retain the numeric fallback `80`.
|
|
181
186
|
|
|
182
187
|
```bash
|
|
183
|
-
npm run server # default preset,
|
|
184
|
-
npm run server -- billing # one bundle,
|
|
188
|
+
npm run server # default preset, config.ports.ts backend
|
|
189
|
+
npm run server -- billing # one bundle, config.ports.ts backend
|
|
185
190
|
npm run server -- billing,vehicles 4001 # merge bundles, listen on 4001
|
|
186
191
|
```
|
|
187
192
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/argv.ts
|
|
2
|
+
import { getPortOverride, registerPortOverride } from "@luckystack/core/config";
|
|
3
|
+
|
|
1
4
|
// src/portResolution.ts
|
|
2
5
|
var normalizeServerPort = (value, source = "server port") => {
|
|
3
6
|
let normalized;
|
|
@@ -12,20 +15,17 @@ var normalizeServerPort = (value, source = "server port") => {
|
|
|
12
15
|
};
|
|
13
16
|
var resolveServerPort = ({
|
|
14
17
|
optionsPort,
|
|
15
|
-
parsedPort
|
|
16
|
-
defaultPort
|
|
17
|
-
envPort
|
|
18
|
+
parsedPort,
|
|
19
|
+
defaultPort
|
|
18
20
|
}) => {
|
|
19
21
|
if (optionsPort !== void 0) return normalizeServerPort(optionsPort, "options.port");
|
|
20
|
-
if (
|
|
22
|
+
if (parsedPort !== void 0 && parsedPort !== null) return normalizeServerPort(parsedPort, "argv port");
|
|
21
23
|
if (defaultPort !== void 0) return normalizeServerPort(defaultPort, "options.defaultPort");
|
|
22
|
-
if (envPort !== void 0) return normalizeServerPort(envPort, "SERVER_PORT");
|
|
23
24
|
return 80;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
// src/argv.ts
|
|
27
28
|
var parsedBundles = [];
|
|
28
|
-
var parsedPort = null;
|
|
29
29
|
var hasRun = false;
|
|
30
30
|
var PORT_PATTERN = /^\d+$/;
|
|
31
31
|
var parseServerArgv = (argv) => {
|
|
@@ -52,13 +52,10 @@ var applyServerArgv = () => {
|
|
|
52
52
|
hasRun = true;
|
|
53
53
|
const result = parseServerArgv(process.argv.slice(2));
|
|
54
54
|
parsedBundles = result.bundles;
|
|
55
|
-
|
|
56
|
-
if (parsedPort !== null) {
|
|
57
|
-
process.env.SERVER_PORT = String(parsedPort);
|
|
58
|
-
}
|
|
55
|
+
registerPortOverride(result.port);
|
|
59
56
|
};
|
|
60
57
|
var getParsedBundles = () => parsedBundles;
|
|
61
|
-
var getParsedPort = () =>
|
|
58
|
+
var getParsedPort = () => getPortOverride() ?? null;
|
|
62
59
|
|
|
63
60
|
export {
|
|
64
61
|
normalizeServerPort,
|
|
@@ -68,4 +65,4 @@ export {
|
|
|
68
65
|
getParsedBundles,
|
|
69
66
|
getParsedPort
|
|
70
67
|
};
|
|
71
|
-
//# sourceMappingURL=chunk-
|
|
68
|
+
//# sourceMappingURL=chunk-HJXOI6LY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/argv.ts","../src/portResolution.ts"],"sourcesContent":["//? Positional argv parser for the LuckyStack server boot. Replaces legacy\n//? environment toggles with a single shape:\n//?\n//? npm run server -- billing,vehicles 4001\n//?\n//? Arg 0 = comma-separated preset list (runtime maps merged across them).\n//? Arg 1 = listen port (numeric).\n//?\n//? `applyServerArgv()` is called by the side-effect module\n//? `@luckystack/server/parseArgv`, which consumers import as the FIRST line\n//? of their `server.ts` so the parsed port lands in the browser-safe core/config\n//? registry before `config.ts` evaluates its top-level OAuth callback base.\n\nimport { getPortOverride, registerPortOverride } from '@luckystack/core/config';\nimport { normalizeServerPort } from './portResolution';\n\nexport interface ParsedServerArgv {\n bundles: string[];\n port: number | null;\n}\n\nlet parsedBundles: string[] = [];\nlet hasRun = false;\n\nconst PORT_PATTERN = /^\\d+$/;\n\nexport const parseServerArgv = (argv: string[]): ParsedServerArgv => {\n if (argv.length > 2) {\n throw new Error(\n `[luckystack:argv] unexpected positional argument(s): \"${argv.slice(2).join(' ')}\". ` +\n `Usage: npm run server -- <bundle[,bundle...]> [port]`,\n );\n }\n\n const bundles = argv[0] && argv[0].length > 0\n ? [...new Set(argv[0].split(',').map((s) => s.trim()).filter(Boolean))]\n : [];\n\n let port: number | null = null;\n const portArg = argv[1];\n if (portArg !== undefined) {\n if (!PORT_PATTERN.test(portArg)) {\n throw new Error(\n `[luckystack:argv] port argument must be numeric, got: \"${portArg}\". ` +\n `Usage: npm run server -- <bundle[,bundle...]> [port]`,\n );\n }\n port = normalizeServerPort(portArg, 'port argument');\n }\n\n return { bundles, port };\n};\n\nexport const applyServerArgv = (): void => {\n if (hasRun) return;\n hasRun = true;\n\n const result = parseServerArgv(process.argv.slice(2));\n parsedBundles = result.bundles;\n registerPortOverride(result.port);\n};\n\nexport const getParsedBundles = (): string[] => parsedBundles;\nexport const getParsedPort = (): number | null => getPortOverride() ?? null;\n","export interface ResolveServerPortInput {\n optionsPort?: number | string;\n parsedPort?: number | null;\n defaultPort?: number | string;\n}\n\nexport const normalizeServerPort = (\n value: number | string,\n source = 'server port',\n): number => {\n let normalized: number;\n if (typeof value === 'number') normalized = value;\n else normalized = /^\\d+$/.test(value) ? Number(value) : Number.NaN;\n\n if (!Number.isInteger(normalized) || normalized < 0 || normalized > 65_535) {\n throw new RangeError(\n `[luckystack:port] ${source} must be an integer from 0 through 65535, got: \"${String(value)}\".`,\n );\n }\n\n return normalized;\n};\n\n//? One explicit, testable precedence chain. The scaffold passes ports.backend as\n//? `defaultPort`; generic consumers that omit every source retain port 80.\nexport const resolveServerPort = ({\n optionsPort,\n parsedPort,\n defaultPort,\n}: ResolveServerPortInput): number => {\n if (optionsPort !== undefined) return normalizeServerPort(optionsPort, 'options.port');\n if (parsedPort !== undefined && parsedPort !== null) return normalizeServerPort(parsedPort, 'argv port');\n if (defaultPort !== undefined) return normalizeServerPort(defaultPort, 'options.defaultPort');\n return 80;\n};\n"],"mappings":";AAaA,SAAS,iBAAiB,4BAA4B;;;ACP/C,IAAM,sBAAsB,CACjC,OACA,SAAS,kBACE;AACX,MAAI;AACJ,MAAI,OAAO,UAAU,SAAU,cAAa;AAAA,MACvC,cAAa,QAAQ,KAAK,KAAK,IAAI,OAAO,KAAK,IAAI,OAAO;AAE/D,MAAI,CAAC,OAAO,UAAU,UAAU,KAAK,aAAa,KAAK,aAAa,OAAQ;AAC1E,UAAM,IAAI;AAAA,MACR,qBAAqB,MAAM,mDAAmD,OAAO,KAAK,CAAC;AAAA,IAC7F;AAAA,EACF;AAEA,SAAO;AACT;AAIO,IAAM,oBAAoB,CAAC;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AACF,MAAsC;AACpC,MAAI,gBAAgB,OAAW,QAAO,oBAAoB,aAAa,cAAc;AACrF,MAAI,eAAe,UAAa,eAAe,KAAM,QAAO,oBAAoB,YAAY,WAAW;AACvG,MAAI,gBAAgB,OAAW,QAAO,oBAAoB,aAAa,qBAAqB;AAC5F,SAAO;AACT;;;ADbA,IAAI,gBAA0B,CAAC;AAC/B,IAAI,SAAS;AAEb,IAAM,eAAe;AAEd,IAAM,kBAAkB,CAAC,SAAqC;AACnE,MAAI,KAAK,SAAS,GAAG;AACnB,UAAM,IAAI;AAAA,MACR,yDAAyD,KAAK,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC;AAAA,IAElF;AAAA,EACF;AAEA,QAAM,UAAU,KAAK,CAAC,KAAK,KAAK,CAAC,EAAE,SAAS,IACxC,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,CAAC,CAAC,IACpE,CAAC;AAEL,MAAI,OAAsB;AAC1B,QAAM,UAAU,KAAK,CAAC;AACtB,MAAI,YAAY,QAAW;AACzB,QAAI,CAAC,aAAa,KAAK,OAAO,GAAG;AAC/B,YAAM,IAAI;AAAA,QACR,0DAA0D,OAAO;AAAA,MAEnE;AAAA,IACF;AACA,WAAO,oBAAoB,SAAS,eAAe;AAAA,EACrD;AAEA,SAAO,EAAE,SAAS,KAAK;AACzB;AAEO,IAAM,kBAAkB,MAAY;AACzC,MAAI,OAAQ;AACZ,WAAS;AAET,QAAM,SAAS,gBAAgB,QAAQ,KAAK,MAAM,CAAC,CAAC;AACpD,kBAAgB,OAAO;AACvB,uBAAqB,OAAO,IAAI;AAClC;AAEO,IAAM,mBAAmB,MAAgB;AACzC,IAAM,gBAAgB,MAAqB,gBAAgB,KAAK;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -84,16 +84,14 @@ interface CreateLuckyStackServerOptions {
|
|
|
84
84
|
* by `@luckystack/server/parseArgv`
|
|
85
85
|
* 3. `options.defaultPort` (the project's single-instance default, e.g. the
|
|
86
86
|
* `backend` port from `config.ports.ts`)
|
|
87
|
-
* 4. `
|
|
88
|
-
* 5. `80`
|
|
87
|
+
* 4. `80` for generic consumers that supplied no port source
|
|
89
88
|
*/
|
|
90
89
|
port?: number | string;
|
|
91
90
|
/**
|
|
92
91
|
* Project's single-instance default backend port. Sits BELOW positional argv
|
|
93
|
-
* so a multi-instance boot (`npm run server -- <preset> <port>`) still wins
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* truth without re-introducing `SERVER_PORT` to `.env`.
|
|
92
|
+
* so a multi-instance boot (`npm run server -- <preset> <port>`) still wins.
|
|
93
|
+
* The scaffold passes the `backend` value from `config.ports.ts` here, so the
|
|
94
|
+
* listen port has one static source of truth.
|
|
97
95
|
*/
|
|
98
96
|
defaultPort?: number;
|
|
99
97
|
/** Bind address. Defaults to process.env.SERVER_IP or '127.0.0.1'. */
|
|
@@ -199,6 +197,23 @@ interface BootstrapLuckyStackOptions extends CreateLuckyStackServerOptions {
|
|
|
199
197
|
declare const OVERLAY_ORDER: string[];
|
|
200
198
|
type OverlayLoader = () => Promise<void>;
|
|
201
199
|
declare const registerOverlayLoader: (loader: OverlayLoader) => void;
|
|
200
|
+
interface OverlayEntries {
|
|
201
|
+
/** Files to import, in load order: `index.*` first, then the rest alphabetically. */
|
|
202
|
+
fileNames: string[];
|
|
203
|
+
/**
|
|
204
|
+
* Subdirectories the flat walk skipped, minus conventional test folders.
|
|
205
|
+
* The caller warns about these — silently dropping them meant overlay code
|
|
206
|
+
* one level down simply never ran, with nothing to point at.
|
|
207
|
+
*/
|
|
208
|
+
ignoredDirectoryNames: string[];
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Decide what a single overlay package folder contributes. Exported because
|
|
212
|
+
* both the runtime walk here and `scripts/bundleServer.mjs` must agree exactly:
|
|
213
|
+
* a file the bundler includes but the loader skips (or the reverse) is a
|
|
214
|
+
* dev/prod divergence, which is the defect class ADR 0046 exists for.
|
|
215
|
+
*/
|
|
216
|
+
declare const collectOverlayEntries: (packageDir: string) => OverlayEntries;
|
|
202
217
|
declare const bootstrapLuckyStack: (options?: BootstrapLuckyStackOptions) => Promise<RunningLuckyStackServer>;
|
|
203
218
|
|
|
204
219
|
interface BootstrapRequirements {
|
|
@@ -310,4 +325,4 @@ declare const registerSecurityHeaders: (builder: SecurityHeadersBuilder | null)
|
|
|
310
325
|
/** Read the active builder (or null). */
|
|
311
326
|
declare const getSecurityHeadersBuilder: () => SecurityHeadersBuilder | null;
|
|
312
327
|
|
|
313
|
-
export { type BootstrapLuckyStackOptions, type BootstrapRequirements, type CreateLuckyStackServerOptions, type CustomRouteHandler, type CustomRoutePhase, type FaviconHandler, type GeneratedRuntimeMapsModule, OVERLAY_ORDER, type OnLocationUpdatePayload, type OnSocketConnectPayload, type OnSocketDisconnectPayload, type OriginExemptMatcher, type ParsedServerArgv, type PostRoomJoinPayload, type PostRoomLeavePayload, type PreRoomJoinPayload, type PreRoomLeavePayload, type ProdRuntimeMapsLoaderOptions, type RegisterCustomRouteOptions, type RouteContext, type RunningLuckyStackServer, type SecurityHeadersBuilder, type StaticFileHandler, type StopLuckyStackServerOptions, applyServerArgv, bootstrapLuckyStack, clearCustomRoutes, clearOriginExemptPaths, createLuckyStackServer, createProdRuntimeMapsProvider, getCustomRoutes, getOriginExemptPaths, getParsedBundles, getParsedPort, getPreParamsCustomRoutes, getSecurityHeadersBuilder, isOriginExemptPath, parseServerArgv, registerCustomRoute, registerOriginExemptPath, registerOverlayLoader, registerProdRuntimeMapsProvider, registerSecurityHeaders, verifyBootstrap };
|
|
328
|
+
export { type BootstrapLuckyStackOptions, type BootstrapRequirements, type CreateLuckyStackServerOptions, type CustomRouteHandler, type CustomRoutePhase, type FaviconHandler, type GeneratedRuntimeMapsModule, OVERLAY_ORDER, type OnLocationUpdatePayload, type OnSocketConnectPayload, type OnSocketDisconnectPayload, type OriginExemptMatcher, type OverlayEntries, type ParsedServerArgv, type PostRoomJoinPayload, type PostRoomLeavePayload, type PreRoomJoinPayload, type PreRoomLeavePayload, type ProdRuntimeMapsLoaderOptions, type RegisterCustomRouteOptions, type RouteContext, type RunningLuckyStackServer, type SecurityHeadersBuilder, type StaticFileHandler, type StopLuckyStackServerOptions, applyServerArgv, bootstrapLuckyStack, clearCustomRoutes, clearOriginExemptPaths, collectOverlayEntries, createLuckyStackServer, createProdRuntimeMapsProvider, getCustomRoutes, getOriginExemptPaths, getParsedBundles, getParsedPort, getPreParamsCustomRoutes, getSecurityHeadersBuilder, isOriginExemptPath, parseServerArgv, registerCustomRoute, registerOriginExemptPath, registerOverlayLoader, registerProdRuntimeMapsProvider, registerSecurityHeaders, verifyBootstrap };
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
normalizeServerPort,
|
|
6
6
|
parseServerArgv,
|
|
7
7
|
resolveServerPort
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-HJXOI6LY.js";
|
|
9
9
|
|
|
10
10
|
// src/createServer.ts
|
|
11
11
|
import http from "http";
|
|
@@ -2156,7 +2156,7 @@ var areEquivalentFunctionEntries = (left, right) => {
|
|
|
2156
2156
|
if (!isRuntimeMapRecord(left) || !isRuntimeMapRecord(right)) return false;
|
|
2157
2157
|
const leftKeys = Object.keys(left);
|
|
2158
2158
|
const rightKeys = Object.keys(right);
|
|
2159
|
-
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) &&
|
|
2159
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && areEquivalentFunctionEntries(left[key], right[key]));
|
|
2160
2160
|
};
|
|
2161
2161
|
var mergeInto = (target, source, kind, fromPreset, keyOrigin) => {
|
|
2162
2162
|
for (const key of Object.keys(source)) {
|
|
@@ -2326,7 +2326,7 @@ var writeDevServerInfo = (ip, port) => {
|
|
|
2326
2326
|
`);
|
|
2327
2327
|
} catch (error) {
|
|
2328
2328
|
getLogger12().debug(
|
|
2329
|
-
`[dev-server-info] could not write port file (proxy will fall back to
|
|
2329
|
+
`[dev-server-info] could not write port file (proxy will fall back to config.ports.ts): ${error instanceof Error ? error.message : String(error)}`
|
|
2330
2330
|
);
|
|
2331
2331
|
}
|
|
2332
2332
|
};
|
|
@@ -2411,7 +2411,7 @@ var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((re
|
|
|
2411
2411
|
return;
|
|
2412
2412
|
}
|
|
2413
2413
|
getLogger13().error(
|
|
2414
|
-
`Port ${String(attemptPort)} is already in use \u2014 the server did NOT start. Another \`npm run server\` is probably still running (stop it), or
|
|
2414
|
+
`Port ${String(attemptPort)} is already in use \u2014 the server did NOT start. Another \`npm run server\` is probably still running (stop it), or choose a free port in config.ports.ts / positional argv, or set SERVER_PORT_AUTO_INCREMENT=1 to auto-pick the next free port.`
|
|
2415
2415
|
);
|
|
2416
2416
|
reject(err);
|
|
2417
2417
|
};
|
|
@@ -2462,12 +2462,11 @@ var createLuckyStackServer = async (options = {}) => {
|
|
|
2462
2462
|
const port = resolveServerPort({
|
|
2463
2463
|
optionsPort: options.port,
|
|
2464
2464
|
parsedPort: getParsedPort(),
|
|
2465
|
-
defaultPort: options.defaultPort
|
|
2466
|
-
envPort: process.env.SERVER_PORT
|
|
2465
|
+
defaultPort: options.defaultPort
|
|
2467
2466
|
});
|
|
2468
2467
|
const ip = options.ip ?? process.env.SERVER_IP ?? "127.0.0.1";
|
|
2469
2468
|
const enableDevTools = options.enableDevTools ?? resolveRuntimeMode() !== "production";
|
|
2470
|
-
registerBindAddress({ ip, port });
|
|
2469
|
+
registerBindAddress({ ip, port, configuredPort: options.defaultPort });
|
|
2471
2470
|
if (enableDevTools) {
|
|
2472
2471
|
await initDevTools();
|
|
2473
2472
|
}
|
|
@@ -2521,7 +2520,7 @@ var createLuckyStackServer = async (options = {}) => {
|
|
|
2521
2520
|
import path3 from "path";
|
|
2522
2521
|
import fs2 from "fs";
|
|
2523
2522
|
import { pathToFileURL } from "url";
|
|
2524
|
-
import { ROOT_DIR, tryCatch as tryCatch11 } from "@luckystack/core";
|
|
2523
|
+
import { ROOT_DIR, tryCatch as tryCatch11, getLogger as getLogger14, isTestFile, isTestDirectory } from "@luckystack/core";
|
|
2525
2524
|
var OVERLAY_ORDER = [
|
|
2526
2525
|
// Core registries first — clients, paths, routing rules. Anything below
|
|
2527
2526
|
// depends on these being in place.
|
|
@@ -2559,6 +2558,24 @@ var registeredOverlayLoader = null;
|
|
|
2559
2558
|
var registerOverlayLoader = (loader) => {
|
|
2560
2559
|
registeredOverlayLoader = loader;
|
|
2561
2560
|
};
|
|
2561
|
+
var collectOverlayEntries = (packageDir) => {
|
|
2562
|
+
const indexCandidates = ["index.ts", "index.js"];
|
|
2563
|
+
const entries = fs2.readdirSync(packageDir, { withFileTypes: true }).toSorted((left, right) => left.name.localeCompare(right.name));
|
|
2564
|
+
const ignoredDirectoryNames = [];
|
|
2565
|
+
const rest = [];
|
|
2566
|
+
for (const entry of entries) {
|
|
2567
|
+
if (entry.isDirectory()) {
|
|
2568
|
+
if (!isTestDirectory(entry.name)) ignoredDirectoryNames.push(entry.name);
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2571
|
+
if (indexCandidates.includes(entry.name)) continue;
|
|
2572
|
+
if (!entry.name.endsWith(".ts") && !entry.name.endsWith(".js")) continue;
|
|
2573
|
+
if (isTestFile(entry.name)) continue;
|
|
2574
|
+
rest.push(entry.name);
|
|
2575
|
+
}
|
|
2576
|
+
const indexFileNames = indexCandidates.filter((candidate) => entries.some((entry) => entry.isFile() && entry.name === candidate));
|
|
2577
|
+
return { fileNames: [...indexFileNames, ...rest], ignoredDirectoryNames };
|
|
2578
|
+
};
|
|
2562
2579
|
var loadOverlayFolder = async (overlayRoot) => {
|
|
2563
2580
|
const overlayAbs = path3.isAbsolute(overlayRoot) ? overlayRoot : path3.join(ROOT_DIR, overlayRoot);
|
|
2564
2581
|
if (!fs2.existsSync(overlayAbs)) {
|
|
@@ -2567,15 +2584,14 @@ var loadOverlayFolder = async (overlayRoot) => {
|
|
|
2567
2584
|
for (const packageName of OVERLAY_ORDER) {
|
|
2568
2585
|
const packageDir = path3.join(overlayAbs, packageName);
|
|
2569
2586
|
if (!fs2.existsSync(packageDir)) continue;
|
|
2570
|
-
const
|
|
2571
|
-
for (const
|
|
2572
|
-
|
|
2587
|
+
const { fileNames, ignoredDirectoryNames } = collectOverlayEntries(packageDir);
|
|
2588
|
+
for (const directoryName of ignoredDirectoryNames) {
|
|
2589
|
+
getLogger14().warn(
|
|
2590
|
+
`[luckystack:overlay] ignoring \`${overlayRoot}/${packageName}/${directoryName}/\` \u2014 the overlay walk does not recurse. Move the file(s) up into \`${overlayRoot}/${packageName}/\`, or import them from a file that is there.`
|
|
2591
|
+
);
|
|
2573
2592
|
}
|
|
2574
|
-
const
|
|
2575
|
-
|
|
2576
|
-
if (indexCandidates.includes(entry)) continue;
|
|
2577
|
-
if (!entry.endsWith(".ts") && !entry.endsWith(".js")) continue;
|
|
2578
|
-
await importIfExists(path3.join(packageDir, entry));
|
|
2593
|
+
for (const fileName of fileNames) {
|
|
2594
|
+
await importIfExists(path3.join(packageDir, fileName));
|
|
2579
2595
|
}
|
|
2580
2596
|
}
|
|
2581
2597
|
};
|
|
@@ -2615,6 +2631,7 @@ export {
|
|
|
2615
2631
|
bootstrapLuckyStack,
|
|
2616
2632
|
clearCustomRoutes,
|
|
2617
2633
|
clearOriginExemptPaths,
|
|
2634
|
+
collectOverlayEntries,
|
|
2618
2635
|
createLuckyStackServer,
|
|
2619
2636
|
createProdRuntimeMapsProvider,
|
|
2620
2637
|
getCustomRoutes,
|