@looop-games/cli 0.1.31 → 0.1.32
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 +12 -0
- package/lib/dev.mjs +13 -3
- package/lib/ports.mjs +43 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,18 @@ Versions: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
## [0.1.32] - 2026-08-23
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- `looop dev` no longer hands your game a **browser-blocked port**. It derives the
|
|
21
|
+
multiplayer and services ports from the game port, and a few combinations land
|
|
22
|
+
on ports browsers refuse to connect to (`ERR_UNSAFE_PORT` — e.g. game port 8050
|
|
23
|
+
derives multiplayer port 2049/NFS). The game booted but its room socket died
|
|
24
|
+
instantly, so it rendered blank with no owned player — and because `curl` and
|
|
25
|
+
headless tests ignore the blocklist, every check looked healthy. Dev now skips
|
|
26
|
+
any port block a browser can't reach, and warns if you pin an unreachable one
|
|
27
|
+
with `--port`.
|
|
28
|
+
|
|
17
29
|
## [0.1.31] - 2026-08-21
|
|
18
30
|
|
|
19
31
|
### Changed
|
package/lib/dev.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { ensureEngine } from './engine.mjs';
|
|
|
16
16
|
import { createStaticServer } from './static-server.mjs';
|
|
17
17
|
import { createLlmShim, DEFAULT_API_BASE } from './llm-shim.mjs';
|
|
18
18
|
import { getToken, getApiBase } from './config.mjs';
|
|
19
|
-
import { resolvePorts, portInUse, killPort, lanIp } from './ports.mjs';
|
|
19
|
+
import { resolvePorts, portInUse, killPort, lanIp, isRestricted, nextBrowserSafe } from './ports.mjs';
|
|
20
20
|
import { assertNoInertOverride, isFrameworkV2, isSinglePlayerV2, scanPrimitives } from './primitives.mjs';
|
|
21
21
|
import { buildDevRoomServer, DEV_OVERRIDES_DIR } from './room-server.mjs';
|
|
22
22
|
import { createFileWatcher, createRoomReloader } from './room-reload.mjs';
|
|
@@ -73,8 +73,18 @@ export async function dev({ cwd = process.cwd(), port, noMp = process.env.NO_MP
|
|
|
73
73
|
requested: explicit,
|
|
74
74
|
});
|
|
75
75
|
for (const s of steppedAside) {
|
|
76
|
-
const who = s.
|
|
77
|
-
|
|
76
|
+
const who = s.reason === 'browser-restricted'
|
|
77
|
+
? 'browser-restricted (mp/shim would be blocked) — stepping past'
|
|
78
|
+
: `held by ${s.projectDir ? `another Looop game (${basename(s.projectDir)})` : 'another app'} — leaving it alone`;
|
|
79
|
+
log(`→ :${s.port} (${who})`);
|
|
80
|
+
}
|
|
81
|
+
// An EXPLICIT --port is obeyed as given, but if it derives a browser-blocked
|
|
82
|
+
// port (e.g. --port 8050 → mp 2049/NFS) the game would look broken with no
|
|
83
|
+
// clue, so say so loudly rather than silently binding it.
|
|
84
|
+
for (const [name, p] of [['multiplayer', ports.mp], ['services', ports.shim], ['static', ports.static]]) {
|
|
85
|
+
if (isRestricted(p)) {
|
|
86
|
+
log(`⚠ ${name} port :${p} is browser-restricted (ERR_UNSAFE_PORT) — browsers will refuse it. Re-run with a --port whose ${name} port is reachable (e.g. --port ${nextBrowserSafe(ports.static)}).`);
|
|
87
|
+
}
|
|
78
88
|
}
|
|
79
89
|
const children = [];
|
|
80
90
|
const servers = [];
|
package/lib/ports.mjs
CHANGED
|
@@ -35,6 +35,40 @@ export const STATIC_PORT_BASE = 8000;
|
|
|
35
35
|
export const MP_PORT_BASE = 1999;
|
|
36
36
|
export const SHIM_PORT_BASE = 8788;
|
|
37
37
|
|
|
38
|
+
// Ports a BROWSER refuses to connect to (ERR_UNSAFE_PORT), from Chromium's
|
|
39
|
+
// net/base/port_util.cc `kRestrictedPorts` (Firefox bans the same set). We
|
|
40
|
+
// derive mp/shim from the static port, so a block landing ANY of the three on
|
|
41
|
+
// one of these silently breaks the browser connection — while curl and a
|
|
42
|
+
// headless probe connect fine, so every server-side check looks healthy. The
|
|
43
|
+
// one that bites in the normal range is 2049 (NFS): static 8050 → mp 2049. The
|
|
44
|
+
// full set keeps it robust if the derivation range ever shifts. Reported by a
|
|
45
|
+
// creator (feedback fb_c690e35cff30511e).
|
|
46
|
+
export const RESTRICTED_PORTS = new Set([
|
|
47
|
+
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79,
|
|
48
|
+
87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137,
|
|
49
|
+
139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532,
|
|
50
|
+
540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720, 1723,
|
|
51
|
+
2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669, 6697,
|
|
52
|
+
10080,
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export function isRestricted(port) {
|
|
56
|
+
return RESTRICTED_PORTS.has(port);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True if a static base derives a block where static, mp, OR shim is browser-restricted. */
|
|
60
|
+
export function blockIsRestricted(staticPort) {
|
|
61
|
+
const p = portsFor(staticPort);
|
|
62
|
+
return isRestricted(p.static) || isRestricted(p.mp) || isRestricted(p.shim);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The first static port ≥ `staticPort` whose whole derived block is browser-reachable. */
|
|
66
|
+
export function nextBrowserSafe(staticPort) {
|
|
67
|
+
let p = staticPort;
|
|
68
|
+
while (blockIsRestricted(p)) p += 1;
|
|
69
|
+
return p;
|
|
70
|
+
}
|
|
71
|
+
|
|
38
72
|
// Ports move in BLOCKS: the browser derives mp/shim from location.port, so the
|
|
39
73
|
// whole stack shifts together. 8000/1999/8788 → 8010/2009/8798 → …
|
|
40
74
|
export const BLOCK_STRIDE = 10;
|
|
@@ -97,6 +131,15 @@ export async function resolvePorts({
|
|
|
97
131
|
const steppedAside = [];
|
|
98
132
|
for (let i = 0; i < blocks; i++) {
|
|
99
133
|
const ports = portsFor(base + i * BLOCK_STRIDE);
|
|
134
|
+
// A block whose static/mp/shim lands on a browser-restricted port is
|
|
135
|
+
// unusable no matter how free it is — the browser refuses the connection
|
|
136
|
+
// (ERR_UNSAFE_PORT) even though curl and server checks pass. Skip it before
|
|
137
|
+
// probing, so auto-allocation only ever hands the creator a browser-
|
|
138
|
+
// reachable stack. (feedback fb_c690e35cff30511e — mp 2049/NFS on base 8050.)
|
|
139
|
+
if (blockIsRestricted(base + i * BLOCK_STRIDE)) {
|
|
140
|
+
steppedAside.push({ port: ports.static, projectDir: null, reason: 'browser-restricted' });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
100
143
|
const owner = (await portInUse(ports.static)) ? await whoIsOn(ports.static) : undefined;
|
|
101
144
|
|
|
102
145
|
if (owner === undefined) {
|