@jxsuite/schema 1.5.0 → 1.8.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/class-schema.json +1 -0
- package/defs/children-value.schema.ts +10 -1
- package/defs/class-def.schema.ts +1 -0
- package/defs/element-def.schema.ts +1 -1
- package/defs/expression-node.schema.ts +4 -1
- package/defs/external-class-def.schema.ts +20 -2
- package/defs/head-entry.schema.ts +6 -1
- package/defs/image-config.schema.ts +7 -0
- package/defs/index.ts +2 -1
- package/defs/project-config.schema.ts +299 -7
- package/defs/tag-expression.schema.ts +98 -0
- package/defs/tag-name.schema.ts +13 -1
- package/package.json +8 -3
- package/project-schema.json +441 -5
- package/schema.json +186 -11
- package/schemas/project.core.schema.json +401 -5
- package/src/asset-paths.ts +21 -0
- package/src/component-meta.ts +143 -0
- package/src/extension-registry.ts +12 -0
- package/src/format-registry.ts +32 -2
- package/src/guards.ts +62 -1
- package/src/ijson.ts +205 -0
- package/src/intl.ts +155 -0
- package/src/locale.ts +429 -0
- package/src/media-type.ts +217 -0
- package/src/parse.ts +78 -1
- package/src/schema.ts +56 -3
- package/types.ts +191 -4
package/src/parse.ts
CHANGED
|
@@ -4,10 +4,20 @@
|
|
|
4
4
|
* Raw `JSON.parse()` returns `any`, which silently disables checking for everything downstream.
|
|
5
5
|
* These helpers are the single sanctioned crossing point: they parse, structurally verify, and
|
|
6
6
|
* return the domain type — failures carry the source path.
|
|
7
|
+
*
|
|
8
|
+
* Being the single crossing point is also what makes it the right place to enforce I-JSON (RFC
|
|
9
|
+
* 7493). `JSON.parse` accepts documents that mean something other than what they say — a repeated
|
|
10
|
+
* key whose first value it discards, an integer it rounds — and a Jx document does not stay JSON:
|
|
11
|
+
* it round-trips through markdown frontmatter and a CRDT, each rebuilding the object from the
|
|
12
|
+
* parsed value. Whatever was dropped here is dropped for good.
|
|
13
|
+
*
|
|
14
|
+
* It is also where identifiers are normalized (UAX #31 §R4), for the same reason: a name that means
|
|
15
|
+
* one thing on one keyboard and another thing on another is a defect nothing downstream can see.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
import type { JxClassDef, JxDocument, ProjectConfig } from "../types";
|
|
10
19
|
import { isJsonObject } from "./guards";
|
|
20
|
+
import { describeIJsonProblem, findIJsonProblems } from "./ijson";
|
|
11
21
|
|
|
12
22
|
// ─── Error helpers ──────────────────────────────────────────────────────────────
|
|
13
23
|
|
|
@@ -21,6 +31,63 @@ export function toError(e: unknown): Error {
|
|
|
21
31
|
return e instanceof Error ? e : new Error(String(e));
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
// ─── Unicode normalization ──────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Text outside ASCII, which is the only text whose normalization form can differ. Testing for it
|
|
38
|
+
* first keeps this walk close to free on the pure-ASCII documents that are almost all of them.
|
|
39
|
+
*/
|
|
40
|
+
const NON_ASCII = /[^\p{ASCII}]/u;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Recursively put every key and every string value into Normalization Form C.
|
|
44
|
+
*
|
|
45
|
+
* **The defect this fixes.** A state name is an identifier: `"état"` declared in `state`,
|
|
46
|
+
* referenced as `${state.état}` in a template and as `#/state/état` in a `$ref`. Typed on macOS it
|
|
47
|
+
* arrives decomposed (`e` + U+0301); typed on Windows or pasted from most of the web it arrives
|
|
48
|
+
* precomposed (U+00E9). Those are two different JavaScript property names, so a document whose
|
|
49
|
+
* declaration and reference were typed on different machines builds cleanly, emits a valid bundle,
|
|
50
|
+
* and renders nothing — `state.état` is simply `undefined`. There is no error to see and no symptom
|
|
51
|
+
* to search for. UAX #31 §R4 exists for exactly this, and NFC is the form the web platform assumes
|
|
52
|
+
* everywhere else.
|
|
53
|
+
*
|
|
54
|
+
* **Why values are normalized too, and why that is not a content change.** Two canonically
|
|
55
|
+
* equivalent strings are, by the definition in UAX #15, the same text: a conforming renderer must
|
|
56
|
+
* display them identically, and no process may distinguish them. So normalizing a `textContent` is
|
|
57
|
+
* a no-op for meaning. It is also unavoidable, because Jx has no syntactic boundary between content
|
|
58
|
+
* and code — `"Café ${state.été}"` is one string carrying both, and normalizing only "identifier
|
|
59
|
+
* positions" would mean parsing every template to find them.
|
|
60
|
+
*
|
|
61
|
+
* **Why here and not on the source text.** `"état"` is pure ASCII until `JSON.parse` turns the
|
|
62
|
+
* escape into a combining mark, so normalizing the raw text would miss exactly the documents a
|
|
63
|
+
* generator wrote.
|
|
64
|
+
*
|
|
65
|
+
* @param {unknown} value - Any parsed JSON value
|
|
66
|
+
* @returns {unknown} The same shape, with all text in NFC
|
|
67
|
+
*/
|
|
68
|
+
function normalizeIdentifiers(value: unknown): unknown {
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
return NON_ASCII.test(value) ? value.normalize("NFC") : value;
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
return value.map((item) => normalizeIdentifiers(item));
|
|
74
|
+
}
|
|
75
|
+
if (isJsonObject(value)) {
|
|
76
|
+
const out: Record<string, unknown> = {};
|
|
77
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
78
|
+
/*
|
|
79
|
+
* A later key wins on collision, matching what `JSON.parse` does with a literal duplicate.
|
|
80
|
+
* It cannot happen through this boundary — I-JSON already rejected duplicate keys, and two
|
|
81
|
+
* keys differing only by normalization form are not duplicates to `JSON.parse` — so this is
|
|
82
|
+
* a statement about which is authoritative rather than a branch anything reaches.
|
|
83
|
+
*/
|
|
84
|
+
out[NON_ASCII.test(key) ? key.normalize("NFC") : key] = normalizeIdentifiers(entry);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
24
91
|
// ─── Parse helpers ──────────────────────────────────────────────────────────────
|
|
25
92
|
|
|
26
93
|
function parseObject(text: string, sourcePath: string, what: string): Record<string, unknown> {
|
|
@@ -35,7 +102,17 @@ function parseObject(text: string, sourcePath: string, what: string): Record<str
|
|
|
35
102
|
if (!isJsonObject(parsed)) {
|
|
36
103
|
throw new Error(`Invalid ${what} at ${sourcePath}: expected a JSON object`);
|
|
37
104
|
}
|
|
38
|
-
|
|
105
|
+
/*
|
|
106
|
+
* An error rather than a warning, because both problems are silent data loss and neither has a
|
|
107
|
+
* reading under which the author got what they wrote. The whole repository — 375 documents —
|
|
108
|
+
* was already clean when this landed, so this rejects mistakes rather than existing work.
|
|
109
|
+
*/
|
|
110
|
+
const problems = findIJsonProblems(text);
|
|
111
|
+
if (problems.length > 0) {
|
|
112
|
+
const described = problems.map((problem) => describeIJsonProblem(problem)).join("; ");
|
|
113
|
+
throw new Error(`Invalid ${what} at ${sourcePath}: ${described}`);
|
|
114
|
+
}
|
|
115
|
+
return normalizeIdentifiers(parsed) as Record<string, unknown>;
|
|
39
116
|
}
|
|
40
117
|
|
|
41
118
|
/** Parse a Jx document (.json page/layout/component source). */
|
package/src/schema.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { listAll as listElements } from "@webref/elements";
|
|
|
26
26
|
import css from "@webref/css";
|
|
27
27
|
import idl from "@webref/idl";
|
|
28
28
|
|
|
29
|
+
import { elementTagNameSchema, tagExpressionSchema } from "../defs/tag-expression.schema";
|
|
29
30
|
import { tagNameSchema } from "../defs/tag-name.schema";
|
|
30
31
|
import {
|
|
31
32
|
boolOrRefSchema,
|
|
@@ -287,6 +288,8 @@ export async function generateSchema() {
|
|
|
287
288
|
...tagNameSchema,
|
|
288
289
|
examples: [...tagExamples, "my-counter", "todo-app", "user-card"],
|
|
289
290
|
},
|
|
291
|
+
TagExpression: tagExpressionSchema,
|
|
292
|
+
ElementTagName: elementTagNameSchema,
|
|
290
293
|
JsonSchemaType: jsonSchemaTypeSchema,
|
|
291
294
|
HeadEntry: headEntrySchema,
|
|
292
295
|
ImageConfig: imageConfigSchema,
|
|
@@ -342,9 +345,16 @@ export async function generateSchema() {
|
|
|
342
345
|
},
|
|
343
346
|
$head: {
|
|
344
347
|
description:
|
|
345
|
-
"Page-level <head> entries. Array of
|
|
348
|
+
"Page-level <head> entries. Array of head entries for meta tags, " +
|
|
346
349
|
"link tags, script tags, etc. Merged with layout and site-level $head entries.",
|
|
347
|
-
|
|
350
|
+
/* `HeadEntry`, NOT `ElementDef`. These were typed as elements, which was harmless only
|
|
351
|
+
while an element's tagName was a plain name — the moment it could be chosen at creation,
|
|
352
|
+
a head tag could be too, and `head-merger.ts` splices one straight into the built page as
|
|
353
|
+
`<${tag} …>`. A head tag is static by nature (the runtime's `injectHead` builds a CSS
|
|
354
|
+
selector out of it before it ever creates anything), and `HeadEntry` already says so.
|
|
355
|
+
It had been an orphan def until now: its only inbound `$ref` was its own recursive
|
|
356
|
+
`children`. */
|
|
357
|
+
items: { $ref: "#/$defs/HeadEntry" },
|
|
348
358
|
type: "array",
|
|
349
359
|
},
|
|
350
360
|
$id: {
|
|
@@ -352,6 +362,30 @@ export async function generateSchema() {
|
|
|
352
362
|
examples: ["Counter", "TodoApp", "UserCard"],
|
|
353
363
|
type: "string",
|
|
354
364
|
},
|
|
365
|
+
$dir: {
|
|
366
|
+
description:
|
|
367
|
+
"Base direction for this page's <html> element, overriding `defaults.dir`. " +
|
|
368
|
+
"Right-to-left content renders left-to-right without it.",
|
|
369
|
+
enum: ["ltr", "rtl", "auto"],
|
|
370
|
+
type: "string",
|
|
371
|
+
},
|
|
372
|
+
$lang: {
|
|
373
|
+
description:
|
|
374
|
+
"Language tag for this page's <html> element, overriding `defaults.lang` " +
|
|
375
|
+
"(site-architecture.md §8.4).",
|
|
376
|
+
examples: ["en", "fr", "ar-EG"],
|
|
377
|
+
type: "string",
|
|
378
|
+
},
|
|
379
|
+
$translationKey: {
|
|
380
|
+
description:
|
|
381
|
+
"This page's identity across languages, overriding the one its path implies " +
|
|
382
|
+
"(site-architecture.md §13.5). Two routes are translations when their keys match; " +
|
|
383
|
+
"the default key is the path with the locale prefix removed, which is right whenever " +
|
|
384
|
+
"the paths are parallel and wrong for a localized slug — /fr-ca/a-propos/ shares " +
|
|
385
|
+
"nothing with /about/ until one of them says so.",
|
|
386
|
+
examples: ["about", "blog/hello-world"],
|
|
387
|
+
type: "string",
|
|
388
|
+
},
|
|
355
389
|
$layout: {
|
|
356
390
|
description:
|
|
357
391
|
"Layout reference for pages. String path to a layout JSON file, " +
|
|
@@ -359,6 +393,24 @@ export async function generateSchema() {
|
|
|
359
393
|
examples: ["./layouts/base.json"],
|
|
360
394
|
oneOf: [{ type: "string" }, { const: false, type: "boolean" }],
|
|
361
395
|
},
|
|
396
|
+
$shadow: {
|
|
397
|
+
description:
|
|
398
|
+
"Render this component into a shadow root instead of the light DOM (spec.md §16.6). " +
|
|
399
|
+
"Off unless set: a shadow root isolates the component's styles from the page and " +
|
|
400
|
+
"replaces the <slot> emulation with real slot distribution, so it changes how the " +
|
|
401
|
+
"component composes. Overrides `defaults.shadow` in both directions — `false` opts " +
|
|
402
|
+
"one component out of a project that opted in.",
|
|
403
|
+
oneOf: [
|
|
404
|
+
{ enum: ["open", "closed"], type: "string" },
|
|
405
|
+
{ const: false, type: "boolean" },
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
$sitemap: {
|
|
409
|
+
description:
|
|
410
|
+
"Set false to exclude this page from sitemap.xml (site-architecture.md §8.4.1). " +
|
|
411
|
+
"Every other page with a URL is included.",
|
|
412
|
+
type: "boolean",
|
|
413
|
+
},
|
|
362
414
|
$media: {
|
|
363
415
|
additionalProperties: { type: "string" },
|
|
364
416
|
description:
|
|
@@ -668,7 +720,8 @@ export async function validateWithSchema(
|
|
|
668
720
|
doc: Record<string, unknown>,
|
|
669
721
|
schema: Record<string, unknown>,
|
|
670
722
|
) {
|
|
671
|
-
let Ajv: AjvCtor
|
|
723
|
+
let Ajv: AjvCtor;
|
|
724
|
+
let addFormats: AddFormatsFn;
|
|
672
725
|
try {
|
|
673
726
|
// The generated schema is JSON Schema 2020-12, so use the matching Ajv build
|
|
674
727
|
// (the default `ajv` export is draft-07 and can't compile a 2020-12 schema).
|
package/types.ts
CHANGED
|
@@ -200,7 +200,11 @@ export interface JxServerFnDef {
|
|
|
200
200
|
export interface JxHeadEntry {
|
|
201
201
|
tagName: string;
|
|
202
202
|
attributes?: Record<string, string | boolean>;
|
|
203
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Element content. An **object** is a structured data block — the compiler serializes it to JSON
|
|
205
|
+
* inside the tag, which is how JSON-LD is authored (site-architecture.md §8.5).
|
|
206
|
+
*/
|
|
207
|
+
textContent?: string | Record<string, unknown>;
|
|
204
208
|
children?: (JxHeadEntry | string)[];
|
|
205
209
|
[key: string]: unknown;
|
|
206
210
|
}
|
|
@@ -212,6 +216,8 @@ export interface ImageConfig {
|
|
|
212
216
|
quality?: { webp?: number; avif?: number; jpeg?: number; png?: number };
|
|
213
217
|
sizes?: string;
|
|
214
218
|
lazyLoad?: boolean;
|
|
219
|
+
/** Wrap a multi-format image in a `<picture>` with one `<source>` per format. Default true. */
|
|
220
|
+
picture?: boolean;
|
|
215
221
|
service?: "build" | "cloudflare";
|
|
216
222
|
remoteDomains?: string[];
|
|
217
223
|
}
|
|
@@ -240,8 +246,26 @@ export interface JxStyle {
|
|
|
240
246
|
[property: string]: string | number | JxStyle | undefined;
|
|
241
247
|
}
|
|
242
248
|
|
|
249
|
+
/**
|
|
250
|
+
* A tag chosen when the element is created, from a set the schema enumerates.
|
|
251
|
+
*
|
|
252
|
+
* Both branches resolve to a `TagName`, so the candidates are readable without evaluating anything
|
|
253
|
+
* — which is what lets the compiler emit one template per candidate and `jx validate` refuse an
|
|
254
|
+
* illegal name at authoring time. See `defs/tag-expression.schema.ts` for why this is not a `${…}`
|
|
255
|
+
* template and not a third `$switch` spelling.
|
|
256
|
+
*/
|
|
257
|
+
export type JxTagExpression =
|
|
258
|
+
| { operator: "?:"; target: unknown; value: string; initial: string }
|
|
259
|
+
| { operator: "switch"; target: unknown; cases: Record<string, string>; default: string };
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* An ELEMENT's tag. The document root's and a head entry's stay `string` — see
|
|
263
|
+
* `defs/tag-expression.schema.ts`.
|
|
264
|
+
*/
|
|
265
|
+
export type JxElementTagName = string | { $expression: JxTagExpression };
|
|
266
|
+
|
|
243
267
|
export interface JxElement {
|
|
244
|
-
tagName?:
|
|
268
|
+
tagName?: JxElementTagName;
|
|
245
269
|
textContent?: string | null | JxRef;
|
|
246
270
|
innerHTML?: string;
|
|
247
271
|
/**
|
|
@@ -285,8 +309,19 @@ export interface JxMappedArray {
|
|
|
285
309
|
}
|
|
286
310
|
|
|
287
311
|
export interface JxDocument extends JxElement {
|
|
312
|
+
/**
|
|
313
|
+
* The custom element's NAME, always a literal.
|
|
314
|
+
*
|
|
315
|
+
* Narrowed from {@link JxElement.tagName} deliberately: this string becomes
|
|
316
|
+
* `customElements.define(…)`, the emitted module's file name and a CSS selector prefix, none of
|
|
317
|
+
* which can be chosen per instance. Carrying the rule in the type means `defineElement` and the
|
|
318
|
+
* compiler keep receiving a string without a positional guard anyone can forget to write.
|
|
319
|
+
*/
|
|
320
|
+
tagName?: string;
|
|
288
321
|
state?: Record<string, JxStateDefinition>;
|
|
289
322
|
$layout?: string | false;
|
|
323
|
+
/** This page's identity across languages, overriding the one its path implies (§13.5). */
|
|
324
|
+
$translationKey?: string;
|
|
290
325
|
$paths?: JxPathsDef;
|
|
291
326
|
$elements?: (JxElement | string)[];
|
|
292
327
|
$head?: JxHeadEntry[];
|
|
@@ -483,6 +518,114 @@ export type AdapterId = "static" | "cloudflare-pages" | "cloudflare-workers" | "
|
|
|
483
518
|
* Identifiers only — no secrets — so it travels with the repo and any Studio (local or cloud) can
|
|
484
519
|
* tell whether publishing is set up.
|
|
485
520
|
*/
|
|
521
|
+
/** `dist/_headers` output — the response headers only the build can know (RFC 9111, RFC 8246). */
|
|
522
|
+
/**
|
|
523
|
+
* CSP Level 3, as far as a static build can go.
|
|
524
|
+
*
|
|
525
|
+
* Off by default. Every other security header the build emits is safe to turn on for a site that
|
|
526
|
+
* has never seen one; a policy is the one that can leave a page blank, because it governs code the
|
|
527
|
+
* build cannot see — a third-party script that loads a second script, a widget that opens a frame.
|
|
528
|
+
* Turning it on is one line and one look at the browser console.
|
|
529
|
+
*/
|
|
530
|
+
export interface CspConfig {
|
|
531
|
+
/** `"report-only"` sends `Content-Security-Policy-Report-Only`. Default `"enforce"`. */
|
|
532
|
+
mode?: "enforce" | "report-only";
|
|
533
|
+
/**
|
|
534
|
+
* Replace or remove a computed directive. `false` removes it; a string replaces it wholesale, so
|
|
535
|
+
* an addition means restating the default's sources alongside the new one.
|
|
536
|
+
*/
|
|
537
|
+
directives?: Record<string, string | false>;
|
|
538
|
+
/** Where violation reports go. Emits `report-to`, `report-uri` and `Reporting-Endpoints`. */
|
|
539
|
+
reportUri?: string;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* W3C Web App Manifest. Off unless declared — a manifest is a claim that a site is meant to be
|
|
544
|
+
* installed, and most are not.
|
|
545
|
+
*/
|
|
546
|
+
export interface ManifestConfig {
|
|
547
|
+
enabled?: boolean;
|
|
548
|
+
/** Defaults to the project `name`. */
|
|
549
|
+
name?: string;
|
|
550
|
+
shortName?: string;
|
|
551
|
+
description?: string;
|
|
552
|
+
/** Defaults to `/`. */
|
|
553
|
+
startUrl?: string;
|
|
554
|
+
scope?: string;
|
|
555
|
+
display?: "fullscreen" | "standalone" | "minimal-ui" | "browser";
|
|
556
|
+
orientation?: string;
|
|
557
|
+
themeColor?: string;
|
|
558
|
+
backgroundColor?: string;
|
|
559
|
+
lang?: string;
|
|
560
|
+
dir?: "ltr" | "rtl" | "auto";
|
|
561
|
+
categories?: string[];
|
|
562
|
+
icons?: { src: string; sizes?: string; type?: string; purpose?: string }[];
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* W3C Service Worker. **Off unless declared, and turning it off means `false`, not deleting the
|
|
567
|
+
* key** — a worker is sticky, so a site that once had one must keep serving something at that URL
|
|
568
|
+
* to tell previous visitors to stop.
|
|
569
|
+
*/
|
|
570
|
+
export interface ServiceWorkerConfig {
|
|
571
|
+
enabled?: boolean;
|
|
572
|
+
/** Registration scope. Cannot exceed the worker's own path. Default `"/"`. */
|
|
573
|
+
scope?: string;
|
|
574
|
+
/** URLs fetched and cached at install time. */
|
|
575
|
+
precache?: string[];
|
|
576
|
+
/** Page served for a failed navigation. Added to `precache` if it is not already there. */
|
|
577
|
+
offlineFallback?: string;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* RFC 9116 `security.txt`. `expires` is required by §2.5.5 and an absent or past one is a build
|
|
582
|
+
* error: an expired file advertises a reporting channel while telling the reporter not to trust
|
|
583
|
+
* it.
|
|
584
|
+
*/
|
|
585
|
+
export interface SecurityTxtConfig {
|
|
586
|
+
enabled?: boolean;
|
|
587
|
+
/** At least one is required (§2.5.3): a `mailto:`, `https:` or `tel:` URI. */
|
|
588
|
+
contact?: string[];
|
|
589
|
+
/** RFC 3339 date-time; §2.5.5. */
|
|
590
|
+
expires?: string;
|
|
591
|
+
encryption?: string[];
|
|
592
|
+
acknowledgments?: string[];
|
|
593
|
+
/** BCP 47 tags, validated and canonicalized like `i18n.locales`. */
|
|
594
|
+
preferredLanguages?: string[];
|
|
595
|
+
canonical?: string;
|
|
596
|
+
policy?: string[];
|
|
597
|
+
hiring?: string[];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export interface HeadersConfig {
|
|
601
|
+
/** Emit the file at all. Default true. */
|
|
602
|
+
enabled?: boolean;
|
|
603
|
+
/**
|
|
604
|
+
* `"auto"` marks the content-addressed image variants immutable and revalidates everything else.
|
|
605
|
+
* `"off"` emits no `Cache-Control` at all, for a host that manages caching itself.
|
|
606
|
+
*/
|
|
607
|
+
cache?: "auto" | "off";
|
|
608
|
+
security?: {
|
|
609
|
+
/** `X-Content-Type-Options: nosniff`. Default true. */
|
|
610
|
+
contentTypeOptions?: boolean;
|
|
611
|
+
/** `X-Frame-Options`, or false to omit. Default `"SAMEORIGIN"`. */
|
|
612
|
+
frameOptions?: "DENY" | "SAMEORIGIN" | false;
|
|
613
|
+
/** `Referrer-Policy`, or false to omit. Default `"strict-origin-when-cross-origin"`. */
|
|
614
|
+
referrerPolicy?: string | false;
|
|
615
|
+
/** `Permissions-Policy`, or false to omit. */
|
|
616
|
+
permissionsPolicy?: string | false;
|
|
617
|
+
/**
|
|
618
|
+
* RFC 6797. **Off by default**: a wrong `max-age` locks an apex domain to HTTPS for that long,
|
|
619
|
+
* and the mistake is not visible until a certificate lapses.
|
|
620
|
+
*/
|
|
621
|
+
hsts?: boolean | { maxAge?: number; includeSubDomains?: boolean; preload?: boolean };
|
|
622
|
+
/** CSP Level 3. Off by default — see {@link CspConfig}. */
|
|
623
|
+
csp?: boolean | "report-only" | CspConfig;
|
|
624
|
+
};
|
|
625
|
+
/** Verbatim rules, merged after the generated block. Keys are path patterns. */
|
|
626
|
+
rules?: Record<string, Record<string, string>>;
|
|
627
|
+
}
|
|
628
|
+
|
|
486
629
|
export interface DeployConfig {
|
|
487
630
|
provider: "cloudflare-pages";
|
|
488
631
|
accountId: string;
|
|
@@ -504,6 +647,7 @@ export interface ProjectConfig {
|
|
|
504
647
|
adapter?: AdapterId | (string & Record<never, never>);
|
|
505
648
|
deploy?: DeployConfig;
|
|
506
649
|
sitemap?: boolean;
|
|
650
|
+
headers?: HeadersConfig;
|
|
507
651
|
[key: string]: unknown;
|
|
508
652
|
};
|
|
509
653
|
images?: ImageConfig;
|
|
@@ -515,11 +659,39 @@ export interface ProjectConfig {
|
|
|
515
659
|
*/
|
|
516
660
|
extensions?: string[];
|
|
517
661
|
/** Redirect map: source path → destination (or destination with HTTP status). */
|
|
518
|
-
redirects?: Record<
|
|
662
|
+
redirects?: Record<
|
|
663
|
+
string,
|
|
664
|
+
string | { destination: string; status?: number } | { destination: string; rewrite: true }
|
|
665
|
+
>;
|
|
666
|
+
/** W3C Web App Manifest, emitted as `manifest.webmanifest`. */
|
|
667
|
+
manifest?: ManifestConfig;
|
|
668
|
+
/** RFC 9116, emitted as `.well-known/security.txt`. */
|
|
669
|
+
securityTxt?: SecurityTxtConfig;
|
|
670
|
+
/**
|
|
671
|
+
* W3C Service Worker, emitted as `sw.js`. Off unless declared; `false` emits a tombstone that
|
|
672
|
+
* unregisters a previously deployed worker (site-architecture.md §14.6).
|
|
673
|
+
*/
|
|
674
|
+
serviceWorker?: ServiceWorkerConfig | boolean;
|
|
675
|
+
/**
|
|
676
|
+
* Locale routing. Tags are BCP 47; a malformed one is a build error, since a locale is a URL
|
|
677
|
+
* prefix, an `hreflang` value and an `<html lang>` at once.
|
|
678
|
+
*/
|
|
679
|
+
i18n?: {
|
|
680
|
+
defaultLocale?: string;
|
|
681
|
+
locales?: string[];
|
|
682
|
+
routing?: "prefix-except-default" | "prefix-always";
|
|
683
|
+
};
|
|
519
684
|
defaults?: {
|
|
520
685
|
/** Default layout path; `null` means explicitly no layout. */
|
|
521
686
|
layout?: string | null;
|
|
522
687
|
lang?: string;
|
|
688
|
+
/** Base direction for `<html dir>`. A page's `$dir` overrides it. */
|
|
689
|
+
dir?: "ltr" | "rtl" | "auto";
|
|
690
|
+
/**
|
|
691
|
+
* Default shadow-DOM mode for components. `false` (light DOM) unless set; a component's own
|
|
692
|
+
* `$shadow` overrides it in both directions.
|
|
693
|
+
*/
|
|
694
|
+
shadow?: "open" | "closed" | false;
|
|
523
695
|
charset?: string;
|
|
524
696
|
[key: string]: unknown;
|
|
525
697
|
};
|
|
@@ -550,7 +722,7 @@ export type JxPathsDef =
|
|
|
550
722
|
* `@jxsuite/schema/guards` before use.
|
|
551
723
|
*/
|
|
552
724
|
export interface JxMutableNode {
|
|
553
|
-
tagName?:
|
|
725
|
+
tagName?: JxElementTagName;
|
|
554
726
|
textContent?: string | null | JxRef;
|
|
555
727
|
innerHTML?: string;
|
|
556
728
|
/**
|
|
@@ -639,5 +811,20 @@ export interface ContentLoaderEntry {
|
|
|
639
811
|
toc?: TocEntry[];
|
|
640
812
|
readingTime?: number;
|
|
641
813
|
wordCount?: number;
|
|
814
|
+
/**
|
|
815
|
+
* The source file's modification time, RFC 3339. The fallback a feed uses when an entry carries
|
|
816
|
+
* no authored date, and what lets the sitemap give a generated page its own `<lastmod>` rather
|
|
817
|
+
* than its template's.
|
|
818
|
+
*/
|
|
819
|
+
mtime?: string;
|
|
820
|
+
/** Authored text of a field the date-coercion pass rewrote, keyed by field name. */
|
|
821
|
+
rawDates?: Record<string, unknown>;
|
|
822
|
+
/**
|
|
823
|
+
* The locale this entry was loaded for, set only when its content type's `source` carried a
|
|
824
|
+
* `{locale}` placeholder. It is what lets a `[slug]` route under `/fr/` expand the French
|
|
825
|
+
* entries and not the English ones — without it, two translations of one post share an id and
|
|
826
|
+
* the second silently overwrites the first's route.
|
|
827
|
+
*/
|
|
828
|
+
locale?: string;
|
|
642
829
|
};
|
|
643
830
|
}
|