@astrale-os/sdk 0.1.10 → 0.2.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/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/cli/run.d.ts +0 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +17 -10
- package/dist/cli/run.js.map +1 -1
- package/dist/config/adapter.d.ts +12 -11
- package/dist/config/adapter.d.ts.map +1 -1
- package/dist/config/adapter.js.map +1 -1
- package/dist/config/define-domain.d.ts +13 -29
- package/dist/config/define-domain.d.ts.map +1 -1
- package/dist/config/define-domain.js +22 -33
- package/dist/config/define-domain.js.map +1 -1
- package/dist/config/deploy.d.ts +1 -1
- package/dist/config/deploy.js +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/define/remote-function.d.ts +9 -14
- package/dist/define/remote-function.d.ts.map +1 -1
- package/dist/define/remote-function.js +9 -8
- package/dist/define/remote-function.js.map +1 -1
- package/dist/define/view.d.ts +8 -6
- package/dist/define/view.d.ts.map +1 -1
- package/dist/define/view.js +8 -6
- package/dist/define/view.js.map +1 -1
- package/dist/dispatch/identity.d.ts +10 -9
- package/dist/dispatch/identity.d.ts.map +1 -1
- package/dist/dispatch/identity.js +9 -15
- package/dist/dispatch/identity.js.map +1 -1
- package/dist/domain/binding.d.ts +18 -0
- package/dist/domain/binding.d.ts.map +1 -0
- package/dist/domain/binding.js +29 -0
- package/dist/domain/binding.js.map +1 -0
- package/dist/domain/build-spec.d.ts.map +1 -1
- package/dist/domain/build-spec.js +10 -7
- package/dist/domain/build-spec.js.map +1 -1
- package/dist/domain/define.d.ts +27 -27
- package/dist/domain/define.d.ts.map +1 -1
- package/dist/domain/define.js +31 -49
- package/dist/domain/define.js.map +1 -1
- package/dist/domain/extend-functions.d.ts +44 -0
- package/dist/domain/extend-functions.d.ts.map +1 -0
- package/dist/domain/extend-functions.js +69 -0
- package/dist/domain/extend-functions.js.map +1 -0
- package/dist/domain/extend-views.d.ts +45 -0
- package/dist/domain/extend-views.d.ts.map +1 -0
- package/dist/domain/extend-views.js +116 -0
- package/dist/domain/extend-views.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server/auxiliary-routes.js +1 -1
- package/dist/server/auxiliary-routes.js.map +1 -1
- package/dist/server/worker-entry.d.ts +3 -3
- package/dist/server/worker-entry.js +3 -3
- package/package.json +2 -5
- package/src/auth/index.ts +1 -1
- package/src/cli/run.ts +17 -12
- package/src/config/adapter.ts +9 -11
- package/src/config/define-domain.ts +34 -56
- package/src/config/deploy.ts +1 -1
- package/src/config/index.ts +1 -1
- package/src/define/remote-function.ts +9 -14
- package/src/define/view.ts +8 -6
- package/src/dispatch/identity.ts +15 -21
- package/src/domain/binding.ts +37 -0
- package/src/domain/build-spec.ts +18 -7
- package/src/domain/define.ts +67 -62
- package/src/domain/extend-functions.ts +86 -0
- package/src/domain/extend-views.ts +151 -0
- package/src/index.ts +7 -2
- package/src/server/auxiliary-routes.ts +1 -1
- package/src/server/worker-entry.ts +3 -3
- package/src/domain/extend-core.ts +0 -301
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize the `functions` map (`defineRemoteFunction` entries) into the two
|
|
3
|
+
* halves a standalone-function DOMAIN MEMBER contributes — mirroring how a class
|
|
4
|
+
* method splits across compile + serialize:
|
|
5
|
+
*
|
|
6
|
+
* - `FunctionDeclarations` — the CONTRACT half, fed to `compileDomain`'s 4th
|
|
7
|
+
* arg. The member ref (`function.<slug>`), the layout
|
|
8
|
+
* (`/<origin>/functions/<slug>`), and the `of_domain` edge slug all derive
|
|
9
|
+
* from the map key; the kernel owns that mapping.
|
|
10
|
+
* - `FunctionSchema[]` + per-slug `FunctionBinding` — the IMPL/BINDING half,
|
|
11
|
+
* fed to `serialize` (url-stamped). The worker route is
|
|
12
|
+
* `<url>/<functionsFolder>/<slug>`, DECOUPLED from the graph layout — so
|
|
13
|
+
* `functionsFolder` only names the URL segment, not the node position.
|
|
14
|
+
*
|
|
15
|
+
* A standalone function is NOT a core/instance node: it is attached directly to
|
|
16
|
+
* the Domain via `of_domain` by the kernel-core serializer, so this module never
|
|
17
|
+
* touches `Core`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
|
|
21
|
+
import type { FunctionDeclarations, FunctionSchema } from '@astrale-os/kernel-core/domain'
|
|
22
|
+
|
|
23
|
+
import { zodToJsonSchema } from '@astrale-os/kernel-core/domain'
|
|
24
|
+
|
|
25
|
+
import type { AnyRemoteFunctionDef } from '../define/remote-function'
|
|
26
|
+
|
|
27
|
+
import { resolveBinding } from './binding'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default URL segment for a function's worker route (`<url>/functions/<slug>`).
|
|
31
|
+
* Names the HTTP route only — the graph layout (`/<origin>/functions/<slug>`) is
|
|
32
|
+
* a fixed kernel convention, independent of this.
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_FUNCTIONS_FOLDER = 'functions'
|
|
35
|
+
const SLUG_RE = /^[a-z][a-z0-9-]*$/
|
|
36
|
+
|
|
37
|
+
function assertValidSlugs(functions: Record<string, AnyRemoteFunctionDef>): void {
|
|
38
|
+
for (const slug of Object.keys(functions)) {
|
|
39
|
+
if (!SLUG_RE.test(slug)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`defineRemoteDomain: invalid function slug "${slug}" — must match ${SLUG_RE}.`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Contract half — one declaration per `functions` map key, for `compileDomain`'s
|
|
49
|
+
* `functions` arg. The slug drives ref/path/of_domain; impl/binding arrive at
|
|
50
|
+
* serialize.
|
|
51
|
+
*/
|
|
52
|
+
export function buildFunctionDeclarations(
|
|
53
|
+
functions: Record<string, AnyRemoteFunctionDef>,
|
|
54
|
+
): FunctionDeclarations {
|
|
55
|
+
assertValidSlugs(functions)
|
|
56
|
+
const out: Record<string, { slug: string }> = {}
|
|
57
|
+
for (const slug of Object.keys(functions)) out[slug] = { slug }
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Impl/binding half — `FunctionSchema[]` for `serialize` plus the per-slug
|
|
63
|
+
* `FunctionBinding` map the worker mounts routes from. `ref` is the canonical
|
|
64
|
+
* `function.<slug>` member ref (matching `compiled.$.refs.functions`), so the
|
|
65
|
+
* serializer's per-member impl lookup finds it.
|
|
66
|
+
*/
|
|
67
|
+
export function buildFunctionSchemas(
|
|
68
|
+
functions: Record<string, AnyRemoteFunctionDef>,
|
|
69
|
+
url: string,
|
|
70
|
+
functionsFolder: string,
|
|
71
|
+
): { schemas: FunctionSchema[]; bindings: Record<string, FunctionBinding> } {
|
|
72
|
+
const schemas: FunctionSchema[] = []
|
|
73
|
+
const bindings: Record<string, FunctionBinding> = {}
|
|
74
|
+
for (const [slug, def] of Object.entries(functions)) {
|
|
75
|
+
const binding = resolveBinding(def.binding, url, functionsFolder, slug)
|
|
76
|
+
bindings[slug] = binding
|
|
77
|
+
schemas.push({
|
|
78
|
+
ref: `function.${slug}`,
|
|
79
|
+
inputSchema: zodToJsonSchema(def.inputSchema),
|
|
80
|
+
outputSchema: zodToJsonSchema(def.outputSchema),
|
|
81
|
+
output: 'value',
|
|
82
|
+
binding,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
return { schemas, bindings }
|
|
86
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize the `views` map (`defineView` entries) into the two halves a view
|
|
3
|
+
* DOMAIN MEMBER contributes — mirroring `extend-functions.ts`:
|
|
4
|
+
*
|
|
5
|
+
* - `ViewDeclarations` — the CONTRACT half, fed to `compileDomain`'s members
|
|
6
|
+
* arg. The member ref (`view.<slug>`), the layout (`/<origin>/views/<slug>`),
|
|
7
|
+
* and the `of_domain` edge slug all derive from the map key.
|
|
8
|
+
* - `ViewSchema[]` + per-slug `FunctionBinding` — the IMPL half, fed to
|
|
9
|
+
* `serialize`. A view carries more than a function: a `UI.handshake` mode and
|
|
10
|
+
* `view_for` target edges (resolved here to graph paths). The worker route is
|
|
11
|
+
* `<url>/<viewsFolder>/<slug>`, DECOUPLED from the graph layout.
|
|
12
|
+
*
|
|
13
|
+
* A view is NOT a core/instance node: the kernel-core serializer attaches it to
|
|
14
|
+
* the Domain via `of_domain` (slug `view.<slug>`) and emits its `view_for` edges,
|
|
15
|
+
* so this module never touches `Core`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
|
|
19
|
+
import type { ViewDeclarations, ViewSchema } from '@astrale-os/kernel-core/domain'
|
|
20
|
+
import type { Schema } from '@astrale-os/kernel-dsl'
|
|
21
|
+
|
|
22
|
+
import { DomainPaths } from '@astrale-os/kernel-core/domain'
|
|
23
|
+
import { buildDefOriginMap, scanImportedDefs } from '@astrale-os/kernel-core/domain'
|
|
24
|
+
import {
|
|
25
|
+
buildDefDescriptorMap,
|
|
26
|
+
isAbstract,
|
|
27
|
+
isCorePath,
|
|
28
|
+
isSelfMarker,
|
|
29
|
+
type EdgeEndpoint,
|
|
30
|
+
} from '@astrale-os/kernel-dsl'
|
|
31
|
+
|
|
32
|
+
import type { ViewDef } from '../define/view'
|
|
33
|
+
|
|
34
|
+
import { joinWorkerPath, resolveBinding } from './binding'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default URL segment for a view's worker route (`<url>/views/<slug>`). Names the
|
|
38
|
+
* HTTP route only — the graph layout (`/<origin>/views/<slug>`) is a fixed kernel
|
|
39
|
+
* convention, independent of this.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_VIEWS_FOLDER = 'views'
|
|
42
|
+
const SLUG_RE = /^[a-z][a-z0-9-]*$/
|
|
43
|
+
|
|
44
|
+
// oxlint-disable-next-line no-explicit-any
|
|
45
|
+
type AnyViewDef = ViewDef<any>
|
|
46
|
+
|
|
47
|
+
function assertValidSlugs(views: Record<string, AnyViewDef>): void {
|
|
48
|
+
for (const slug of Object.keys(views)) {
|
|
49
|
+
if (!SLUG_RE.test(slug)) {
|
|
50
|
+
throw new Error(`defineRemoteDomain: invalid view slug "${slug}" — must match ${SLUG_RE}.`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Enforce ViewDef's documented exclusivity: `mount` cannot combine with `render`/`binding`. */
|
|
56
|
+
function assertExclusivity(slug: string, def: AnyViewDef): void {
|
|
57
|
+
if (def.mount && (def.render || def.binding)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`defineRemoteDomain: view "${slug}" sets \`mount\` together with ` +
|
|
60
|
+
`${def.render ? '`render`' : '`binding`'} — they are mutually exclusive.`,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Contract half — one declaration per `views` map key, for `compileDomain`'s
|
|
67
|
+
* `views` arg. The slug drives ref/path/of_domain; impl/binding/view_for arrive
|
|
68
|
+
* at serialize.
|
|
69
|
+
*/
|
|
70
|
+
export function buildViewDeclarations(views: Record<string, AnyViewDef>): ViewDeclarations {
|
|
71
|
+
assertValidSlugs(views)
|
|
72
|
+
const out: Record<string, { slug: string }> = {}
|
|
73
|
+
for (const slug of Object.keys(views)) out[slug] = { slug }
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Impl half — `ViewSchema[]` for `serialize` plus the per-slug `FunctionBinding`
|
|
79
|
+
* map the worker mounts routes from. `viewFor` markers are resolved here to
|
|
80
|
+
* concrete target paths (class meta-nodes) because the kernel-core serializer is
|
|
81
|
+
* pure and does not resolve `defineCore` endpoints.
|
|
82
|
+
*/
|
|
83
|
+
export function buildViewSchemas(
|
|
84
|
+
views: Record<string, AnyViewDef>,
|
|
85
|
+
url: string,
|
|
86
|
+
viewsFolder: string,
|
|
87
|
+
schema: Schema,
|
|
88
|
+
): { schemas: ViewSchema[]; bindings: Record<string, FunctionBinding> } {
|
|
89
|
+
const schemas: ViewSchema[] = []
|
|
90
|
+
const bindings: Record<string, FunctionBinding> = {}
|
|
91
|
+
for (const [slug, def] of Object.entries(views)) {
|
|
92
|
+
assertExclusivity(slug, def)
|
|
93
|
+
// `mount` is a worker-relative SPA path → `<url><mount>`; otherwise the
|
|
94
|
+
// default/overridden `<url>/<viewsFolder>/<slug>`.
|
|
95
|
+
const binding = def.mount
|
|
96
|
+
? { remoteUrl: joinWorkerPath(url, def.mount) }
|
|
97
|
+
: resolveBinding(def.binding, url, viewsFolder, slug)
|
|
98
|
+
bindings[slug] = binding
|
|
99
|
+
// Inline `render` views ship no shell client → default handshake 'none'.
|
|
100
|
+
const handshake = def.handshake ?? (def.render ? 'none' : undefined)
|
|
101
|
+
schemas.push({
|
|
102
|
+
ref: `view.${slug}`,
|
|
103
|
+
name: slug,
|
|
104
|
+
binding,
|
|
105
|
+
...(handshake ? { handshake } : {}),
|
|
106
|
+
viewFor: resolveViewForTargets(def.viewFor, schema),
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
return { schemas, bindings }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve `defineView`'s `viewFor` endpoints to concrete graph path raws.
|
|
114
|
+
* Mirrors kernel-core `compileCore`'s endpoint resolution (the old channel that
|
|
115
|
+
* carried view_for as core edges): `selfOf(Def)` → the def's class/interface
|
|
116
|
+
* meta-node `/<declaringDomain>/{class,interface}.<Name>/self`; a `CorePath`
|
|
117
|
+
* string → its anchored graph path. Empty when the view targets nothing.
|
|
118
|
+
*/
|
|
119
|
+
function resolveViewForTargets(
|
|
120
|
+
viewFor: EdgeEndpoint | readonly EdgeEndpoint[] | undefined,
|
|
121
|
+
schema: Schema,
|
|
122
|
+
): string[] {
|
|
123
|
+
if (!viewFor) return []
|
|
124
|
+
const targets = Array.isArray(viewFor) ? viewFor : [viewFor]
|
|
125
|
+
if (targets.length === 0) return []
|
|
126
|
+
|
|
127
|
+
const descriptorMap = buildDefDescriptorMap(schema)
|
|
128
|
+
const defOrigins = buildDefOriginMap(schema, scanImportedDefs(schema))
|
|
129
|
+
|
|
130
|
+
return targets.map((target) => {
|
|
131
|
+
if (isSelfMarker(target)) {
|
|
132
|
+
const def = target.__def
|
|
133
|
+
const desc = descriptorMap.get(def)
|
|
134
|
+
if (!desc) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
'defineView: `viewFor: selfOf(...)` references a def that is neither in this ' +
|
|
137
|
+
"schema nor any of its `imports`. Add the def's schema to `imports`.",
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
const origin = defOrigins.get(def) ?? schema.domain
|
|
141
|
+
const dp = DomainPaths.of(origin)
|
|
142
|
+
return isAbstract(def) ? dp.interface(desc.name).raw : dp.class(desc.name).raw
|
|
143
|
+
}
|
|
144
|
+
if (isCorePath(target)) {
|
|
145
|
+
return DomainPaths.of(schema.domain).corePath(target).raw
|
|
146
|
+
}
|
|
147
|
+
throw new Error(
|
|
148
|
+
'defineView: unsupported `viewFor` endpoint — use `selfOf(Class)` or a CorePath.',
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,6 @@ export { remoteMethod, remoteClassMethods, remoteInterfaceMethods } from './meth
|
|
|
19
19
|
// (preferred) use `domainWorkerEntry` from `@astrale-os/sdk/server`.
|
|
20
20
|
export { defineDomain, deploy, defineAdapter } from './config'
|
|
21
21
|
export type {
|
|
22
|
-
ClientBinding,
|
|
23
22
|
DefineDomainConfig,
|
|
24
23
|
DomainDefinition,
|
|
25
24
|
DeployConfig,
|
|
@@ -75,7 +74,13 @@ export type {
|
|
|
75
74
|
|
|
76
75
|
// ─── Auth ────────────────────────────────────────────────────────────────
|
|
77
76
|
export type { RemoteIdentityConfig, AuthenticateResult } from './auth'
|
|
78
|
-
export {
|
|
77
|
+
export {
|
|
78
|
+
authenticateRequest,
|
|
79
|
+
bindSelfKernel,
|
|
80
|
+
buildComposedGrant,
|
|
81
|
+
makeSelfKernel,
|
|
82
|
+
signCredential,
|
|
83
|
+
} from './auth'
|
|
79
84
|
export { AuthMissingError, AuthInvalidError } from './auth'
|
|
80
85
|
export { assertPerm, requireOwnership, READ, EDIT, USE, SHARE, ALL } from './auth'
|
|
81
86
|
export type {
|
|
@@ -143,7 +143,7 @@ export function mountAuxiliaryRoutes<TDeps>(config: AuxiliaryRoutesConfig<TDeps>
|
|
|
143
143
|
if (!outValidation.ok) {
|
|
144
144
|
throw new SdkResultValidationError(
|
|
145
145
|
outValidation.issues as SdkResultValidationError['issues'],
|
|
146
|
-
|
|
146
|
+
`function.${slug}`,
|
|
147
147
|
)
|
|
148
148
|
}
|
|
149
149
|
return c.json({ result: outValidation.data })
|
|
@@ -154,9 +154,9 @@ export function clientOrigin(url: URL, request: Request): string {
|
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
156
|
* Build a `before` hook that serves a static-asset `binding` (e.g. a Workers
|
|
157
|
-
* Assets binding) mounted under `base` (default `/ui`) — the runtime half of
|
|
158
|
-
*
|
|
159
|
-
* when no binding is present) so the request falls through to domain dispatch.
|
|
157
|
+
* Assets binding) mounted under `base` (default `/ui`) — the runtime half of an
|
|
158
|
+
* adapter env's client-asset config. Returns `undefined` for non-matching paths
|
|
159
|
+
* (and when no binding is present) so the request falls through to domain dispatch.
|
|
160
160
|
*
|
|
161
161
|
* Asset URLs are rooted at `base` (a client bundler sets `base: '<base>/'`);
|
|
162
162
|
* this hook strips that prefix before delegating to the binding, so
|
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build an augmented `Core<S>` that includes auto-materialized View and
|
|
3
|
-
* standalone-function nodes (one per entry in `defineRemoteDomain`'s `views` /
|
|
4
|
-
* `remoteFunctions` config), plus their parent Folder + optional `view_for`
|
|
5
|
-
* edges. Returns the resolved effective bindings alongside so callers don't
|
|
6
|
-
* recompute them.
|
|
7
|
-
*
|
|
8
|
-
* Standalone callables (the former `RemoteFunction` class) are materialized as
|
|
9
|
-
* the canonical kernel `Function` node class — "remote" is just a `binding`,
|
|
10
|
-
* not a distinct kind, so there is no per-domain class to configure.
|
|
11
|
-
*
|
|
12
|
-
* Conflicts: if the user core already declares a top-level node at
|
|
13
|
-
* `<viewsFolder>` or `<functionsFolder>`, throws — the auto-materialization
|
|
14
|
-
* reserves those slugs.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
|
|
18
|
-
import type {
|
|
19
|
-
AnyEdgeDef,
|
|
20
|
-
AnyNodeDef,
|
|
21
|
-
Core,
|
|
22
|
-
CoreEdgeEntry,
|
|
23
|
-
CoreNodeEntry,
|
|
24
|
-
Schema,
|
|
25
|
-
} from '@astrale-os/kernel-dsl'
|
|
26
|
-
|
|
27
|
-
import { Folder, K, KernelSchema } from '@astrale-os/kernel-core'
|
|
28
|
-
import { buildCorePath } from '@astrale-os/kernel-dsl'
|
|
29
|
-
|
|
30
|
-
/** Canonical node class for standalone callables (the former `RemoteFunction`). */
|
|
31
|
-
const FUNCTION_CLASS = KernelSchema.classes.Function as unknown as AnyNodeDef
|
|
32
|
-
/** Canonical node class for GUI views. */
|
|
33
|
-
const VIEW_CLASS = KernelSchema.classes.View as unknown as AnyNodeDef
|
|
34
|
-
/** Canonical edge class linking a View to its target. */
|
|
35
|
-
const VIEW_FOR_EDGE_CLASS = KernelSchema.classes.view_for as unknown as AnyEdgeDef
|
|
36
|
-
|
|
37
|
-
import type { AnyRemoteFunctionDef } from '../define/remote-function'
|
|
38
|
-
import type { ViewDef } from '../define/view'
|
|
39
|
-
|
|
40
|
-
export const DEFAULT_VIEWS_FOLDER = 'views'
|
|
41
|
-
export const DEFAULT_FUNCTIONS_FOLDER = 'functions'
|
|
42
|
-
const SLUG_RE = /^[a-z][a-z0-9-]*$/
|
|
43
|
-
|
|
44
|
-
export type ExtendCoreConfig = {
|
|
45
|
-
schema: Schema
|
|
46
|
-
origin: string
|
|
47
|
-
userCore?: Core
|
|
48
|
-
/**
|
|
49
|
-
* The worker's serving URL — the base every auto-materialized binding
|
|
50
|
-
* resolves against. OMITTED at define time (the URL is known only to the
|
|
51
|
-
* spec producers): the aux nodes then materialize structure-only (paths,
|
|
52
|
-
* names, refs — what identity/subs resolution needs) with no `binding`
|
|
53
|
-
* stamped and empty binding maps. Every install graph comes from a
|
|
54
|
-
* `materializeRemoteDomain(domain, url)` call where it is present.
|
|
55
|
-
*/
|
|
56
|
-
url?: string
|
|
57
|
-
|
|
58
|
-
viewClass?: AnyNodeDef
|
|
59
|
-
viewForEdgeClass?: AnyEdgeDef
|
|
60
|
-
viewsFolder?: string
|
|
61
|
-
// oxlint-disable-next-line no-explicit-any
|
|
62
|
-
views?: Record<string, ViewDef<any>>
|
|
63
|
-
|
|
64
|
-
functionsFolder?: string
|
|
65
|
-
remoteFunctions?: Record<string, AnyRemoteFunctionDef>
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export type ExtendCoreResult = {
|
|
69
|
-
core: Core
|
|
70
|
-
viewBindings: Record<string, FunctionBinding>
|
|
71
|
-
remoteFunctionBindings: Record<string, FunctionBinding>
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function extendCore(config: ExtendCoreConfig): ExtendCoreResult {
|
|
75
|
-
const {
|
|
76
|
-
schema,
|
|
77
|
-
origin,
|
|
78
|
-
userCore,
|
|
79
|
-
url,
|
|
80
|
-
viewClass = VIEW_CLASS,
|
|
81
|
-
viewForEdgeClass = VIEW_FOR_EDGE_CLASS,
|
|
82
|
-
viewsFolder = DEFAULT_VIEWS_FOLDER,
|
|
83
|
-
views,
|
|
84
|
-
functionsFolder = DEFAULT_FUNCTIONS_FOLDER,
|
|
85
|
-
remoteFunctions,
|
|
86
|
-
} = config
|
|
87
|
-
|
|
88
|
-
validateInputs(config)
|
|
89
|
-
|
|
90
|
-
const nodes: CoreNodeEntry[] = userCore ? [...userCore.__nodes] : []
|
|
91
|
-
const edges: CoreEdgeEntry[] = userCore ? [...userCore.__edges] : []
|
|
92
|
-
const viewBindings: Record<string, FunctionBinding> = {}
|
|
93
|
-
const remoteFunctionBindings: Record<string, FunctionBinding> = {}
|
|
94
|
-
const enableUiHandshake = viewClass === VIEW_CLASS
|
|
95
|
-
|
|
96
|
-
if (views && viewClass) {
|
|
97
|
-
// Enforce ViewDef's documented exclusivity: `mount` (SPA route) cannot be
|
|
98
|
-
// combined with `render` (inline HTML handler) or an explicit `binding` —
|
|
99
|
-
// silently ignoring one of them would deploy a view that behaves
|
|
100
|
-
// differently than its definition reads.
|
|
101
|
-
for (const [slug, def] of Object.entries(views)) {
|
|
102
|
-
if (def.mount && (def.render || def.binding)) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`defineRemoteDomain: view "${slug}" sets \`mount\` together with ` +
|
|
105
|
-
`${def.render ? '`render`' : '`binding`'} — they are mutually exclusive.`,
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
assertNoConflict(nodes, origin, viewsFolder)
|
|
110
|
-
addFolderAndEntries({
|
|
111
|
-
origin,
|
|
112
|
-
folderSlug: viewsFolder,
|
|
113
|
-
nodes,
|
|
114
|
-
entries: Object.entries(views).map(([slug, def]) => {
|
|
115
|
-
const binding = url
|
|
116
|
-
? def.mount
|
|
117
|
-
? { remoteUrl: joinWorkerPath(url, def.mount) }
|
|
118
|
-
: resolveBinding(def.binding, url, viewsFolder, slug)
|
|
119
|
-
: undefined
|
|
120
|
-
if (binding) viewBindings[slug] = binding
|
|
121
|
-
return {
|
|
122
|
-
slug,
|
|
123
|
-
nodeClass: viewClass,
|
|
124
|
-
data: buildViewData(slug, binding, def, enableUiHandshake),
|
|
125
|
-
edges: buildViewForEdges(slug, def, viewForEdgeClass, viewsFolder, origin),
|
|
126
|
-
}
|
|
127
|
-
}),
|
|
128
|
-
edges,
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (remoteFunctions) {
|
|
133
|
-
assertNoConflict(nodes, origin, functionsFolder)
|
|
134
|
-
addFolderAndEntries({
|
|
135
|
-
origin,
|
|
136
|
-
folderSlug: functionsFolder,
|
|
137
|
-
nodes,
|
|
138
|
-
entries: Object.entries(remoteFunctions).map(([slug, def]) => {
|
|
139
|
-
const binding = url ? resolveBinding(def.binding, url, functionsFolder, slug) : undefined
|
|
140
|
-
if (binding) remoteFunctionBindings[slug] = binding
|
|
141
|
-
return {
|
|
142
|
-
slug,
|
|
143
|
-
// Standalone callables materialize as the canonical kernel Function class.
|
|
144
|
-
nodeClass: FUNCTION_CLASS,
|
|
145
|
-
data: {
|
|
146
|
-
...buildFunctionData(slug, binding),
|
|
147
|
-
[K.$.i('Function').ref.key]: def.ref ?? `function.${slug}`,
|
|
148
|
-
},
|
|
149
|
-
edges: [],
|
|
150
|
-
}
|
|
151
|
-
}),
|
|
152
|
-
edges,
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
core: { schema, domain: origin, __nodes: nodes, __edges: edges },
|
|
158
|
-
viewBindings,
|
|
159
|
-
remoteFunctionBindings,
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Join a worker-relative mount path onto the serving url (single slash). */
|
|
164
|
-
function joinWorkerPath(url: string, mount: string): string {
|
|
165
|
-
const base = url.replace(/\/+$/, '')
|
|
166
|
-
return `${base}/${mount.replace(/^\/+/, '')}`
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function resolveBinding(
|
|
170
|
-
override: FunctionBinding | undefined,
|
|
171
|
-
url: string,
|
|
172
|
-
folderSlug: string,
|
|
173
|
-
slug: string,
|
|
174
|
-
): FunctionBinding {
|
|
175
|
-
// Same trailing-slash discipline as `joinWorkerPath` — a `url` ending in `/`
|
|
176
|
-
// must not produce `//` in the binding (the kernel pins iss by exact string).
|
|
177
|
-
const base = url.replace(/\/+$/, '')
|
|
178
|
-
if (override) {
|
|
179
|
-
return {
|
|
180
|
-
...override,
|
|
181
|
-
remoteUrl: override.remoteUrl ?? `${base}/${folderSlug}/${slug}`,
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return { remoteUrl: `${base}/${folderSlug}/${slug}` }
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ── Internal ───────────────────────────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
function validateInputs(config: ExtendCoreConfig): void {
|
|
190
|
-
if (config.views) {
|
|
191
|
-
for (const slug of Object.keys(config.views)) {
|
|
192
|
-
if (!SLUG_RE.test(slug)) {
|
|
193
|
-
throw new Error(`defineRemoteDomain: invalid view slug "${slug}" — must match ${SLUG_RE}.`)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (config.remoteFunctions) {
|
|
198
|
-
for (const slug of Object.keys(config.remoteFunctions)) {
|
|
199
|
-
if (!SLUG_RE.test(slug)) {
|
|
200
|
-
throw new Error(
|
|
201
|
-
`defineRemoteDomain: invalid remote-function slug "${slug}" — must match ${SLUG_RE}.`,
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function assertNoConflict(
|
|
209
|
-
nodes: readonly CoreNodeEntry[],
|
|
210
|
-
origin: string,
|
|
211
|
-
folderSlug: string,
|
|
212
|
-
): void {
|
|
213
|
-
const folderPath = buildCorePath(origin, [folderSlug])
|
|
214
|
-
if (nodes.some((n) => n.path === folderPath)) {
|
|
215
|
-
throw new Error(
|
|
216
|
-
`defineRemoteDomain: top-level core slug "${folderSlug}" is reserved by ` +
|
|
217
|
-
'SDK auto-materialization. Move the conflicting node or rename the ' +
|
|
218
|
-
'`viewsFolder` / `functionsFolder` config.',
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
type EntryDescriptor = {
|
|
224
|
-
slug: string
|
|
225
|
-
nodeClass: AnyNodeDef
|
|
226
|
-
data: Record<string, unknown>
|
|
227
|
-
edges: CoreEdgeEntry[]
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function addFolderAndEntries(args: {
|
|
231
|
-
origin: string
|
|
232
|
-
folderSlug: string
|
|
233
|
-
entries: EntryDescriptor[]
|
|
234
|
-
nodes: CoreNodeEntry[]
|
|
235
|
-
edges: CoreEdgeEntry[]
|
|
236
|
-
}): void {
|
|
237
|
-
const { origin, folderSlug, entries, nodes, edges } = args
|
|
238
|
-
|
|
239
|
-
const folderPath = buildCorePath(origin, [folderSlug])
|
|
240
|
-
nodes.push({
|
|
241
|
-
path: folderPath,
|
|
242
|
-
def: Folder as unknown as AnyNodeDef,
|
|
243
|
-
data: { [K.Named.name.key]: folderSlug },
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
for (const entry of entries) {
|
|
247
|
-
nodes.push({
|
|
248
|
-
path: buildCorePath(origin, [folderSlug, entry.slug]),
|
|
249
|
-
def: entry.nodeClass,
|
|
250
|
-
data: entry.data,
|
|
251
|
-
parent: folderPath,
|
|
252
|
-
})
|
|
253
|
-
for (const e of entry.edges) edges.push(e)
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function buildFunctionData(
|
|
258
|
-
slug: string,
|
|
259
|
-
binding: FunctionBinding | undefined,
|
|
260
|
-
): Record<string, unknown> {
|
|
261
|
-
// Match the kernel-core schema serializer which JSON-stringifies the
|
|
262
|
-
// `Function.binding` value on Function nodes (see
|
|
263
|
-
// kernel/core/domain/serialize/schema.ts). The kernel install validator
|
|
264
|
-
// checks `typeof binding === 'string'` then parses; storing a raw object
|
|
265
|
-
// trips the validator with "missing remote binding" even when the object IS
|
|
266
|
-
// the binding. No binding at all (define-time, no url) → no prop stamped.
|
|
267
|
-
return {
|
|
268
|
-
[K.Named.name.key]: slug,
|
|
269
|
-
...(binding ? { [K.$.i('Function').binding.key]: JSON.stringify(binding) } : {}),
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function buildViewData(
|
|
274
|
-
slug: string,
|
|
275
|
-
binding: FunctionBinding | undefined,
|
|
276
|
-
def: ViewDef,
|
|
277
|
-
enableUiHandshake: boolean,
|
|
278
|
-
): Record<string, unknown> {
|
|
279
|
-
const handshake = def.handshake ?? (def.render ? 'none' : undefined)
|
|
280
|
-
return {
|
|
281
|
-
...buildFunctionData(slug, binding),
|
|
282
|
-
...(enableUiHandshake && handshake ? { [K.UI.handshake.key]: handshake } : {}),
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function buildViewForEdges(
|
|
287
|
-
slug: string,
|
|
288
|
-
def: ViewDef,
|
|
289
|
-
edgeClass: AnyEdgeDef | undefined,
|
|
290
|
-
viewsFolder: string,
|
|
291
|
-
origin: string,
|
|
292
|
-
): CoreEdgeEntry[] {
|
|
293
|
-
if (!def.viewFor || !edgeClass) return []
|
|
294
|
-
const targets = Array.isArray(def.viewFor) ? def.viewFor : [def.viewFor]
|
|
295
|
-
const from = buildCorePath(origin, [viewsFolder, slug])
|
|
296
|
-
return targets.map((target) => ({
|
|
297
|
-
from,
|
|
298
|
-
edge: edgeClass,
|
|
299
|
-
to: target as CoreEdgeEntry['to'],
|
|
300
|
-
}))
|
|
301
|
-
}
|