@jxsuite/site 2.3.1 → 2.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/site",
3
- "version": "2.3.1",
3
+ "version": "2.5.0",
4
4
  "description": "What a Jx project's page at a route consists of — routing, layout, context, head and the document that boots it",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,7 +32,7 @@
32
32
  "test:coverage": "bun test --isolate --coverage"
33
33
  },
34
34
  "dependencies": {
35
- "@jxsuite/runtime": "^6.1.0",
36
- "@jxsuite/schema": "^5.2.0"
35
+ "@jxsuite/runtime": "^6.2.1",
36
+ "@jxsuite/schema": "^5.4.0"
37
37
  }
38
38
  }
package/src/compose.ts CHANGED
@@ -30,8 +30,9 @@ import type { HeadMergeContext } from "./head-merger.ts";
30
30
  import { injectContext } from "./context.ts";
31
31
  import { isNonNullObject, isString } from "@jxsuite/schema/guards";
32
32
  import { localeDirection, localeOfRoute, resolveI18n } from "@jxsuite/schema/locale";
33
- import { parseJxDocument } from "@jxsuite/schema/parse";
33
+ import { isNativeDocumentPath, parseNativeDocument } from "@jxsuite/schema/jxn";
34
34
  import type { JxDocument, JxElement, JxHeadEntry, ProjectConfig } from "@jxsuite/schema/types";
35
+ import { PROJECT_FILE_NAMES, parseProjectSource } from "@jxsuite/schema/project-file";
35
36
 
36
37
  /** Everything the composer settled about a page before a renderer sees it. */
37
38
  export interface ComposedPage {
@@ -88,23 +89,34 @@ async function readDocument(io: SiteIO, path: string): Promise<JxDocument | null
88
89
  if (text === null) {
89
90
  return null;
90
91
  }
91
- if (path.endsWith(".json")) {
92
- return parseJxDocument(text, path);
92
+ if (isNativeDocumentPath(path)) {
93
+ return parseNativeDocument(text, path);
93
94
  }
94
95
  return io.parse ? ((await io.parse(path, text)) ?? null) : null;
95
96
  }
96
97
 
97
- /** The project's configuration, or an empty one when it has none this host can read. */
98
+ /**
99
+ * The project's configuration, or an empty one when it has none this host can read.
100
+ *
101
+ * Both names are read, because a host that can only fetch has no other way to know which one a
102
+ * project has. A project with both has neither, as it has everywhere else (`site-architecture.md`
103
+ * §2.3).
104
+ */
98
105
  export async function readProjectConfig(io: SiteIO): Promise<ProjectConfig> {
99
- const text = await io.read("project.json");
100
- if (text === null) {
106
+ const texts = await Promise.all(PROJECT_FILE_NAMES.map((name) => io.read(name)));
107
+ const found = PROJECT_FILE_NAMES.flatMap((name, index) => {
108
+ const text = texts[index];
109
+ return text === null || text === undefined ? [] : [{ name, text }];
110
+ });
111
+ const [only] = found;
112
+ if (!only || found.length > 1) {
101
113
  return {};
102
114
  }
103
115
  try {
104
- /* SAFETY: only the syntax is checked here, deliberately: every ProjectConfig member is
105
- optional, and shape checking is `validateProject`'s job against the per-project schema,
106
- which `jx build` runs and a live preview of a half-edited config must not refuse over. */
107
- return JSON.parse(text) as ProjectConfig;
116
+ /* Only the syntax is checked here, deliberately: every ProjectConfig member is optional, and
117
+ shape checking is `validateProject`'s job against the per-project schema, which `jx build`
118
+ runs and a live preview of a half-edited config must not refuse over. */
119
+ return parseProjectSource(only.text, only.name);
108
120
  } catch {
109
121
  /* A project whose config does not parse still has pages, and refusing to render any of them
110
122
  would hide the one error the author can act on behind a blank origin. */
@@ -137,7 +149,7 @@ export function unionElements(
137
149
  }
138
150
 
139
151
  /** A component file in the project's own library, keyed by the tag it defines. */
140
- const COMPONENT_FILE = /^components\/([^/]+-[^/]*)\.json$/;
152
+ const COMPONENT_FILE = /^components\/([^/]+-[^/]*)\.(?:json|jxn)$/;
141
153
 
142
154
  /**
143
155
  * A `$ref`'s project path, so the same file written from two places is one key.
@@ -188,9 +200,10 @@ function collectTagNames(node: JxElement | string, into: Set<string>): void {
188
200
  * for free by scanning HTML it has already rendered; here the definition has to be opened. It is
189
201
  * bounded by the components a page actually reaches, and each is opened once.
190
202
  *
191
- * Only `.json` components are discoverable, because a `$ref` is resolved by the browser: the
192
- * runtime fetches the file and parses it as a document, so a component in a format needing an
193
- * extension's parser cannot be registered this way whoever discovers it.
203
+ * Only native components are discoverable, because a `$ref` is resolved by the browser: the runtime
204
+ * fetches the file and parses it as a document, so a component in a format needing an extension's
205
+ * parser cannot be registered this way whoever discovers it. A `.jxn` component qualifies because
206
+ * this origin answers it as JSON (`serve.ts`).
194
207
  */
195
208
  async function discoverElements(io: SiteIO, doc: JxDocument): Promise<{ $ref: string }[]> {
196
209
  const available = new Map<string, string>();
@@ -294,7 +307,7 @@ export async function composePage(
294
307
  const pageDoc = await readDocument(io, route.sourcePath);
295
308
  if (!pageDoc) {
296
309
  throw new ComposeError(
297
- `${route.sourcePath} could not be read as a page. Only .json pages render here; other formats need their extension's parser, which this host does not run.`,
310
+ `${route.sourcePath} could not be read as a page. Only .json and .jxn pages render here; other formats need their extension's parser, which this host does not run.`,
298
311
  );
299
312
  }
300
313
 
package/src/paths.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { PROJECT_FILE_NAMES } from "@jxsuite/schema/project-file";
1
2
  /**
2
3
  * What a site origin serves, and — the part that matters — what it refuses.
3
4
  *
@@ -41,7 +42,7 @@ export const SERVABLE_ROOTS: readonly string[] = [
41
42
  * them twice is nothing.
42
43
  */
43
44
  export const NEVER_SERVABLE: ReadonlySet<string> = new Set([
44
- "project.json",
45
+ ...PROJECT_FILE_NAMES,
45
46
  "package.json",
46
47
  "package-lock.json",
47
48
  "bun.lock",
package/src/serve.ts CHANGED
@@ -13,6 +13,8 @@
13
13
  * site legitimately asks for that this package cannot produce.
14
14
  */
15
15
 
16
+ import { isJxnPath, parseJxn } from "@jxsuite/schema/jxn";
17
+ import { errorMessage } from "@jxsuite/schema/parse";
16
18
  import { candidatePaths, contentTypeFor, normalizeRequestPath } from "./paths.ts";
17
19
  import { ComposeError, composeRoute, readProjectConfig, routeTable } from "./compose.ts";
18
20
  import type { PageRoute, SiteIO } from "./compose.ts";
@@ -104,6 +106,9 @@ export async function serveSite(
104
106
  // A file the tree really has, at the path the published site would serve it from.
105
107
  for (const candidate of candidatePaths(requestPath)) {
106
108
  const bytes = await assets.bytes(candidate);
109
+ if (bytes && isJxnPath(candidate)) {
110
+ return jxnAsJson(bytes, candidate);
111
+ }
107
112
  if (bytes) {
108
113
  return new Response(bytes, {
109
114
  headers: siteHeaders(contentTypeFor(candidate)),
@@ -139,6 +144,34 @@ export async function serveSite(
139
144
  * A render that 500s tells the author nothing they can act on. A page naming the file and the
140
145
  * reason is the difference between "the preview is broken" and "this page needs a `.json` layout".
141
146
  */
147
+ /**
148
+ * A `.jxn` file, as the JSON a reader expects.
149
+ *
150
+ * The runtime fetches a `$ref` and calls `res.json()`, on any origin, so answering JSON here keeps
151
+ * a KDL parser out of every page. A file that does not read answers 422 with the reason, because
152
+ * the runtime reports a failed fetch by its status and this one is the author's to fix. The dev
153
+ * server's canvas origin answers the same way, with its own headers.
154
+ *
155
+ * @param bytes The file.
156
+ * @param path Its path, which names it in an error.
157
+ * @param headers The headers for a response of a media type.
158
+ */
159
+ export function jxnAsJson(
160
+ bytes: Uint8Array<ArrayBuffer>,
161
+ path: string,
162
+ headers: (type: string) => HeadersInit = siteHeaders,
163
+ ): Response {
164
+ try {
165
+ const document = parseJxn(new TextDecoder().decode(bytes), path);
166
+ return Response.json(document, { headers: headers("application/json;charset=utf-8") });
167
+ } catch (error) {
168
+ return new Response(errorMessage(error), {
169
+ headers: headers("text/plain;charset=utf-8"),
170
+ status: 422,
171
+ });
172
+ }
173
+ }
174
+
142
175
  async function composeSafely(
143
176
  io: SiteIO,
144
177
  context: SiteContext,