castle-web-cli 0.4.116 → 0.4.118

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.
@@ -1,4 +1,4 @@
1
- import * as http from "http";
1
+ import * as http from 'http';
2
2
  export declare const IMPORT_API_PREFIX = "/__castle/import/";
3
3
  export interface ImportCandidate {
4
4
  deckId: string;
@@ -14,8 +14,8 @@ export interface ImportList {
14
14
  decks: ImportCandidate[];
15
15
  truncated: boolean;
16
16
  }
17
- export type ImportTab = "mine" | "saved" | "kits";
17
+ export type ImportTab = 'mine' | 'saved' | 'kits';
18
18
  export declare function listImportable(deckDir: string, tab: ImportTab): Promise<ImportList>;
19
19
  export declare function resolveDeckRef(deckDir: string, ref: string): Promise<ImportCandidate>;
20
20
  export declare function sourceFileCount(deckId: string): Promise<number | null>;
21
- export declare function handleImportApi(deckDir: string, req: http.IncomingMessage, res: http.ServerResponse, reqPath: string): boolean;
21
+ export declare function handleImportApi(deckDir: string, req: http.IncomingMessage, res: http.ServerResponse, reqPath: string, onDeckChanged?: () => void): boolean;
@@ -10,17 +10,17 @@
10
10
  // Nothing here goes through the deck iframe or the kit: the Files panel that
11
11
  // opens this picker is a builtin shell panel, and it has to keep working on a
12
12
  // `--kit none` deck that ships no editor app at all.
13
- import * as fs from "fs";
14
- import * as os from "os";
15
- import * as path from "path";
16
- import { nanoid } from "nanoid";
17
- import * as api from "./api.js";
18
- import * as config from "./config.js";
19
- import { runTar } from "./save-deck.js";
20
- import { readCastleJson } from "./castleJson.js";
21
- import { ImportError, addImportTo, importedDeckIds, parseDeckRef } from "./imports.js";
22
- import { errorMessage, readRequestBody, sendJson } from "./httpJson.js";
23
- export const IMPORT_API_PREFIX = "/__castle/import/";
13
+ import * as fs from 'fs';
14
+ import * as os from 'os';
15
+ import * as path from 'path';
16
+ import { nanoid } from 'nanoid';
17
+ import * as api from './api.js';
18
+ import * as config from './config.js';
19
+ import { runTar } from './save-deck.js';
20
+ import { readCastleJson } from './castleJson.js';
21
+ import { ImportError, addImportTo, importedDeckIds, parseDeckRef } from './imports.js';
22
+ import { errorMessage, readRequestBody, sendJson } from './httpJson.js';
23
+ export const IMPORT_API_PREFIX = '/__castle/import/';
24
24
  // Rows per list, and how many decks we are willing to ask the source question
25
25
  // about to fill one. An account can hold hundreds of decks and only some are
26
26
  // web decks, so the scan has to look past the first fifty -- but not forever.
@@ -32,24 +32,24 @@ const SOURCE_CHUNK = 100;
32
32
  const PLAYLIST_CAP = 10;
33
33
  // The bookmarks feed already covers this playlist; listing both would just
34
34
  // duplicate it.
35
- const BOOKMARKS_PLAYLIST_PREFIX = "bookmarks-";
35
+ const BOOKMARKS_PLAYLIST_PREFIX = 'bookmarks-';
36
36
  // The kits list, hardcoded. A playlist can't hold it: playlist feeds drop
37
37
  // unlisted decks, and a kit is published unlisted. Adding a kit means shipping
38
38
  // an update to this list.
39
39
  const KIT_DECK_IDS = [
40
- "ckRZGFW4iPrx", // physics-2d
40
+ 'ckRZGFW4iPrx', // physics-2d
41
41
  ];
42
42
  // Listing is several server round trips, and the picker is opened, closed and
43
43
  // reopened while someone decides. Cached briefly so that costs once.
44
44
  const LIST_TTL_MS = 60_000;
45
45
  const listCache = new Map();
46
46
  function asTab(value) {
47
- return value === "saved" || value === "kits" ? value : "mine";
47
+ return value === 'saved' || value === 'kits' ? value : 'mine';
48
48
  }
49
49
  function toCandidate(row, hasSource) {
50
50
  return {
51
51
  deckId: row.deckId,
52
- title: row.title?.trim() ? row.title.trim() : "Untitled",
52
+ title: row.title?.trim() ? row.title.trim() : 'Untitled',
53
53
  creator: row.creator?.username ?? null,
54
54
  imageUrl: row.initialCard?.backgroundImage?.smallUrl ?? null,
55
55
  parentCreator: row.parentDeck?.creator?.username ?? null,
@@ -101,7 +101,7 @@ async function filterImportable(rows) {
101
101
  // which of the two something landed in is not a distinction worth a tab.
102
102
  async function savedRows() {
103
103
  const userId = config.getUserId();
104
- const bookmarks = await api.feedDecks("bookmarks", LIST_LIMIT);
104
+ const bookmarks = await api.feedDecks('bookmarks', LIST_LIMIT);
105
105
  if (!userId)
106
106
  return bookmarks;
107
107
  const playlists = (await api.myPlaylists(userId, PLAYLIST_CAP * 2))
@@ -111,9 +111,9 @@ async function savedRows() {
111
111
  return [bookmarks, ...lists].flat();
112
112
  }
113
113
  async function tabRows(tab) {
114
- if (tab === "saved")
114
+ if (tab === 'saved')
115
115
  return savedRows();
116
- if (tab === "kits")
116
+ if (tab === 'kits')
117
117
  return api.deckRows(KIT_DECK_IDS);
118
118
  return api.myDecks();
119
119
  }
@@ -137,10 +137,10 @@ export async function listImportable(deckDir, tab) {
137
137
  export async function resolveDeckRef(deckDir, ref) {
138
138
  const deckId = parseDeckRef(ref);
139
139
  if (!deckId) {
140
- throw new ImportError("bad-deck-ref", `Not a deck id or a castle deck link: ${ref}`);
140
+ throw new ImportError('bad-deck-ref', `Not a deck id or a castle deck link: ${ref}`);
141
141
  }
142
142
  if (readCastleJson(deckDir)?.deckId === deckId) {
143
- throw new ImportError("self-import", `That is this deck -- a deck can't import itself.`);
143
+ throw new ImportError('self-import', `That is this deck -- a deck can't import itself.`);
144
144
  }
145
145
  const [rows, source] = await Promise.all([
146
146
  api.deckRows([deckId]),
@@ -148,7 +148,7 @@ export async function resolveDeckRef(deckDir, ref) {
148
148
  ]);
149
149
  const row = rows[0];
150
150
  if (!row)
151
- throw new ImportError("bad-deck-ref", `No deck ${deckId} on the server.`);
151
+ throw new ImportError('bad-deck-ref', `No deck ${deckId} on the server.`);
152
152
  return stampImported(deckDir, [toCandidate(row, source !== null)])[0];
153
153
  }
154
154
  // How many files a deck's source holds. Not on any list query, so it is counted
@@ -172,8 +172,8 @@ export async function sourceFileCount(deckId) {
172
172
  if (!res.ok)
173
173
  return null;
174
174
  fs.writeFileSync(tmpFile, Buffer.from(await res.arrayBuffer()));
175
- const listing = await runTar(["-tzf", tmpFile], { capture: true });
176
- const count = listing.split("\n").filter((line) => line && !line.endsWith("/")).length;
175
+ const listing = await runTar(['-tzf', tmpFile], { capture: true });
176
+ const count = listing.split('\n').filter((line) => line && !line.endsWith('/')).length;
177
177
  fileCountCache.set(key, count);
178
178
  return count;
179
179
  }
@@ -201,37 +201,45 @@ function sendResult(res, work) {
201
201
  sendJson(res, 500, { error: errorMessage(err) });
202
202
  });
203
203
  }
204
- export function handleImportApi(deckDir, req, res, reqPath) {
204
+ export function handleImportApi(deckDir, req, res, reqPath,
205
+ // Called after an import that changed what the page has to load. Adding a kit
206
+ // installs npm packages and can repoint the deck's entry, both under a serve
207
+ // that already optimized deps for the OLD deck -- the running page then holds
208
+ // a module graph in which react resolves to nothing, which surfaces as
209
+ // "Cannot read properties of null (reading 'useState')" until a hand reload.
210
+ onDeckChanged) {
205
211
  const action = reqPath.slice(IMPORT_API_PREFIX.length);
206
- const url = new URL(req.url ?? "/", "http://localhost");
207
- if (action === "list") {
208
- sendResult(res, () => listImportable(deckDir, asTab(url.searchParams.get("tab"))));
212
+ const url = new URL(req.url ?? '/', 'http://localhost');
213
+ if (action === 'list') {
214
+ sendResult(res, () => listImportable(deckDir, asTab(url.searchParams.get('tab'))));
209
215
  return true;
210
216
  }
211
- if (action === "resolve") {
212
- const ref = url.searchParams.get("ref") ?? "";
217
+ if (action === 'resolve') {
218
+ const ref = url.searchParams.get('ref') ?? '';
213
219
  sendResult(res, async () => ({ deck: await resolveDeckRef(deckDir, ref) }));
214
220
  return true;
215
221
  }
216
- if (action === "files") {
217
- const deckId = url.searchParams.get("deckId") ?? "";
222
+ if (action === 'files') {
223
+ const deckId = url.searchParams.get('deckId') ?? '';
218
224
  sendResult(res, async () => ({ fileCount: await sourceFileCount(deckId) }));
219
225
  return true;
220
226
  }
221
- if (action === "add") {
227
+ if (action === 'add') {
222
228
  sendResult(res, async () => {
223
229
  let body;
224
230
  try {
225
231
  body = JSON.parse(await readRequestBody(req));
226
232
  }
227
233
  catch {
228
- throw new ImportError("no-deck-ref", "Invalid JSON body.");
234
+ throw new ImportError('no-deck-ref', 'Invalid JSON body.');
229
235
  }
230
- const deckRef = typeof body.deckRef === "string" ? body.deckRef : "";
236
+ const deckRef = typeof body.deckRef === 'string' ? body.deckRef : '';
231
237
  const result = await addImportTo(deckDir, { deckRef });
232
238
  // The lists said which decks were importable; one of them just stopped
233
239
  // being a candidate (or changed version), so the cached answers are stale.
234
240
  listCache.clear();
241
+ if (result.dependencies.length > 0 || result.adoptedMain)
242
+ onDeckChanged?.();
235
243
  return result;
236
244
  });
237
245
  return true;
package/dist/init.js CHANGED
@@ -166,10 +166,14 @@ export function isPristineBareDeck(dir) {
166
166
  }
167
167
  }
168
168
  // Repoint a deck's entry at an imported deck's main module.
169
+ //
170
+ // Writes the SAME index.html `init --kit` writes, rather than swapping the bare
171
+ // stub's script tag: a kit mounts React into `<div id="root">`, which the bare
172
+ // page has no element for -- the result was a deck that loaded the kit and
173
+ // rendered nothing -- and the bare page also lacks the dark color-scheme
174
+ // declaration, so it painted white before the kit's CSS arrived.
169
175
  export function adoptImportEntry(deckDir, alias, mainFile) {
170
- const file = path.join(deckDir, 'index.html');
171
- const html = fs.readFileSync(file, 'utf8');
172
- fs.writeFileSync(file, html.replace(/<script type="module" src="[^"]*"><\/script>/, `<script type="module" src="/${IMPORTS_DIR}/${alias}/${mainFile}"></script>`), 'utf8');
176
+ writeDeckIndexHtml(deckDir, alias, path.basename(deckDir), mainFile);
173
177
  fs.rmSync(path.join(deckDir, 'game.js'), { force: true });
174
178
  }
175
179
  function scaffoldBare(projectDir) {
package/dist/serve.js CHANGED
@@ -16,12 +16,14 @@ function isPortFree(port) {
16
16
  return new Promise((resolve) => {
17
17
  const srv = net.createServer();
18
18
  srv.once('error', () => resolve(false));
19
- srv.listen(port, () => { srv.close(() => resolve(true)); });
19
+ srv.listen(port, () => {
20
+ srv.close(() => resolve(true));
21
+ });
20
22
  });
21
23
  }
22
24
  async function findFreePorts(startPort) {
23
25
  for (let p = startPort; p < startPort + 100; p += 2) {
24
- if (await isPortFree(p) && await isPortFree(p + 1)) {
26
+ if ((await isPortFree(p)) && (await isPortFree(p + 1))) {
25
27
  return { port: p, wsPort: p + 1 };
26
28
  }
27
29
  }
@@ -158,7 +160,9 @@ function readServeJson(projectDir) {
158
160
  try {
159
161
  const raw = fs.readFileSync(path.join(projectDir, '.castle', 'serve.json'), 'utf8');
160
162
  const info = JSON.parse(raw);
161
- if (typeof info.port !== 'number' || typeof info.wsPort !== 'number' || typeof info.pid !== 'number')
163
+ if (typeof info.port !== 'number' ||
164
+ typeof info.wsPort !== 'number' ||
165
+ typeof info.pid !== 'number')
162
166
  return null;
163
167
  return { port: info.port, wsPort: info.wsPort, pid: info.pid };
164
168
  }
@@ -275,6 +279,12 @@ export async function serve(dir, options = {}) {
275
279
  const ideServer = createIdeServer({
276
280
  deckDir: projectDir,
277
281
  deckLabel: path.basename(projectDir),
282
+ // Same closure the agent server gets: invalidate then reload every tab.
283
+ // `viteHolder` is read lazily because Vite is created further down.
284
+ restart: () => {
285
+ invalidateModuleCaches(viteHolder.vite);
286
+ broadcast({ type: 'restart' });
287
+ },
278
288
  });
279
289
  process.on('exit', () => ideServer.shutdown());
280
290
  // The agent panel backend: router conversation + background task agents,
@@ -298,7 +308,11 @@ export async function serve(dir, options = {}) {
298
308
  process.on('exit', () => agentServer.shutdown());
299
309
  const vite = await createServer({
300
310
  root: projectDir,
301
- plugins: [castlePlugin(wsPort, ideServer, agentServer), importsAliasPlugin(), sceneFilesPlugin()],
311
+ plugins: [
312
+ castlePlugin(wsPort, ideServer, agentServer),
313
+ importsAliasPlugin(),
314
+ sceneFilesPlugin(),
315
+ ],
302
316
  server: {
303
317
  port,
304
318
  strictPort: true,
@@ -482,7 +496,9 @@ function startWSServer(port, projectDir, logFile, screenshotsDir, viteHolder) {
482
496
  }
483
497
  // Nobody to ask -- say so now rather than leaving the CLI to sit out
484
498
  // its whole timeout for an answer that can't come.
485
- if (delivered === 0 && msg.type === 'import_list_request' && typeof msg.requestId === 'string') {
499
+ if (delivered === 0 &&
500
+ msg.type === 'import_list_request' &&
501
+ typeof msg.requestId === 'string') {
486
502
  forgetRequest(msg.requestId);
487
503
  ws.send(JSON.stringify({ type: 'import_list_response', requestId: msg.requestId }));
488
504
  }