@jxsuite/server 4.4.4 → 4.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/server",
3
- "version": "4.4.4",
3
+ "version": "4.5.0",
4
4
  "description": "Jx development server with live reload, server-side proxy, and studio integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,22 +44,22 @@
44
44
  "@jxsuite/ai": "^0.39.3",
45
45
  "@jxsuite/catalog": "^0.1.0",
46
46
  "@jxsuite/collab": "^0.11.1",
47
- "@jxsuite/compiler": "^5.1.0",
48
- "@jxsuite/create": "^1.3.13",
49
- "@jxsuite/import": "^0.40.7",
47
+ "@jxsuite/compiler": "^5.1.2",
48
+ "@jxsuite/create": "^1.3.14",
49
+ "@jxsuite/import": "^0.40.9",
50
50
  "@jxsuite/protocol": "^2.6.1",
51
- "@jxsuite/runtime": "^4.0.3",
51
+ "@jxsuite/runtime": "^4.1.0",
52
52
  "@jxsuite/schema": "^3.0.0",
53
- "@jxsuite/site": "^2.0.3",
54
- "@jxsuite/starters": "^2.0.0",
53
+ "@jxsuite/site": "^2.0.4",
54
+ "@jxsuite/starters": "^2.0.1",
55
55
  "chokidar": "^5.0.0",
56
56
  "kysely": "^0.29.6",
57
- "oxfmt": "^0.70.0",
57
+ "oxfmt": "^0.72.0",
58
58
  "zod": "^4.6.5"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@jxsuite/auth": "^0.6.2",
62
62
  "@jxsuite/connector": "^0.6.2",
63
- "@jxsuite/parser": "^2.0.0"
63
+ "@jxsuite/parser": "^2.0.1"
64
64
  }
65
65
  }
package/src/dev-vars.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * Dev-vars — `<project>/.dev.vars` parsing (wrangler convention, specs/extensions.md §13).
3
3
  *
4
4
  * Secret VALUES never enter project.json; locally they live in the git-ignored .dev.vars file and
5
- * the dev server merges them over process.env when constructing mount environments. The format is
5
+ * the dev server merges them over process.env when constructing the `env` it hands project code:
6
+ * the extension mounts' and the server-function proxy's (`projectDevEnv`). The format is
6
7
  * dotenv-shaped: KEY=VALUE lines, `#` comments, optional single/double quotes. The compiler CLI
7
8
  * keeps its own tiny copy (db-push.ts) — the compiler cannot depend on this package.
8
9
  */
@@ -59,3 +60,16 @@ export function loadDevVars(projectRoot: string): Record<string, string> {
59
60
  return {};
60
61
  }
61
62
  }
63
+
64
+ /**
65
+ * The `env` a development host hands project code that runs server-side: `process.env` merged under
66
+ * the project's `.dev.vars`, plus `JX_PROJECT_ROOT`. The extension mounts (server.md §3) and the
67
+ * server-function proxy (server.md §3.3) both build theirs here, so a server function reads the
68
+ * same secrets a mount does.
69
+ *
70
+ * @param {string} root - Absolute project root
71
+ * @returns {Record<string, string | undefined>}
72
+ */
73
+ export function projectDevEnv(root: string): Record<string, string | undefined> {
74
+ return { ...process.env, ...loadDevVars(root), JX_PROJECT_ROOT: root };
75
+ }
package/src/jx-mounts.ts CHANGED
@@ -7,7 +7,8 @@
7
7
  *
8
8
  * Dev conveniences over the production worker:
9
9
  *
10
- * - `env` is process.env merged under the project's `.dev.vars` plus `JX_PROJECT_ROOT`;
10
+ * - `env` is process.env merged under the project's `.dev.vars` plus `JX_PROJECT_ROOT`
11
+ * (`projectDevEnv`, which the server-function proxy shares);
11
12
  * - Connector classes with `local: "<provider>"` are stood in by the registry's class for that
12
13
  * provider (e.g. D1 → sqlite at `<project>/.jx/data/<connection>.sqlite`);
13
14
  * - Mount options set `autoSync: true`, so table schemas sync additively on first touch.
@@ -18,7 +19,7 @@
18
19
  import { existsSync, readFileSync, statSync } from "node:fs";
19
20
  import { resolve } from "node:path";
20
21
  import { buildProjectExtensionRegistry } from "@jxsuite/compiler/format-host";
21
- import { loadDevVars } from "./dev-vars.ts";
22
+ import { projectDevEnv } from "./dev-vars.ts";
22
23
  import type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
23
24
  import type { FormatEntry } from "@jxsuite/schema/format-registry";
24
25
  import type { ProjectConfig } from "@jxsuite/schema/types";
@@ -121,11 +122,7 @@ async function buildRuntime(
121
122
  }
122
123
 
123
124
  const connectors = await resolveConnectorStandins(registry);
124
- const env: Record<string, unknown> = {
125
- ...process.env,
126
- ...loadDevVars(projectRoot),
127
- JX_PROJECT_ROOT: projectRoot,
128
- };
125
+ const env: Record<string, unknown> = projectDevEnv(projectRoot);
129
126
 
130
127
  // One shared mutable context, passed to every mount in order (specs/extensions.md §11).
131
128
  const ctx: Record<string, unknown> = {};
package/src/resolve.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  /** Resolve.js — Generic $src module proxy + timing: "server" function proxy */
2
2
 
3
3
  import { siteBasePath } from "@jxsuite/schema/asset-paths";
4
- import { dirname, relative, resolve } from "node:path";
4
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
5
5
  import { errorMessage, parseClassDef } from "@jxsuite/schema/parse";
6
6
  import { existsSync, readFileSync, statSync } from "node:fs";
7
7
  import { createRequire } from "node:module";
8
+ import { projectDevEnv } from "./dev-vars.ts";
8
9
  import { containedPath } from "./net-guard.ts";
9
10
  import { buildProjectExtensionRegistry } from "@jxsuite/compiler/format-host";
10
11
  import { loadProjectSections } from "@jxsuite/compiler/project-sections";
@@ -56,6 +57,41 @@ function isImportable(p: string, root: string, activeProjectRoot: string | null)
56
57
  return false;
57
58
  }
58
59
 
60
+ /**
61
+ * The directory holding the document a `$base` names, which a relative `$src` resolves against.
62
+ *
63
+ * Hosts spell a base two ways. A page served at a site URL (`jx dev`, the live preview) names a
64
+ * path under `root`, and a single leading slash always means that, even when the path would also
65
+ * read as an absolute one: a root `/app` may hold a site directory `app/`. Studio's canvas
66
+ * addresses a project by its absolute filesystem path instead (`documentBase` in
67
+ * `packages/studio`), so its base arrives as `//abs/project/pages/x.json`, or
68
+ * `/C:/project/pages/x.json` on Windows: the spelling the static file lanes map back to the file
69
+ * (`serveProjectFile`). A `file:` URL's path is a filesystem path too. Those spellings name the
70
+ * path itself only when it lies under `root` or the active project, so any other path keeps meaning
71
+ * a site URL. The pathname is percent-decoded first, because a URL encodes a space in a directory
72
+ * name and the filesystem does not.
73
+ *
74
+ * @param {string} $base - The document URL the runtime sent
75
+ * @param {string} root - The root a site URL names a path under
76
+ * @param {string} projectRoot - The active project, which may sit outside `root`
77
+ * @returns {string} An absolute directory
78
+ */
79
+ function baseDirectory($base: string, root: string, projectRoot: string): string {
80
+ const url = new URL($base);
81
+ const pathname = decodeURIComponent(url.pathname);
82
+ const urlDir = pathname.slice(0, pathname.lastIndexOf("/") + 1);
83
+ const spellsPath = urlDir.startsWith("//") || /^\/[A-Za-z]:/.test(urlDir);
84
+ const fsDir = spellsPath ? urlDir.slice(1) : url.protocol === "file:" ? urlDir : null;
85
+ if (
86
+ fsDir !== null &&
87
+ isAbsolute(fsDir) &&
88
+ (containedPath(fsDir, root) !== null || containedPath(fsDir, projectRoot) !== null)
89
+ ) {
90
+ return resolve(fsDir);
91
+ }
92
+ return resolve(root, `.${urlDir}`);
93
+ }
94
+
59
95
  /** Per-project context cache, invalidated when project.json changes on disk. */
60
96
  const projectContextCache = new Map<
61
97
  string,
@@ -267,13 +303,20 @@ export async function handleResolve(
267
303
  }
268
304
 
269
305
  /**
270
- * Handle POST /**jx_server** — proxy timing: "server" function calls. In dev mode, the runtime
271
- * sends these instead of hitting the production Hono handler.
306
+ * Handle POST `/__jx_server__` — proxy timing: "server" function calls. It is the only way an
307
+ * interpreting runtime calls a server function (spec.md §11.4): the runtime never imports the
308
+ * module in the browser, it posts the call here, and the function runs server-side with `env`.
309
+ *
310
+ * A relative `$src` resolves against the directory of the document `$base` names (`baseDirectory`):
311
+ * a site URL's path under `root`, or the absolute path Studio's canvas sends. The imported module
312
+ * stays cached for as long as the host runs, so an edit to it takes effect when the host restarts.
272
313
  *
273
314
  * @param {Request} req
274
- * @param {string} root
315
+ * @param {string} root - The root a site-URL `$base`, or a `$src` without one, resolves under
316
+ * @param {string} [projectRoot] - The active project: the function's `env` carries its `.dev.vars`,
317
+ * and a module under it is importable as one under `root` is. `root` when omitted
275
318
  */
276
- export async function handleServerFunction(req: Request, root: string) {
319
+ export async function handleServerFunction(req: Request, root: string, projectRoot = root) {
277
320
  let body: ServerFunctionBody;
278
321
  try {
279
322
  body = (await req.json()) as ServerFunctionBody;
@@ -288,18 +331,14 @@ export async function handleServerFunction(req: Request, root: string) {
288
331
 
289
332
  let moduleAbsPath;
290
333
  try {
291
- if ($base) {
292
- const docUrlPath = new URL($base).pathname;
293
- const docDir = docUrlPath.slice(0, docUrlPath.lastIndexOf("/") + 1);
294
- moduleAbsPath = resolve(resolve(root, `.${docDir}`), $src);
295
- } else {
296
- moduleAbsPath = resolve(root, $src);
297
- }
334
+ moduleAbsPath = $base
335
+ ? resolve(baseDirectory($base, root, projectRoot), $src)
336
+ : resolve(root, $src);
298
337
  } catch (error) {
299
338
  return problem("invalidRequest", `Cannot resolve $src: ${errorMessage(error)}`);
300
339
  }
301
340
 
302
- if (!isImportable(moduleAbsPath, root, null)) {
341
+ if (!isImportable(moduleAbsPath, root, projectRoot)) {
303
342
  return new Response(`$src "${$src}" escapes the project root`, { status: 403 });
304
343
  }
305
344
 
@@ -320,11 +359,12 @@ export async function handleServerFunction(req: Request, root: string) {
320
359
  }
321
360
 
322
361
  try {
323
- // Match the production contract `fn(args, env)` (compiler emits `fn(args, c.env)`): the dev
324
- // Proxy runs server-side, so `process.env` is the analogous environment binding.
362
+ /* Match the production contract `fn(args, env)` (compiler emits `fn(args, c.env)`): the dev
363
+ proxy runs server-side, so its `env` is the extension mounts' own, `process.env` merged under
364
+ the project's `.dev.vars` plus `JX_PROJECT_ROOT` (server.md §3.3). */
325
365
  const result = await (fn as (args: Record<string, unknown>, env: unknown) => unknown)(
326
366
  args,
327
- process.env,
367
+ projectDevEnv(projectRoot),
328
368
  );
329
369
  return Response.json(result ?? null);
330
370
  } catch (error) {
package/src/server.ts CHANGED
@@ -359,9 +359,11 @@ export async function createDevServer(options: {
359
359
  return handleResolve(req, absRoot, activeProjectRoot);
360
360
  }
361
361
 
362
- // Timing: "server" function proxy
362
+ /* Timing: "server" function proxy. Its `env` carries the active project's `.dev.vars`, as the
363
+ extension mounts' does below, and a module under that project imports even when it sits
364
+ outside absRoot, as the static lanes below serve it. */
363
365
  if (path === "/__jx_server__" && req.method === "POST") {
364
- return handleServerFunction(req, absRoot);
366
+ return handleServerFunction(req, absRoot, activeProjectRoot ?? absRoot);
365
367
  }
366
368
 
367
369
  // Extension server mounts (/_jx/data etc.) — registry-driven, same wire contract as the