@jxsuite/server 4.5.0 → 6.0.0
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 +18 -18
- 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 +96 -54
- 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 +28 -38
- package/src/problem.ts +3 -6
- package/src/project-server.ts +39 -9
- 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 +19 -10
- 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 +269 -82
- package/src/server.ts +52 -28
- package/src/site-output.ts +11 -5
- package/src/sse.ts +16 -2
- package/src/studio-api.ts +389 -145
- package/src/types.ts +17 -7
- package/src/watch.ts +8 -6
package/src/oauth-loopback.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* string of its own and `Referrer-Policy: no-referrer`, so nothing carries it onward.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { isJsonObject, isNumber, isString } from "@jxsuite/schema/guards";
|
|
31
32
|
import { secretsMatch } from "./net-guard.ts";
|
|
32
33
|
|
|
33
34
|
/** The loopback redirect path this server hosts. */
|
|
@@ -345,29 +346,33 @@ export async function exchangeCode(params: {
|
|
|
345
346
|
headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
|
|
346
347
|
method: "POST",
|
|
347
348
|
});
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
error_description?: unknown;
|
|
352
|
-
expires_in?: unknown;
|
|
353
|
-
refresh_token?: unknown;
|
|
354
|
-
scope?: unknown;
|
|
355
|
-
token_type?: unknown;
|
|
356
|
-
} | null;
|
|
349
|
+
// A body that is not JSON reads as no payload, and is reported by status below.
|
|
350
|
+
const payload: unknown = await response.json().catch(() => null);
|
|
351
|
+
const fields = isJsonObject(payload) ? payload : null;
|
|
357
352
|
|
|
358
|
-
if (
|
|
359
|
-
const description =
|
|
360
|
-
|
|
353
|
+
if (fields && isString(fields.error)) {
|
|
354
|
+
const description = isString(fields.error_description)
|
|
355
|
+
? fields.error_description
|
|
356
|
+
: fields.error;
|
|
361
357
|
throw new Error(`The provider refused the sign-in: ${description}`);
|
|
362
358
|
}
|
|
363
|
-
if (!response.ok || !
|
|
359
|
+
if (!response.ok || !fields || !isString(fields.access_token)) {
|
|
364
360
|
throw new Error(`The token exchange failed (HTTP ${response.status}).`);
|
|
365
361
|
}
|
|
362
|
+
// RFC 6749 §5.1 makes these optional; each is kept only when the provider sent the right type.
|
|
363
|
+
const optional: Pick<TokenResult, "expiresIn" | "refreshToken" | "scope"> = {};
|
|
364
|
+
if (isNumber(fields.expires_in)) {
|
|
365
|
+
optional.expiresIn = fields.expires_in;
|
|
366
|
+
}
|
|
367
|
+
if (isString(fields.refresh_token)) {
|
|
368
|
+
optional.refreshToken = fields.refresh_token;
|
|
369
|
+
}
|
|
370
|
+
if (isString(fields.scope)) {
|
|
371
|
+
optional.scope = fields.scope;
|
|
372
|
+
}
|
|
366
373
|
return {
|
|
367
|
-
accessToken:
|
|
368
|
-
...
|
|
369
|
-
|
|
370
|
-
...(typeof payload.scope === "string" ? { scope: payload.scope } : {}),
|
|
371
|
-
tokenType: typeof payload.token_type === "string" ? payload.token_type : "bearer",
|
|
374
|
+
accessToken: fields.access_token,
|
|
375
|
+
...optional,
|
|
376
|
+
tokenType: isString(fields.token_type) ? fields.token_type : "bearer",
|
|
372
377
|
};
|
|
373
378
|
}
|
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;
|
|
@@ -104,8 +88,8 @@ export function dependenciesNeedInstall(root: string): boolean {
|
|
|
104
88
|
|
|
105
89
|
/**
|
|
106
90
|
* Path to the bun executable. Uses the bun running the current process (`process.execPath`) so a
|
|
107
|
-
* packaged desktop app spawns the bun bundled with its
|
|
108
|
-
*
|
|
91
|
+
* packaged desktop app spawns the bun bundled with its Electron distribution rather than relying on
|
|
92
|
+
* a system-wide `bun` in `$PATH` (which end users may not have). Falls back to a PATH lookup if
|
|
109
93
|
* `execPath` is somehow unset.
|
|
110
94
|
*/
|
|
111
95
|
export function bunExecutable(): string {
|
|
@@ -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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared project server factory. Generalizes the chromium variant's ad-hoc in-process Bun.serve
|
|
3
|
-
* into one createProjectServer() used by the chromium desktop launcher
|
|
4
|
-
* electrobun).
|
|
3
|
+
* into one createProjectServer() used by the chromium desktop launcher and the Electron hub.
|
|
5
4
|
*
|
|
6
5
|
* It serves three surfaces from a single loopback-bound Bun.serve:
|
|
7
6
|
*
|
|
@@ -43,18 +42,44 @@ import {
|
|
|
43
42
|
import { problem } from "./problem.ts";
|
|
44
43
|
import { createLoopbackAuthorizer, OAUTH_CALLBACK_PATH } from "./oauth-loopback.ts";
|
|
45
44
|
import type { LoopbackAuthorizer } from "./oauth-loopback.ts";
|
|
45
|
+
import type { FsEventBatch } from "./sse.ts";
|
|
46
|
+
import type { JsonPrimitive, JsonValue } from "@jxsuite/schema/types";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What a handler answers, sent as the reply frame's `result`: anything `JSON.stringify` encodes,
|
|
50
|
+
* with `undefined` sent as `null`. Objects are admitted by kind rather than by JSON shape because
|
|
51
|
+
* handlers answer with typed values, and TypeScript never treats an interface as a `JsonValue`.
|
|
52
|
+
*/
|
|
53
|
+
export type RpcResult = JsonPrimitive | object | undefined;
|
|
54
|
+
|
|
55
|
+
/** One RPC method: the request frame's decoded `params` in, the reply's `result` out. */
|
|
56
|
+
export type RpcHandler = (params: JsonValue | undefined) => Promise<RpcResult>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What an unsolicited push carries as its `params`: JSON, or the filesystem batch the sidebar
|
|
60
|
+
* consumes (the same batch the dev server sends as its SSE `fs` event).
|
|
61
|
+
*/
|
|
62
|
+
export type RpcPushParams = JsonValue | FsEventBatch;
|
|
63
|
+
|
|
64
|
+
/** An RPC request frame as the shell sends it: the id the reply echoes, and what to dispatch. */
|
|
65
|
+
export interface RpcRequestFrame {
|
|
66
|
+
id: number;
|
|
67
|
+
method: string;
|
|
68
|
+
params?: JsonValue;
|
|
69
|
+
}
|
|
46
70
|
|
|
47
71
|
/** A resolved per-window session: its project root plus its RPC handler map. */
|
|
48
72
|
export interface ProjectServerSession {
|
|
49
73
|
projectRoot: string | null;
|
|
50
|
-
handlers: Record<string,
|
|
74
|
+
handlers: Record<string, RpcHandler>;
|
|
51
75
|
}
|
|
52
76
|
|
|
53
77
|
export interface CreateProjectServerOptions {
|
|
54
78
|
/**
|
|
55
|
-
* Resolve the session for a given window id (the `win` query param / stashed socket data).
|
|
56
|
-
*
|
|
57
|
-
* A missing/unknown window returns `null`, which the routes treat as a 404
|
|
79
|
+
* Resolve the session for a given window id (the `win` query param / stashed socket data). the
|
|
80
|
+
* single-window launcher passes `() => defaultSession`; the Electron hub swaps it for a
|
|
81
|
+
* per-window lookup. A missing/unknown window returns `null`, which the routes treat as a 404
|
|
82
|
+
* fail-closed.
|
|
58
83
|
*/
|
|
59
84
|
resolveSession: (winId: string | null) => ProjectServerSession | null;
|
|
60
85
|
/** Absolute dir holding the studio shell + canvas.html + dist/. */
|
|
@@ -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 (
|
|
@@ -249,9 +257,12 @@ export interface RemapCtx {
|
|
|
249
257
|
* Recompute a single path reference for a rename, preserving its original style. Returns the new
|
|
250
258
|
* string, or null when nothing changes.
|
|
251
259
|
*
|
|
252
|
-
* `rootRelativeBare` makes
|
|
253
|
-
*
|
|
254
|
-
*
|
|
260
|
+
* `rootRelativeBare` makes the value resolve against the project root rather than the document
|
|
261
|
+
* directory, in every spelling: `./layouts/base.json` in `pages/blog/[slug].json` names
|
|
262
|
+
* `layouts/base.json`, because the compiler resolves `$layout` with `resolve(projectRoot, ref)`
|
|
263
|
+
* (layout-resolver.ts). Treating a `./` value as document-relative there missed every page's own
|
|
264
|
+
* `$layout`, so a renamed layout left them pointing at the old name. Used for `$layout` and for
|
|
265
|
+
* `build.copy` keys, which are project paths too.
|
|
255
266
|
*/
|
|
256
267
|
export function rewriteRef(
|
|
257
268
|
cls: { core: string; suffix: string; rooted: boolean },
|
|
@@ -262,10 +273,8 @@ export function rewriteRef(
|
|
|
262
273
|
if (rooted) {
|
|
263
274
|
return rewriteSiteUrl(core, suffix, ctx);
|
|
264
275
|
}
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
const baseOld = useRoot ? ctx.root : ctx.docOldDir;
|
|
268
|
-
const baseNew = useRoot ? ctx.root : ctx.docNewDir;
|
|
276
|
+
const baseOld = rootRelativeBare ? ctx.root : ctx.docOldDir;
|
|
277
|
+
const baseNew = rootRelativeBare ? ctx.root : ctx.docNewDir;
|
|
269
278
|
|
|
270
279
|
const targetOld = joinPosix(baseOld, core);
|
|
271
280
|
const targetNew = remapTarget(targetOld, ctx.oldAbs, ctx.newAbs);
|