@jxsuite/server 0.0.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 ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@jxsuite/server",
3
+ "version": "0.0.1",
4
+ "description": "Jx development server with live reload, server-side proxy, and studio integration",
5
+ "license": "MIT",
6
+ "files": [
7
+ "src/"
8
+ ],
9
+ "type": "module",
10
+ "exports": {
11
+ ".": "./src/server.js"
12
+ },
13
+ "scripts": {
14
+ "upgrade": "bunx npm-check-updates -u && bun install"
15
+ },
16
+ "dependencies": {
17
+ "chokidar": "^5.0.0"
18
+ }
19
+ }
package/src/build.js ADDED
@@ -0,0 +1,67 @@
1
+ /** Build.js — Configurable Bun.build pipeline */
2
+
3
+ /**
4
+ * Build all entries with sensible defaults (browser target, ESM, linked sourcemaps).
5
+ *
6
+ * @param {{
7
+ * entrypoints: string[];
8
+ * outdir: string;
9
+ * match?: Function | RegExp;
10
+ * label?: string;
11
+ * }[]} builds
12
+ */
13
+ export async function buildAll(builds) {
14
+ for (const entry of builds) {
15
+ const { match: _match, label, ...opts } = entry;
16
+ const result = await Bun.build({
17
+ target: "browser",
18
+ format: "esm",
19
+ sourcemap: "linked",
20
+ ...opts,
21
+ });
22
+ if (!result.success) result.logs.forEach((l) => console.error(l));
23
+ else console.log(`Built → ${entry.outdir}/${label ?? "bundle"}.js`);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Rebuild entries whose match function/regex matches the changed filename.
29
+ *
30
+ * @param {{
31
+ * entrypoints: string[];
32
+ * outdir: string;
33
+ * match?: Function | RegExp;
34
+ * label?: string;
35
+ * }[]} builds
36
+ * @param {string} changedFile
37
+ * @returns {Promise<{ rebuilt: string[]; success: boolean }>}
38
+ */
39
+ export async function rebuild(builds, changedFile) {
40
+ const rebuilt = [];
41
+ let ok = true;
42
+ for (const entry of builds) {
43
+ if (!entry.match) continue;
44
+ const matches =
45
+ typeof entry.match === "function"
46
+ ? entry.match(changedFile)
47
+ : entry.match instanceof RegExp
48
+ ? entry.match.test(changedFile)
49
+ : false;
50
+ if (!matches) continue;
51
+ const { match: _match, label, ...opts } = entry;
52
+ const result = await Bun.build({
53
+ target: "browser",
54
+ format: "esm",
55
+ sourcemap: "linked",
56
+ ...opts,
57
+ });
58
+ if (result.success) {
59
+ rebuilt.push(label ?? entry.outdir);
60
+ console.log(`Rebuilt → ${entry.outdir}/${label ?? "bundle"}.js (${changedFile} changed)`);
61
+ } else {
62
+ result.logs.forEach((l) => console.error(l));
63
+ ok = false;
64
+ }
65
+ }
66
+ return { rebuilt, success: ok };
67
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Code-api.js — OXC-powered code services for the studio function editor
3
+ *
4
+ * Endpoints under /__studio/code/* that provide formatting (oxfmt), minification (Bun.Transpiler),
5
+ * and linting (oxlint CLI) for JS snippets.
6
+ */
7
+
8
+ import { tmpdir } from "node:os";
9
+ import { join, resolve } from "node:path";
10
+ import { unlink } from "node:fs/promises";
11
+ import { format } from "oxfmt";
12
+
13
+ const OXLINT_BIN = resolve(
14
+ import.meta.dir,
15
+ "../../node_modules/.bin",
16
+ process.platform === "win32" ? "oxlint.exe" : "oxlint",
17
+ );
18
+
19
+ // ─── Wrapper utilities ───────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * @param {string} body
23
+ * @param {string[]} [args]
24
+ */
25
+ function wrapBody(body, args = ["state", "event"]) {
26
+ const params = args.join(", ");
27
+ return `function __jx_fn__(${params}) {\n${body}\n}`;
28
+ }
29
+
30
+ /** @param {string} formatted */
31
+ function unwrapFormatted(formatted) {
32
+ const lines = formatted.split("\n");
33
+ // Remove first line (function header) and last non-empty line (closing brace)
34
+ let end = lines.length - 1;
35
+ while (end > 0 && lines[end].trim() === "") end--;
36
+ if (lines[end].trim() === "}") end--;
37
+ const bodyLines = lines.slice(1, end + 1);
38
+ // Dedent by one tab (oxfmt uses the project's indentStyle)
39
+ return bodyLines.map((l) => (l.startsWith("\t") ? l.slice(1) : l)).join("\n");
40
+ }
41
+
42
+ /**
43
+ * @param {any[]} diagnostics
44
+ * @param {number} headerLen
45
+ */
46
+ function adjustDiagnostics(diagnostics, headerLen) {
47
+ return diagnostics
48
+ .filter((d) => {
49
+ const line = d.labels?.[0]?.span?.line;
50
+ return line == null || line > 1;
51
+ })
52
+ .map((d) => ({
53
+ ...d,
54
+ labels: d.labels.map((/** @type {any} */ label) => ({
55
+ ...label,
56
+ span: {
57
+ ...label.span,
58
+ offset: label.span.offset - headerLen,
59
+ line: label.span.line - 1,
60
+ },
61
+ })),
62
+ }));
63
+ }
64
+
65
+ // ─── Reusable transpiler ─────────────────────────────────────────────────────
66
+
67
+ const minifier = new Bun.Transpiler({ minifyWhitespace: true });
68
+
69
+ // ─── Handler ─────────────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * @param {Request} req
73
+ * @param {URL} url
74
+ */
75
+ export async function handleCodeApi(req, url) {
76
+ const path = url.pathname;
77
+ if (!path.startsWith("/__studio/code/") || req.method !== "POST") return null;
78
+
79
+ let body;
80
+ try {
81
+ body = await req.json();
82
+ } catch {
83
+ return new Response("Invalid JSON", { status: 400 });
84
+ }
85
+
86
+ const action = path.slice("/__studio/code/".length);
87
+
88
+ // ── Format ─────────────────────────────────────────────────────────────────
89
+
90
+ if (action === "format") {
91
+ const { code, args } = body;
92
+ if (!code?.trim()) return Response.json({ code: "", errors: [] });
93
+
94
+ try {
95
+ const wrapped = wrapBody(code, args);
96
+ const result = await format("fn.js", wrapped, { useTabs: true });
97
+ return Response.json({
98
+ code: unwrapFormatted(result.code),
99
+ errors: result.errors,
100
+ });
101
+ } catch (/** @type {any} */ e) {
102
+ return Response.json({ code, errors: [{ message: e.message }] });
103
+ }
104
+ }
105
+
106
+ // ── Minify ─────────────────────────────────────────────────────────────────
107
+
108
+ if (action === "minify") {
109
+ const { code } = body;
110
+ if (!code?.trim()) return Response.json({ code: "" });
111
+
112
+ try {
113
+ const minified = minifier.transformSync(code).trim();
114
+ return Response.json({ code: minified });
115
+ } catch (/** @type {any} */ e) {
116
+ return Response.json({ code, error: e.message });
117
+ }
118
+ }
119
+
120
+ // ── Lint ───────────────────────────────────────────────────────────────────
121
+
122
+ if (action === "lint") {
123
+ const { code, args } = body;
124
+ if (!code?.trim()) return Response.json({ diagnostics: [] });
125
+
126
+ const wrapped = wrapBody(code, args);
127
+ const headerLen = wrapped.indexOf("\n") + 1;
128
+ const tmpFile = join(
129
+ tmpdir(),
130
+ `__jx_lint_${Date.now()}_${Math.random().toString(36).slice(2)}.js`,
131
+ );
132
+
133
+ try {
134
+ await Bun.write(tmpFile, wrapped);
135
+ const proc = Bun.spawn([OXLINT_BIN, "--format=json", "-A", "no-unused-vars", tmpFile], {
136
+ stdout: "pipe",
137
+ stderr: "pipe",
138
+ });
139
+ const output = await new Response(proc.stdout).text();
140
+ await proc.exited;
141
+
142
+ const parsed = JSON.parse(output);
143
+ const adjusted = adjustDiagnostics(parsed.diagnostics || [], headerLen);
144
+ return Response.json({ diagnostics: adjusted });
145
+ } catch (/** @type {any} */ e) {
146
+ return Response.json({ diagnostics: [], error: e.message });
147
+ } finally {
148
+ try {
149
+ await unlink(tmpFile);
150
+ } catch {}
151
+ }
152
+ }
153
+
154
+ return null;
155
+ }
package/src/resolve.js ADDED
@@ -0,0 +1,206 @@
1
+ /** Resolve.js — Generic $src module proxy + timing: "server" function proxy */
2
+
3
+ import { resolve, relative, dirname } from "node:path";
4
+ import { readFileSync } from "node:fs";
5
+
6
+ /**
7
+ * Handle POST /**jx_resolve** — proxy $prototype + $src entries.
8
+ *
9
+ * @param {Request} req
10
+ * @param {string} root
11
+ */
12
+ export async function handleResolve(req, root) {
13
+ let body;
14
+ try {
15
+ body = await req.json();
16
+ } catch {
17
+ return new Response("Invalid JSON body", { status: 400 });
18
+ }
19
+
20
+ const { $src, $prototype, $export: xport, $base, ...config } = body;
21
+ if (!$src) return new Response("Missing $src", { status: 400 });
22
+
23
+ let moduleAbsPath;
24
+ try {
25
+ if ($base) {
26
+ const docUrlPath = new URL($base).pathname;
27
+ const docDir = docUrlPath.slice(0, docUrlPath.lastIndexOf("/") + 1);
28
+ moduleAbsPath = resolve(resolve(root, "." + docDir), $src);
29
+ } else {
30
+ moduleAbsPath = resolve(root, $src);
31
+ }
32
+ } catch (/** @type {any} */ e) {
33
+ return new Response(`Cannot resolve $src "${$src}": ${e.message}`, { status: 400 });
34
+ }
35
+
36
+ // Rebase relative config paths from doc-relative to CWD-relative
37
+ if ($base) {
38
+ const docUrlPath = new URL($base).pathname;
39
+ const docDir = docUrlPath.slice(0, docUrlPath.lastIndexOf("/") + 1);
40
+ const docAbsDir = resolve(root, "." + docDir);
41
+ for (const [k, v] of Object.entries(config)) {
42
+ if (typeof v === "string" && (v.startsWith("./") || v.startsWith("../"))) {
43
+ config[k] = "./" + relative(process.cwd(), resolve(docAbsDir, v));
44
+ }
45
+ }
46
+ }
47
+
48
+ // .class.json: read schema, follow $implementation to the real JS module
49
+ if (moduleAbsPath.endsWith(".class.json")) {
50
+ try {
51
+ const content = readFileSync(moduleAbsPath, "utf8");
52
+ const classDef = JSON.parse(content);
53
+
54
+ if (classDef.$implementation) {
55
+ // Hybrid mode: redirect to the JS implementation
56
+ const implPath = resolve(dirname(moduleAbsPath), classDef.$implementation);
57
+ const exportName = xport ?? classDef.title ?? $prototype;
58
+ const mod = await import(implPath);
59
+ const ExportedClass = mod[exportName] ?? mod.default?.[exportName];
60
+ if (typeof ExportedClass !== "function") {
61
+ return new Response(`Export "${exportName}" not found in "${classDef.$implementation}"`, {
62
+ status: 500,
63
+ });
64
+ }
65
+ const instance = new ExportedClass(config);
66
+ const value =
67
+ typeof instance.resolve === "function"
68
+ ? await instance.resolve()
69
+ : "value" in instance
70
+ ? instance.value
71
+ : instance;
72
+ return Response.json(value);
73
+ }
74
+
75
+ // Self-contained: construct class from schema
76
+ const DynClass = classFromSchema(classDef);
77
+ const instance = /** @type {any} */ (new DynClass(config));
78
+ const value =
79
+ typeof instance.resolve === "function"
80
+ ? await instance.resolve()
81
+ : "value" in instance
82
+ ? instance.value
83
+ : instance;
84
+ return Response.json(value);
85
+ } catch (/** @type {any} */ e) {
86
+ return Response.json({ error: e.message }, { status: 500 });
87
+ }
88
+ }
89
+
90
+ // Non-Function $prototype must use .class.json as entrypoint
91
+ return new Response(
92
+ `Non-Function $prototype "${$prototype}" requires a .class.json $src, got "${$src}". ` +
93
+ `Wrap the class in a .class.json schema with $implementation.`,
94
+ { status: 400 },
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Handle POST /**jx_server** — proxy timing: "server" function calls. In dev mode, the runtime
100
+ * sends these instead of hitting the production Hono handler.
101
+ *
102
+ * @param {Request} req
103
+ * @param {string} root
104
+ */
105
+ export async function handleServerFunction(req, root) {
106
+ let body;
107
+ try {
108
+ body = await req.json();
109
+ } catch {
110
+ return new Response("Invalid JSON body", { status: 400 });
111
+ }
112
+
113
+ const { $src, $export: xport, $base, arguments: args = {} } = body;
114
+ if (!$src || !xport) return new Response("Missing $src or $export", { status: 400 });
115
+
116
+ let moduleAbsPath;
117
+ try {
118
+ if ($base) {
119
+ const docUrlPath = new URL($base).pathname;
120
+ const docDir = docUrlPath.slice(0, docUrlPath.lastIndexOf("/") + 1);
121
+ moduleAbsPath = resolve(resolve(root, "." + docDir), $src);
122
+ } else {
123
+ moduleAbsPath = resolve(root, $src);
124
+ }
125
+ } catch (/** @type {any} */ e) {
126
+ return new Response(`Cannot resolve $src: ${e.message}`, { status: 400 });
127
+ }
128
+
129
+ let mod;
130
+ try {
131
+ mod = await import(moduleAbsPath);
132
+ } catch (/** @type {any} */ e) {
133
+ return new Response(`Failed to import "${$src}": ${e.message}`, { status: 500 });
134
+ }
135
+
136
+ const fn = mod[xport] ?? mod.default?.[xport];
137
+ if (typeof fn !== "function") {
138
+ return new Response(`Export "${xport}" not found in "${$src}"`, { status: 500 });
139
+ }
140
+
141
+ try {
142
+ const result = await fn(args);
143
+ return Response.json(result ?? null);
144
+ } catch (/** @type {any} */ e) {
145
+ return Response.json({ error: e.message }, { status: 500 });
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Dynamically construct a class from a .class.json schema definition. Server-side variant — no
151
+ * private field limitations.
152
+ *
153
+ * @param {any} classDef
154
+ */
155
+ function classFromSchema(classDef) {
156
+ const fields = classDef.$defs?.fields ?? {};
157
+ const ctor = classDef.$defs?.constructor;
158
+ const methods = classDef.$defs?.methods ?? {};
159
+
160
+ class DynClass {
161
+ constructor(config = {}) {
162
+ const self = /** @type {any} */ (this);
163
+ const cfg = /** @type {Record<string, any>} */ (config);
164
+ for (const [key, field] of Object.entries(fields)) {
165
+ const id = field.identifier ?? key;
166
+ if (cfg[id] !== undefined) self[id] = cfg[id];
167
+ else if (field.initializer !== undefined) self[id] = field.initializer;
168
+ else if (field.default !== undefined) self[id] = structuredClone(field.default);
169
+ else self[id] = null;
170
+ }
171
+ if (ctor?.body) {
172
+ const bodyStr = Array.isArray(ctor.body) ? ctor.body.join("\n") : ctor.body;
173
+ new Function("config", bodyStr).call(this, config);
174
+ }
175
+ }
176
+ }
177
+
178
+ for (const [key, method] of Object.entries(methods)) {
179
+ const name = method.identifier ?? key;
180
+ const params = (method.parameters ?? []).map((/** @type {any} */ p) => {
181
+ if (p.$ref) return p.$ref.split("/").pop();
182
+ return p.identifier ?? p.name ?? "arg";
183
+ });
184
+ const bodyStr = Array.isArray(method.body) ? method.body.join("\n") : (method.body ?? "");
185
+
186
+ if (method.role === "accessor") {
187
+ /** @type {any} */
188
+ const descriptor = {};
189
+ if (method.getter) descriptor.get = new Function(method.getter.body);
190
+ if (method.setter) {
191
+ const sp = (method.setter.parameters ?? []).map(
192
+ (/** @type {any} */ p) => p.$ref?.split("/").pop() ?? "v",
193
+ );
194
+ descriptor.set = new Function(...sp, method.setter.body);
195
+ }
196
+ Object.defineProperty(DynClass.prototype, name, { ...descriptor, configurable: true });
197
+ } else if (method.scope === "static") {
198
+ /** @type {any} */ (DynClass)[name] = new Function(...params, bodyStr);
199
+ } else {
200
+ /** @type {any} */ (DynClass.prototype)[name] = new Function(...params, bodyStr);
201
+ }
202
+ }
203
+
204
+ Object.defineProperty(DynClass, "name", { value: classDef.title, configurable: true });
205
+ return DynClass;
206
+ }
package/src/server.js ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * @example
3
+ * import { createDevServer } from "@jxplatform/server";
4
+ *
5
+ * await createDevServer({
6
+ * root: import.meta.dir,
7
+ * builds: [{ entrypoints: ["./src/app.js"], outdir: "./dist", match: /src/, label: "app" }],
8
+ * });
9
+ *
10
+ * jxplatform/server — Jx development server
11
+ *
12
+ * Provides builds, live reload, $src module proxying, timing: "server" function
13
+ * proxying, and studio filesystem integration as a single createDevServer() call.
14
+ */
15
+
16
+ import { resolve, join } from "node:path";
17
+ import { buildAll } from "./build.js";
18
+ import { createWatcher, injectSSE } from "./watch.js";
19
+ import { handleResolve, handleServerFunction } from "./resolve.js";
20
+ import { handleStudioApi } from "./studio-api.js";
21
+ import { handleCodeApi } from "./code-api.js";
22
+ import { existsSync, readFileSync } from "node:fs";
23
+
24
+ /**
25
+ * Resolve an npm-style bare specifier from a URL path via node_modules. Handles scoped packages
26
+ * (@scope/pkg/subpath) and respects package.json exports. Strips leading directory segments (e.g.
27
+ * /pages/@scope/pkg/file → @scope/pkg/file).
28
+ *
29
+ * @param {string} root - Absolute project root
30
+ * @param {string} urlPath - URL pathname (e.g. "/pages/@jxplatform/parser/Foo.class.json")
31
+ * @returns {string | null} Absolute file path or null
32
+ */
33
+ function resolveNpmPath(root, urlPath) {
34
+ let segments = urlPath.split("/").filter(Boolean);
35
+
36
+ // If "node_modules" appears in the path, use everything before it as a subdirectory
37
+ // prefix and everything after as the package specifier.
38
+ // e.g. /examples/demo/node_modules/@scope/pkg → root=root/examples/demo, pkg=@scope/pkg
39
+ const nmIdx = segments.indexOf("node_modules");
40
+ if (nmIdx >= 0) {
41
+ if (nmIdx > 0) root = join(root, ...segments.slice(0, nmIdx));
42
+ segments = segments.slice(nmIdx + 1);
43
+ }
44
+
45
+ // Find the package start — either @scope/pkg or unscoped pkg
46
+ let start = -1;
47
+ let isScoped = false;
48
+ for (let i = 0; i < segments.length; i++) {
49
+ if (segments[i].startsWith("@")) {
50
+ start = i;
51
+ isScoped = true;
52
+ break;
53
+ }
54
+ }
55
+
56
+ /** @type {string} */
57
+ let pkgDir = "";
58
+ /** @type {string} */
59
+ let subpath = "";
60
+
61
+ if (isScoped) {
62
+ if (start < 0 || start + 1 >= segments.length) return null;
63
+ const scope = segments[start];
64
+ const pkg = segments[start + 1];
65
+ subpath = segments.slice(start + 2).join("/");
66
+ pkgDir = join(root, "node_modules", scope, pkg);
67
+ } else {
68
+ // Unscoped: try each segment as a package name in node_modules
69
+ for (let i = 0; i < segments.length; i++) {
70
+ const candidate = join(root, "node_modules", segments[i]);
71
+ if (existsSync(join(candidate, "package.json"))) {
72
+ start = i;
73
+ pkgDir = candidate;
74
+ subpath = segments.slice(i + 1).join("/");
75
+ break;
76
+ }
77
+ }
78
+ if (start < 0) return null;
79
+ }
80
+
81
+ const pkgJsonPath = join(pkgDir, "package.json");
82
+ if (!existsSync(pkgJsonPath)) return null;
83
+
84
+ // If there's a subpath, check package.json exports first
85
+ if (subpath) {
86
+ try {
87
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
88
+ const exportKey = `./${subpath}`;
89
+ if (pkgJson.exports && pkgJson.exports[exportKey]) {
90
+ const mapped = join(pkgDir, pkgJson.exports[exportKey]);
91
+ if (existsSync(mapped)) return mapped;
92
+ }
93
+ } catch {}
94
+ // Fall back to direct path
95
+ const direct = join(pkgDir, subpath);
96
+ if (existsSync(direct)) return direct;
97
+ }
98
+
99
+ // Bare package (no subpath): resolve entry point
100
+ try {
101
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
102
+ const exp = pkgJson.exports?.["."];
103
+ const entry =
104
+ (typeof exp === "object" ? (exp.import ?? exp.default) : exp) ??
105
+ pkgJson.module ??
106
+ pkgJson.main;
107
+ if (entry && typeof entry === "string") {
108
+ const resolved = join(pkgDir, entry);
109
+ if (existsSync(resolved)) return resolved;
110
+ }
111
+ } catch {}
112
+
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Create and start a Jx development server.
118
+ *
119
+ * @param {object} options
120
+ * @param {string} options.root - Project root (absolute or relative)
121
+ * @param {number} [options.port] - Server port. Default is `3000`
122
+ * @param {{
123
+ * entrypoints: string[];
124
+ * outdir: string;
125
+ * match?: Function | RegExp;
126
+ * label?: string;
127
+ * }[]} [options.builds]
128
+ * - Bun.build entries with optional match regex
129
+ * @param {boolean | object} [options.watch] - Watch config or false to disable. Default is `true`
130
+ * @param {boolean} [options.studio] - Enable /**studio/* endpoints. Default is `true`
131
+ * @param {Function} [options.middleware] - Custom route handler (req, url) => Response|null
132
+ * @returns {Promise<object>} The Bun.serve server object
133
+ */
134
+ export async function createDevServer(options) {
135
+ const {
136
+ root,
137
+ port = 3000,
138
+ builds = [],
139
+ watch = true,
140
+ studio: enableStudio = true,
141
+ middleware,
142
+ } = options;
143
+
144
+ if (!root) throw new Error("@jxplatform/server: root is required");
145
+ const absRoot = resolve(root);
146
+
147
+ // ─── Build pipeline ─────────────────────────────────────────────────────────
148
+
149
+ if (builds.length > 0) {
150
+ await buildAll(builds);
151
+ }
152
+
153
+ // ─── File watcher + SSE ─────────────────────────────────────────────────────
154
+
155
+ let handleSSE = null;
156
+ if (watch !== false) {
157
+ const watchOpts = typeof watch === "object" ? watch : {};
158
+ const watcher = createWatcher(absRoot, builds, watchOpts);
159
+ handleSSE = watcher.handleSSE;
160
+ }
161
+
162
+ // Bundle cache for npm packages (bare specifier → bundled JS)
163
+ /** @type {Map<string, string>} */
164
+ const bundleCache = new Map();
165
+
166
+ // ─── HTTP server ────────────────────────────────────────────────────────────
167
+
168
+ const server = Bun.serve({
169
+ port,
170
+
171
+ async fetch(req) {
172
+ const url = new URL(req.url);
173
+ let path = url.pathname;
174
+ if (path.endsWith("/")) path += "index.html";
175
+ else if (path === "") path = "/index.html";
176
+
177
+ // SSE live reload
178
+ if (handleSSE && path === "/__reload") {
179
+ return handleSSE();
180
+ }
181
+
182
+ // $prototype + $src proxy
183
+ if (path === "/__jx_resolve__" && req.method === "POST") {
184
+ return handleResolve(req, absRoot);
185
+ }
186
+
187
+ // timing: "server" function proxy
188
+ if (path === "/__jx_server__" && req.method === "POST") {
189
+ return handleServerFunction(req, absRoot);
190
+ }
191
+
192
+ // Studio filesystem API
193
+ if (enableStudio && path.startsWith("/__studio/")) {
194
+ const codeRes = await handleCodeApi(req, url);
195
+ if (codeRes) return codeRes;
196
+
197
+ const res = await handleStudioApi(req, url, absRoot);
198
+ if (res) return res;
199
+ }
200
+
201
+ // Custom middleware
202
+ if (middleware) {
203
+ const res = await middleware(req, url);
204
+ if (res) return res;
205
+ }
206
+
207
+ // Static files
208
+ const file = Bun.file(resolve(absRoot, "." + path));
209
+ if (!(await file.exists())) {
210
+ // Resolve npm-style bare specifiers via node_modules.
211
+ // Bundle on-demand so internal bare specifiers (e.g. lit/...) resolve.
212
+ const resolved = resolveNpmPath(absRoot, path);
213
+ if (resolved) {
214
+ const cacheKey = resolved;
215
+ if (!bundleCache.has(cacheKey)) {
216
+ try {
217
+ const result = await Bun.build({
218
+ entrypoints: [resolved],
219
+ format: "esm",
220
+ minify: false,
221
+ });
222
+ if (result.success && result.outputs.length > 0) {
223
+ bundleCache.set(cacheKey, await result.outputs[0].text());
224
+ }
225
+ } catch (/** @type {any} */ e) {
226
+ console.error("Bundle failed for", resolved, e);
227
+ }
228
+ }
229
+ const bundled = bundleCache.get(cacheKey);
230
+ if (bundled) {
231
+ return new Response(bundled, {
232
+ headers: { "Content-Type": "application/javascript; charset=utf-8" },
233
+ });
234
+ }
235
+ }
236
+ return new Response("Not found", { status: 404 });
237
+ }
238
+
239
+ if (handleSSE && path.endsWith(".html")) {
240
+ const html = await file.text();
241
+ return new Response(injectSSE(html), {
242
+ headers: { "Content-Type": "text/html; charset=utf-8" },
243
+ });
244
+ }
245
+
246
+ return new Response(file);
247
+ },
248
+ });
249
+
250
+ console.log(`\n@jxplatform/server listening on http://localhost:${server.port}`);
251
+
252
+ return server;
253
+ }