@jxsuite/schema 5.6.0 → 6.0.2
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/README.md +1 -0
- package/defs/image-config.schema.ts +14 -2
- package/package.json +8 -4
- package/project-schema.json +3 -1
- package/schema.json +88 -1
- package/schemas/project.core.schema.json +3 -1
- package/src/document-scripts.ts +304 -0
- package/src/dynamic-tag.ts +570 -0
- package/src/guards.ts +179 -4
- package/src/jxn/complete.ts +582 -0
- package/src/jxn/document-scopes.ts +581 -0
- package/src/jxn/document.ts +6 -0
- package/src/jxn/event-handlers.ts +5 -0
- package/src/jxn/index.ts +14 -2
- package/src/jxn/outline.ts +452 -0
- package/src/jxn/parse.ts +234 -13
- package/src/jxn/schema-types.ts +299 -0
- package/src/jxn/schema-walk.ts +221 -0
- package/src/jxn/serialize.ts +113 -5
- package/src/jxn/state-types.ts +271 -0
- package/src/jxn/string-pieces.ts +240 -0
- package/src/jxn/virtual-document.ts +1077 -0
- package/src/jxn/virtual-types.ts +94 -169
- package/src/schema.ts +22 -0
- package/types.ts +16 -0
package/src/jxn/virtual-types.ts
CHANGED
|
@@ -19,19 +19,27 @@
|
|
|
19
19
|
*
|
|
20
20
|
* @docs framework/concepts/functions
|
|
21
21
|
*/
|
|
22
|
-
import {
|
|
22
|
+
import { isJsonObject } from "../guards";
|
|
23
23
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
boundEvents,
|
|
25
|
+
eventLines,
|
|
26
|
+
eventType,
|
|
27
|
+
itemType,
|
|
28
|
+
EXCEPT,
|
|
29
|
+
heritageOf,
|
|
30
|
+
membersOf,
|
|
31
|
+
readsRow,
|
|
32
|
+
rootSite,
|
|
33
|
+
siteInto,
|
|
34
|
+
} from "./document-scopes";
|
|
35
|
+
import type { Site, StateScope } from "./document-scopes";
|
|
36
|
+
import { definitionParts, definitionsOf, pointerInto, propertyName, written } from "./schema-types";
|
|
37
|
+
import { declaredParameters, stateIndex, typeOfEntry } from "./state-types";
|
|
38
|
+
import type { OutsideState } from "./state-types";
|
|
39
|
+
import type { JsonObject, JsonValue, JxElement } from "../../types";
|
|
40
|
+
|
|
41
|
+
export { givenByProject, outsideAt } from "./state-types";
|
|
42
|
+
export type { OutsideState, ProjectGives } from "./state-types";
|
|
35
43
|
|
|
36
44
|
/** Where a body sits in a document. */
|
|
37
45
|
export type VirtualTarget =
|
|
@@ -66,164 +74,73 @@ export interface VirtualBody {
|
|
|
66
74
|
*/
|
|
67
75
|
export const UNRESOLVED_MODULE_CODES: ReadonlySet<number> = new Set([2307, 2792, 7016]);
|
|
68
76
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
/** The
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/** Whether a key can be written as a bare property name. */
|
|
76
|
-
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/u;
|
|
77
|
-
|
|
78
|
-
/** A property name as TypeScript writes it. */
|
|
79
|
-
function propertyName(key: string): string {
|
|
80
|
-
return IDENTIFIER.test(key) ? key : JSON.stringify(key);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** The type a value's shape gives it, widened the way a mutable binding widens. */
|
|
84
|
-
function typeOfValue(value: JsonValue | undefined, depth: number): string {
|
|
85
|
-
if (isString(value)) {
|
|
86
|
-
return "string";
|
|
87
|
-
}
|
|
88
|
-
if (isNumber(value)) {
|
|
89
|
-
return "number";
|
|
90
|
-
}
|
|
91
|
-
if (isBoolean(value)) {
|
|
92
|
-
return "boolean";
|
|
93
|
-
}
|
|
94
|
-
if (Array.isArray(value)) {
|
|
95
|
-
const types = [...new Set(value.map((item) => typeOfValue(item, depth + 1)))];
|
|
96
|
-
if (types.length === 0 || types.length > MAX_ITEM_TYPES || types.includes("any")) {
|
|
97
|
-
return "any[]";
|
|
98
|
-
}
|
|
99
|
-
return types.length === 1 ? `${types[0]}[]` : `(${types.join(" | ")})[]`;
|
|
100
|
-
}
|
|
101
|
-
if (isJsonObject(value) && depth < MAX_NESTING) {
|
|
102
|
-
const members = Object.entries(value).map(
|
|
103
|
-
([key, item]) => `${propertyName(key)}: ${typeOfValue(item, depth + 1)}`,
|
|
104
|
-
);
|
|
105
|
-
return members.length === 0 ? "Record<string, any>" : `{ ${members.join("; ")} }`;
|
|
106
|
-
}
|
|
107
|
-
return "any";
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** The type a JSON Schema `type` keyword names. */
|
|
111
|
-
function typeOfKeyword(type: JsonValue | undefined): string {
|
|
112
|
-
switch (type) {
|
|
113
|
-
case "string": {
|
|
114
|
-
return "string";
|
|
115
|
-
}
|
|
116
|
-
case "number":
|
|
117
|
-
case "integer": {
|
|
118
|
-
return "number";
|
|
119
|
-
}
|
|
120
|
-
case "boolean": {
|
|
121
|
-
return "boolean";
|
|
122
|
-
}
|
|
123
|
-
case "array": {
|
|
124
|
-
return "any[]";
|
|
125
|
-
}
|
|
126
|
-
default: {
|
|
127
|
-
return "any";
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** The type of one `state` entry, by the shapes of `spec.md` §5. */
|
|
133
|
-
function typeOfEntry(
|
|
134
|
-
entry: JsonValue | undefined,
|
|
135
|
-
role: "computed" | "callable" | undefined,
|
|
136
|
-
): string {
|
|
137
|
-
if (isString(entry)) {
|
|
138
|
-
// A template is a computed value of a type the document does not give.
|
|
139
|
-
return entry.includes("${") ? "any" : "string";
|
|
140
|
-
}
|
|
141
|
-
if (!isJsonObject(entry)) {
|
|
142
|
-
return typeOfValue(entry, 0);
|
|
143
|
-
}
|
|
144
|
-
if (isFunctionDef(entry)) {
|
|
145
|
-
return role === "computed" ? "any" : "(...args: any[]) => any";
|
|
146
|
-
}
|
|
147
|
-
if (isString(entry.$prototype) || isExpressionDef(entry) || isServerFnDef(entry)) {
|
|
148
|
-
return "any";
|
|
149
|
-
}
|
|
150
|
-
if ("default" in entry) {
|
|
151
|
-
const { default: initial } = entry;
|
|
152
|
-
return initial === null ? typeOfKeyword(entry.type) : typeOfValue(initial, 0);
|
|
153
|
-
}
|
|
154
|
-
return hasSchemaKeywords(entry) ? typeOfKeyword(entry.type) : typeOfValue(entry, 0);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** The `state` object of an element, when it has one. */
|
|
158
|
-
function stateOf(node: JsonValue | undefined): Record<string, JsonValue> | null {
|
|
159
|
-
return isJsonObject(node) && isJsonObject(node.state) ? node.state : null;
|
|
77
|
+
/** What a target names: the function entry, and what surrounds the element that holds it. */
|
|
78
|
+
interface Located {
|
|
79
|
+
definition: JsonValue | undefined;
|
|
80
|
+
/** The type of the event the body is given. */
|
|
81
|
+
event: string;
|
|
82
|
+
site: Site;
|
|
160
83
|
}
|
|
161
84
|
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
*/
|
|
166
|
-
function scopeChain(doc: JxElement, path: readonly (string | number)[]): JsonValue[] {
|
|
167
|
-
// SAFETY: a document is JSON data; its types name what it holds.
|
|
168
|
-
let current: JsonValue | undefined = doc as JsonValue;
|
|
169
|
-
const chain: JsonValue[] = [current];
|
|
170
|
-
for (const segment of path) {
|
|
171
|
-
current = Array.isArray(current)
|
|
172
|
-
? current[Number(segment)]
|
|
173
|
-
: isJsonObject(current)
|
|
174
|
-
? current[String(segment)]
|
|
175
|
-
: undefined;
|
|
176
|
-
if (stateOf(current)) {
|
|
177
|
-
// SAFETY: `stateOf` only answers for an object.
|
|
178
|
-
chain.push(current as JsonValue);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return chain;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** The function entry a target names. */
|
|
185
|
-
function definitionOf(doc: JxElement, target: VirtualTarget): JsonValue | undefined {
|
|
186
|
-
// SAFETY: a document is JSON data; its types name what it holds.
|
|
187
|
-
const root = doc as JsonValue;
|
|
85
|
+
/** Find a target in a document. */
|
|
86
|
+
function locate(doc: JsonObject, target: VirtualTarget): Located {
|
|
87
|
+
let site = rootSite(doc);
|
|
188
88
|
if (target.kind === "state") {
|
|
189
|
-
|
|
89
|
+
const event = boundEvents(doc).get(pointerInto("/state", target.key)) ?? "any";
|
|
90
|
+
return { definition: isJsonObject(doc.state) ? doc.state[target.key] : undefined, event, site };
|
|
190
91
|
}
|
|
191
|
-
let owner: JsonValue | undefined =
|
|
92
|
+
let owner: JsonValue | undefined = doc;
|
|
93
|
+
let pointer = "";
|
|
192
94
|
for (const segment of target.path) {
|
|
193
|
-
|
|
95
|
+
const child: JsonValue | undefined = Array.isArray(owner)
|
|
194
96
|
? owner[Number(segment)]
|
|
195
97
|
: isJsonObject(owner)
|
|
196
98
|
? owner[String(segment)]
|
|
197
99
|
: undefined;
|
|
100
|
+
pointer = pointerInto(pointer, segment);
|
|
101
|
+
site = siteInto(site, owner, segment, child, pointer);
|
|
102
|
+
owner = child;
|
|
198
103
|
}
|
|
199
|
-
return isJsonObject(owner)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** The names a function is called with: `state` first, then the declared names (`spec.md` §5.3). */
|
|
203
|
-
function parametersOf(definition: JsonValue | undefined): string[] {
|
|
204
|
-
if (!isJsonObject(definition)) {
|
|
205
|
-
return ["state"];
|
|
206
|
-
}
|
|
207
|
-
const declared = Array.isArray(definition.parameters)
|
|
208
|
-
? // SAFETY: `parameters` holds bare names or CEM parameter objects, the form `paramNames` reads.
|
|
209
|
-
paramNames(definition.parameters as Parameters<typeof paramNames>[0])
|
|
210
|
-
: (Array.isArray(definition.arguments) ? definition.arguments : []).filter(
|
|
211
|
-
(name): name is string => isString(name),
|
|
212
|
-
);
|
|
213
|
-
return declared[0] === "state" ? declared : ["state", ...declared];
|
|
104
|
+
return isJsonObject(owner)
|
|
105
|
+
? { definition: owner[target.eventKey], event: eventType(owner, target.eventKey, site), site }
|
|
106
|
+
: { definition: undefined, event: "any", site };
|
|
214
107
|
}
|
|
215
108
|
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Write the `State` a body reads: one interface for each element on the way down that has `state`,
|
|
111
|
+
* each under the one around it, and over them the row of the map the body is drawn in.
|
|
112
|
+
*/
|
|
113
|
+
function stateLines(site: Site, index: string, defs: boolean): string[] {
|
|
114
|
+
const chain: StateScope[] = [];
|
|
115
|
+
for (let at: StateScope | null = site.scope; at; at = at.parent) {
|
|
116
|
+
chain.unshift(at);
|
|
117
|
+
}
|
|
118
|
+
const inRow = site.row !== null && readsRow(site.scope);
|
|
119
|
+
// The innermost is `State` itself, unless the row is written over it.
|
|
120
|
+
const nameOf = (scope: StateScope): string =>
|
|
121
|
+
scope === site.scope && !inRow ? "State" : `State$${chain.indexOf(scope)}`;
|
|
122
|
+
// The helper is declared only where a scope reads through it: an unused one is a finding.
|
|
123
|
+
const lines: string[] = chain.some((scope) => membersOf(scope).shadowed.length > 0)
|
|
124
|
+
? [EXCEPT]
|
|
125
|
+
: [];
|
|
126
|
+
for (const scope of chain) {
|
|
127
|
+
const { own, shadowed } = membersOf(scope);
|
|
128
|
+
const around = scope.parent ? nameOf(scope.parent) : null;
|
|
129
|
+
const heritage = around === null ? "" : ` extends ${heritageOf(around, shadowed)}`;
|
|
130
|
+
lines.push(`interface ${nameOf(scope)}${heritage} {`);
|
|
131
|
+
for (const { entry, key, role } of own) {
|
|
132
|
+
lines.push(` ${propertyName(key)}: ${typeOfEntry(entry, role, defs)};`);
|
|
224
133
|
}
|
|
134
|
+
lines.push(...(around === null ? [` ${index}`, "}"] : ["}"]));
|
|
225
135
|
}
|
|
226
|
-
|
|
136
|
+
if (site.row && inRow) {
|
|
137
|
+
lines.push(
|
|
138
|
+
`interface State extends ${nameOf(site.scope)} {`,
|
|
139
|
+
` $map: { item: ${itemType(site.row, nameOf)}; index: number };`,
|
|
140
|
+
"}",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return lines;
|
|
227
144
|
}
|
|
228
145
|
|
|
229
146
|
/**
|
|
@@ -233,30 +150,38 @@ function stateMembers(chain: readonly JsonValue[]): Map<string, string> {
|
|
|
233
150
|
* @param target Which body: a root `state` entry, or an inline handler on an element.
|
|
234
151
|
* @param body The body's text, which need not be the text the document holds yet (an editor's
|
|
235
152
|
* buffer is checked as typed).
|
|
153
|
+
* @param outside What the document's project gives it, which makes `State` open. Nothing, when the
|
|
154
|
+
* reader knows of no project.
|
|
236
155
|
* @returns The file to check, and where the body sits in it.
|
|
237
156
|
*/
|
|
238
157
|
export function generateBodyTypes(
|
|
239
158
|
doc: JxElement,
|
|
240
159
|
target: VirtualTarget,
|
|
241
160
|
body: string,
|
|
161
|
+
outside: OutsideState = {},
|
|
242
162
|
): VirtualBody {
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const signature = params.map((name) => `${name}: ${name === "state" ? "State" : "any"}`);
|
|
163
|
+
// SAFETY: a document is JSON data; its types name what it holds.
|
|
164
|
+
const data = doc as JsonObject;
|
|
165
|
+
const located = locate(data, target);
|
|
166
|
+
const defs = definitionsOf(data);
|
|
167
|
+
const declared = declaredParameters(located.definition, { defs: defs !== null, pointer: "" });
|
|
168
|
+
const params = declared.map((parameter) => parameter.name);
|
|
250
169
|
/* The event a handler receives is bound to a name only when the entry declares one, though the
|
|
251
170
|
runtime passes it and `window.event` holds it. Not declaring it would call a working handler
|
|
252
|
-
an error, so it is
|
|
171
|
+
an error, so it is declared wherever it is not a parameter. */
|
|
172
|
+
const event = eventLines(located.event, params);
|
|
173
|
+
/* A handler is called with its state and the event, in that order (`spec.md` §5.3), and a
|
|
174
|
+
parameter that declares a type is that type, the event's place included (§23.3). */
|
|
175
|
+
const signature = declared.map((parameter, at) => {
|
|
176
|
+
const untyped = at === 1 ? event.parameter : "any";
|
|
177
|
+
const type = parameter.type === null ? untyped : written(parameter.type);
|
|
178
|
+
return `${parameter.name}: ${at === 0 ? "State" : type}`;
|
|
179
|
+
});
|
|
253
180
|
const head = [
|
|
254
181
|
"export {};",
|
|
255
|
-
|
|
256
|
-
...
|
|
257
|
-
|
|
258
|
-
"declare const event: any;",
|
|
259
|
-
`async function body(${["this: any", ...signature].join(", ")}) {`,
|
|
182
|
+
...(defs ? [written(definitionParts(defs, false))] : []),
|
|
183
|
+
...stateLines(located.site, stateIndex(doc, outside), defs !== null),
|
|
184
|
+
`${event.head}async function body(${["this: any", ...signature].join(", ")}) {`,
|
|
260
185
|
"",
|
|
261
186
|
].join("\n");
|
|
262
187
|
return {
|
package/src/schema.ts
CHANGED
|
@@ -408,6 +408,28 @@ export async function generateSchema() {
|
|
|
408
408
|
examples: ["./layouts/base.json"],
|
|
409
409
|
oneOf: [{ type: "string" }, { const: false, type: "boolean" }],
|
|
410
410
|
},
|
|
411
|
+
$script: {
|
|
412
|
+
additionalProperties: false,
|
|
413
|
+
description:
|
|
414
|
+
"This document's own modules, as TypeScript module text (spec.md §23.2). " +
|
|
415
|
+
'A state entry names an export of one with `$src: "#client"` or `$src: "#server"`. ' +
|
|
416
|
+
"`client` runs in the browser; `server` runs only on the server and is never sent to one.",
|
|
417
|
+
properties: {
|
|
418
|
+
client: {
|
|
419
|
+
description:
|
|
420
|
+
'The module the browser runs. A Function entry with `$src: "#client"` is one of ' +
|
|
421
|
+
"its exports.",
|
|
422
|
+
type: "string",
|
|
423
|
+
},
|
|
424
|
+
server: {
|
|
425
|
+
description:
|
|
426
|
+
'The module the server runs. A `timing: "server"` entry with `$src: "#server"` is ' +
|
|
427
|
+
"one of its exports, called with `(args, env)`.",
|
|
428
|
+
type: "string",
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
type: "object",
|
|
432
|
+
},
|
|
411
433
|
$shadow: {
|
|
412
434
|
description:
|
|
413
435
|
"Render this component into a shadow root instead of the light DOM (spec.md §16.6). " +
|
package/types.ts
CHANGED
|
@@ -385,6 +385,20 @@ export interface JxMappedArray {
|
|
|
385
385
|
key?: JxRef;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
/**
|
|
389
|
+
* A document's own modules, as `$script` holds them: TypeScript module text for each side.
|
|
390
|
+
*
|
|
391
|
+
* A type alias rather than an interface, because a document member has to be a `JxDocumentValue`,
|
|
392
|
+
* and only an alias is given the implicit index signature that makes an object type one.
|
|
393
|
+
*/
|
|
394
|
+
// oxlint-disable-next-line typescript/consistent-type-definitions -- See above.
|
|
395
|
+
export type JxDocumentScripts = {
|
|
396
|
+
/** The module the browser runs. Its exports are what `$src: "#client"` entries name. */
|
|
397
|
+
client?: string;
|
|
398
|
+
/** The module the server runs. Its exports are what `$src: "#server"` entries name. */
|
|
399
|
+
server?: string;
|
|
400
|
+
};
|
|
401
|
+
|
|
388
402
|
export interface JxDocument extends JxElement {
|
|
389
403
|
/**
|
|
390
404
|
* The custom element's NAME, always a literal.
|
|
@@ -397,6 +411,8 @@ export interface JxDocument extends JxElement {
|
|
|
397
411
|
tagName?: string;
|
|
398
412
|
state?: Record<string, JxStateDefinition>;
|
|
399
413
|
$layout?: string | false;
|
|
414
|
+
/** This document's own client and server modules (spec.md §23.2). */
|
|
415
|
+
$script?: JxDocumentScripts;
|
|
400
416
|
/** This page's identity across languages, overriding the one its path implies (§13.5). */
|
|
401
417
|
$translationKey?: string;
|
|
402
418
|
$paths?: JxPathsDef;
|