@jxsuite/server 1.3.0 → 2.1.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 +19 -16
- package/src/ai-api.ts +30 -13
- package/src/build.ts +20 -17
- package/src/code-api.ts +2 -1
- package/src/collab.ts +35 -4
- package/src/data-api.ts +8 -1
- package/src/import-api.ts +5 -4
- package/src/jx-mounts.ts +11 -0
- package/src/net-guard.ts +188 -28
- package/src/oauth-loopback.ts +373 -0
- package/src/packages.ts +13 -2
- package/src/problem.ts +89 -0
- package/src/project-server.ts +84 -11
- package/src/refactor/apply.ts +3 -57
- package/src/refactor/find-refs.ts +0 -0
- package/src/refactor/index.ts +7 -0
- package/src/refactor/refs.ts +103 -63
- package/src/refactor/scan.ts +70 -0
- package/src/refactor/watcher.ts +4 -0
- package/src/resolve.ts +9 -10
- package/src/server.ts +76 -14
- package/src/site-output.ts +111 -0
- package/src/site-preview.ts +106 -0
- package/src/studio-api.ts +356 -86
- package/src/watch.ts +54 -3
package/src/refactor/apply.ts
CHANGED
|
@@ -9,16 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { basename, dirname, extname, relative, resolve } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { stat, writeFile } from "node:fs/promises";
|
|
13
13
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
14
14
|
import { isUnder } from "./paths.ts";
|
|
15
15
|
import { rewriteDocRefs, rewriteTagName } from "./refs.ts";
|
|
16
|
+
import { documentGlob, fwd, loadDoc, skipScanPath } from "./scan.ts";
|
|
16
17
|
import type { RefChange } from "./refs.ts";
|
|
17
18
|
import type { FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
18
19
|
|
|
19
|
-
/** Normalise a path to forward slashes. */
|
|
20
|
-
const fwd = (p: string) => p.replaceAll("\\", "/");
|
|
21
|
-
|
|
22
20
|
export interface FileChange {
|
|
23
21
|
path: string;
|
|
24
22
|
count: number;
|
|
@@ -51,20 +49,6 @@ export interface ApplyRenameOptions {
|
|
|
51
49
|
registry: FormatRegistry;
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
/** A parsed document plus the serializer that round-trips it back to disk. */
|
|
55
|
-
interface LoadedDoc {
|
|
56
|
-
doc: unknown;
|
|
57
|
-
serialize: ((doc: unknown) => Promise<string>) | null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const isJsonPath = (p: string) => p.endsWith(".json");
|
|
61
|
-
|
|
62
|
-
/** Skip vendored / build / agent directories (mirrors the component-discovery filter). */
|
|
63
|
-
function skip(match: string): boolean {
|
|
64
|
-
const f = fwd(match);
|
|
65
|
-
return match.includes("node_modules") || f.includes("dist/") || f.includes(".claude/");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
52
|
/** Derive a custom-element tag from a component filename (strips `.class.json` or the extension). */
|
|
69
53
|
export function deriveTag(absPath: string): string {
|
|
70
54
|
const base = basename(absPath);
|
|
@@ -75,44 +59,6 @@ export function deriveTag(absPath: string): string {
|
|
|
75
59
|
return ext ? base.slice(0, -ext.length) : base;
|
|
76
60
|
}
|
|
77
61
|
|
|
78
|
-
/** Read + parse a file, returning the doc and a matching serializer (null when none is available). */
|
|
79
|
-
async function loadDoc(fp: string, registry: FormatRegistry): Promise<LoadedDoc> {
|
|
80
|
-
const raw = await readFile(fp, "utf8");
|
|
81
|
-
if (isJsonPath(fp)) {
|
|
82
|
-
const trailingNewline = raw.endsWith("\n");
|
|
83
|
-
return {
|
|
84
|
-
doc: JSON.parse(raw) as unknown,
|
|
85
|
-
serialize: (doc) =>
|
|
86
|
-
Promise.resolve(JSON.stringify(doc, null, 2) + (trailingNewline ? "\n" : "")),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
const ext = extname(fp);
|
|
90
|
-
const parseEntry = registry.byExtension(ext, "parse");
|
|
91
|
-
if (!parseEntry) {
|
|
92
|
-
throw new Error(`No parser for "${ext}"`);
|
|
93
|
-
}
|
|
94
|
-
const doc = await parseEntry.call("parse", raw);
|
|
95
|
-
const serializeEntry = registry.byExtension(ext, "serialize");
|
|
96
|
-
return {
|
|
97
|
-
doc,
|
|
98
|
-
serialize: serializeEntry
|
|
99
|
-
? async (d) => {
|
|
100
|
-
const out = await serializeEntry.call("serialize", d);
|
|
101
|
-
return typeof out === "string" ? out : String(out);
|
|
102
|
-
}
|
|
103
|
-
: null,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Build the recursive glob covering JSON plus every registered document/content extension. */
|
|
108
|
-
function documentGlob(registry: FormatRegistry) {
|
|
109
|
-
const exts = new Set(["json"]);
|
|
110
|
-
for (const ext of registry.documentExtensions()) {
|
|
111
|
-
exts.add(ext.replace(/^\./, ""));
|
|
112
|
-
}
|
|
113
|
-
return new Bun.Glob(`**/*.{${[...exts].join(",")}}`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
62
|
/**
|
|
117
63
|
* Rewrite references (and, for components, the tag) across the project after a rename. The caller
|
|
118
64
|
* must have already moved the file on disk.
|
|
@@ -162,7 +108,7 @@ export async function applyRename(opts: ApplyRenameOptions): Promise<RenameRepor
|
|
|
162
108
|
let tagRefsUpdated = 0;
|
|
163
109
|
|
|
164
110
|
for await (const match of documentGlob(opts.registry).scan({ cwd: opts.root, dot: false })) {
|
|
165
|
-
if (
|
|
111
|
+
if (skipScanPath(match)) {
|
|
166
112
|
continue;
|
|
167
113
|
}
|
|
168
114
|
const fp = fwd(resolve(opts.root, match));
|
|
Binary file
|
package/src/refactor/index.ts
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
export { applyRename, deriveTag } from "./apply.ts";
|
|
7
7
|
export type { ApplyRenameOptions, FileChange, RenameReport } from "./apply.ts";
|
|
8
|
+
export { findReferences, invalidateReferenceCache } from "./find-refs.ts";
|
|
9
|
+
export type {
|
|
10
|
+
FindReferencesOptions,
|
|
11
|
+
ReferenceFile,
|
|
12
|
+
ReferenceHit,
|
|
13
|
+
ReferencesResult,
|
|
14
|
+
} from "./find-refs.ts";
|
|
8
15
|
export { coalesceFsEvents, toFsEvent } from "./fs-events.ts";
|
|
9
16
|
export type { FsEventPayload } from "./fs-events.ts";
|
|
10
17
|
export { createFsWatcher } from "./watcher.ts";
|
package/src/refactor/refs.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Refs.ts — pure document reference engine
|
|
2
|
+
* Refs.ts — pure document reference engine, shared by the rename refactor and the usage query.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
4
|
+
* One structure-aware walk, two directions. {@link walkDocRefs} dispatches by JSON key and hands
|
|
5
|
+
* every reference-bearing string to a visitor; whatever the visitor returns is written back, so a
|
|
6
|
+
* visitor that always returns `null` is a pure reader. Three passes are built on it:
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* - `rewriteDocRefs` — recompute every file-path reference across a rename (Pillar B).
|
|
9
|
+
* - `rewriteTagName` — rename a custom-element tag wherever it is used (Pillar C).
|
|
10
|
+
* - `countTagUses` — the same tag walk, counting instead of writing (the usage query).
|
|
11
|
+
*
|
|
12
|
+
* The walk dispatches by JSON key rather than searching strings, and mutates the passed document in
|
|
13
|
+
* place — callers parse a fresh object per file, so this is safe. Precision comes from
|
|
14
|
+
* `rewriteRef`'s resolve-and-compare gate in paths.ts: a reference is only rewritten (or counted as
|
|
15
|
+
* a usage) when it actually resolves to the file in question.
|
|
16
|
+
*
|
|
17
|
+
* The visitor seam is what stops the read side from being a second, drifting copy of the key list.
|
|
18
|
+
* A new reference-bearing key added below is a key the usage count sees on the same commit.
|
|
12
19
|
*/
|
|
13
20
|
|
|
14
21
|
import { classifyRef, rewriteRef } from "./paths.ts";
|
|
@@ -26,79 +33,74 @@ export interface RewriteResult {
|
|
|
26
33
|
changes: RefChange[];
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* One visit of a reference-bearing string.
|
|
38
|
+
*
|
|
39
|
+
* @param value — the raw authored reference, exactly as written.
|
|
40
|
+
* @param refType — which key carried it (`$ref`, `$layout`, `attr`, `imports`, `$elements`, `url`).
|
|
41
|
+
* @param rootRelativeBare — whether a bare value resolves against the project root (`$layout`).
|
|
42
|
+
* @returns A replacement string to write back, or `null` to leave the document untouched.
|
|
43
|
+
*/
|
|
44
|
+
export type RefVisitor = (
|
|
45
|
+
value: string,
|
|
46
|
+
refType: string,
|
|
47
|
+
rootRelativeBare: boolean,
|
|
48
|
+
) => string | null;
|
|
49
|
+
|
|
29
50
|
/** Matches `url(...)` targets inside a CSS string value, capturing the optional quote. */
|
|
30
51
|
const URL_RE = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g;
|
|
31
52
|
|
|
32
|
-
/**
|
|
33
|
-
function
|
|
53
|
+
/** Offer a scalar string reference under `key` to the visitor, writing back any replacement. */
|
|
54
|
+
function visitScalar(
|
|
34
55
|
container: Record<string, unknown>,
|
|
35
56
|
key: string,
|
|
36
57
|
refType: string,
|
|
37
|
-
|
|
38
|
-
changes: RefChange[],
|
|
58
|
+
visit: RefVisitor,
|
|
39
59
|
rootRelativeBare: boolean,
|
|
40
60
|
): void {
|
|
41
61
|
const value = container[key];
|
|
42
|
-
|
|
43
|
-
if (cls.kind !== "path") {
|
|
62
|
+
if (typeof value !== "string") {
|
|
44
63
|
return;
|
|
45
64
|
}
|
|
46
|
-
const out =
|
|
65
|
+
const out = visit(value, refType, rootRelativeBare);
|
|
47
66
|
if (out !== null) {
|
|
48
|
-
changes.push({ from: value as string, refType, to: out });
|
|
49
67
|
container[key] = out;
|
|
50
68
|
}
|
|
51
69
|
}
|
|
52
70
|
|
|
53
|
-
/**
|
|
54
|
-
function
|
|
55
|
-
arr: unknown[],
|
|
56
|
-
index: number,
|
|
57
|
-
refType: string,
|
|
58
|
-
ctx: RemapCtx,
|
|
59
|
-
changes: RefChange[],
|
|
60
|
-
): void {
|
|
71
|
+
/** Offer a string array member (e.g. an `$elements` entry) to the visitor. */
|
|
72
|
+
function visitIndexed(arr: unknown[], index: number, refType: string, visit: RefVisitor): void {
|
|
61
73
|
const value = arr[index];
|
|
62
|
-
|
|
63
|
-
if (cls.kind !== "path") {
|
|
74
|
+
if (typeof value !== "string") {
|
|
64
75
|
return;
|
|
65
76
|
}
|
|
66
|
-
const out =
|
|
77
|
+
const out = visit(value, refType, false);
|
|
67
78
|
if (out !== null) {
|
|
68
|
-
changes.push({ from: value as string, refType, to: out });
|
|
69
79
|
arr[index] = out;
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
|
|
73
|
-
/**
|
|
74
|
-
function
|
|
83
|
+
/** Offer every `url(...)` target inside a CSS string to the visitor. */
|
|
84
|
+
function visitUrls(value: string, visit: RefVisitor): string {
|
|
75
85
|
return value.replaceAll(URL_RE, (match, quote: string, inner: string) => {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
return match;
|
|
79
|
-
}
|
|
80
|
-
const out = rewriteRef(cls, ctx, false);
|
|
81
|
-
if (out === null) {
|
|
82
|
-
return match;
|
|
83
|
-
}
|
|
84
|
-
changes.push({ from: inner.trim(), refType: "url", to: out });
|
|
85
|
-
return `url(${quote}${out}${quote})`;
|
|
86
|
+
const out = visit(inner.trim(), "url", false);
|
|
87
|
+
return out === null ? match : `url(${quote}${out}${quote})`;
|
|
86
88
|
});
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
/** Walk a `style` value tree,
|
|
90
|
-
function walkStyle(node: unknown,
|
|
91
|
+
/** Walk a `style` value tree, visiting `url(...)` in every string it contains. */
|
|
92
|
+
function walkStyle(node: unknown, visit: RefVisitor): void {
|
|
91
93
|
if (Array.isArray(node)) {
|
|
92
94
|
const arr = node as unknown[];
|
|
93
95
|
for (let i = 0; i < arr.length; i += 1) {
|
|
94
96
|
const item = arr[i];
|
|
95
97
|
if (typeof item === "string") {
|
|
96
|
-
const out =
|
|
98
|
+
const out = visitUrls(item, visit);
|
|
97
99
|
if (out !== item) {
|
|
98
100
|
arr[i] = out;
|
|
99
101
|
}
|
|
100
102
|
} else {
|
|
101
|
-
walkStyle(item,
|
|
103
|
+
walkStyle(item, visit);
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
return;
|
|
@@ -110,21 +112,26 @@ function walkStyle(node: unknown, ctx: RemapCtx, changes: RefChange[]): void {
|
|
|
110
112
|
for (const key of Object.keys(obj)) {
|
|
111
113
|
const value = obj[key];
|
|
112
114
|
if (typeof value === "string") {
|
|
113
|
-
const out =
|
|
115
|
+
const out = visitUrls(value, visit);
|
|
114
116
|
if (out !== value) {
|
|
115
117
|
obj[key] = out;
|
|
116
118
|
}
|
|
117
119
|
} else {
|
|
118
|
-
walkStyle(value,
|
|
120
|
+
walkStyle(value, visit);
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Recursive document walk dispatching each reference-bearing key to `visit`.
|
|
127
|
+
*
|
|
128
|
+
* This is the single definition of "what counts as a reference" in a Jx document. Mutates `doc`
|
|
129
|
+
* only where the visitor asks it to.
|
|
130
|
+
*/
|
|
131
|
+
export function walkDocRefs(node: unknown, visit: RefVisitor): void {
|
|
125
132
|
if (Array.isArray(node)) {
|
|
126
133
|
for (const item of node as unknown[]) {
|
|
127
|
-
|
|
134
|
+
walkDocRefs(item, visit);
|
|
128
135
|
}
|
|
129
136
|
return;
|
|
130
137
|
}
|
|
@@ -137,28 +144,28 @@ function walk(node: unknown, ctx: RemapCtx, changes: RefChange[]): void {
|
|
|
137
144
|
switch (key) {
|
|
138
145
|
case "$ref": {
|
|
139
146
|
// File-path $ref only; state-scope refs (#/…, $map/…) are filtered by classifyRef.
|
|
140
|
-
|
|
147
|
+
visitScalar(obj, key, "$ref", visit, false);
|
|
141
148
|
break;
|
|
142
149
|
}
|
|
143
150
|
case "$layout": {
|
|
144
|
-
|
|
151
|
+
visitScalar(obj, key, "$layout", visit, true);
|
|
145
152
|
break;
|
|
146
153
|
}
|
|
147
154
|
case "$src":
|
|
148
155
|
case "$implementation": {
|
|
149
|
-
|
|
156
|
+
visitScalar(obj, key, key, visit, false);
|
|
150
157
|
break;
|
|
151
158
|
}
|
|
152
159
|
case "src":
|
|
153
160
|
case "href": {
|
|
154
|
-
|
|
161
|
+
visitScalar(obj, key, "attr", visit, false);
|
|
155
162
|
break;
|
|
156
163
|
}
|
|
157
164
|
case "imports": {
|
|
158
165
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
159
166
|
const imp = value as Record<string, unknown>;
|
|
160
167
|
for (const name of Object.keys(imp)) {
|
|
161
|
-
|
|
168
|
+
visitScalar(imp, name, "imports", visit, false);
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
171
|
break;
|
|
@@ -168,22 +175,22 @@ function walk(node: unknown, ctx: RemapCtx, changes: RefChange[]): void {
|
|
|
168
175
|
const els = value as unknown[];
|
|
169
176
|
for (let i = 0; i < els.length; i += 1) {
|
|
170
177
|
if (typeof els[i] === "string") {
|
|
171
|
-
|
|
178
|
+
visitIndexed(els, i, "$elements", visit);
|
|
172
179
|
} else {
|
|
173
|
-
|
|
180
|
+
walkDocRefs(els[i], visit);
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
} else {
|
|
177
|
-
|
|
184
|
+
walkDocRefs(value, visit);
|
|
178
185
|
}
|
|
179
186
|
break;
|
|
180
187
|
}
|
|
181
188
|
case "style": {
|
|
182
|
-
walkStyle(value,
|
|
189
|
+
walkStyle(value, visit);
|
|
183
190
|
break;
|
|
184
191
|
}
|
|
185
192
|
default: {
|
|
186
|
-
|
|
193
|
+
walkDocRefs(value, visit);
|
|
187
194
|
}
|
|
188
195
|
}
|
|
189
196
|
}
|
|
@@ -192,12 +199,31 @@ function walk(node: unknown, ctx: RemapCtx, changes: RefChange[]): void {
|
|
|
192
199
|
/** Rewrite every file-path reference in `doc` for a rename described by `ctx`. Mutates `doc`. */
|
|
193
200
|
export function rewriteDocRefs(doc: unknown, ctx: RemapCtx): RewriteResult {
|
|
194
201
|
const changes: RefChange[] = [];
|
|
195
|
-
|
|
202
|
+
walkDocRefs(doc, (value, refType, rootRelativeBare) => {
|
|
203
|
+
const cls = classifyRef(value);
|
|
204
|
+
if (cls.kind !== "path") {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const out = rewriteRef(cls, ctx, rootRelativeBare);
|
|
208
|
+
if (out === null) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
changes.push({ from: value, refType, to: out });
|
|
212
|
+
return out;
|
|
213
|
+
});
|
|
196
214
|
return { changes, doc };
|
|
197
215
|
}
|
|
198
216
|
|
|
199
|
-
/**
|
|
200
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Recursive tag walk. With `newTag` set, every `tagName === oldTag` is rewritten; with `newTag`
|
|
219
|
+
* null, they are only counted — the read side of the same traversal.
|
|
220
|
+
*/
|
|
221
|
+
function walkTags(
|
|
222
|
+
node: unknown,
|
|
223
|
+
oldTag: string,
|
|
224
|
+
newTag: string | null,
|
|
225
|
+
counter: { n: number },
|
|
226
|
+
): void {
|
|
201
227
|
if (Array.isArray(node)) {
|
|
202
228
|
for (const item of node as unknown[]) {
|
|
203
229
|
walkTags(item, oldTag, newTag, counter);
|
|
@@ -209,7 +235,9 @@ function walkTags(node: unknown, oldTag: string, newTag: string, counter: { n: n
|
|
|
209
235
|
}
|
|
210
236
|
const obj = node as Record<string, unknown>;
|
|
211
237
|
if (obj.tagName === oldTag) {
|
|
212
|
-
|
|
238
|
+
if (newTag !== null) {
|
|
239
|
+
obj.tagName = newTag;
|
|
240
|
+
}
|
|
213
241
|
counter.n += 1;
|
|
214
242
|
}
|
|
215
243
|
for (const key of Object.keys(obj)) {
|
|
@@ -234,3 +262,15 @@ export function rewriteTagName(
|
|
|
234
262
|
}
|
|
235
263
|
return { count: counter.n, doc };
|
|
236
264
|
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* How many nodes in `doc` carry `tagName === tag`. Read-only; `doc` is never touched.
|
|
268
|
+
*
|
|
269
|
+
* A component's OWN definition file answers 1 for its root node, which is why the usage query skips
|
|
270
|
+
* the definition file rather than subtracting one — a component may legitimately nest itself.
|
|
271
|
+
*/
|
|
272
|
+
export function countTagUses(doc: unknown, tag: string): number {
|
|
273
|
+
const counter = { n: 0 };
|
|
274
|
+
walkTags(doc, tag, null, counter);
|
|
275
|
+
return counter.n;
|
|
276
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan.ts — the project document sweep the refactor engines share.
|
|
3
|
+
*
|
|
4
|
+
* `applyRename` (write pass) and `findReferences` (read pass) walk exactly the same file set: every
|
|
5
|
+
* JSON document plus every extension the project's format registry claims, minus the vendored /
|
|
6
|
+
* build / agent directories. Keeping one definition of "the project's documents" is the point — a
|
|
7
|
+
* rename that rewrites a file the usage count never looked at is a lie in both directions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { extname } from "node:path";
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import type { FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
13
|
+
|
|
14
|
+
/** Normalise a path to forward slashes (Windows `path` returns backslashes). */
|
|
15
|
+
export const fwd = (p: string) => p.replaceAll("\\", "/");
|
|
16
|
+
|
|
17
|
+
/** A parsed document plus the serializer that round-trips it back to disk (null when none). */
|
|
18
|
+
export interface LoadedDoc {
|
|
19
|
+
doc: unknown;
|
|
20
|
+
serialize: ((doc: unknown) => Promise<string>) | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isJsonPath = (p: string) => p.endsWith(".json");
|
|
24
|
+
|
|
25
|
+
/** Skip vendored / build / agent directories (mirrors the component-discovery filter). */
|
|
26
|
+
export function skipScanPath(match: string): boolean {
|
|
27
|
+
const f = fwd(match);
|
|
28
|
+
return match.includes("node_modules") || f.includes("dist/") || f.includes(".claude/");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Build the recursive glob covering JSON plus every registered document/content extension. */
|
|
32
|
+
export function documentGlob(registry: FormatRegistry): Bun.Glob {
|
|
33
|
+
const exts = new Set(["json"]);
|
|
34
|
+
for (const ext of registry.documentExtensions()) {
|
|
35
|
+
exts.add(ext.replace(/^\./, ""));
|
|
36
|
+
}
|
|
37
|
+
return new Bun.Glob(`**/*.{${[...exts].join(",")}}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read + parse a file, returning the doc and a matching serializer (null when the format can parse
|
|
42
|
+
* but not serialize). Throws when no parser claims the extension.
|
|
43
|
+
*/
|
|
44
|
+
export async function loadDoc(fp: string, registry: FormatRegistry): Promise<LoadedDoc> {
|
|
45
|
+
const raw = await readFile(fp, "utf8");
|
|
46
|
+
if (isJsonPath(fp)) {
|
|
47
|
+
const trailingNewline = raw.endsWith("\n");
|
|
48
|
+
return {
|
|
49
|
+
doc: JSON.parse(raw) as unknown,
|
|
50
|
+
serialize: (doc) =>
|
|
51
|
+
Promise.resolve(JSON.stringify(doc, null, 2) + (trailingNewline ? "\n" : "")),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const ext = extname(fp);
|
|
55
|
+
const parseEntry = registry.byExtension(ext, "parse");
|
|
56
|
+
if (!parseEntry) {
|
|
57
|
+
throw new Error(`No parser for "${ext}"`);
|
|
58
|
+
}
|
|
59
|
+
const doc = await parseEntry.call("parse", raw);
|
|
60
|
+
const serializeEntry = registry.byExtension(ext, "serialize");
|
|
61
|
+
return {
|
|
62
|
+
doc,
|
|
63
|
+
serialize: serializeEntry
|
|
64
|
+
? async (d) => {
|
|
65
|
+
const out = await serializeEntry.call("serialize", d);
|
|
66
|
+
return typeof out === "string" ? out : String(out);
|
|
67
|
+
}
|
|
68
|
+
: null,
|
|
69
|
+
};
|
|
70
|
+
}
|
package/src/refactor/watcher.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { watch as chokidarWatch } from "chokidar";
|
|
10
10
|
import { coalesceFsEvents, toFsEvent } from "./fs-events.ts";
|
|
11
|
+
import { invalidateReferenceCache } from "./find-refs.ts";
|
|
11
12
|
import type { FsEventPayload } from "./fs-events.ts";
|
|
12
13
|
|
|
13
14
|
const IGNORE_SEGMENTS = ["node_modules", ".git", "dist", ".jx", ".direnv", ".devenv"];
|
|
@@ -45,6 +46,9 @@ export function createFsWatcher(
|
|
|
45
46
|
if (!event) {
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
49
|
+
// Any move under the root can change who references what, so the usage cache drops the whole
|
|
50
|
+
// Project rather than trying to derive which queries this one path could have affected.
|
|
51
|
+
invalidateReferenceCache(root);
|
|
48
52
|
buffer = coalesceFsEvents(buffer, event);
|
|
49
53
|
if (timer) {
|
|
50
54
|
clearTimeout(timer);
|
package/src/resolve.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { loadAssetMounts } from "@jxsuite/compiler/asset-mounts";
|
|
|
11
11
|
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
12
12
|
import type { DynamicClass } from "@jxsuite/runtime/types";
|
|
13
13
|
import type { JxClassDef, ProjectConfig } from "@jxsuite/schema/types";
|
|
14
|
+
import { problem } from "./problem.ts";
|
|
14
15
|
|
|
15
16
|
interface ModuleNamespace {
|
|
16
17
|
default?: Record<string, unknown>;
|
|
@@ -126,12 +127,12 @@ export async function handleResolve(
|
|
|
126
127
|
try {
|
|
127
128
|
body = (await req.json()) as ResolveBody;
|
|
128
129
|
} catch {
|
|
129
|
-
return
|
|
130
|
+
return problem("invalidRequest", "Invalid JSON body");
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
const { $src, $prototype, $export: xport, $base, ...config } = body;
|
|
133
134
|
if (!$src) {
|
|
134
|
-
return
|
|
135
|
+
return problem("invalidRequest", "Missing $src");
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
let moduleAbsPath;
|
|
@@ -200,7 +201,7 @@ export async function handleResolve(
|
|
|
200
201
|
// A trusted (bare-specifier) class file names its own sibling implementation; only a
|
|
201
202
|
// Project-local relative $src is held to the root-escape guard.
|
|
202
203
|
if (!trustedResolution && !isImportable(implPath, root, activeProjectRoot)) {
|
|
203
|
-
return
|
|
204
|
+
return problem("pathOutsideProject", "$implementation escapes the project root");
|
|
204
205
|
}
|
|
205
206
|
const exportName = xport ?? classDef.title ?? $prototype;
|
|
206
207
|
const mod = (await import(implPath)) as ModuleNamespace;
|
|
@@ -236,7 +237,7 @@ export async function handleResolve(
|
|
|
236
237
|
: instance;
|
|
237
238
|
return Response.json(value);
|
|
238
239
|
} catch (error) {
|
|
239
|
-
return
|
|
240
|
+
return problem("internalError", errorMessage(error));
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
243
|
|
|
@@ -260,12 +261,12 @@ export async function handleServerFunction(req: Request, root: string) {
|
|
|
260
261
|
try {
|
|
261
262
|
body = (await req.json()) as ServerFunctionBody;
|
|
262
263
|
} catch {
|
|
263
|
-
return
|
|
264
|
+
return problem("invalidRequest", "Invalid JSON body");
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
const { $src, $export: xport, $base, arguments: args = {} } = body;
|
|
267
268
|
if (!$src || !xport) {
|
|
268
|
-
return
|
|
269
|
+
return problem("invalidRequest", "Missing $src or $export");
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
let moduleAbsPath;
|
|
@@ -278,9 +279,7 @@ export async function handleServerFunction(req: Request, root: string) {
|
|
|
278
279
|
moduleAbsPath = resolve(root, $src);
|
|
279
280
|
}
|
|
280
281
|
} catch (error) {
|
|
281
|
-
return
|
|
282
|
-
status: 400,
|
|
283
|
-
});
|
|
282
|
+
return problem("invalidRequest", `Cannot resolve $src: ${errorMessage(error)}`);
|
|
284
283
|
}
|
|
285
284
|
|
|
286
285
|
if (!isImportable(moduleAbsPath, root, null)) {
|
|
@@ -312,7 +311,7 @@ export async function handleServerFunction(req: Request, root: string) {
|
|
|
312
311
|
);
|
|
313
312
|
return Response.json(result ?? null);
|
|
314
313
|
} catch (error) {
|
|
315
|
-
return
|
|
314
|
+
return problem("internalError", errorMessage(error));
|
|
316
315
|
}
|
|
317
316
|
}
|
|
318
317
|
|