castle-web-cli 0.4.133 → 0.4.134

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.
@@ -131,5 +131,5 @@
131
131
  "main": "main.jsx",
132
132
  "autoUpdateWhenImported": true,
133
133
  "title": "physics-2d",
134
- "publishedVersion": "2026-08-21T23:15:26.298Z"
134
+ "publishedVersion": "2026-08-21T23:53:35.620Z"
135
135
  }
@@ -70,7 +70,7 @@ async function readDeckFile(path) {
70
70
  // no serve, a race with a delete, or a declared type that is binary (an image
71
71
  // the deck claims) -- the serve flags those rather than mangling them, and an
72
72
  // image decoded as utf-8 is nothing.
73
- async function readDeclaredFile(file) {
73
+ export async function readDeclaredFile(file) {
74
74
  try {
75
75
  const res = await fetch(`/__castle/files/read?path=${encodeURIComponent(file)}`);
76
76
  if (!res.ok) return null;
@@ -82,7 +82,12 @@ async function readDeclaredFile(file) {
82
82
  }
83
83
  }
84
84
 
85
- async function hydrateDeclaredFiles(ensurePath) {
85
+ // `knownExts` / `knownFiles` are the CALLER's, because each kit globs a
86
+ // different set: physics-3d also knows `.pxmodel`, and a type one kit already
87
+ // handles is not a declared type that needs fetching. Returns the extensions
88
+ // found as well as the files, so the caller can teach its own change
89
+ // classifier about them.
90
+ export async function hydrateDeclaredFiles({ knownExts, knownFiles, ensurePath }) {
86
91
  let info;
87
92
  let listing;
88
93
  try {
@@ -97,11 +102,10 @@ async function hydrateDeclaredFiles(ensurePath) {
97
102
  const exts = (info.fileTypes ?? [])
98
103
  .filter((type) => type.data || (typeof type.editor === 'string' && type.editor !== 'kit'))
99
104
  .map((type) => type.ext)
100
- .filter((ext) => typeof ext === 'string' && ext && !DATA_EXTS.includes(ext));
105
+ .filter((ext) => typeof ext === 'string' && ext && !knownExts.includes(ext));
101
106
  if (exts.length === 0) return null;
102
- for (const ext of exts) declaredDataExts.add(ext);
103
107
  const paths = (listing.files ?? []).filter(
104
- (file) => initialFiles[file] === undefined && exts.some((ext) => file.endsWith(ext))
108
+ (file) => knownFiles[file] === undefined && exts.some((ext) => file.endsWith(ext))
105
109
  );
106
110
  const entries = await Promise.all(
107
111
  paths.map(async (file) => {
@@ -114,11 +118,11 @@ async function hydrateDeclaredFiles(ensurePath) {
114
118
  // of visiblePaths is not in it. The shell can still open that file by path,
115
119
  // and an editor handed '' for it would be back to the original bug -- so make
116
120
  // sure whatever was actually opened is present, curated or not.
117
- if (ensurePath && initialFiles[ensurePath] === undefined && extra[ensurePath] === undefined) {
121
+ if (ensurePath && knownFiles[ensurePath] === undefined && extra[ensurePath] === undefined) {
118
122
  const one = await readDeclaredFile(ensurePath);
119
123
  if (one !== null) extra[ensurePath] = one;
120
124
  }
121
- return Object.keys(extra).length ? extra : null;
125
+ return { extra, exts };
122
126
  }
123
127
 
124
128
  // Deck files as live state: starts from the build-time glob snapshot, fills in
@@ -142,11 +146,20 @@ export function useLiveDeckFiles({ shouldSkipPath, ensurePath, live = true } = {
142
146
 
143
147
  useEffect(() => {
144
148
  let alive = true;
145
- void hydrateDeclaredFiles(ensurePath).then((extra) => {
149
+ void hydrateDeclaredFiles({
150
+ knownExts: DATA_EXTS,
151
+ knownFiles: initialFiles,
152
+ ensurePath,
153
+ }).then((found) => {
146
154
  if (!alive) return;
147
- // `current` wins: by the time this lands the editor may already have made
148
- // an optimistic edit, and the disk copy behind it is stale.
149
- if (extra) setFiles((current) => ({ ...extra, ...current }));
155
+ if (found) {
156
+ for (const ext of found.exts) declaredDataExts.add(ext);
157
+ // `current` wins: by the time this lands the editor may already have
158
+ // made an optimistic edit, and the disk copy behind it is stale.
159
+ if (Object.keys(found.extra).length) {
160
+ setFiles((current) => ({ ...found.extra, ...current }));
161
+ }
162
+ }
150
163
  setHydrated(true);
151
164
  });
152
165
  return () => {
@@ -25,7 +25,8 @@
25
25
  "scenes/**",
26
26
  "blueprints/**",
27
27
  "behaviors/**",
28
- "assets/**"
28
+ "assets/**",
29
+ "theme.style"
29
30
  ],
30
31
  "fileTypes": [
31
32
  {
@@ -64,12 +65,12 @@
64
65
  "imports": {
65
66
  "castle.physics-2d": {
66
67
  "deckId": "ckRZGFW4iPrx",
67
- "version": "2026-08-14T21:20:31.152Z"
68
+ "version": "2026-08-21T23:52:56.888Z"
68
69
  }
69
70
  },
70
71
  "main": "main.jsx",
71
72
  "autoUpdateWhenImported": true,
72
73
  "deckId": "JH0SclbPVP0y",
73
74
  "cardId": "eXfAaUdbSU5A",
74
- "publishedVersion": "2026-08-21T23:15:28.646Z"
75
+ "publishedVersion": "2026-08-21T23:53:37.801Z"
75
76
  }
@@ -66,8 +66,9 @@ function descendantIds(parts, rootId) {
66
66
 
67
67
  export function PxModelEditor({ path }) {
68
68
  const saver = useFileSaver3D();
69
- const { files, setFiles, dataVersion } = useLiveDeckFiles3D({
69
+ const { files, setFiles, dataVersion, hydrated } = useLiveDeckFiles3D({
70
70
  shouldSkipPath: saver.hasPending,
71
+ ensurePath: path,
71
72
  });
72
73
  const text = files[path];
73
74
  useEffect(() => {
@@ -265,6 +266,16 @@ export function PxModelEditor({ path }) {
265
266
  );
266
267
  }
267
268
 
269
+ if (!hydrated && files[path] === undefined) {
270
+ return (
271
+ <div className={styles.panelBare}>
272
+ <MainEditor>
273
+ <EditorBody>loading…</EditorBody>
274
+ </MainEditor>
275
+ </div>
276
+ );
277
+ }
278
+
268
279
  return (
269
280
  <div className={styles.panelBare}>
270
281
  <MainEditor>
@@ -65,8 +65,9 @@ export function Scene3DEditor({ path }) {
65
65
  const stashKey = `scene3d-editor:${path}`;
66
66
  const [stash] = useState(() => takeReloadState(stashKey));
67
67
  const saver = useFileSaver3D();
68
- const { files, setFiles, dataVersion } = useLiveDeckFiles3D({
68
+ const { files, setFiles, dataVersion, hydrated } = useLiveDeckFiles3D({
69
69
  shouldSkipPath: saver.hasPending,
70
+ ensurePath: path,
70
71
  });
71
72
  const text = files[path] ?? '';
72
73
  const onChangeText = (next) => {
@@ -305,6 +306,16 @@ export function Scene3DEditor({ path }) {
305
306
  onSelectActorIds([]);
306
307
  };
307
308
 
309
+ if (!hydrated && files[path] === undefined) {
310
+ return (
311
+ <div className={styles.panelBare}>
312
+ <MainEditor>
313
+ <EditorBody>loading…</EditorBody>
314
+ </MainEditor>
315
+ </div>
316
+ );
317
+ }
318
+
308
319
  return (
309
320
  <div className={styles.panelBare}>
310
321
  <MainEditor>
@@ -8,6 +8,7 @@
8
8
  import { useEffect, useRef, useState } from 'react';
9
9
  import { onFilesChanged } from 'castle-web-sdk';
10
10
  import { initialFiles, initialMedia, isImageFile } from '@imports/castle.physics-2d/engine/files';
11
+ import { hydrateDeclaredFiles } from '@imports/castle.physics-2d/engine/liveReload';
11
12
  import { parseFull } from '@imports/castle.physics-2d/engine/pxart';
12
13
  import { imageArt } from '@imports/castle.physics-2d/engine/art';
13
14
 
@@ -31,8 +32,15 @@ export const initialFiles3D = {
31
32
  const DATA_EXTS = ['.scene', '.pxart', '.drawing', '.pxmodel'];
32
33
  const CODE_EXTS = ['.js', '.jsx', '.ts', '.tsx', '.css'];
33
34
 
35
+ // Extensions the DECK declared that this kit has never heard of, learned from
36
+ // the serve at mount. Module-level because the change classifier runs per
37
+ // event, with no state to thread through -- same shape as the imported kit's.
38
+ const declaredDataExts = new Set();
39
+
34
40
  export function isDataFile3D(path) {
35
- return DATA_EXTS.some((ext) => path.endsWith(ext));
41
+ if (DATA_EXTS.some((ext) => path.endsWith(ext))) return true;
42
+ for (const ext of declaredDataExts) if (path.endsWith(ext)) return true;
43
+ return false;
36
44
  }
37
45
 
38
46
  function needsContextReload(change) {
@@ -81,8 +89,12 @@ async function applyDataChanges(changes, setFiles, setDataVersion) {
81
89
  // A code change does nothing here, matching the imported hook: only a page
82
90
  // reload picks up new code, and throwing one at a panel someone is working in
83
91
  // loses their place, so the panel header offers it instead.
84
- export function useLiveDeckFiles3D({ shouldSkipPath } = {}) {
92
+ export function useLiveDeckFiles3D({ shouldSkipPath, ensurePath } = {}) {
85
93
  const [files, setFiles] = useState(initialFiles3D);
94
+ // False until the declared-type read has settled. An editor mounted before
95
+ // then would be handed '' for a file that exists, and an edit made in that
96
+ // window writes its empty document over the real one.
97
+ const [hydrated, setHydrated] = useState(false);
86
98
  // Bumped on every applied data change. The editors watch it to re-apply when
87
99
  // a file they REFERENCE changed -- their own `text` is unchanged then, so it
88
100
  // is the only signal that the drawing under an actor moved.
@@ -90,6 +102,30 @@ export function useLiveDeckFiles3D({ shouldSkipPath } = {}) {
90
102
  const skipRef = useRef(shouldSkipPath);
91
103
  skipRef.current = shouldSkipPath;
92
104
 
105
+ // Same gap, same fix as the imported kit: the glob above is a compile-time
106
+ // transform, so a type the DECK declares is invisible to it. `initialFiles3D`
107
+ // is what this kit already knows, models included.
108
+ useEffect(() => {
109
+ let alive = true;
110
+ void hydrateDeclaredFiles({
111
+ knownExts: DATA_EXTS,
112
+ knownFiles: initialFiles3D,
113
+ ensurePath,
114
+ }).then((found) => {
115
+ if (!alive) return;
116
+ if (found) {
117
+ for (const ext of found.exts) declaredDataExts.add(ext);
118
+ if (Object.keys(found.extra).length) {
119
+ setFiles((current) => ({ ...found.extra, ...current }));
120
+ }
121
+ }
122
+ setHydrated(true);
123
+ });
124
+ return () => {
125
+ alive = false;
126
+ };
127
+ }, [ensurePath]);
128
+
93
129
  useEffect(
94
130
  () =>
95
131
  onFilesChanged((event) => {
@@ -102,7 +138,7 @@ export function useLiveDeckFiles3D({ shouldSkipPath } = {}) {
102
138
  []
103
139
  );
104
140
 
105
- return { files, setFiles, dataVersion };
141
+ return { files, setFiles, dataVersion, hydrated };
106
142
  }
107
143
 
108
144
  // The sprites map with parsed pxarts CACHED BY TEXT: re-parsing every file on
@@ -0,0 +1,3 @@
1
+ {
2
+ "palette": "endesga-64"
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.133",
3
+ "version": "0.4.134",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"