@joystack/cli 0.1.51 → 0.1.53
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 +19 -0
- package/lib/inject.mjs +36 -5
- package/lib/joystack-node-doors.mjs +1 -0
- package/lib/publish-checks.mjs +16 -0
- package/lib/static-server.mjs +31 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,25 @@ Versions: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
## [0.1.53] - 2026-10-20
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `joystack dev` finds your game's own dev tools, the files
|
|
22
|
+
`tools/<name>.tool.js`, runs them in the page, and serves the
|
|
23
|
+
`joystack/tools` door they import.
|
|
24
|
+
- `joystack publish` refuses game code that imports `joystack/tools`: only a
|
|
25
|
+
tool file may, and a published page has no such door.
|
|
26
|
+
|
|
27
|
+
## [0.1.52] - 2026-10-14
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `joystack dev` serves `three/addons/` (the glTF loader and the skeleton
|
|
32
|
+
utilities the engine's `.glb` support imports), so a game that declares a
|
|
33
|
+
model file in `assets.js` loads it on the dev server as it will when
|
|
34
|
+
published.
|
|
35
|
+
|
|
17
36
|
## [0.1.51] - 2026-10-13
|
|
18
37
|
|
|
19
38
|
### Added
|
package/lib/inject.mjs
CHANGED
|
@@ -22,6 +22,16 @@ export const PLATFORM_URL = '/shared/platform/platform.js';
|
|
|
22
22
|
// that keeps a dev surface off published pages: the production injector never
|
|
23
23
|
// names this module, so there is nothing to strip and nothing to trust.
|
|
24
24
|
export const TOOLBOX_URL = '/shared/ui/toolbox/toolbox.js';
|
|
25
|
+
// The dev tools (shared/ui/devtools/): the runtime that runs a game's tools
|
|
26
|
+
// and talks to whatever draws them, the maker around the game or the
|
|
27
|
+
// overlay over it. Dev-only, like the toolbox. The overlay is named here and
|
|
28
|
+
// imported only when the runtime finds no page around the game to draw.
|
|
29
|
+
export const DEVTOOLS_URL = '/shared/ui/devtools/runtime.js';
|
|
30
|
+
export const DEVTOOLS_OVERLAY_URL = '/shared/ui/devtools/overlay/overlay.js';
|
|
31
|
+
// What a game's own tool files (`tools/<name>.tool.js`) import as
|
|
32
|
+
// `joystack/tools`. Dev pages only: a published page never loads a tool, so
|
|
33
|
+
// the production import map (V2_BARE_SPECIFIERS) has no such door.
|
|
34
|
+
export const DEVTOOLS_DOOR = { 'joystack/tools': '/shared/ui/devtools/devtools.js' };
|
|
25
35
|
// The laptop note (shared/ui/note/): the conversation listener a page carries
|
|
26
36
|
// when the dev server was started by a coding-agent session. Dev-only, like
|
|
27
37
|
// the toolbox, and on every page the creator opens, not only the game.
|
|
@@ -63,6 +73,11 @@ export const V2_IMPORTS = {
|
|
|
63
73
|
// three, mapped to the engine's own vendored same-origin copy so a three-backed
|
|
64
74
|
// game needs no import map of its own (kept in sync with V2_BARE_SPECIFIERS).
|
|
65
75
|
three: '/shared/ui/room/vendor/three.js',
|
|
76
|
+
// The three addons the engine vendors (the glTF loader and the skeleton
|
|
77
|
+
// utilities), under the specifier the engine's own model loader imports them
|
|
78
|
+
// by, so a declared `.glb` opens in a game's page. The published map names
|
|
79
|
+
// each file (V2_BARE_SPECIFIERS), since it hashes per file.
|
|
80
|
+
'three/addons/': '/shared/ui/room/vendor/three-addons/',
|
|
66
81
|
};
|
|
67
82
|
|
|
68
83
|
// One classic script setting a page global, and its old name when the
|
|
@@ -72,17 +87,26 @@ function setGlobal(scope, name, value) {
|
|
|
72
87
|
return `<script>${scope}.${name} = ${old ? `${scope}.${old} = ` : ''}${jsJson(value)};</script>`;
|
|
73
88
|
}
|
|
74
89
|
|
|
75
|
-
|
|
90
|
+
// The page's import map: the framework's doors for a v2 game, and
|
|
91
|
+
// `joystack/tools` when the game has tool files, which import it. A v1 page
|
|
92
|
+
// with tools gets a map of that door alone; a page with neither, none.
|
|
93
|
+
function importMap(v2, tools) {
|
|
94
|
+
if (!v2 && !tools) return '';
|
|
95
|
+
const imports = { ...(v2 ? withLegacyDoors(V2_IMPORTS) : {}), ...(tools ? DEVTOOLS_DOOR : {}) };
|
|
96
|
+
return `<script type="importmap">${jsJson({ imports })}</script>`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function injectHeadTags(html, slug, { identity = DEV_IDENTITY, platformUrl = PLATFORM_URL, toolbox = false, toolboxUrl = TOOLBOX_URL, devtools = false, gameTools = [], skeleton = null, shareHost = null } = {}) {
|
|
76
100
|
const parts = [];
|
|
101
|
+
const tools = devtools ? gameTools : [];
|
|
77
102
|
// A framework-v2 game resolves `joystack`/`joystack/client` through an import map and
|
|
78
103
|
// reads its lowered graph from globalThis.__JOYSTACK_SKELETON__. BOTH must precede
|
|
79
104
|
// any module script (the platform tag AND game.js): a browser ignores an import
|
|
80
105
|
// map once a module has begun executing, and startGame() reads the skeleton at
|
|
81
106
|
// module-eval time. So they go FIRST.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
107
|
+
const map = importMap(!!skeleton, tools.length > 0);
|
|
108
|
+
if (map) parts.push(map);
|
|
109
|
+
if (skeleton) parts.push(setGlobal('globalThis', '__JOYSTACK_SKELETON__', skeleton));
|
|
86
110
|
// A page served through `joystack dev --share`'s tunnel: the hostname it was
|
|
87
111
|
// shared on, which the engine's isSharedDevPage compares with the page's own
|
|
88
112
|
// before treating a public name as a dev page. Ahead of every module script,
|
|
@@ -102,6 +126,13 @@ export function injectHeadTags(html, slug, { identity = DEV_IDENTITY, platformUr
|
|
|
102
126
|
`<script type="module">import { ensureToolbox } from "${toolboxUrl}"; ensureToolbox();</script>`,
|
|
103
127
|
);
|
|
104
128
|
}
|
|
129
|
+
// Inject-if-present like the toolbox: only an engine that carries the
|
|
130
|
+
// runtime gets its tag.
|
|
131
|
+
if (devtools) {
|
|
132
|
+
parts.push(
|
|
133
|
+
`<script type="module">import { startDevtools } from "${DEVTOOLS_URL}"; startDevtools({ overlay: () => import("${DEVTOOLS_OVERLAY_URL}")${tools.length ? `, gameTools: ${jsJson(tools)}` : ''} });</script>`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
105
136
|
const inject = '\n ' + parts.join('\n ');
|
|
106
137
|
const m = /<head[^>]*>/i.exec(html);
|
|
107
138
|
if (m) return html.slice(0, m.index + m[0].length) + inject + html.slice(m.index + m[0].length);
|
package/lib/publish-checks.mjs
CHANGED
|
@@ -17,6 +17,9 @@ import { LEGACY_ROOT_DOOR_FILE, withLegacyDoors } from './legacy-names.mjs';
|
|
|
17
17
|
const SHARED_REF_RE = /["'(=]\s*\/(shared\/[A-Za-z0-9_@./-]+)/g;
|
|
18
18
|
const CODE_FILE_RE = /\.(js|mjs|html|css)$/i;
|
|
19
19
|
const ROOT_DOOR = 'joystack';
|
|
20
|
+
const TOOLS_DOOR = 'joystack/tools';
|
|
21
|
+
// What `joystack dev` runs as a tool: a `.tool.js` file directly in `tools/`.
|
|
22
|
+
const TOOL_FILE_RE = /^tools\/[^/]+\.tool\.js$/;
|
|
20
23
|
|
|
21
24
|
function decodeText(bytes) {
|
|
22
25
|
try {
|
|
@@ -72,4 +75,17 @@ export function checkGameCode(files, engine) {
|
|
|
72
75
|
for (const door of used) {
|
|
73
76
|
if (!doors[door]) throw new Error(`engine ${engine.version} does not ship the \`${door}\` import`);
|
|
74
77
|
}
|
|
78
|
+
checkToolsDoor(texts);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A game's dev tools are the one place `joystack/tools` resolves, and only on
|
|
82
|
+
// the pages `joystack dev` serves: game code importing it would load there
|
|
83
|
+
// and fail in the published page.
|
|
84
|
+
function checkToolsDoor(texts) {
|
|
85
|
+
for (const [path, text] of texts) {
|
|
86
|
+
if (!text || TOOL_FILE_RE.test(path) || !CODE_FILE_RE.test(path)) continue;
|
|
87
|
+
if (text.includes(`'${TOOLS_DOOR}'`) || text.includes(`"${TOOLS_DOOR}"`)) {
|
|
88
|
+
throw new Error(`${path} imports ${TOOLS_DOOR}, which only a dev tool file (tools/<name>.tool.js) may import: a published page has no such door`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
75
91
|
}
|
package/lib/static-server.mjs
CHANGED
|
@@ -13,11 +13,11 @@ import http from 'node:http';
|
|
|
13
13
|
import { connect as connectTcp } from 'node:net';
|
|
14
14
|
import { randomBytes } from 'node:crypto';
|
|
15
15
|
import { EventEmitter } from 'node:events';
|
|
16
|
-
import { readFileSync, statSync, existsSync, realpathSync } from 'node:fs';
|
|
16
|
+
import { readFileSync, readdirSync, statSync, existsSync, realpathSync } from 'node:fs';
|
|
17
17
|
import { extname, join, normalize, relative, sep } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { gunzipSync } from 'node:zlib';
|
|
20
|
-
import { injectHeadTags, injectNoteTag, rewriteHtmlScripts, rewriteJsImports, NOTE_URL, PLATFORM_URL, TOOLBOX_URL } from './inject.mjs';
|
|
20
|
+
import { injectHeadTags, injectNoteTag, rewriteHtmlScripts, rewriteJsImports, NOTE_URL, PLATFORM_URL, TOOLBOX_URL, DEVTOOLS_URL } from './inject.mjs';
|
|
21
21
|
import { createRelay, RELAY_PATH } from './relay/relay.mjs';
|
|
22
22
|
import { WHOAMI_PATH } from './ports.mjs';
|
|
23
23
|
import {
|
|
@@ -232,6 +232,16 @@ function sameOrigin(req, { requireOrigin = false, shareHost = null } = {}) {
|
|
|
232
232
|
return !!shareHost && isShareHost(req.headers.host, shareHost) && origin === `https://${req.headers.host}`;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
// Whether a game page gets the dev tools' runtime: when the engine carries
|
|
236
|
+
// it, and the page is served on this machine, or through a share link that
|
|
237
|
+
// belongs to the creator (the maker's live link, `shareIsOwner`), where the
|
|
238
|
+
// page around the game draws the tools. On the maker that is everyone the
|
|
239
|
+
// maker lets onto the live link, not only the game's owner. Never for a
|
|
240
|
+
// tester on `joystack dev --share`.
|
|
241
|
+
function devtoolsRide({ shared, shareIsOwner, carried }) {
|
|
242
|
+
return carried && (!shared || shareIsOwner);
|
|
243
|
+
}
|
|
244
|
+
|
|
235
245
|
// Whether a Host header names the shared tunnel hostname (with or without a port).
|
|
236
246
|
function isShareHost(header, shareHost) {
|
|
237
247
|
return !!shareHost && String(header ?? '').replace(/:\d+$/, '').toLowerCase() === shareHost;
|
|
@@ -389,6 +399,22 @@ export function createStaticServer({
|
|
|
389
399
|
return relative(realDir, real).split(sep).some((seg) => STATE_DIR_NAMES.includes(seg.toLowerCase()));
|
|
390
400
|
}
|
|
391
401
|
|
|
402
|
+
// The game's own dev tools: each `<name>.tool.js` directly in its `tools/`
|
|
403
|
+
// folder, as the page reaches them, in name order. Read on every page load,
|
|
404
|
+
// so a tool written since shows on the next reload.
|
|
405
|
+
function toolFiles(urlDir) {
|
|
406
|
+
const dir = resolveUrl(urlDir);
|
|
407
|
+
if (!dir) return [];
|
|
408
|
+
try {
|
|
409
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
410
|
+
.filter((e) => e.isFile() && e.name.endsWith('.tool.js'))
|
|
411
|
+
.map((e) => e.name).sort()
|
|
412
|
+
.map((name) => `${urlDir}/${encodeURIComponent(name)}`);
|
|
413
|
+
} catch {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
392
418
|
const reload = new EventEmitter();
|
|
393
419
|
reload.setMaxListeners(0);
|
|
394
420
|
// The build a new session is stamped with: the version's, or, serving the
|
|
@@ -442,9 +468,12 @@ export function createStaticServer({
|
|
|
442
468
|
// pinned engine gets no 404ing tag (it gains the toolbox at `joystack
|
|
443
469
|
// update`). Per-request, so a bundle swap mid-session is honoured.
|
|
444
470
|
const toolbox = !shared && !!resolveUrl(TOOLBOX_URL);
|
|
471
|
+
const devtools = devtoolsRide({ shared, shareIsOwner, carried: !!resolveUrl(DEVTOOLS_URL) });
|
|
445
472
|
html = injectHeadTags(html, m[1], {
|
|
446
473
|
...(tabIdentity ? { identity: tabIdentity } : {}),
|
|
447
474
|
toolbox,
|
|
475
|
+
devtools,
|
|
476
|
+
gameTools: devtools ? toolFiles(`/games/${m[1]}/tools`) : [],
|
|
448
477
|
skeleton: currentSkeleton,
|
|
449
478
|
...(shared ? { shareHost } : {}),
|
|
450
479
|
});
|
package/package.json
CHANGED