@statorjs/stator 1.1.0 → 1.1.1
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/package.json +2 -2
- package/src/server/banner.ts +24 -0
- package/src/server/create-app.ts +12 -0
- package/src/server/dev.ts +23 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statorjs/stator",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Server-canonical web framework: business logic in composable state machines, UI as a thin renderer binding machine outputs to DOM positions. Ships TypeScript source (Vite/tsx-native by design).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"pino-pretty": "^13.1.3",
|
|
89
89
|
"vite": "^6.0.0",
|
|
90
90
|
"vitest": "^2.1.0",
|
|
91
|
-
"@statorjs/stator": "1.1.
|
|
91
|
+
"@statorjs/stator": "1.1.1"
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
|
94
94
|
"typecheck": "tsc --noEmit",
|
package/src/server/banner.ts
CHANGED
|
@@ -40,12 +40,18 @@ function lanAddress(): string | undefined {
|
|
|
40
40
|
|
|
41
41
|
export function printDevBanner(info: {
|
|
42
42
|
port: number
|
|
43
|
+
/** The port originally asked for, when the server had to shift off it. */
|
|
44
|
+
requestedPort?: number
|
|
43
45
|
machines: number
|
|
44
46
|
routes: number
|
|
45
47
|
inspector?: boolean
|
|
46
48
|
}): void {
|
|
47
49
|
const v = statorVersion()
|
|
48
50
|
const lan = lanAddress()
|
|
51
|
+
const shifted =
|
|
52
|
+
info.requestedPort !== undefined && info.requestedPort !== info.port
|
|
53
|
+
? [` ${c.dim(`port ${info.requestedPort} was busy — using ${info.port}`)}`]
|
|
54
|
+
: []
|
|
49
55
|
const lines = [
|
|
50
56
|
'',
|
|
51
57
|
` ${c.copper(c.bold('stator'))}${v ? c.dim(` v${v}`) : ''} ${c.dim('dev server')}`,
|
|
@@ -58,6 +64,7 @@ export function printDevBanner(info: {
|
|
|
58
64
|
info.routes === 1 ? '' : 's'
|
|
59
65
|
}${info.inspector ? ' · inspector on' : ''} — Ctrl+C to stop`,
|
|
60
66
|
)}`,
|
|
67
|
+
...shifted,
|
|
61
68
|
'',
|
|
62
69
|
]
|
|
63
70
|
process.stdout.write(`${lines.join('\n')}\n`)
|
|
@@ -88,3 +95,20 @@ export function installGracefulShutdown(close: () => Promise<void> | void, quiet
|
|
|
88
95
|
process.on('SIGINT', handler)
|
|
89
96
|
process.on('SIGTERM', handler)
|
|
90
97
|
}
|
|
98
|
+
|
|
99
|
+
/** First free TCP port at or above `start` (bounded probe). The dev plane
|
|
100
|
+
* auto-shifts off busy ports like every modern dev server; production
|
|
101
|
+
* never calls this — a taken port there is a deploy error to surface. */
|
|
102
|
+
export async function findFreePort(start: number, attempts = 10): Promise<number> {
|
|
103
|
+
const { createServer } = await import('node:net')
|
|
104
|
+
for (let port = start; port < start + attempts; port++) {
|
|
105
|
+
const free = await new Promise<boolean>((resolve) => {
|
|
106
|
+
const probe = createServer()
|
|
107
|
+
probe.once('error', () => resolve(false))
|
|
108
|
+
probe.once('listening', () => probe.close(() => resolve(true)))
|
|
109
|
+
probe.listen(port)
|
|
110
|
+
})
|
|
111
|
+
if (free) return port
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`stator: no free port found in ${start}–${start + attempts - 1}`)
|
|
114
|
+
}
|
package/src/server/create-app.ts
CHANGED
|
@@ -76,6 +76,18 @@ export async function createApp(config: CreateAppConfig): Promise<StatorApp> {
|
|
|
76
76
|
logger.info({ port, machines: defs.length, routes: routes.length }, 'listening')
|
|
77
77
|
resolveFn()
|
|
78
78
|
})
|
|
79
|
+
// Production is strict about its port (a collision is a deploy
|
|
80
|
+
// error) — but says so in one line, not a stack trace.
|
|
81
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
82
|
+
if (err.code === 'EADDRINUSE') {
|
|
83
|
+
logger.error(
|
|
84
|
+
{ port },
|
|
85
|
+
`port ${port} is already in use — is another instance running? (set PORT to change it)`,
|
|
86
|
+
)
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
89
|
+
throw err
|
|
90
|
+
})
|
|
79
91
|
// Ctrl+C / SIGTERM (deploy rollover) exits 0, not 130 — quiet in
|
|
80
92
|
// prod: the structured logs are the record.
|
|
81
93
|
installGracefulShutdown(() => new Promise<void>((done) => server.close(() => done())), true)
|
package/src/server/dev.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { createServer as createViteServer, type ViteDevServer } from 'vite'
|
|
|
7
7
|
import { compile } from '../compiler/index.ts'
|
|
8
8
|
import { machineStub, stator } from '../vite/index.ts'
|
|
9
9
|
import type { AppStore } from './app-store.ts'
|
|
10
|
-
import { installGracefulShutdown, printDevBanner } from './banner.ts'
|
|
10
|
+
import { findFreePort, installGracefulShutdown, printDevBanner } from './banner.ts'
|
|
11
11
|
import { logger } from './logger.ts'
|
|
12
12
|
import type { MachineStore } from './machine-store.ts'
|
|
13
13
|
import type { DiscoveredRoute } from './route-discovery.ts'
|
|
@@ -52,10 +52,14 @@ export interface DevApp {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export async function createDevApp(config: DevAppConfig): Promise<DevApp> {
|
|
55
|
+
// Vite's HMR websocket defaults to 24678 for EVERY dev server — two
|
|
56
|
+
// stator apps side by side would fight over it (the loser's live reload
|
|
57
|
+
// silently dies). Probe a free one instead.
|
|
58
|
+
const hmrPort = await findFreePort(24678)
|
|
55
59
|
const vite = await createViteServer({
|
|
56
60
|
root: resolve(config.root),
|
|
57
61
|
appType: 'custom',
|
|
58
|
-
server: { middlewareMode: true },
|
|
62
|
+
server: { middlewareMode: true, hmr: { port: hmrPort } },
|
|
59
63
|
// machineStub keeps server machines out of browser module graphs: island
|
|
60
64
|
// imports of a machine resolve to a `{ name }` stub (SSR loads are
|
|
61
65
|
// untouched — `options.ssr` gates it).
|
|
@@ -199,12 +203,23 @@ export async function createDevApp(config: DevAppConfig): Promise<DevApp> {
|
|
|
199
203
|
await vite.close()
|
|
200
204
|
await new Promise<void>((done) => server.close(() => done()))
|
|
201
205
|
})
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
// Busy port? Shift up like every modern dev server (the requested
|
|
207
|
+
// port is someone's other project, not an error).
|
|
208
|
+
return findFreePort(port).then(
|
|
209
|
+
(freePort) =>
|
|
210
|
+
new Promise((resolveFn) => {
|
|
211
|
+
server.listen(freePort, () => {
|
|
212
|
+
printDevBanner({
|
|
213
|
+
port: freePort,
|
|
214
|
+
requestedPort: port,
|
|
215
|
+
machines: machineCount,
|
|
216
|
+
routes: routes.length,
|
|
217
|
+
inspector: true,
|
|
218
|
+
})
|
|
219
|
+
resolveFn()
|
|
220
|
+
})
|
|
221
|
+
}),
|
|
222
|
+
)
|
|
208
223
|
},
|
|
209
224
|
close: () => vite.close(),
|
|
210
225
|
}
|