@cynodia/axiom-compiler 0.5.2-alpha.1 → 0.6.1-alpha.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/README.md +2 -2
- package/dist/codegen.d.ts +16 -0
- package/dist/codegen.js +21 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/normalize.js +60 -4
- package/dist/server.d.ts +18 -0
- package/dist/server.js +90 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -20,13 +20,13 @@ Main exports: `compileToIR`, `compileToHtml`, `createThemeStylesheet`,
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npm install @cynodia/axiom-compiler
|
|
23
|
+
npm install @cynodia/axiom-compiler
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Most applications should install the facade package instead, which re-exports this one:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npm install @cynodia/axiom
|
|
29
|
+
npm install @cynodia/axiom
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
|
package/dist/codegen.d.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import type { ApplicationGraph, ApplicationIR, Appearance } from '@cynodia/axiom-core';
|
|
2
2
|
import type { CompileOptions } from './normalize.js';
|
|
3
|
+
/**
|
|
4
|
+
* How a generated page reaches its authority.
|
|
5
|
+
*
|
|
6
|
+
* `true` means the standard semantic endpoint on the current origin, which is what the
|
|
7
|
+
* reference host serves — so an application with server-authoritative state needs no
|
|
8
|
+
* endpoint string and no gateway code of its own.
|
|
9
|
+
*/
|
|
10
|
+
export type RemoteOption = boolean | {
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
};
|
|
3
14
|
export interface HtmlOptions extends CompileOptions {
|
|
4
15
|
title?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Configure the page to talk to an authority. Omitted, it defaults to `true` whenever the
|
|
18
|
+
* IR contains a remote action, because such a page cannot work without one.
|
|
19
|
+
*/
|
|
20
|
+
remote?: RemoteOption;
|
|
5
21
|
/**
|
|
6
22
|
* Pins the appearance of the emitted page. Omitted, the theme decides — and a theme set
|
|
7
23
|
* to `system` follows the reader's own preference.
|
package/dist/codegen.js
CHANGED
|
@@ -21,6 +21,24 @@ export function compileIRToHtml(ir, options = {}) {
|
|
|
21
21
|
const title = options.title ?? ir.name;
|
|
22
22
|
const stylesheet = createThemeStylesheet(ir.theme);
|
|
23
23
|
const appearance = options.appearance ?? (ir.theme.appearance === 'system' ? undefined : ir.theme.appearance);
|
|
24
|
+
// A page with remote actions is not usable without a gateway, so one is configured by
|
|
25
|
+
// default rather than left for an author to discover.
|
|
26
|
+
const needsRemote = (ir.remoteActionIds ?? []).length > 0;
|
|
27
|
+
const remote = options.remote ?? needsRemote;
|
|
28
|
+
const remoteConfig = remote === false
|
|
29
|
+
? null
|
|
30
|
+
: {
|
|
31
|
+
endpoint: (remote === true ? undefined : remote.endpoint) ?? '/axiom',
|
|
32
|
+
...(remote !== true && remote.timeoutMs !== undefined ? { timeoutMs: remote.timeoutMs } : {}),
|
|
33
|
+
};
|
|
34
|
+
const bootstrap = remoteConfig
|
|
35
|
+
? [
|
|
36
|
+
`const __axiomRemote = createHttpRemoteGateway(${JSON.stringify(remoteConfig)});`,
|
|
37
|
+
'const __axiomApp = createAxiomRuntime({ ir: __AXIOM_IR__, rootElement: __axiomRoot, host: createBrowserHost(), remote: __axiomRemote });',
|
|
38
|
+
]
|
|
39
|
+
: [
|
|
40
|
+
'const __axiomApp = createAxiomRuntime({ ir: __AXIOM_IR__, rootElement: __axiomRoot, host: createBrowserHost() });',
|
|
41
|
+
];
|
|
24
42
|
return [
|
|
25
43
|
'<!DOCTYPE html>',
|
|
26
44
|
`<html lang="en"${appearance ? ` data-axiom-appearance="${appearance}"` : ''}>`,
|
|
@@ -38,8 +56,10 @@ export function compileIRToHtml(ir, options = {}) {
|
|
|
38
56
|
runtimeSource,
|
|
39
57
|
`const __AXIOM_IR__ = ${payload};`,
|
|
40
58
|
'const __axiomRoot = document.getElementById("app");',
|
|
41
|
-
|
|
59
|
+
...bootstrap,
|
|
42
60
|
'globalThis.__AXIOM_APP__ = __axiomApp;',
|
|
61
|
+
// `start()` is the whole startup sequence: it renders synchronously and then loads
|
|
62
|
+
// authoritative state when a gateway is configured.
|
|
43
63
|
'__axiomApp.start();',
|
|
44
64
|
' </script>',
|
|
45
65
|
'</body>',
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/normalize.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { actionGuards, inferExpressionType, inferLocationType, itemTypeOf, locationFieldIds, locationRootStateId, resolvePresentationMap, semanticContextFromGraph, validateGraph, } from '@cynodia/axiom-core';
|
|
1
|
+
import { actionAuthority, actionGuards, authorityContext, inferExpressionType, inferLocationType, itemTypeOf, locationFieldIds, locationRootStateId, referencedIds, resolvePresentationMap, semanticContextFromGraph, stateAuthority, validateGraph, } from '@cynodia/axiom-core';
|
|
2
2
|
import { isUINode } from '@cynodia/axiom-core';
|
|
3
3
|
export class GraphValidationError extends Error {
|
|
4
4
|
problems;
|
|
@@ -43,6 +43,18 @@ export function compileToIR(graph, options = {}) {
|
|
|
43
43
|
throw new GraphValidationError(result);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
// The authority boundary decides what may cross into the client at all.
|
|
47
|
+
const authorityOf = authorityContext(graph.listNodes(), graph.principalEntityId);
|
|
48
|
+
const hiddenStateIds = new Set();
|
|
49
|
+
for (const state of authorityOf.states.values()) {
|
|
50
|
+
if (state.serverOnly) {
|
|
51
|
+
hiddenStateIds.add(state.id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const remoteActionIds = [];
|
|
55
|
+
const authority = {};
|
|
56
|
+
/** Nothing that reads state the client may not observe may reach the client. */
|
|
57
|
+
const readsHiddenState = (expressions) => expressions.some((expression) => referencedIds(expression).some((id) => hiddenStateIds.has(id)));
|
|
46
58
|
const nodes = {};
|
|
47
59
|
const actions = {};
|
|
48
60
|
const uiNodes = {};
|
|
@@ -61,10 +73,43 @@ export function compileToIR(graph, options = {}) {
|
|
|
61
73
|
case 'entity':
|
|
62
74
|
entities.push(node);
|
|
63
75
|
break;
|
|
64
|
-
case 'state':
|
|
65
|
-
|
|
76
|
+
case 'state': {
|
|
77
|
+
if (hiddenStateIds.has(node.id)) {
|
|
78
|
+
// Server-only state is not merely unwritable here; it is absent.
|
|
79
|
+
delete nodes[node.id];
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
authority[node.id] = stateAuthority(node);
|
|
83
|
+
states.push(stateAuthority(node) === 'server'
|
|
84
|
+
? // The authority owns the value. A seed shipped to the client would be a
|
|
85
|
+
// second, unauthoritative source of truth.
|
|
86
|
+
{ ...node, initialValue: undefined }
|
|
87
|
+
: node);
|
|
66
88
|
break;
|
|
89
|
+
}
|
|
67
90
|
case 'action': {
|
|
91
|
+
if (actionAuthority(node, authorityOf) === 'server') {
|
|
92
|
+
// A remote action reaches the client as its name, its parameters and nothing
|
|
93
|
+
// else: no operations to replay, no guards to fake, no authorization to satisfy.
|
|
94
|
+
remoteActionIds.push(node.id);
|
|
95
|
+
const remote = {
|
|
96
|
+
id: node.id,
|
|
97
|
+
kind: 'action',
|
|
98
|
+
...(node.name ? { name: node.name } : {}),
|
|
99
|
+
...(node.parameters ? { parameters: node.parameters } : {}),
|
|
100
|
+
...(node.destructive !== undefined ? { destructive: node.destructive } : {}),
|
|
101
|
+
...(node.requiresConfirmation !== undefined
|
|
102
|
+
? { requiresConfirmation: node.requiresConfirmation }
|
|
103
|
+
: {}),
|
|
104
|
+
...(node.confirmationMessage ? { confirmationMessage: node.confirmationMessage } : {}),
|
|
105
|
+
...(node.confirmation ? { confirmation: node.confirmation } : {}),
|
|
106
|
+
...(node.metadata ? { metadata: node.metadata } : {}),
|
|
107
|
+
operations: [],
|
|
108
|
+
};
|
|
109
|
+
actions[node.id] = remote;
|
|
110
|
+
nodes[node.id] = remote;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
68
113
|
// Guards are authoring sugar: the IR carries conditions and failures aligned.
|
|
69
114
|
const guards = actionGuards(node);
|
|
70
115
|
actions[node.id] = {
|
|
@@ -76,9 +121,17 @@ export function compileToIR(graph, options = {}) {
|
|
|
76
121
|
break;
|
|
77
122
|
}
|
|
78
123
|
case 'constraint':
|
|
124
|
+
if (readsHiddenState([node.expression])) {
|
|
125
|
+
delete nodes[node.id];
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
79
128
|
constraints.push(node);
|
|
80
129
|
break;
|
|
81
130
|
case 'transition-constraint':
|
|
131
|
+
if (readsHiddenState([node.expression])) {
|
|
132
|
+
delete nodes[node.id];
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
82
135
|
transitionConstraints.push(node);
|
|
83
136
|
break;
|
|
84
137
|
case 'route':
|
|
@@ -149,11 +202,14 @@ export function compileToIR(graph, options = {}) {
|
|
|
149
202
|
constraints,
|
|
150
203
|
transitionConstraints,
|
|
151
204
|
routes,
|
|
152
|
-
|
|
205
|
+
// An edge naming a node the client does not receive would tell it that node exists.
|
|
206
|
+
edges: graph.semanticEdges().filter((edge) => nodes[edge.from] && nodes[edge.to]),
|
|
153
207
|
locationTypes,
|
|
154
208
|
locationRoots,
|
|
155
209
|
locationRequired,
|
|
156
210
|
repeatIdentityFields,
|
|
211
|
+
authority,
|
|
212
|
+
remoteActionIds,
|
|
157
213
|
theme,
|
|
158
214
|
presentation,
|
|
159
215
|
};
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ApplicationGraph, ServerIR } from '@cynodia/axiom-core';
|
|
2
|
+
import type { CompileOptions } from './normalize.js';
|
|
3
|
+
/**
|
|
4
|
+
* Compiles the authoritative half of a graph.
|
|
5
|
+
*
|
|
6
|
+
* What an authority needs is not what a client needs, and the difference is the point: the
|
|
7
|
+
* Server IR carries the rules a client must never be trusted with, and none of the UI,
|
|
8
|
+
* presentation, theme or routing that decides nothing.
|
|
9
|
+
*
|
|
10
|
+
* The result is plain JSON — deterministic, closure-free and free of anything specific to
|
|
11
|
+
* a language or host — so a conforming runtime in another language can execute it and reach
|
|
12
|
+
* the same semantic result.
|
|
13
|
+
*/
|
|
14
|
+
export declare function compileToServerIR(graph: ApplicationGraph, options?: CompileOptions): ServerIR;
|
|
15
|
+
export declare function serializeServerIR(ir: ServerIR): string;
|
|
16
|
+
/** Whether a graph has any authoritative half at all. */
|
|
17
|
+
export declare function hasServerAuthority(graph: ApplicationGraph): boolean;
|
|
18
|
+
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { SERVER_IR_CONTRACT, actionAuthority, actionGuards, authorityContext, isObservable, serverStateClosure, stateAuthority, validateGraph, } from '@cynodia/axiom-core';
|
|
2
|
+
import { GraphValidationError } from './normalize.js';
|
|
3
|
+
/**
|
|
4
|
+
* Compiles the authoritative half of a graph.
|
|
5
|
+
*
|
|
6
|
+
* What an authority needs is not what a client needs, and the difference is the point: the
|
|
7
|
+
* Server IR carries the rules a client must never be trusted with, and none of the UI,
|
|
8
|
+
* presentation, theme or routing that decides nothing.
|
|
9
|
+
*
|
|
10
|
+
* The result is plain JSON — deterministic, closure-free and free of anything specific to
|
|
11
|
+
* a language or host — so a conforming runtime in another language can execute it and reach
|
|
12
|
+
* the same semantic result.
|
|
13
|
+
*/
|
|
14
|
+
export function compileToServerIR(graph, options = {}) {
|
|
15
|
+
if (options.validate !== false) {
|
|
16
|
+
const result = validateGraph(graph);
|
|
17
|
+
if (!result.valid) {
|
|
18
|
+
throw new GraphValidationError(result);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const nodes = graph.listNodes();
|
|
22
|
+
const context = authorityContext(nodes, graph.principalEntityId);
|
|
23
|
+
const needed = serverStateClosure(context);
|
|
24
|
+
const entities = [];
|
|
25
|
+
const constraints = [];
|
|
26
|
+
const transitionConstraints = [];
|
|
27
|
+
for (const node of nodes) {
|
|
28
|
+
if (node.kind === 'entity') {
|
|
29
|
+
entities.push(node);
|
|
30
|
+
}
|
|
31
|
+
else if (node.kind === 'constraint') {
|
|
32
|
+
constraints.push(node);
|
|
33
|
+
}
|
|
34
|
+
else if (node.kind === 'transition-constraint') {
|
|
35
|
+
transitionConstraints.push(node);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const states = [];
|
|
39
|
+
const observableStateIds = [];
|
|
40
|
+
for (const state of context.states.values()) {
|
|
41
|
+
if (!needed.has(state.id)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
states.push(state);
|
|
45
|
+
if (stateAuthority(state) === 'server' && isObservable(state)) {
|
|
46
|
+
observableStateIds.push(state.id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const actions = {};
|
|
50
|
+
for (const action of context.actions.values()) {
|
|
51
|
+
if (actionAuthority(action, context) !== 'server') {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Guards are authoring sugar in both halves: the IR carries conditions and failures
|
|
55
|
+
// aligned by position, exactly as the client IR does. An authority that read `guards`
|
|
56
|
+
// and not `preconditions` would silently not check them.
|
|
57
|
+
const guards = actionGuards(action);
|
|
58
|
+
actions[action.id] = {
|
|
59
|
+
...action,
|
|
60
|
+
preconditions: guards.map((guard) => guard.condition),
|
|
61
|
+
failureModes: guards.map((guard) => guard.failureMode ?? { code: 'precondition-failed' }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const fields = {};
|
|
65
|
+
for (const entry of graph.listFields()) {
|
|
66
|
+
fields[entry.field.id] = entry;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
contract: SERVER_IR_CONTRACT,
|
|
70
|
+
id: graph.id,
|
|
71
|
+
name: graph.name,
|
|
72
|
+
version: graph.version,
|
|
73
|
+
entities,
|
|
74
|
+
fields,
|
|
75
|
+
states,
|
|
76
|
+
actions,
|
|
77
|
+
constraints,
|
|
78
|
+
transitionConstraints,
|
|
79
|
+
...(graph.principalEntityId ? { principalEntityId: graph.principalEntityId } : {}),
|
|
80
|
+
observableStateIds,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function serializeServerIR(ir) {
|
|
84
|
+
return JSON.stringify(ir);
|
|
85
|
+
}
|
|
86
|
+
/** Whether a graph has any authoritative half at all. */
|
|
87
|
+
export function hasServerAuthority(graph) {
|
|
88
|
+
const context = authorityContext(graph.listNodes(), graph.principalEntityId);
|
|
89
|
+
return [...context.states.values()].some((state) => stateAuthority(state) === 'server');
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1-alpha.1",
|
|
4
4
|
"description": "Normalizes an Axiom application graph and emits a self-contained page.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AskTech AS",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@cynodia/axiom-core": "0.
|
|
35
|
-
"@cynodia/axiom-runtime": "0.
|
|
34
|
+
"@cynodia/axiom-core": "0.6.1-alpha.1",
|
|
35
|
+
"@cynodia/axiom-runtime": "0.6.1-alpha.1"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc -b tsconfig.json tsconfig.test.json",
|