@jxsuite/site 2.3.0 → 2.4.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 +3 -3
- package/src/compose.ts +11 -9
- package/src/paths.ts +2 -1
- package/src/serve.ts +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/site",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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.0
|
|
36
|
-
"@jxsuite/schema": "^5.
|
|
35
|
+
"@jxsuite/runtime": "^6.2.0",
|
|
36
|
+
"@jxsuite/schema": "^5.3.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 {
|
|
33
|
+
import { isNativeDocumentPath, parseNativeDocument } from "@jxsuite/schema/jxn";
|
|
34
34
|
import type { JxDocument, JxElement, JxHeadEntry, ProjectConfig } from "@jxsuite/schema/types";
|
|
35
|
+
import { PROJECT_FILE_NAME } 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,15 +89,15 @@ async function readDocument(io: SiteIO, path: string): Promise<JxDocument | null
|
|
|
88
89
|
if (text === null) {
|
|
89
90
|
return null;
|
|
90
91
|
}
|
|
91
|
-
if (path
|
|
92
|
-
return
|
|
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
98
|
/** The project's configuration, or an empty one when it has none this host can read. */
|
|
98
99
|
export async function readProjectConfig(io: SiteIO): Promise<ProjectConfig> {
|
|
99
|
-
const text = await io.read(
|
|
100
|
+
const text = await io.read(PROJECT_FILE_NAME);
|
|
100
101
|
if (text === null) {
|
|
101
102
|
return {};
|
|
102
103
|
}
|
|
@@ -137,7 +138,7 @@ export function unionElements(
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
/** A component file in the project's own library, keyed by the tag it defines. */
|
|
140
|
-
const COMPONENT_FILE = /^components\/([^/]+-[^/]*)\.json$/;
|
|
141
|
+
const COMPONENT_FILE = /^components\/([^/]+-[^/]*)\.(?:json|jxn)$/;
|
|
141
142
|
|
|
142
143
|
/**
|
|
143
144
|
* A `$ref`'s project path, so the same file written from two places is one key.
|
|
@@ -188,9 +189,10 @@ function collectTagNames(node: JxElement | string, into: Set<string>): void {
|
|
|
188
189
|
* for free by scanning HTML it has already rendered; here the definition has to be opened. It is
|
|
189
190
|
* bounded by the components a page actually reaches, and each is opened once.
|
|
190
191
|
*
|
|
191
|
-
* Only
|
|
192
|
-
*
|
|
193
|
-
*
|
|
192
|
+
* Only native components are discoverable, because a `$ref` is resolved by the browser: the runtime
|
|
193
|
+
* fetches the file and parses it as a document, so a component in a format needing an extension's
|
|
194
|
+
* parser cannot be registered this way whoever discovers it. A `.jxn` component qualifies because
|
|
195
|
+
* this origin answers it as JSON (`serve.ts`).
|
|
194
196
|
*/
|
|
195
197
|
async function discoverElements(io: SiteIO, doc: JxDocument): Promise<{ $ref: string }[]> {
|
|
196
198
|
const available = new Map<string, string>();
|
|
@@ -294,7 +296,7 @@ export async function composePage(
|
|
|
294
296
|
const pageDoc = await readDocument(io, route.sourcePath);
|
|
295
297
|
if (!pageDoc) {
|
|
296
298
|
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.`,
|
|
299
|
+
`${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
300
|
);
|
|
299
301
|
}
|
|
300
302
|
|
package/src/paths.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PROJECT_FILE_NAME } 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
|
-
|
|
45
|
+
PROJECT_FILE_NAME,
|
|
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,25 @@ 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.
|
|
153
|
+
*/
|
|
154
|
+
function jxnAsJson(bytes: Uint8Array<ArrayBuffer>, path: string): Response {
|
|
155
|
+
try {
|
|
156
|
+
const document = parseJxn(new TextDecoder().decode(bytes), path);
|
|
157
|
+
return Response.json(document, { headers: siteHeaders("application/json;charset=utf-8") });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return new Response(errorMessage(error), {
|
|
160
|
+
headers: siteHeaders("text/plain;charset=utf-8"),
|
|
161
|
+
status: 422,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
142
166
|
async function composeSafely(
|
|
143
167
|
io: SiteIO,
|
|
144
168
|
context: SiteContext,
|