@jxsuite/server 0.28.5 → 0.30.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 +9 -8
- package/src/claude-session.ts +2 -2
- package/src/code-api.ts +11 -6
- package/src/refactor/apply.ts +215 -0
- package/src/refactor/fs-events.ts +71 -0
- package/src/refactor/index.ts +11 -0
- package/src/refactor/paths.ts +187 -0
- package/src/refactor/refs.ts +236 -0
- package/src/refactor/watcher.ts +70 -0
- package/src/resolve.ts +41 -12
- package/src/server.ts +26 -14
- package/src/studio-api.ts +199 -91
- package/src/watch.ts +33 -2
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refs.ts — pure document reference engine for rename-refactoring.
|
|
3
|
+
*
|
|
4
|
+
* Two passes, both operating on a parsed (JSON) document tree with no filesystem access: -
|
|
5
|
+
* `rewriteDocRefs` — recompute every file-path reference across a rename (Pillar B). -
|
|
6
|
+
* `rewriteTagName` — rename a custom-element tag wherever it is used (Pillar C).
|
|
7
|
+
*
|
|
8
|
+
* The walk is structure-aware (it dispatches by JSON key), not a blind string search, and mutates
|
|
9
|
+
* the passed document in place — callers parse a fresh object per file, so this is safe. Precision
|
|
10
|
+
* comes from `rewriteRef`'s resolve-and-compare gate in paths.ts: a reference is only rewritten
|
|
11
|
+
* when it actually resolves to the renamed file.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { classifyRef, rewriteRef } from "./paths.ts";
|
|
15
|
+
import type { RemapCtx } from "./paths.ts";
|
|
16
|
+
|
|
17
|
+
/** A single reference rewrite, for the rename report. */
|
|
18
|
+
export interface RefChange {
|
|
19
|
+
refType: string;
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RewriteResult {
|
|
25
|
+
doc: unknown;
|
|
26
|
+
changes: RefChange[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Matches `url(...)` targets inside a CSS string value, capturing the optional quote. */
|
|
30
|
+
const URL_RE = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g;
|
|
31
|
+
|
|
32
|
+
/** Recompute a scalar string reference under `key`, recording any change. */
|
|
33
|
+
function rewriteScalar(
|
|
34
|
+
container: Record<string, unknown>,
|
|
35
|
+
key: string,
|
|
36
|
+
refType: string,
|
|
37
|
+
ctx: RemapCtx,
|
|
38
|
+
changes: RefChange[],
|
|
39
|
+
rootRelativeBare: boolean,
|
|
40
|
+
): void {
|
|
41
|
+
const value = container[key];
|
|
42
|
+
const cls = classifyRef(value);
|
|
43
|
+
if (cls.kind !== "path") {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const out = rewriteRef(cls, ctx, rootRelativeBare);
|
|
47
|
+
if (out !== null) {
|
|
48
|
+
changes.push({ from: value as string, refType, to: out });
|
|
49
|
+
container[key] = out;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Recompute a string array member (e.g. an `$elements` entry), recording any change. */
|
|
54
|
+
function rewriteIndexed(
|
|
55
|
+
arr: unknown[],
|
|
56
|
+
index: number,
|
|
57
|
+
refType: string,
|
|
58
|
+
ctx: RemapCtx,
|
|
59
|
+
changes: RefChange[],
|
|
60
|
+
): void {
|
|
61
|
+
const value = arr[index];
|
|
62
|
+
const cls = classifyRef(value);
|
|
63
|
+
if (cls.kind !== "path") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const out = rewriteRef(cls, ctx, false);
|
|
67
|
+
if (out !== null) {
|
|
68
|
+
changes.push({ from: value as string, refType, to: out });
|
|
69
|
+
arr[index] = out;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Rewrite every `url(...)` target inside a CSS string. */
|
|
74
|
+
function rewriteUrls(value: string, ctx: RemapCtx, changes: RefChange[]): string {
|
|
75
|
+
return value.replaceAll(URL_RE, (match, quote: string, inner: string) => {
|
|
76
|
+
const cls = classifyRef(inner.trim());
|
|
77
|
+
if (cls.kind !== "path") {
|
|
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
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Walk a `style` value tree, rewriting `url(...)` in every string it contains. */
|
|
90
|
+
function walkStyle(node: unknown, ctx: RemapCtx, changes: RefChange[]): void {
|
|
91
|
+
if (Array.isArray(node)) {
|
|
92
|
+
const arr = node as unknown[];
|
|
93
|
+
for (let i = 0; i < arr.length; i += 1) {
|
|
94
|
+
const item = arr[i];
|
|
95
|
+
if (typeof item === "string") {
|
|
96
|
+
const out = rewriteUrls(item, ctx, changes);
|
|
97
|
+
if (out !== item) {
|
|
98
|
+
arr[i] = out;
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
walkStyle(item, ctx, changes);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!node || typeof node !== "object") {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const obj = node as Record<string, unknown>;
|
|
110
|
+
for (const key of Object.keys(obj)) {
|
|
111
|
+
const value = obj[key];
|
|
112
|
+
if (typeof value === "string") {
|
|
113
|
+
const out = rewriteUrls(value, ctx, changes);
|
|
114
|
+
if (out !== value) {
|
|
115
|
+
obj[key] = out;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
walkStyle(value, ctx, changes);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Recursive document walk dispatching each reference-bearing key. */
|
|
124
|
+
function walk(node: unknown, ctx: RemapCtx, changes: RefChange[]): void {
|
|
125
|
+
if (Array.isArray(node)) {
|
|
126
|
+
for (const item of node as unknown[]) {
|
|
127
|
+
walk(item, ctx, changes);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!node || typeof node !== "object") {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const obj = node as Record<string, unknown>;
|
|
135
|
+
for (const key of Object.keys(obj)) {
|
|
136
|
+
const value = obj[key];
|
|
137
|
+
switch (key) {
|
|
138
|
+
case "$ref": {
|
|
139
|
+
// File-path $ref only; state-scope refs (#/…, $map/…) are filtered by classifyRef.
|
|
140
|
+
rewriteScalar(obj, key, "$ref", ctx, changes, false);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "$layout": {
|
|
144
|
+
rewriteScalar(obj, key, "$layout", ctx, changes, true);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "$src":
|
|
148
|
+
case "$implementation": {
|
|
149
|
+
rewriteScalar(obj, key, key, ctx, changes, false);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "src":
|
|
153
|
+
case "href": {
|
|
154
|
+
rewriteScalar(obj, key, "attr", ctx, changes, false);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "imports": {
|
|
158
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
159
|
+
const imp = value as Record<string, unknown>;
|
|
160
|
+
for (const name of Object.keys(imp)) {
|
|
161
|
+
rewriteScalar(imp, name, "imports", ctx, changes, false);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "$elements": {
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
const els = value as unknown[];
|
|
169
|
+
for (let i = 0; i < els.length; i += 1) {
|
|
170
|
+
if (typeof els[i] === "string") {
|
|
171
|
+
rewriteIndexed(els, i, "$elements", ctx, changes);
|
|
172
|
+
} else {
|
|
173
|
+
walk(els[i], ctx, changes);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
walk(value, ctx, changes);
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case "style": {
|
|
182
|
+
walkStyle(value, ctx, changes);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
default: {
|
|
186
|
+
walk(value, ctx, changes);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Rewrite every file-path reference in `doc` for a rename described by `ctx`. Mutates `doc`. */
|
|
193
|
+
export function rewriteDocRefs(doc: unknown, ctx: RemapCtx): RewriteResult {
|
|
194
|
+
const changes: RefChange[] = [];
|
|
195
|
+
walk(doc, ctx, changes);
|
|
196
|
+
return { changes, doc };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Recursive walk setting `tagName: newTag` wherever it currently equals `oldTag`. */
|
|
200
|
+
function walkTags(node: unknown, oldTag: string, newTag: string, counter: { n: number }): void {
|
|
201
|
+
if (Array.isArray(node)) {
|
|
202
|
+
for (const item of node as unknown[]) {
|
|
203
|
+
walkTags(item, oldTag, newTag, counter);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!node || typeof node !== "object") {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const obj = node as Record<string, unknown>;
|
|
211
|
+
if (obj.tagName === oldTag) {
|
|
212
|
+
obj.tagName = newTag;
|
|
213
|
+
counter.n += 1;
|
|
214
|
+
}
|
|
215
|
+
for (const key of Object.keys(obj)) {
|
|
216
|
+
walkTags(obj[key], oldTag, newTag, counter);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Rename a custom-element tag throughout `doc` — instance nodes (`<old-tag>`), mapped-list
|
|
222
|
+
* `map.tagName`, and the component definition file's own root `tagName`. File references (`$ref`,
|
|
223
|
+
* `$elements`, `cases`) are intentionally left to `rewriteDocRefs`, so the two passes compose.
|
|
224
|
+
* Mutates `doc`.
|
|
225
|
+
*/
|
|
226
|
+
export function rewriteTagName(
|
|
227
|
+
doc: unknown,
|
|
228
|
+
oldTag: string,
|
|
229
|
+
newTag: string,
|
|
230
|
+
): { doc: unknown; count: number } {
|
|
231
|
+
const counter = { n: 0 };
|
|
232
|
+
if (oldTag !== newTag) {
|
|
233
|
+
walkTags(doc, oldTag, newTag, counter);
|
|
234
|
+
}
|
|
235
|
+
return { count: counter.n, doc };
|
|
236
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watcher.ts — a small chokidar wrapper that emits coalesced, batched FS-event payloads to a sink.
|
|
3
|
+
*
|
|
4
|
+
* Used by the desktop project session (which has no SSE stream) to push filesystem changes to its
|
|
5
|
+
* webview over RPC. The dev server keeps its own watcher in watch.ts (intertwined with
|
|
6
|
+
* build/reload); both share the pure `toFsEvent`/`coalesceFsEvents` helpers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { watch as chokidarWatch } from "chokidar";
|
|
10
|
+
import { coalesceFsEvents, toFsEvent } from "./fs-events.ts";
|
|
11
|
+
import type { FsEventPayload } from "./fs-events.ts";
|
|
12
|
+
|
|
13
|
+
const IGNORE_SEGMENTS = ["node_modules", ".git", "dist", ".jx", ".direnv", ".devenv"];
|
|
14
|
+
|
|
15
|
+
function isIgnored(path: string): boolean {
|
|
16
|
+
const f = path.replaceAll("\\", "/");
|
|
17
|
+
return IGNORE_SEGMENTS.some((s) => f.includes(`/${s}/`) || f.endsWith(`/${s}`));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FsWatcherHandle {
|
|
21
|
+
close: () => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Watch `root`, coalescing rapid changes and delivering batches to `sink`. Returns a handle whose
|
|
26
|
+
* `close()` stops the watcher and clears any pending batch.
|
|
27
|
+
*/
|
|
28
|
+
export function createFsWatcher(
|
|
29
|
+
root: string,
|
|
30
|
+
sink: (events: FsEventPayload[]) => void,
|
|
31
|
+
opts: { debounce?: number } = {},
|
|
32
|
+
): FsWatcherHandle {
|
|
33
|
+
const debounceMs = opts.debounce ?? 40;
|
|
34
|
+
let buffer: FsEventPayload[] = [];
|
|
35
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
|
|
37
|
+
const watcher = chokidarWatch(root, {
|
|
38
|
+
ignoreInitial: true,
|
|
39
|
+
ignorePermissionErrors: true,
|
|
40
|
+
ignored: (path) => isIgnored(path),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
watcher.on("all", (eventType, changedPath) => {
|
|
44
|
+
const event = toFsEvent(eventType, root, changedPath);
|
|
45
|
+
if (!event) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
buffer = coalesceFsEvents(buffer, event);
|
|
49
|
+
if (timer) {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
}
|
|
52
|
+
timer = setTimeout(() => {
|
|
53
|
+
if (buffer.length > 0) {
|
|
54
|
+
sink(buffer);
|
|
55
|
+
buffer = [];
|
|
56
|
+
}
|
|
57
|
+
}, debounceMs);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
close: async () => {
|
|
62
|
+
if (timer) {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
timer = null;
|
|
65
|
+
}
|
|
66
|
+
buffer = [];
|
|
67
|
+
await watcher.close();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
package/src/resolve.ts
CHANGED
|
@@ -6,7 +6,34 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { loadContentTypes } from "@jxsuite/compiler/content-loader";
|
|
8
8
|
import type { DynamicClass } from "@jxsuite/runtime/types";
|
|
9
|
-
import type { JxClassDef } from "@jxsuite/schema/types";
|
|
9
|
+
import type { JxClassDef, ProjectConfig } from "@jxsuite/schema/types";
|
|
10
|
+
|
|
11
|
+
interface ModuleNamespace {
|
|
12
|
+
default?: Record<string, unknown>;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ClassInstance {
|
|
17
|
+
resolve?: () => unknown;
|
|
18
|
+
value?: unknown;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ResolveBody {
|
|
23
|
+
$src?: string;
|
|
24
|
+
$prototype?: string;
|
|
25
|
+
$export?: string;
|
|
26
|
+
$base?: string;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ServerFunctionBody {
|
|
31
|
+
$src?: string;
|
|
32
|
+
$export?: string;
|
|
33
|
+
$base?: string;
|
|
34
|
+
arguments?: Record<string, unknown>;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
10
37
|
|
|
11
38
|
/**
|
|
12
39
|
* Lazy-load project context (project.json + content types) for class instantiation.
|
|
@@ -24,7 +51,7 @@ async function loadProjectContext(projectRoot: string) {
|
|
|
24
51
|
return null;
|
|
25
52
|
}
|
|
26
53
|
try {
|
|
27
|
-
const config = JSON.parse(readFileSync(projectJsonPath, "utf8"));
|
|
54
|
+
const config = JSON.parse(readFileSync(projectJsonPath, "utf8")) as ProjectConfig;
|
|
28
55
|
const contentTypes = config.contentTypes
|
|
29
56
|
? await loadContentTypes(projectRoot, config)
|
|
30
57
|
: new Map();
|
|
@@ -46,9 +73,9 @@ export async function handleResolve(
|
|
|
46
73
|
root: string,
|
|
47
74
|
activeProjectRoot: string | null = null,
|
|
48
75
|
) {
|
|
49
|
-
let body;
|
|
76
|
+
let body: ResolveBody;
|
|
50
77
|
try {
|
|
51
|
-
body = await req.json();
|
|
78
|
+
body = (await req.json()) as ResolveBody;
|
|
52
79
|
} catch {
|
|
53
80
|
return new Response("Invalid JSON body", { status: 400 });
|
|
54
81
|
}
|
|
@@ -114,14 +141,16 @@ export async function handleResolve(
|
|
|
114
141
|
// Hybrid mode: redirect to the JS implementation
|
|
115
142
|
const implPath = resolve(dirname(moduleAbsPath), classDef.$implementation);
|
|
116
143
|
const exportName = xport ?? classDef.title ?? $prototype;
|
|
117
|
-
const mod = await import(implPath);
|
|
118
|
-
const ExportedClass =
|
|
144
|
+
const mod = (await import(implPath)) as ModuleNamespace;
|
|
145
|
+
const ExportedClass =
|
|
146
|
+
exportName === undefined ? undefined : (mod[exportName] ?? mod.default?.[exportName]);
|
|
119
147
|
if (typeof ExportedClass !== "function") {
|
|
120
148
|
return new Response(`Export "${exportName}" not found in "${classDef.$implementation}"`, {
|
|
121
149
|
status: 500,
|
|
122
150
|
});
|
|
123
151
|
}
|
|
124
|
-
const
|
|
152
|
+
const ClassCtor = ExportedClass as new (config: unknown) => ClassInstance;
|
|
153
|
+
const instance = new ClassCtor(config);
|
|
125
154
|
const value =
|
|
126
155
|
typeof instance.resolve === "function"
|
|
127
156
|
? await instance.resolve()
|
|
@@ -165,9 +194,9 @@ export async function handleResolve(
|
|
|
165
194
|
* @param {string} root
|
|
166
195
|
*/
|
|
167
196
|
export async function handleServerFunction(req: Request, root: string) {
|
|
168
|
-
let body;
|
|
197
|
+
let body: ServerFunctionBody;
|
|
169
198
|
try {
|
|
170
|
-
body = await req.json();
|
|
199
|
+
body = (await req.json()) as ServerFunctionBody;
|
|
171
200
|
} catch {
|
|
172
201
|
return new Response("Invalid JSON body", { status: 400 });
|
|
173
202
|
}
|
|
@@ -192,9 +221,9 @@ export async function handleServerFunction(req: Request, root: string) {
|
|
|
192
221
|
});
|
|
193
222
|
}
|
|
194
223
|
|
|
195
|
-
let mod;
|
|
224
|
+
let mod: ModuleNamespace;
|
|
196
225
|
try {
|
|
197
|
-
mod = await import(moduleAbsPath);
|
|
226
|
+
mod = (await import(moduleAbsPath)) as ModuleNamespace;
|
|
198
227
|
} catch (error) {
|
|
199
228
|
return new Response(`Failed to import "${$src}": ${errorMessage(error)}`, {
|
|
200
229
|
status: 500,
|
|
@@ -209,7 +238,7 @@ export async function handleServerFunction(req: Request, root: string) {
|
|
|
209
238
|
}
|
|
210
239
|
|
|
211
240
|
try {
|
|
212
|
-
const result = await fn(args);
|
|
241
|
+
const result = await (fn as (args: Record<string, unknown>) => unknown)(args);
|
|
213
242
|
return Response.json(result ?? null);
|
|
214
243
|
} catch (error) {
|
|
215
244
|
return Response.json({ error: errorMessage(error) }, { status: 500 });
|
package/src/server.ts
CHANGED
|
@@ -30,6 +30,14 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
30
30
|
* @param {string} urlPath - URL pathname (e.g. "/pages/@jxsuite/parser/Foo.class.json")
|
|
31
31
|
* @returns {string | null} Absolute file path or null
|
|
32
32
|
*/
|
|
33
|
+
interface PackageJson {
|
|
34
|
+
exports?: Record<string, string | { import?: string; default?: string }>;
|
|
35
|
+
customElements?: string;
|
|
36
|
+
module?: string;
|
|
37
|
+
main?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
function resolveNpmPath(rootDir: string, urlPath: string) {
|
|
34
42
|
let root = rootDir;
|
|
35
43
|
let segments = urlPath.split("/").filter(Boolean);
|
|
@@ -49,7 +57,7 @@ function resolveNpmPath(rootDir: string, urlPath: string) {
|
|
|
49
57
|
let start = -1;
|
|
50
58
|
let isScoped = false;
|
|
51
59
|
for (let i = 0; i < segments.length; i++) {
|
|
52
|
-
if (segments[i]
|
|
60
|
+
if (segments[i]!.startsWith("@")) {
|
|
53
61
|
start = i;
|
|
54
62
|
isScoped = true;
|
|
55
63
|
break;
|
|
@@ -63,14 +71,14 @@ function resolveNpmPath(rootDir: string, urlPath: string) {
|
|
|
63
71
|
if (start < 0 || start + 1 >= segments.length) {
|
|
64
72
|
return null;
|
|
65
73
|
}
|
|
66
|
-
const scope = segments[start]
|
|
67
|
-
const pkg = segments[start + 1]
|
|
74
|
+
const scope = segments[start]!;
|
|
75
|
+
const pkg = segments[start + 1]!;
|
|
68
76
|
subpath = segments.slice(start + 2).join("/");
|
|
69
77
|
pkgDir = join(root, "node_modules", scope, pkg);
|
|
70
78
|
} else {
|
|
71
79
|
// Unscoped: try each segment as a package name in node_modules
|
|
72
80
|
for (let i = 0; i < segments.length; i++) {
|
|
73
|
-
const candidate = join(root, "node_modules", segments[i]);
|
|
81
|
+
const candidate = join(root, "node_modules", segments[i]!);
|
|
74
82
|
if (existsSync(join(candidate, "package.json"))) {
|
|
75
83
|
start = i;
|
|
76
84
|
pkgDir = candidate;
|
|
@@ -91,10 +99,11 @@ function resolveNpmPath(rootDir: string, urlPath: string) {
|
|
|
91
99
|
// If there's a subpath, check package.json exports first
|
|
92
100
|
if (subpath) {
|
|
93
101
|
try {
|
|
94
|
-
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
102
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as PackageJson;
|
|
95
103
|
const exportKey = `./${subpath}`;
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
const exportVal = pkgJson.exports?.[exportKey];
|
|
105
|
+
if (typeof exportVal === "string") {
|
|
106
|
+
const mapped = join(pkgDir, exportVal);
|
|
98
107
|
if (existsSync(mapped)) {
|
|
99
108
|
return mapped;
|
|
100
109
|
}
|
|
@@ -107,7 +116,7 @@ function resolveNpmPath(rootDir: string, urlPath: string) {
|
|
|
107
116
|
}
|
|
108
117
|
// CEM-relative: subpath may be relative to the custom elements manifest directory
|
|
109
118
|
try {
|
|
110
|
-
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
119
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as PackageJson;
|
|
111
120
|
if (pkgJson.customElements) {
|
|
112
121
|
const cemDir = pkgJson.customElements.replace(/\/[^/]+$/, "");
|
|
113
122
|
const cemRelative = join(pkgDir, cemDir, subpath);
|
|
@@ -120,7 +129,7 @@ function resolveNpmPath(rootDir: string, urlPath: string) {
|
|
|
120
129
|
|
|
121
130
|
// Bare package (no subpath): resolve entry point
|
|
122
131
|
try {
|
|
123
|
-
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
132
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as PackageJson;
|
|
124
133
|
const exp = pkgJson.exports?.["."];
|
|
125
134
|
const entry =
|
|
126
135
|
(typeof exp === "object" ? (exp.import ?? exp.default) : exp) ??
|
|
@@ -236,7 +245,7 @@ export async function createDevServer(options: {
|
|
|
236
245
|
if (enableStudio && path.startsWith("/__studio/")) {
|
|
237
246
|
// Activate project — tells the server which project root to use for static file fallback
|
|
238
247
|
if (path === "/__studio/activate" && req.method === "POST") {
|
|
239
|
-
const body = await req.json();
|
|
248
|
+
const body = (await req.json()) as { root?: string };
|
|
240
249
|
const raw = body.root || null;
|
|
241
250
|
// Always store as absolute path
|
|
242
251
|
activeProjectRoot = raw ? resolve(absRoot, raw) : null;
|
|
@@ -265,9 +274,12 @@ export async function createDevServer(options: {
|
|
|
265
274
|
// Static files
|
|
266
275
|
|
|
267
276
|
// If the URL path is an absolute filesystem path under the active project, serve directly.
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
277
|
+
// A POSIX absolute path arrives as "//abs/path"; a Windows one as "/C:/dir/file" (leading
|
|
278
|
+
// Slash + forward slashes), so drop the slash before the drive letter. Compare with
|
|
279
|
+
// Separators normalised, since activeProjectRoot is OS-native (backslashes on Windows).
|
|
280
|
+
const fsPath = path.startsWith("//") ? path.slice(1) : path.replace(/^\/([A-Za-z]:)/, "$1");
|
|
281
|
+
const normSep = (p: string) => p.replaceAll("\\", "/");
|
|
282
|
+
if (activeProjectRoot && normSep(fsPath).startsWith(normSep(activeProjectRoot))) {
|
|
271
283
|
const file = Bun.file(fsPath);
|
|
272
284
|
if (await file.exists()) {
|
|
273
285
|
return new Response(file);
|
|
@@ -302,7 +314,7 @@ export async function createDevServer(options: {
|
|
|
302
314
|
minify: false,
|
|
303
315
|
});
|
|
304
316
|
if (result.success && result.outputs.length > 0) {
|
|
305
|
-
bundleCache.set(cacheKey, await result.outputs[0]
|
|
317
|
+
bundleCache.set(cacheKey, await result.outputs[0]!.text());
|
|
306
318
|
}
|
|
307
319
|
} catch (error) {
|
|
308
320
|
console.error("Bundle failed for", resolved, error);
|