@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.
- package/package.json +10 -10
- package/src/ai-api.ts +8 -4
- package/src/build.ts +6 -6
- package/src/code-api.ts +29 -16
- package/src/collab.ts +9 -5
- package/src/data-api.ts +190 -137
- package/src/dev-vars.ts +28 -4
- package/src/dev.ts +3 -0
- package/src/extension-catalog.ts +62 -32
- package/src/import-api.ts +95 -53
- package/src/jx-mounts.ts +30 -33
- package/src/live-preview.ts +16 -10
- package/src/oauth-loopback.ts +23 -18
- package/src/packages.ts +26 -36
- package/src/problem.ts +3 -6
- package/src/project-server.ts +34 -4
- package/src/refactor/apply.ts +3 -2
- package/src/refactor/find-refs.ts +0 -0
- package/src/refactor/fs-events.ts +14 -15
- package/src/refactor/paths.ts +11 -3
- package/src/refactor/refs.ts +60 -63
- package/src/refactor/scan.ts +13 -19
- package/src/refactor/watcher.ts +5 -5
- package/src/resolve.ts +235 -83
- package/src/server.ts +51 -29
- package/src/site-output.ts +11 -5
- package/src/sse.ts +16 -2
- package/src/studio-api.ts +264 -115
- package/src/types.ts +17 -7
- package/src/watch.ts +8 -6
package/src/refactor/refs.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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 (
|
|
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:
|
|
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,
|
|
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:
|
|
137
|
+
function visitIndexed(arr: JsonValue[], index: number, refType: string, visit: RefVisitor): void {
|
|
130
138
|
const value = arr[index];
|
|
131
|
-
if (
|
|
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:
|
|
157
|
+
function walkStyle(node: JsonValue | undefined, visit: RefVisitor): void {
|
|
150
158
|
if (Array.isArray(node)) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
172
|
+
if (!isJsonObject(node)) {
|
|
166
173
|
return;
|
|
167
174
|
}
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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:
|
|
194
|
+
export function walkDocRefs(node: JsonValue | undefined, visit: RefVisitor): void {
|
|
189
195
|
if (Array.isArray(node)) {
|
|
190
|
-
for (const item of node
|
|
196
|
+
for (const item of node) {
|
|
191
197
|
walkDocRefs(item, visit);
|
|
192
198
|
}
|
|
193
199
|
return;
|
|
194
200
|
}
|
|
195
|
-
if (!node
|
|
201
|
+
if (!isJsonObject(node)) {
|
|
196
202
|
return;
|
|
197
203
|
}
|
|
198
|
-
const
|
|
199
|
-
|
|
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(
|
|
209
|
+
visitScalar(node, key, "$ref", visit, false);
|
|
205
210
|
break;
|
|
206
211
|
}
|
|
207
212
|
case "$layout": {
|
|
208
|
-
visitScalar(
|
|
213
|
+
visitScalar(node, key, "$layout", visit, true);
|
|
209
214
|
break;
|
|
210
215
|
}
|
|
211
216
|
case "$src":
|
|
212
217
|
case "$implementation": {
|
|
213
|
-
visitScalar(
|
|
218
|
+
visitScalar(node, key, key, visit, false);
|
|
214
219
|
break;
|
|
215
220
|
}
|
|
216
221
|
case "src":
|
|
217
222
|
case "href": {
|
|
218
|
-
visitScalar(
|
|
223
|
+
visitScalar(node, key, "attr", visit, false);
|
|
219
224
|
break;
|
|
220
225
|
}
|
|
221
226
|
case "imports": {
|
|
222
|
-
if (
|
|
223
|
-
const
|
|
224
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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(
|
|
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 (
|
|
259
|
-
visitKeys(value
|
|
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(
|
|
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 (
|
|
298
|
+
if (isString(value)) {
|
|
296
299
|
if (!PROSE_KEYS.has(key) && looksLikeFileRef(value)) {
|
|
297
|
-
visitScalar(
|
|
300
|
+
visitScalar(node, key, "path", visit, false);
|
|
298
301
|
}
|
|
299
302
|
} else if (Array.isArray(value)) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
350
|
+
for (const item of node) {
|
|
349
351
|
walkTags(item, oldTag, newTag, counter);
|
|
350
352
|
}
|
|
351
353
|
return;
|
|
352
354
|
}
|
|
353
|
-
if (!node
|
|
355
|
+
if (!isJsonObject(node)) {
|
|
354
356
|
return;
|
|
355
357
|
}
|
|
356
|
-
|
|
357
|
-
if (obj.tagName === oldTag) {
|
|
358
|
+
if (node.tagName === oldTag) {
|
|
358
359
|
if (newTag !== null) {
|
|
359
|
-
|
|
360
|
+
node.tagName = newTag;
|
|
360
361
|
}
|
|
361
362
|
counter.n += 1;
|
|
362
363
|
}
|
|
363
|
-
for (const key of Object.keys(
|
|
364
|
-
walkTags(
|
|
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:
|
|
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;
|
package/src/refactor/scan.ts
CHANGED
|
@@ -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:
|
|
20
|
+
doc: JsonValue;
|
|
26
21
|
/** Round-trips the whole document. Null when the format declares no `serialize`. */
|
|
27
|
-
serialize: ((doc:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/refactor/watcher.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* build/reload); both share the pure `toFsEvent`/`coalesceFsEvents` helpers.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { watch
|
|
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 =
|
|
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", (
|
|
75
|
-
|
|
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 {
|