@jxsuite/server 4.4.5 → 4.5.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.
@@ -26,6 +26,8 @@
26
26
  * @docs studio/projects/pages-layouts-components
27
27
  */
28
28
 
29
+ import { isJsonObject, isString } from "@jxsuite/schema/guards";
30
+ import type { JsonObject, JsonValue } from "@jxsuite/schema/types";
29
31
  import { classifyRef, looksLikeFileRef, rewriteRef } from "./paths.ts";
30
32
  import type { RemapCtx } from "./paths.ts";
31
33
 
@@ -37,10 +39,16 @@ export interface RefChange {
37
39
  }
38
40
 
39
41
  export interface RewriteResult {
40
- doc: unknown;
42
+ doc: JsonValue;
41
43
  changes: RefChange[];
42
44
  }
43
45
 
46
+ /** The outcome of {@link rewriteTagName}: the same document, and how many nodes were renamed. */
47
+ export interface TagRewriteResult {
48
+ doc: JsonValue;
49
+ count: number;
50
+ }
51
+
44
52
  /**
45
53
  * One visit of a reference-bearing string.
46
54
  *
@@ -74,14 +82,14 @@ const PROSE_KEYS = new Set(["textContent", "innerHTML"]);
74
82
 
75
83
  /** Offer a scalar string reference under `key` to the visitor, writing back any replacement. */
76
84
  function visitScalar(
77
- container: Record<string, unknown>,
85
+ container: JsonObject,
78
86
  key: string,
79
87
  refType: string,
80
88
  visit: RefVisitor,
81
89
  rootRelativeBare: boolean,
82
90
  ): void {
83
91
  const value = container[key];
84
- if (typeof value !== "string") {
92
+ if (!isString(value)) {
85
93
  return;
86
94
  }
87
95
  const out = visit(value, refType, rootRelativeBare);
@@ -104,13 +112,13 @@ function visitScalar(
104
112
  * would.
105
113
  */
106
114
  function visitKeys(
107
- container: Record<string, unknown>,
115
+ container: JsonObject,
108
116
  refType: string,
109
117
  visit: RefVisitor,
110
118
  rootRelativeBare: boolean,
111
119
  ): void {
112
120
  const entries = Object.entries(container);
113
- const renamed = entries.map(([key, value]): [string, unknown] => {
121
+ const renamed = entries.map(([key, value]): [string, JsonValue] => {
114
122
  const out = visit(key, refType, rootRelativeBare);
115
123
  return [out === null ? key : out, value];
116
124
  });
@@ -126,9 +134,9 @@ function visitKeys(
126
134
  }
127
135
 
128
136
  /** Offer a string array member (e.g. an `$elements` entry) to the visitor. */
129
- function visitIndexed(arr: unknown[], index: number, refType: string, visit: RefVisitor): void {
137
+ function visitIndexed(arr: JsonValue[], index: number, refType: string, visit: RefVisitor): void {
130
138
  const value = arr[index];
131
- if (typeof value !== "string") {
139
+ if (!isString(value)) {
132
140
  return;
133
141
  }
134
142
  const out = visit(value, refType, false);
@@ -146,15 +154,14 @@ function visitUrls(value: string, visit: RefVisitor): string {
146
154
  }
147
155
 
148
156
  /** Walk a `style` value tree, visiting `url(...)` in every string it contains. */
149
- function walkStyle(node: unknown, visit: RefVisitor): void {
157
+ function walkStyle(node: JsonValue | undefined, visit: RefVisitor): void {
150
158
  if (Array.isArray(node)) {
151
- const arr = node as unknown[];
152
- for (let i = 0; i < arr.length; i += 1) {
153
- const item = arr[i];
154
- if (typeof item === "string") {
159
+ for (let i = 0; i < node.length; i += 1) {
160
+ const item = node[i];
161
+ if (isString(item)) {
155
162
  const out = visitUrls(item, visit);
156
163
  if (out !== item) {
157
- arr[i] = out;
164
+ node[i] = out;
158
165
  }
159
166
  } else {
160
167
  walkStyle(item, visit);
@@ -162,16 +169,15 @@ function walkStyle(node: unknown, visit: RefVisitor): void {
162
169
  }
163
170
  return;
164
171
  }
165
- if (!node || typeof node !== "object") {
172
+ if (!isJsonObject(node)) {
166
173
  return;
167
174
  }
168
- const obj = node as Record<string, unknown>;
169
- for (const key of Object.keys(obj)) {
170
- const value = obj[key];
171
- if (typeof value === "string") {
175
+ for (const key of Object.keys(node)) {
176
+ const value = node[key];
177
+ if (isString(value)) {
172
178
  const out = visitUrls(value, visit);
173
179
  if (out !== value) {
174
- obj[key] = out;
180
+ node[key] = out;
175
181
  }
176
182
  } else {
177
183
  walkStyle(value, visit);
@@ -185,56 +191,53 @@ function walkStyle(node: unknown, visit: RefVisitor): void {
185
191
  * This is the single definition of "what counts as a reference" in a Jx document. Mutates `doc`
186
192
  * only where the visitor asks it to.
187
193
  */
188
- export function walkDocRefs(node: unknown, visit: RefVisitor): void {
194
+ export function walkDocRefs(node: JsonValue | undefined, visit: RefVisitor): void {
189
195
  if (Array.isArray(node)) {
190
- for (const item of node as unknown[]) {
196
+ for (const item of node) {
191
197
  walkDocRefs(item, visit);
192
198
  }
193
199
  return;
194
200
  }
195
- if (!node || typeof node !== "object") {
201
+ if (!isJsonObject(node)) {
196
202
  return;
197
203
  }
198
- const obj = node as Record<string, unknown>;
199
- for (const key of Object.keys(obj)) {
200
- const value = obj[key];
204
+ for (const key of Object.keys(node)) {
205
+ const value = node[key];
201
206
  switch (key) {
202
207
  case "$ref": {
203
208
  // File-path $ref only; state-scope refs (#/…, $map/…) are filtered by classifyRef.
204
- visitScalar(obj, key, "$ref", visit, false);
209
+ visitScalar(node, key, "$ref", visit, false);
205
210
  break;
206
211
  }
207
212
  case "$layout": {
208
- visitScalar(obj, key, "$layout", visit, true);
213
+ visitScalar(node, key, "$layout", visit, true);
209
214
  break;
210
215
  }
211
216
  case "$src":
212
217
  case "$implementation": {
213
- visitScalar(obj, key, key, visit, false);
218
+ visitScalar(node, key, key, visit, false);
214
219
  break;
215
220
  }
216
221
  case "src":
217
222
  case "href": {
218
- visitScalar(obj, key, "attr", visit, false);
223
+ visitScalar(node, key, "attr", visit, false);
219
224
  break;
220
225
  }
221
226
  case "imports": {
222
- if (value && typeof value === "object" && !Array.isArray(value)) {
223
- const imp = value as Record<string, unknown>;
224
- for (const name of Object.keys(imp)) {
225
- visitScalar(imp, name, "imports", visit, false);
227
+ if (isJsonObject(value)) {
228
+ for (const name of Object.keys(value)) {
229
+ visitScalar(value, name, "imports", visit, false);
226
230
  }
227
231
  }
228
232
  break;
229
233
  }
230
234
  case "$elements": {
231
235
  if (Array.isArray(value)) {
232
- const els = value as unknown[];
233
- for (let i = 0; i < els.length; i += 1) {
234
- if (typeof els[i] === "string") {
235
- visitIndexed(els, i, "$elements", visit);
236
+ for (let i = 0; i < value.length; i += 1) {
237
+ if (isString(value[i])) {
238
+ visitIndexed(value, i, "$elements", visit);
236
239
  } else {
237
- walkDocRefs(els[i], visit);
240
+ walkDocRefs(value[i], visit);
238
241
  }
239
242
  }
240
243
  } else {
@@ -255,8 +258,8 @@ export function walkDocRefs(node: unknown, visit: RefVisitor): void {
255
258
  * renaming a file at the project root could rewrite an unrelated copy DESTINATION that
256
259
  * merely shared its name.
257
260
  */
258
- if (value && typeof value === "object" && !Array.isArray(value)) {
259
- visitKeys(value as Record<string, unknown>, "copy", visit, true);
261
+ if (isJsonObject(value)) {
262
+ visitKeys(value, "copy", visit, true);
260
263
  }
261
264
  break;
262
265
  }
@@ -272,7 +275,7 @@ export function walkDocRefs(node: unknown, visit: RefVisitor): void {
272
275
  * resolve-and-compare gate, which only matches a value resolving to the file or directory
273
276
  * in question. A remote `https://…` source is filtered by `classifyRef` before that.
274
277
  */
275
- visitScalar(obj, key, "source", visit, false);
278
+ visitScalar(node, key, "source", visit, false);
276
279
  break;
277
280
  }
278
281
  /*
@@ -292,17 +295,16 @@ export function walkDocRefs(node: unknown, visit: RefVisitor): void {
292
295
  * file in question.
293
296
  */
294
297
  default: {
295
- if (typeof value === "string") {
298
+ if (isString(value)) {
296
299
  if (!PROSE_KEYS.has(key) && looksLikeFileRef(value)) {
297
- visitScalar(obj, key, "path", visit, false);
300
+ visitScalar(node, key, "path", visit, false);
298
301
  }
299
302
  } else if (Array.isArray(value)) {
300
- const arr = value as unknown[];
301
- for (let i = 0; i < arr.length; i += 1) {
302
- const item = arr[i];
303
- if (typeof item === "string") {
303
+ for (let i = 0; i < value.length; i += 1) {
304
+ const item = value[i];
305
+ if (isString(item)) {
304
306
  if (!PROSE_KEYS.has(key) && looksLikeFileRef(item)) {
305
- visitIndexed(arr, i, "path", visit);
307
+ visitIndexed(value, i, "path", visit);
306
308
  }
307
309
  } else {
308
310
  walkDocRefs(item, visit);
@@ -317,7 +319,7 @@ export function walkDocRefs(node: unknown, visit: RefVisitor): void {
317
319
  }
318
320
 
319
321
  /** Rewrite every file-path reference in `doc` for a rename described by `ctx`. Mutates `doc`. */
320
- export function rewriteDocRefs(doc: unknown, ctx: RemapCtx): RewriteResult {
322
+ export function rewriteDocRefs(doc: JsonValue, ctx: RemapCtx): RewriteResult {
321
323
  const changes: RefChange[] = [];
322
324
  walkDocRefs(doc, (value, refType, rootRelativeBare) => {
323
325
  const cls = classifyRef(value);
@@ -339,29 +341,28 @@ export function rewriteDocRefs(doc: unknown, ctx: RemapCtx): RewriteResult {
339
341
  * null, they are only counted — the read side of the same traversal.
340
342
  */
341
343
  function walkTags(
342
- node: unknown,
344
+ node: JsonValue | undefined,
343
345
  oldTag: string,
344
346
  newTag: string | null,
345
347
  counter: { n: number },
346
348
  ): void {
347
349
  if (Array.isArray(node)) {
348
- for (const item of node as unknown[]) {
350
+ for (const item of node) {
349
351
  walkTags(item, oldTag, newTag, counter);
350
352
  }
351
353
  return;
352
354
  }
353
- if (!node || typeof node !== "object") {
355
+ if (!isJsonObject(node)) {
354
356
  return;
355
357
  }
356
- const obj = node as Record<string, unknown>;
357
- if (obj.tagName === oldTag) {
358
+ if (node.tagName === oldTag) {
358
359
  if (newTag !== null) {
359
- obj.tagName = newTag;
360
+ node.tagName = newTag;
360
361
  }
361
362
  counter.n += 1;
362
363
  }
363
- for (const key of Object.keys(obj)) {
364
- walkTags(obj[key], oldTag, newTag, counter);
364
+ for (const key of Object.keys(node)) {
365
+ walkTags(node[key], oldTag, newTag, counter);
365
366
  }
366
367
  }
367
368
 
@@ -371,11 +372,7 @@ function walkTags(
371
372
  * `$elements`, `cases`) are intentionally left to `rewriteDocRefs`, so the two passes compose.
372
373
  * Mutates `doc`.
373
374
  */
374
- export function rewriteTagName(
375
- doc: unknown,
376
- oldTag: string,
377
- newTag: string,
378
- ): { doc: unknown; count: number } {
375
+ export function rewriteTagName(doc: JsonValue, oldTag: string, newTag: string): TagRewriteResult {
379
376
  const counter = { n: 0 };
380
377
  if (oldTag !== newTag) {
381
378
  walkTags(doc, oldTag, newTag, counter);
@@ -389,7 +386,7 @@ export function rewriteTagName(
389
386
  * A component's OWN definition file answers 1 for its root node, which is why the usage query skips
390
387
  * the definition file rather than subtracting one — a component may legitimately nest itself.
391
388
  */
392
- export function countTagUses(doc: unknown, tag: string): number {
389
+ export function countTagUses(doc: JsonValue, tag: string): number {
393
390
  const counter = { n: 0 };
394
391
  walkTags(doc, tag, null, counter);
395
392
  return counter.n;
@@ -9,22 +9,17 @@
9
9
 
10
10
  import { extname } from "node:path";
11
11
  import { readFile } from "node:fs/promises";
12
- import type { FormatRegistry } from "@jxsuite/schema/format-registry";
12
+ import type { FormatRegistry, ValueEdit } from "@jxsuite/schema/format-registry";
13
+ import type { JsonValue } from "@jxsuite/schema/types";
13
14
 
14
15
  /** Normalise a path to forward slashes (Windows `path` returns backslashes). */
15
16
  export const fwd = (p: string) => p.replaceAll("\\", "/");
16
17
 
17
- /** One authored value to replace in a document's own source text. */
18
- export interface ValueEdit {
19
- from: string;
20
- to: string;
21
- }
22
-
23
18
  /** A parsed document and the two ways it might be written back. */
24
19
  export interface LoadedDoc {
25
- doc: unknown;
20
+ doc: JsonValue;
26
21
  /** Round-trips the whole document. Null when the format declares no `serialize`. */
27
- serialize: ((doc: unknown) => Promise<string>) | null;
22
+ serialize: ((doc: JsonValue) => Promise<string>) | null;
28
23
  /**
29
24
  * Replaces authored values in the file's own bytes, changing nothing else. Null when the format
30
25
  * declares no `rewrite`.
@@ -84,7 +79,8 @@ export async function loadDoc(fp: string, registry: FormatRegistry): Promise<Loa
84
79
  if (isJsonPath(fp)) {
85
80
  const trailingNewline = raw.endsWith("\n");
86
81
  return {
87
- doc: JSON.parse(raw) as unknown,
82
+ // SAFETY: JSON.parse without a reviver only ever yields a JSON value; its `any` says no more.
83
+ doc: JSON.parse(raw) as JsonValue,
88
84
  rewrite: null,
89
85
  serialize: (doc) =>
90
86
  Promise.resolve(JSON.stringify(doc, null, 2) + (trailingNewline ? "\n" : "")),
@@ -95,22 +91,20 @@ export async function loadDoc(fp: string, registry: FormatRegistry): Promise<Loa
95
91
  if (!parseEntry) {
96
92
  throw new Error(`No parser for "${ext}"`);
97
93
  }
98
- const doc = await parseEntry.call("parse", raw);
94
+ /* SAFETY: read as a JSON tree. A `parse` answer is a JxDocument or a ContentLoaderEntry[]
95
+ (specs/extensions.md §8), both JSON trees but for leaves such as a frontmatter Date. Every walk
96
+ over it in refs.ts re-checks each node with Array.isArray / isJsonObject / isString before
97
+ reading it, so such a leaf is skipped rather than misread. */
98
+ const doc = (await parseEntry.call("parse", raw)) as JsonValue;
99
99
  const serializeEntry = registry.byExtension(ext, "serialize");
100
100
  const rewriteEntry = registry.byExtension(ext, "rewrite");
101
101
  return {
102
102
  doc,
103
103
  rewrite: rewriteEntry
104
- ? async (edits) => {
105
- const out = await rewriteEntry.call("rewrite", raw, edits);
106
- return typeof out === "string" ? out : String(out);
107
- }
104
+ ? async (edits) => String(await rewriteEntry.call("rewrite", raw, edits))
108
105
  : null,
109
106
  serialize: serializeEntry
110
- ? async (d) => {
111
- const out = await serializeEntry.call("serialize", d);
112
- return typeof out === "string" ? out : String(out);
113
- }
107
+ ? async (d) => String(await serializeEntry.call("serialize", d))
114
108
  : null,
115
109
  };
116
110
  }
@@ -6,7 +6,8 @@
6
6
  * build/reload); both share the pure `toFsEvent`/`coalesceFsEvents` helpers.
7
7
  */
8
8
 
9
- import { watch as chokidarWatch } from "chokidar";
9
+ import { watch } from "chokidar";
10
+ import { errorMessage } from "@jxsuite/schema/parse";
10
11
  import { coalesceFsEvents, toFsEvent } from "./fs-events.ts";
11
12
  import { invalidateReferenceCache } from "./find-refs.ts";
12
13
  import { createWatchIgnore } from "../watch-policy.ts";
@@ -36,7 +37,7 @@ export function createFsWatcher(
36
37
  let buffer: FsEventPayload[] = [];
37
38
  let timer: ReturnType<typeof setTimeout> | null = null;
38
39
 
39
- const watcher = chokidarWatch(root, {
40
+ const watcher = watch(root, {
40
41
  /* Left on deliberately: a symlink inside a project is project content, and its events belong
41
42
  in the sidebar. It is the link pointing OUT of the root that walks the watcher off into the
42
43
  rest of the filesystem, and createWatchIgnore drops exactly those — along with the sockets
@@ -71,9 +72,8 @@ export function createFsWatcher(
71
72
  directory holding a few unix sockets took the whole launcher down rather than losing a few
72
73
  watches. The filter above means these should now be rare; the listener means they are never
73
74
  fatal when they are not. */
74
- watcher.on("error", (err) => {
75
- const error = err as NodeJS.ErrnoException;
76
- console.error(`[fs-watch] ${error.message ?? String(error)}`);
75
+ watcher.on("error", (cause) => {
76
+ console.error(`[fs-watch] ${errorMessage(cause)}`);
77
77
  });
78
78
 
79
79
  return {