@jxsuite/server 1.2.0 → 1.3.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 +12 -12
- package/src/code-api.ts +35 -7
- package/src/net-guard.ts +19 -3
- package/src/project-server.ts +4 -3
- package/src/resolve.ts +29 -4
- package/src/server.ts +21 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Jx development server with live reload, server-side proxy, and studio integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,21 +35,21 @@
|
|
|
35
35
|
"test:coverage": "bun test --isolate --coverage"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jxsuite/collab": "^0.5.
|
|
39
|
-
"@jxsuite/compiler": "^1.
|
|
40
|
-
"@jxsuite/create": "^1.2.
|
|
41
|
-
"@jxsuite/import": "^0.38.
|
|
42
|
-
"@jxsuite/protocol": "^0.6.
|
|
43
|
-
"@jxsuite/runtime": "^1.3.
|
|
44
|
-
"@jxsuite/schema": "^1.
|
|
45
|
-
"@jxsuite/starters": "^1.2.
|
|
38
|
+
"@jxsuite/collab": "^0.5.1",
|
|
39
|
+
"@jxsuite/compiler": "^1.4.0",
|
|
40
|
+
"@jxsuite/create": "^1.2.1",
|
|
41
|
+
"@jxsuite/import": "^0.38.1",
|
|
42
|
+
"@jxsuite/protocol": "^0.6.1",
|
|
43
|
+
"@jxsuite/runtime": "^1.3.1",
|
|
44
|
+
"@jxsuite/schema": "^1.4.0",
|
|
45
|
+
"@jxsuite/starters": "^1.2.1",
|
|
46
46
|
"chokidar": "^5.0.0",
|
|
47
47
|
"kysely": "^0.29.4",
|
|
48
48
|
"zod": "^4.4.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@jxsuite/auth": "^0.4.
|
|
52
|
-
"@jxsuite/connector": "^0.4.
|
|
53
|
-
"@jxsuite/parser": "^1.
|
|
51
|
+
"@jxsuite/auth": "^0.4.1",
|
|
52
|
+
"@jxsuite/connector": "^0.4.1",
|
|
53
|
+
"@jxsuite/parser": "^1.4.0"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/code-api.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
10
|
-
import {
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
11
12
|
import { unlink } from "node:fs/promises";
|
|
12
13
|
import { format } from "oxfmt";
|
|
13
14
|
|
|
@@ -26,11 +27,32 @@ interface CodeApiBody {
|
|
|
26
27
|
args?: string[];
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const OXLINT_NAME = process.platform === "win32" ? "oxlint.exe" : "oxlint";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Locate the oxlint CLI: explicit JX_OXLINT_BIN override (empty value = disable lint outright),
|
|
34
|
+
* then a walk up from this module probing node_modules/.bin (finds the workspace root in the
|
|
35
|
+
* monorepo and the hoisted bin for npm consumers), then PATH. Null when oxlint is unavailable —
|
|
36
|
+
* e.g. the packaged desktop app, where the lint endpoint quietly returns no diagnostics.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveOxlintBin(startDir: string = import.meta.dir): string | null {
|
|
39
|
+
const override = process.env.JX_OXLINT_BIN;
|
|
40
|
+
if (override !== undefined) {
|
|
41
|
+
return override || null;
|
|
42
|
+
}
|
|
43
|
+
let dir = startDir;
|
|
44
|
+
for (;;) {
|
|
45
|
+
const candidate = join(dir, "node_modules", ".bin", OXLINT_NAME);
|
|
46
|
+
if (existsSync(candidate)) {
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
const parent = dirname(dir);
|
|
50
|
+
if (parent === dir) {
|
|
51
|
+
return Bun.which("oxlint", { PATH: process.env.PATH ?? "" });
|
|
52
|
+
}
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
34
56
|
|
|
35
57
|
// ─── Wrapper utilities ───────────────────────────────────────────────────────
|
|
36
58
|
|
|
@@ -151,6 +173,12 @@ export async function handleCodeApi(req: Request, url: URL) {
|
|
|
151
173
|
return Response.json({ diagnostics: [] });
|
|
152
174
|
}
|
|
153
175
|
|
|
176
|
+
const oxlintBin = resolveOxlintBin();
|
|
177
|
+
if (!oxlintBin) {
|
|
178
|
+
// No oxlint anywhere (packaged desktop app) — lint quietly disables.
|
|
179
|
+
return Response.json({ diagnostics: [] });
|
|
180
|
+
}
|
|
181
|
+
|
|
154
182
|
const wrapped = wrapBody(code, args);
|
|
155
183
|
const headerLen = wrapped.indexOf("\n") + 1;
|
|
156
184
|
const tmpFile = join(
|
|
@@ -160,7 +188,7 @@ export async function handleCodeApi(req: Request, url: URL) {
|
|
|
160
188
|
|
|
161
189
|
try {
|
|
162
190
|
await Bun.write(tmpFile, wrapped);
|
|
163
|
-
const proc = Bun.spawn([
|
|
191
|
+
const proc = Bun.spawn([oxlintBin, "--format=json", "-A", "no-unused-vars", tmpFile], {
|
|
164
192
|
stderr: "pipe",
|
|
165
193
|
stdout: "pipe",
|
|
166
194
|
});
|
package/src/net-guard.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
20
20
|
import { realpathSync } from "node:fs";
|
|
21
|
+
import { resolveAssetUrl } from "@jxsuite/schema/asset-paths";
|
|
22
|
+
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
21
23
|
|
|
22
24
|
export const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
23
25
|
|
|
@@ -151,14 +153,28 @@ export async function serveContained(absPath: string, root: string): Promise<Res
|
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
/**
|
|
154
|
-
* Try to serve a project file at its natural URL:
|
|
155
|
-
* public/. Each candidate goes through containedPath
|
|
156
|
-
* URL pathname (leading slash, forward slashes).
|
|
156
|
+
* Try to serve a project file at its natural URL: an extension asset mount, then
|
|
157
|
+
* absolute-under-root, then root-relative, then public/. Each candidate goes through containedPath
|
|
158
|
+
* (realpath). `decodedPath` is the once-decoded URL pathname (leading slash, forward slashes).
|
|
159
|
+
*
|
|
160
|
+
* Mounts come first and are contained against their own directory rather than the project root:
|
|
161
|
+
* they exist precisely to publish directories that may sit outside it (extensions.md §8.5).
|
|
157
162
|
*/
|
|
158
163
|
export async function serveProjectFile(
|
|
159
164
|
decodedPath: string,
|
|
160
165
|
root: string,
|
|
166
|
+
mounts: readonly AssetMount[] = [],
|
|
161
167
|
): Promise<Response | null> {
|
|
168
|
+
for (const mount of [...mounts].toSorted((a, b) => b.urlPrefix.length - a.urlPrefix.length)) {
|
|
169
|
+
const mounted = resolveAssetUrl([mount], decodedPath);
|
|
170
|
+
if (!mounted) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const res = await serveContained(mounted, mount.dir);
|
|
174
|
+
if (res) {
|
|
175
|
+
return res;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
162
178
|
// Map the URL pathname back to a filesystem path. On Windows the browser requests an absolute
|
|
163
179
|
// Path as /C:/Users/… (leading slash + forward slashes); drop the slash before the drive letter.
|
|
164
180
|
// A POSIX absolute path arrives as //abs/path.
|
package/src/project-server.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import { resolve, sep } from "node:path";
|
|
29
29
|
import { handleJxMounts } from "./jx-mounts.ts";
|
|
30
|
-
import { handleResolve, handleServerFunction } from "./resolve.ts";
|
|
30
|
+
import { handleResolve, handleServerFunction, projectAssetMounts } from "./resolve.ts";
|
|
31
31
|
import { handleAiApi } from "./ai-api.ts";
|
|
32
32
|
import { handleImportApi } from "./import-api.ts";
|
|
33
33
|
import { resolveNpmPath } from "./server.ts";
|
|
@@ -206,8 +206,9 @@ export function createProjectServer(options: CreateProjectServerOptions): Projec
|
|
|
206
206
|
return handleServerFunction(req, root);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
// 6. Project files at natural URLs.
|
|
210
|
-
|
|
209
|
+
// 6. Project files at natural URLs, including extension asset mounts (§8.5) — that is what
|
|
210
|
+
// Lets a canvas preview show an image a content entry references relative to itself.
|
|
211
|
+
const fileRes = await serveProjectFile(normPath, root, await projectAssetMounts(root));
|
|
211
212
|
if (fileRes) {
|
|
212
213
|
return fileRes;
|
|
213
214
|
}
|
package/src/resolve.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { createRequire } from "node:module";
|
|
|
7
7
|
import { containedPath } from "./net-guard.ts";
|
|
8
8
|
import { buildProjectExtensionRegistry } from "@jxsuite/compiler/format-host";
|
|
9
9
|
import { loadProjectSections } from "@jxsuite/compiler/project-sections";
|
|
10
|
+
import { loadAssetMounts } from "@jxsuite/compiler/asset-mounts";
|
|
11
|
+
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
10
12
|
import type { DynamicClass } from "@jxsuite/runtime/types";
|
|
11
13
|
import type { JxClassDef, ProjectConfig } from "@jxsuite/schema/types";
|
|
12
14
|
|
|
@@ -53,7 +55,10 @@ function isImportable(p: string, root: string, activeProjectRoot: string | null)
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** Per-project context cache, invalidated when project.json changes on disk. */
|
|
56
|
-
const projectContextCache = new Map<
|
|
58
|
+
const projectContextCache = new Map<
|
|
59
|
+
string,
|
|
60
|
+
{ mtime: number; context: Record<string, unknown>; mounts: AssetMount[] }
|
|
61
|
+
>();
|
|
57
62
|
|
|
58
63
|
/**
|
|
59
64
|
* Lazy-load project context (project.json + extension-loaded sections) for class instantiation.
|
|
@@ -64,6 +69,12 @@ const projectContextCache = new Map<string, { mtime: number; context: Record<str
|
|
|
64
69
|
* @returns {Promise<Record<string, unknown> | null>} `{ config, root, ...sections }` or null
|
|
65
70
|
*/
|
|
66
71
|
async function loadProjectContext(projectRoot: string) {
|
|
72
|
+
const entry = await loadProjectEntry(projectRoot);
|
|
73
|
+
return entry?.context ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Load (or reuse) the cached project context plus its extension asset mounts. */
|
|
77
|
+
async function loadProjectEntry(projectRoot: string) {
|
|
67
78
|
const projectJsonPath = resolve(projectRoot, "project.json");
|
|
68
79
|
if (!existsSync(projectJsonPath)) {
|
|
69
80
|
return null;
|
|
@@ -72,19 +83,33 @@ async function loadProjectContext(projectRoot: string) {
|
|
|
72
83
|
const { mtimeMs } = statSync(projectJsonPath);
|
|
73
84
|
const cached = projectContextCache.get(projectRoot);
|
|
74
85
|
if (cached && cached.mtime === mtimeMs) {
|
|
75
|
-
return cached
|
|
86
|
+
return cached;
|
|
76
87
|
}
|
|
77
88
|
const config = JSON.parse(readFileSync(projectJsonPath, "utf8")) as ProjectConfig;
|
|
78
89
|
const registry = await buildProjectExtensionRegistry(projectRoot, config);
|
|
79
90
|
const sections = await loadProjectSections(projectRoot, config, registry);
|
|
91
|
+
const { mounts } = await loadAssetMounts(registry, config, projectRoot);
|
|
80
92
|
const context = { config, root: projectRoot, ...sections };
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
const entry = { context, mounts, mtime: mtimeMs };
|
|
94
|
+
projectContextCache.set(projectRoot, entry);
|
|
95
|
+
return entry;
|
|
83
96
|
} catch {
|
|
84
97
|
return null;
|
|
85
98
|
}
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Extension asset mounts for a project (specs/extensions.md §8.5) — the directories the dev server
|
|
103
|
+
* serves at their site URLs, so content-relative images resolve exactly as they will once built.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} projectRoot
|
|
106
|
+
* @returns {Promise<AssetMount[]>} Mounts, or an empty list when there is no project here
|
|
107
|
+
*/
|
|
108
|
+
export async function projectAssetMounts(projectRoot: string): Promise<AssetMount[]> {
|
|
109
|
+
const entry = await loadProjectEntry(projectRoot);
|
|
110
|
+
return entry?.mounts ?? [];
|
|
111
|
+
}
|
|
112
|
+
|
|
88
113
|
/**
|
|
89
114
|
* Handle POST /**jx_resolve** — proxy $prototype + $src entries.
|
|
90
115
|
*
|
package/src/server.ts
CHANGED
|
@@ -18,9 +18,14 @@ import { buildAll } from "./build.ts";
|
|
|
18
18
|
import { createCollabRegistry } from "./collab.ts";
|
|
19
19
|
import { createWatcher, injectSSE } from "./watch.ts";
|
|
20
20
|
import { handleJxMounts } from "./jx-mounts.ts";
|
|
21
|
-
import { handleResolve, handleServerFunction } from "./resolve.ts";
|
|
21
|
+
import { handleResolve, handleServerFunction, projectAssetMounts } from "./resolve.ts";
|
|
22
22
|
import { assertAccessible, handleStudioApi } from "./studio-api.ts";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
containedPath,
|
|
25
|
+
decodeAndNormalizePath,
|
|
26
|
+
originHostGate,
|
|
27
|
+
serveProjectFile,
|
|
28
|
+
} from "./net-guard.ts";
|
|
24
29
|
import { handleCodeApi } from "./code-api.ts";
|
|
25
30
|
import { handleAiApi } from "./ai-api.ts";
|
|
26
31
|
import { handleImportApi } from "./import-api.ts";
|
|
@@ -403,6 +408,20 @@ export async function createDevServer(options: {
|
|
|
403
408
|
}
|
|
404
409
|
}
|
|
405
410
|
|
|
411
|
+
// Extension asset mounts (§8.5) publish directories that may sit outside the project root,
|
|
412
|
+
// Such as an external content source's co-located images. They resolve here exactly as they
|
|
413
|
+
// Will once the site is built, so dev and production render the same URLs.
|
|
414
|
+
if (activeProjectRoot) {
|
|
415
|
+
const mountRes = await serveProjectFile(
|
|
416
|
+
path,
|
|
417
|
+
activeProjectRoot,
|
|
418
|
+
await projectAssetMounts(activeProjectRoot),
|
|
419
|
+
);
|
|
420
|
+
if (mountRes) {
|
|
421
|
+
return new Response(mountRes.body, { headers: NO_CACHE });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
406
425
|
const rootSafe = containedPath(resolve(absRoot, `.${path}`), absRoot);
|
|
407
426
|
const file = rootSafe ? Bun.file(rootSafe) : null;
|
|
408
427
|
if (!file || !(await file.exists())) {
|