@jxsuite/server 4.5.0 → 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 +5 -5
- 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 +15 -5
- 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 +27 -27
- 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 +180 -68
- package/src/server.ts +47 -27
- 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/packages.ts
CHANGED
|
@@ -8,42 +8,23 @@
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { readFile, writeFile } from "node:fs/promises";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* comparison anyway to tell an upgrade from a project deliberately pinned ahead — is where it
|
|
25
|
-
* belongs. A backend that filtered here could only describe an up-to-date package as an absence.
|
|
26
|
-
*/
|
|
27
|
-
export interface PackageVersionInfo {
|
|
28
|
-
name: string;
|
|
29
|
-
/** The version range pinned in package.json (e.g. "^0.19.0"). */
|
|
30
|
-
current: string;
|
|
31
|
-
/** The newest version published for this package on the registry. */
|
|
32
|
-
latest: string;
|
|
33
|
-
dev?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Result of a package mutation that runs `bun install` (install / set-versions). */
|
|
37
|
-
export interface PackageOpResult {
|
|
38
|
-
ok: boolean;
|
|
39
|
-
/** Combined stdout/stderr from the bun invocation, surfaced to the user on failure. */
|
|
40
|
-
log?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
11
|
+
import type { PackageInfo, PackageOpResult, PackageVersionInfo } from "@jxsuite/protocol";
|
|
12
|
+
import type { JsonValue } from "@jxsuite/schema/types";
|
|
13
|
+
|
|
14
|
+
/* The package rows and operation result are the Studio wire contract (`packages*` routes), declared
|
|
15
|
+
once in @jxsuite/protocol; re-exported so this module's public names stay. Every
|
|
16
|
+
registry-range dependency gets a `PackageVersionInfo` row, behind or not: deciding whether a row
|
|
17
|
+
is an upgrade needs a semver comparison against the range's base, and the caller, which has to
|
|
18
|
+
make that comparison anyway to tell an upgrade from a project deliberately pinned ahead, is where
|
|
19
|
+
it belongs. A backend that filtered here could only describe an up-to-date package as an
|
|
20
|
+
absence. */
|
|
21
|
+
export type { PackageInfo, PackageOpResult, PackageVersionInfo } from "@jxsuite/protocol";
|
|
22
|
+
|
|
23
|
+
/** The manifest members this module edits; every other member is written back unchanged. */
|
|
43
24
|
interface PackageJson {
|
|
44
25
|
dependencies?: Record<string, string>;
|
|
45
26
|
devDependencies?: Record<string, string>;
|
|
46
|
-
[key: string]:
|
|
27
|
+
[key: string]: JsonValue | undefined;
|
|
47
28
|
}
|
|
48
29
|
|
|
49
30
|
/** Minimal fetch signature (injectable for tests; the global `fetch` satisfies it). */
|
|
@@ -72,6 +53,9 @@ async function readPackageJson(root: string): Promise<PackageJson | null> {
|
|
|
72
53
|
return null;
|
|
73
54
|
}
|
|
74
55
|
try {
|
|
56
|
+
/* SAFETY: package.json is the npm manifest `bun install` reads, an object whose
|
|
57
|
+
`dependencies`/`devDependencies` map package names to version-spec strings; text that is
|
|
58
|
+
not JSON throws into the catch below. */
|
|
75
59
|
return JSON.parse(await readFile(pkgPath, "utf8")) as PackageJson;
|
|
76
60
|
} catch {
|
|
77
61
|
return null;
|
|
@@ -120,7 +104,11 @@ async function runBun(args: string[], cwd: string): Promise<PackageOpResult> {
|
|
|
120
104
|
proc.exited,
|
|
121
105
|
]);
|
|
122
106
|
const log = `${stdout}${stderr}`.trim();
|
|
123
|
-
|
|
107
|
+
const result: PackageOpResult = { ok: exitCode === 0 };
|
|
108
|
+
if (log) {
|
|
109
|
+
result.log = log;
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
124
112
|
}
|
|
125
113
|
|
|
126
114
|
/** Run `bun install` in the project root. */
|
|
@@ -165,6 +153,8 @@ export async function fetchLatestVersion(
|
|
|
165
153
|
if (!res.ok) {
|
|
166
154
|
return null;
|
|
167
155
|
}
|
|
156
|
+
/* SAFETY: the registry's version endpoint answers that version's manifest, whose `version`
|
|
157
|
+
is a string; a body that is not JSON (or is `null`) throws into the catch, giving null. */
|
|
168
158
|
const data = (await res.json()) as { version?: string };
|
|
169
159
|
return data.version ?? null;
|
|
170
160
|
} catch {
|
|
@@ -228,6 +218,7 @@ export async function setPackageVersions(
|
|
|
228
218
|
}
|
|
229
219
|
let pkg: PackageJson;
|
|
230
220
|
try {
|
|
221
|
+
// SAFETY: same npm manifest contract as readPackageJson; unparseable text is caught below.
|
|
231
222
|
pkg = JSON.parse(await readFile(pkgPath, "utf8")) as PackageJson;
|
|
232
223
|
} catch (error) {
|
|
233
224
|
return { log: `Unparseable package.json: ${String(error)}`, ok: false };
|
|
@@ -239,8 +230,7 @@ export async function setPackageVersions(
|
|
|
239
230
|
pkg.dependencies[update.name] = update.version;
|
|
240
231
|
} else {
|
|
241
232
|
const bucket = update.dev ? "devDependencies" : "dependencies";
|
|
242
|
-
pkg[bucket] = { ...
|
|
243
|
-
(pkg[bucket] as Record<string, string>)[update.name] = update.version;
|
|
233
|
+
pkg[bucket] = { ...pkg[bucket], [update.name]: update.version };
|
|
244
234
|
}
|
|
245
235
|
}
|
|
246
236
|
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
package/src/problem.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { PROBLEM_MEDIA_TYPE, problemDetails } from "@jxsuite/protocol";
|
|
15
15
|
import type { ProblemTypeName } from "@jxsuite/protocol";
|
|
16
|
+
import type { JsonObject } from "@jxsuite/schema/types";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* A failure response for one occurrence of a declared problem type.
|
|
@@ -23,14 +24,10 @@ import type { ProblemTypeName } from "@jxsuite/protocol";
|
|
|
23
24
|
*
|
|
24
25
|
* @param {ProblemTypeName} name - A declared type from `PROBLEM_TYPES`
|
|
25
26
|
* @param {string} [detail] - What happened this time; the line a human reads
|
|
26
|
-
* @param {
|
|
27
|
+
* @param {JsonObject} [extensions] - Extension members this type documents
|
|
27
28
|
* @returns {Response}
|
|
28
29
|
*/
|
|
29
|
-
export function problem(
|
|
30
|
-
name: ProblemTypeName,
|
|
31
|
-
detail?: string,
|
|
32
|
-
extensions?: Record<string, unknown>,
|
|
33
|
-
): Response {
|
|
30
|
+
export function problem(name: ProblemTypeName, detail?: string, extensions?: JsonObject): Response {
|
|
34
31
|
const body = problemDetails(name, detail, extensions);
|
|
35
32
|
// The explicit Content-Type is the point: `Response.json` would send `application/json`, and
|
|
36
33
|
// RFC 9457 §3 is what tells a client this body is a problem rather than a result.
|
package/src/project-server.ts
CHANGED
|
@@ -43,11 +43,36 @@ import {
|
|
|
43
43
|
import { problem } from "./problem.ts";
|
|
44
44
|
import { createLoopbackAuthorizer, OAUTH_CALLBACK_PATH } from "./oauth-loopback.ts";
|
|
45
45
|
import type { LoopbackAuthorizer } from "./oauth-loopback.ts";
|
|
46
|
+
import type { FsEventBatch } from "./sse.ts";
|
|
47
|
+
import type { JsonPrimitive, JsonValue } from "@jxsuite/schema/types";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What a handler answers, sent as the reply frame's `result`: anything `JSON.stringify` encodes,
|
|
51
|
+
* with `undefined` sent as `null`. Objects are admitted by kind rather than by JSON shape because
|
|
52
|
+
* handlers answer with typed values, and TypeScript never treats an interface as a `JsonValue`.
|
|
53
|
+
*/
|
|
54
|
+
export type RpcResult = JsonPrimitive | object | undefined;
|
|
55
|
+
|
|
56
|
+
/** One RPC method: the request frame's decoded `params` in, the reply's `result` out. */
|
|
57
|
+
export type RpcHandler = (params: JsonValue | undefined) => Promise<RpcResult>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* What an unsolicited push carries as its `params`: JSON, or the filesystem batch the sidebar
|
|
61
|
+
* consumes (the same batch the dev server sends as its SSE `fs` event).
|
|
62
|
+
*/
|
|
63
|
+
export type RpcPushParams = JsonValue | FsEventBatch;
|
|
64
|
+
|
|
65
|
+
/** An RPC request frame as the shell sends it: the id the reply echoes, and what to dispatch. */
|
|
66
|
+
export interface RpcRequestFrame {
|
|
67
|
+
id: number;
|
|
68
|
+
method: string;
|
|
69
|
+
params?: JsonValue;
|
|
70
|
+
}
|
|
46
71
|
|
|
47
72
|
/** A resolved per-window session: its project root plus its RPC handler map. */
|
|
48
73
|
export interface ProjectServerSession {
|
|
49
74
|
projectRoot: string | null;
|
|
50
|
-
handlers: Record<string,
|
|
75
|
+
handlers: Record<string, RpcHandler>;
|
|
51
76
|
}
|
|
52
77
|
|
|
53
78
|
export interface CreateProjectServerOptions {
|
|
@@ -89,7 +114,7 @@ export interface ProjectServerHandle {
|
|
|
89
114
|
* @param winId Deliver only to sockets stashed with this window id; omit for every socket.
|
|
90
115
|
* @returns How many sockets the message reached — 0 means nothing is listening yet.
|
|
91
116
|
*/
|
|
92
|
-
push: (method: string, params?:
|
|
117
|
+
push: (method: string, params?: RpcPushParams, winId?: string | null) => number;
|
|
93
118
|
/**
|
|
94
119
|
* The RFC 8252 loopback authorization host. The desktop launcher drives it: `begin()` for the URL
|
|
95
120
|
* to open in the user's browser, then `await pending.code` and {@link exchangeCode}.
|
|
@@ -337,9 +362,14 @@ export function createProjectServer(options: CreateProjectServerOptions): Projec
|
|
|
337
362
|
sockets.delete(ws);
|
|
338
363
|
},
|
|
339
364
|
async message(ws, raw) {
|
|
340
|
-
let msg:
|
|
365
|
+
let msg: RpcRequestFrame;
|
|
341
366
|
try {
|
|
342
|
-
|
|
367
|
+
/* SAFETY: trust boundary. A binary frame decodes as UTF-8, exactly as JSON.parse's own
|
|
368
|
+
string coercion would, and the parse yields a JSON value. The only client on this
|
|
369
|
+
token-gated socket is the shell's RPC transport, which sends `{ id, method, params }`;
|
|
370
|
+
a frame that breaks that finds no handler under its `method` and is answered with an
|
|
371
|
+
error (a `null` frame throws in this callback, as it always has). */
|
|
372
|
+
msg = JSON.parse(raw.toString()) as RpcRequestFrame;
|
|
343
373
|
} catch {
|
|
344
374
|
ws.send(JSON.stringify({ error: "Invalid JSON", id: 0 }));
|
|
345
375
|
return;
|
package/src/refactor/apply.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { basename, dirname, extname, relative, resolve } from "node:path";
|
|
12
12
|
import { stat, writeFile } from "node:fs/promises";
|
|
13
|
+
import { isJsonObject, isString } from "@jxsuite/schema/guards";
|
|
13
14
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
14
15
|
import { isUnder } from "./paths.ts";
|
|
15
16
|
import { rewriteDocRefs, rewriteTagName } from "./refs.ts";
|
|
@@ -99,9 +100,9 @@ export async function applyRename(opts: ApplyRenameOptions): Promise<RenameRepor
|
|
|
99
100
|
if (!isDir) {
|
|
100
101
|
try {
|
|
101
102
|
const { doc } = await loadDoc(absTo, opts.registry);
|
|
102
|
-
const oldTag = (doc
|
|
103
|
+
const oldTag = isJsonObject(doc) ? doc.tagName : undefined;
|
|
103
104
|
const newTag = deriveTag(absTo);
|
|
104
|
-
if (
|
|
105
|
+
if (isString(oldTag) && oldTag.includes("-") && newTag !== oldTag) {
|
|
105
106
|
if (newTag.includes("-")) {
|
|
106
107
|
tagRename = { from: oldTag, to: newTag };
|
|
107
108
|
} else {
|
|
Binary file
|
|
@@ -5,26 +5,25 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { relative } from "node:path";
|
|
8
|
+
import type { FsEvent } from "@jxsuite/protocol";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
10
|
+
/* A filesystem change as broadcast to the studio shell is the protocol's `FsEvent`, declared once
|
|
11
|
+
there; `FsEventPayload` stays this module's public name for it. */
|
|
12
|
+
export type { FsEvent as FsEventPayload } from "@jxsuite/protocol";
|
|
13
|
+
|
|
14
|
+
const VALID: ReadonlySet<string> = new Set(["add", "change", "unlink", "addDir", "unlinkDir"]);
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** Whether chokidar's `eventType` is one of the five changes the sidebar is told about. */
|
|
17
|
+
function isFsEventType(eventType: string): eventType is FsEvent["type"] {
|
|
18
|
+
return VALID.has(eventType);
|
|
19
|
+
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Map one chokidar event to a payload, or null for events we ignore (`ready`/`raw`/`error`) or
|
|
20
23
|
* paths that escape the root.
|
|
21
24
|
*/
|
|
22
|
-
export function toFsEvent(
|
|
23
|
-
eventType
|
|
24
|
-
root: string,
|
|
25
|
-
changedPath: string,
|
|
26
|
-
): FsEventPayload | null {
|
|
27
|
-
if (!VALID.has(eventType)) {
|
|
25
|
+
export function toFsEvent(eventType: string, root: string, changedPath: string): FsEvent | null {
|
|
26
|
+
if (!isFsEventType(eventType)) {
|
|
28
27
|
return null;
|
|
29
28
|
}
|
|
30
29
|
const path = relative(root, changedPath).replaceAll("\\", "/");
|
|
@@ -34,7 +33,7 @@ export function toFsEvent(
|
|
|
34
33
|
return {
|
|
35
34
|
isDir: eventType === "addDir" || eventType === "unlinkDir",
|
|
36
35
|
path,
|
|
37
|
-
type: eventType
|
|
36
|
+
type: eventType,
|
|
38
37
|
};
|
|
39
38
|
}
|
|
40
39
|
|
|
@@ -42,7 +41,7 @@ export function toFsEvent(
|
|
|
42
41
|
* Fold one event into a buffer, collapsing redundant churn per path (last-write-wins, except
|
|
43
42
|
* add+unlink cancels, add+change stays add, and unlink+add becomes a re-add). Returns a new array.
|
|
44
43
|
*/
|
|
45
|
-
export function coalesceFsEvents(buffer:
|
|
44
|
+
export function coalesceFsEvents(buffer: FsEvent[], next: FsEvent): FsEvent[] {
|
|
46
45
|
const prev = buffer.find((e) => e.path === next.path);
|
|
47
46
|
if (!prev) {
|
|
48
47
|
return [...buffer, next];
|
package/src/refactor/paths.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
import { BUILD_LANES, projectPathsForSiteUrl, siteUrlForPath } from "@jxsuite/schema/asset-paths";
|
|
21
21
|
import type { AssetLane, AssetMount } from "@jxsuite/schema/asset-paths";
|
|
22
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
23
|
+
import type { JsonValue } from "@jxsuite/schema/types";
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* The lanes a rewritten site URL is re-emitted through, most-publishable first.
|
|
@@ -43,6 +45,12 @@ const AUTHORED_LANES: readonly AssetLane[] = [...BUILD_LANES, "root"];
|
|
|
43
45
|
*/
|
|
44
46
|
const READ_LANES: readonly AssetLane[] = ["mounts", "root", "public"];
|
|
45
47
|
|
|
48
|
+
/** A path-like value split at its first `?` or `#`: the path itself, and what trails it. */
|
|
49
|
+
export interface RefParts {
|
|
50
|
+
core: string;
|
|
51
|
+
suffix: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
/** Classification of a reference string value. */
|
|
47
55
|
export type RefClass =
|
|
48
56
|
| { kind: "none" }
|
|
@@ -51,7 +59,7 @@ export type RefClass =
|
|
|
51
59
|
| { kind: "path"; core: string; suffix: string; rooted: boolean };
|
|
52
60
|
|
|
53
61
|
/** Split a trailing `?query` or `#fragment` off a path-like value. */
|
|
54
|
-
export function splitQueryHash(value: string):
|
|
62
|
+
export function splitQueryHash(value: string): RefParts {
|
|
55
63
|
for (let i = 0; i < value.length; i += 1) {
|
|
56
64
|
const ch = value[i];
|
|
57
65
|
if (ch === "?" || ch === "#") {
|
|
@@ -68,8 +76,8 @@ export function splitQueryHash(value: string): { core: string; suffix: string }
|
|
|
68
76
|
* actual precision, so bare npm specifiers (which never resolve to the renamed file) are simply
|
|
69
77
|
* never matched.
|
|
70
78
|
*/
|
|
71
|
-
export function classifyRef(value:
|
|
72
|
-
if (
|
|
79
|
+
export function classifyRef(value: JsonValue | undefined): RefClass {
|
|
80
|
+
if (!isString(value) || value.length === 0) {
|
|
73
81
|
return { kind: "none" };
|
|
74
82
|
}
|
|
75
83
|
if (
|
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;
|