@prisma-next/framework-components 0.13.0-dev.3 → 0.13.0-dev.31
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/authoring.d.mts +2 -2
- package/dist/authoring.mjs +1 -1
- package/dist/{codec-DCQAerzB.d.mts → codec-types-yY3eSmi0.d.mts} +90 -67
- package/dist/codec-types-yY3eSmi0.d.mts.map +1 -0
- package/dist/codec.d.mts +23 -2
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +2 -1
- package/dist/codec.mjs.map +1 -1
- package/dist/components.d.mts +1 -1
- package/dist/control.d.mts +12 -24
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +15 -2
- package/dist/control.mjs.map +1 -1
- package/dist/execution.d.mts +1 -1
- package/dist/{framework-authoring-R0TYCkvG.d.mts → framework-authoring-C0UK0DAr.d.mts} +116 -9
- package/dist/framework-authoring-C0UK0DAr.d.mts.map +1 -0
- package/dist/{framework-authoring-CnwPJCO4.mjs → framework-authoring-Dv5F3EFC.mjs} +51 -24
- package/dist/framework-authoring-Dv5F3EFC.mjs.map +1 -0
- package/dist/{framework-components-DDQXmW0b.d.mts → framework-components-qbWtCu6G.d.mts} +3 -3
- package/dist/{framework-components-DDQXmW0b.d.mts.map → framework-components-qbWtCu6G.d.mts.map} +1 -1
- package/dist/ir.d.mts +11 -4
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +26 -6
- package/dist/ir.mjs.map +1 -1
- package/dist/{psl-ast-Cn50B-UG.d.mts → psl-ast-D5WPsvPp.d.mts} +11 -37
- package/dist/psl-ast-D5WPsvPp.d.mts.map +1 -0
- package/dist/psl-ast.d.mts +4 -4
- package/dist/psl-ast.mjs +18 -32
- package/dist/psl-ast.mjs.map +1 -1
- package/dist/resolve-codec-DR7uyr_c.mjs +47 -0
- package/dist/resolve-codec-DR7uyr_c.mjs.map +1 -0
- package/dist/runtime-error-B2gWOtgH.mjs +37 -0
- package/dist/runtime-error-B2gWOtgH.mjs.map +1 -0
- package/dist/runtime.d.mts +12 -10
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +1 -33
- package/dist/runtime.mjs.map +1 -1
- package/package.json +7 -7
- package/src/control/control-migration-types.ts +5 -22
- package/src/control/control-stack.ts +28 -3
- package/src/control/psl-ast.ts +10 -61
- package/src/control/psl-extension-block-validator.ts +11 -9
- package/src/execution/runtime-error.ts +5 -55
- package/src/exports/authoring.ts +1 -0
- package/src/exports/codec.ts +2 -0
- package/src/exports/control.ts +0 -1
- package/src/exports/ir.ts +1 -1
- package/src/ir/storage.ts +33 -6
- package/src/shared/codec-descriptor.ts +5 -0
- package/src/shared/codec-types.ts +21 -1
- package/src/shared/framework-authoring.ts +118 -35
- package/src/shared/psl-extension-block.ts +80 -5
- package/src/shared/resolve-codec.ts +64 -0
- package/src/shared/runtime-error.ts +50 -0
- package/dist/codec-DCQAerzB.d.mts.map +0 -1
- package/dist/framework-authoring-CnwPJCO4.mjs.map +0 -1
- package/dist/framework-authoring-R0TYCkvG.d.mts.map +0 -1
- package/dist/psl-ast-Cn50B-UG.d.mts.map +0 -1
|
@@ -107,15 +107,17 @@ export function validateExtensionBlock(
|
|
|
107
107
|
const nodeKeys = new Set(Object.keys(node.parameters));
|
|
108
108
|
|
|
109
109
|
// 1. Unknown parameters — keys in the node not in the descriptor.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
if (!descriptor.variadicParameters) {
|
|
111
|
+
for (const key of nodeKeys) {
|
|
112
|
+
if (!descriptorKeys.has(key)) {
|
|
113
|
+
const captured = node.parameters[key];
|
|
114
|
+
diagnostics.push({
|
|
115
|
+
code: 'PSL_EXTENSION_UNKNOWN_PARAMETER',
|
|
116
|
+
message: `Unknown parameter "${key}" in "${descriptor.keyword}" block "${node.name}". The descriptor does not declare this parameter.`,
|
|
117
|
+
sourceId,
|
|
118
|
+
span: captured?.span ?? node.span,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
1
|
+
import type { RuntimeErrorEnvelope } from '../shared/runtime-error';
|
|
2
|
+
import { runtimeError } from '../shared/runtime-error';
|
|
3
|
+
|
|
4
|
+
export type { RuntimeErrorEnvelope } from '../shared/runtime-error';
|
|
5
|
+
export { isRuntimeError, runtimeError } from '../shared/runtime-error';
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Stable code emitted by the runtime when an in-flight `execute()`
|
|
@@ -30,55 +29,6 @@ export type RuntimeAbortedPhase =
|
|
|
30
29
|
| 'afterExecute'
|
|
31
30
|
| 'onRow';
|
|
32
31
|
|
|
33
|
-
/**
|
|
34
|
-
* Type guard for the runtime-error envelope produced by `runtimeError`.
|
|
35
|
-
*
|
|
36
|
-
* Prefer this over duck-typing on `error.code` directly so consumers stay
|
|
37
|
-
* insulated from the envelope's internal shape.
|
|
38
|
-
*/
|
|
39
|
-
export function isRuntimeError(error: unknown): error is RuntimeErrorEnvelope {
|
|
40
|
-
return (
|
|
41
|
-
error instanceof Error &&
|
|
42
|
-
'code' in error &&
|
|
43
|
-
typeof (error as { code?: unknown }).code === 'string' &&
|
|
44
|
-
'category' in error &&
|
|
45
|
-
'severity' in error
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function runtimeError(
|
|
50
|
-
code: string,
|
|
51
|
-
message: string,
|
|
52
|
-
details?: Record<string, unknown>,
|
|
53
|
-
): RuntimeErrorEnvelope {
|
|
54
|
-
const error = new Error(message) as RuntimeErrorEnvelope;
|
|
55
|
-
Object.defineProperty(error, 'name', {
|
|
56
|
-
value: 'RuntimeError',
|
|
57
|
-
configurable: true,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
return Object.assign(error, {
|
|
61
|
-
code,
|
|
62
|
-
category: resolveCategory(code),
|
|
63
|
-
severity: 'error' as const,
|
|
64
|
-
message,
|
|
65
|
-
details,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function resolveCategory(code: string): RuntimeErrorEnvelope['category'] {
|
|
70
|
-
const prefix = code.split('.')[0] ?? 'RUNTIME';
|
|
71
|
-
switch (prefix) {
|
|
72
|
-
case 'PLAN':
|
|
73
|
-
case 'CONTRACT':
|
|
74
|
-
case 'LINT':
|
|
75
|
-
case 'BUDGET':
|
|
76
|
-
return prefix;
|
|
77
|
-
default:
|
|
78
|
-
return 'RUNTIME';
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
32
|
/**
|
|
83
33
|
* Construct a `RUNTIME.ABORTED` envelope. Phase distinguishes where the
|
|
84
34
|
* abort was observed — codec call sites (`encode` / `decode` / `stream`)
|
package/src/exports/authoring.ts
CHANGED
package/src/exports/codec.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type {
|
|
|
16
16
|
CodecLookup,
|
|
17
17
|
CodecMeta,
|
|
18
18
|
CodecRef,
|
|
19
|
+
CodecRegistry,
|
|
19
20
|
CodecTrait,
|
|
20
21
|
} from '../shared/codec-types';
|
|
21
22
|
export { emptyCodecLookup, voidParamsSchema } from '../shared/codec-types';
|
|
@@ -26,3 +27,4 @@ export type {
|
|
|
26
27
|
ColumnTypeDescriptor,
|
|
27
28
|
} from '../shared/column-spec';
|
|
28
29
|
export { column } from '../shared/column-spec';
|
|
30
|
+
export { materializeCodec, validateCodecTypeParams } from '../shared/resolve-codec';
|
package/src/exports/control.ts
CHANGED
package/src/exports/ir.ts
CHANGED
|
@@ -4,5 +4,5 @@ export { freezeNode, IRNodeBase } from '../ir/ir-node';
|
|
|
4
4
|
export type { Namespace } from '../ir/namespace';
|
|
5
5
|
export { NamespaceBase, UNBOUND_NAMESPACE_ID } from '../ir/namespace';
|
|
6
6
|
export type { EntityCoordinate, Storage } from '../ir/storage';
|
|
7
|
-
export { elementCoordinates } from '../ir/storage';
|
|
7
|
+
export { elementCoordinates, entityAt } from '../ir/storage';
|
|
8
8
|
export type { StorageType } from '../ir/storage-type';
|
package/src/ir/storage.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StorageBase } from '@prisma-next/contract/types';
|
|
2
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
2
3
|
import type { IRNode } from './ir-node';
|
|
3
4
|
import type { Namespace } from './namespace';
|
|
4
5
|
|
|
@@ -30,11 +31,11 @@ export interface EntityCoordinate {
|
|
|
30
31
|
* value, yielded as {@link EntityCoordinate} tuples with
|
|
31
32
|
* `plane: 'storage'` (the parameter type binds the plane).
|
|
32
33
|
*
|
|
33
|
-
* Iterates each namespace's `entries`
|
|
34
|
+
* Iterates each namespace's `entries` kind maps structurally. Skips
|
|
34
35
|
* non-object `entries`; `id` and `kind` are not walked (`kind` is
|
|
35
36
|
* non-enumerable on concretions). For every entity-kind key under
|
|
36
37
|
* `entries` whose value is a non-null object, yields one coordinate per
|
|
37
|
-
* entity name in that map. No family-specific
|
|
38
|
+
* entity name in that map. No family-specific kind vocabulary is required.
|
|
38
39
|
*/
|
|
39
40
|
export function* elementCoordinates(
|
|
40
41
|
storage: Pick<StorageBase, 'namespaces'>,
|
|
@@ -42,15 +43,41 @@ export function* elementCoordinates(
|
|
|
42
43
|
for (const [namespaceId, ns] of Object.entries(storage.namespaces)) {
|
|
43
44
|
const entries = ns.entries;
|
|
44
45
|
if (entries === null || typeof entries !== 'object') continue;
|
|
45
|
-
for (const [entityKind,
|
|
46
|
-
if (
|
|
47
|
-
for (const entityName of Object.keys(
|
|
46
|
+
for (const [entityKind, kindMap] of Object.entries(entries)) {
|
|
47
|
+
if (kindMap === null || typeof kindMap !== 'object') continue;
|
|
48
|
+
for (const entityName of Object.keys(kindMap)) {
|
|
48
49
|
yield { plane: 'storage', namespaceId, entityKind, entityName };
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
56
|
+
return typeof value === 'object' && value !== null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Looks up a single entity in a `Storage`-shaped value by its full coordinate.
|
|
61
|
+
* Returns `undefined` if the namespace, entity kind, or entity name is absent.
|
|
62
|
+
* The type parameter is a caller assertion — the walk itself is structural
|
|
63
|
+
* and cannot verify the entity's shape.
|
|
64
|
+
*/
|
|
65
|
+
export function entityAt<T = unknown>(
|
|
66
|
+
storage: Pick<StorageBase, 'namespaces'>,
|
|
67
|
+
coord: Pick<EntityCoordinate, 'namespaceId' | 'entityKind' | 'entityName'>,
|
|
68
|
+
): T | undefined {
|
|
69
|
+
const ns = storage.namespaces[coord.namespaceId];
|
|
70
|
+
if (ns === undefined) return undefined;
|
|
71
|
+
const entries = ns.entries;
|
|
72
|
+
if (!isRecord(entries)) return undefined;
|
|
73
|
+
const kindMap = entries[coord.entityKind];
|
|
74
|
+
if (!isRecord(kindMap)) return undefined;
|
|
75
|
+
if (!Object.hasOwn(kindMap, coord.entityName)) return undefined;
|
|
76
|
+
return blindCast<T | undefined, 'caller asserts the entity type at this coordinate'>(
|
|
77
|
+
kindMap[coord.entityName],
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
/**
|
|
55
82
|
* Framework-level promise that every Contract IR / Schema IR carries a
|
|
56
83
|
* collection of namespaces keyed by namespace id. Family storage
|
|
@@ -66,7 +93,7 @@ export function* elementCoordinates(
|
|
|
66
93
|
* is honest at every layer.
|
|
67
94
|
*
|
|
68
95
|
* Extends `IRNode` so the framework's IR-walking surfaces (verifiers,
|
|
69
|
-
* serializers) can dispatch on `Storage`-typed
|
|
96
|
+
* serializers) can dispatch on `Storage`-typed fields through the same
|
|
70
97
|
* IR-node alphabet as every other node — the structural dual already
|
|
71
98
|
* holds in code (every concrete storage class extends an IR-node base);
|
|
72
99
|
* the interface promotion makes the typing honest.
|
|
@@ -43,6 +43,8 @@ export interface CodecDescriptor<P = void> {
|
|
|
43
43
|
readonly isParameterized: boolean;
|
|
44
44
|
/** Emit-path string renderer for `contract.d.ts`. Returns the TypeScript output type expression for given params (e.g. `Vector<1536>`). Optional; absent renderers cause the emitter to fall back to the codec's base output type. Non-parameterized codecs typically omit it. */
|
|
45
45
|
readonly renderOutputType?: (params: P) => string | undefined;
|
|
46
|
+
/** Emit-path string renderer for the `contract.d.ts` *input* position (create/update values). Returns the TypeScript input type expression for given params. Optional; absent renderers fall back to the codec's base input type. A codec supplies this when its write type is narrower than the generic codec input — e.g. an enum whose input should be the literal member union, not `string`. */
|
|
47
|
+
readonly renderInputType?: (params: P) => string | undefined;
|
|
46
48
|
/** The curried higher-order codec. For non-parameterized codecs, the factory is constant — every call returns the same shared codec instance. For parameterized codecs, the factory is called once per `storage.types` instance (or once per inline-`typeParams` column), with `ctx` carrying the column set the resulting codec serves. */
|
|
47
49
|
readonly factory: (params: P) => (ctx: CodecInstanceContext) => Codec;
|
|
48
50
|
}
|
|
@@ -78,6 +80,9 @@ export abstract class CodecDescriptorImpl<TParams = void> implements CodecDescri
|
|
|
78
80
|
/** Optional emit-path string renderer for `contract.d.ts`. Returns the TypeScript output type expression for the given params (e.g. `Vector<1536>`). Non-parameterized codecs typically omit it. */
|
|
79
81
|
renderOutputType?(params: TParams): string | undefined;
|
|
80
82
|
|
|
83
|
+
/** Optional emit-path string renderer for the `contract.d.ts` input position. Returns the TypeScript input type expression for the given params; supplied when the write type is narrower than the generic codec input (e.g. an enum's literal member union). */
|
|
84
|
+
renderInputType?(params: TParams): string | undefined;
|
|
85
|
+
|
|
81
86
|
/**
|
|
82
87
|
* Materialize a curried codec factory for the given params. Concrete subclasses override with a typed return type (e.g. `factory<N>(params: { length: N }): (ctx) => VectorCodec<N>`); per-codec helpers read the typed return at the *direct* call site, which is what preserves method-level generics. Type extraction (e.g. `ReturnType<D['factory']>`) widens method generics to their constraint — that's why the column-helper surface is per-codec, not polymorphic.
|
|
83
88
|
*/
|
|
@@ -36,7 +36,7 @@ export interface CodecCallContext {
|
|
|
36
36
|
/**
|
|
37
37
|
* Codec-id-keyed read surface threaded into emit and authoring paths.
|
|
38
38
|
*
|
|
39
|
-
* - `get(id)` returns
|
|
39
|
+
* - `get(id)` returns a representative {@link Codec} instance for the codec id (used by `family.deserializeContract` for `decodeJson` of literal column defaults). For parameterized codecs whose factory requires concrete params, this may return `undefined` — use `CodecRegistry.forCodecRef` instead.
|
|
40
40
|
* - `targetTypesFor(id)` exposes the codec-id-keyed `targetTypes` metadata the runtime instance no longer carries (TML-2357). Returns the same array `CodecDescriptor.targetTypes` would; for Mongo (whose registration doesn't yet resolve through the unified descriptor map — TML-2324) the family-side assembly populates this directly from the contributor's codec metadata.
|
|
41
41
|
* - `metaFor(id)` exposes the codec-id-keyed `meta` (e.g. SQL-side `db.sql.postgres.nativeType`) the runtime instance no longer carries.
|
|
42
42
|
* - `renderOutputTypeFor(id, params)` exposes the codec-id-keyed `renderOutputType` renderer the runtime instance no longer carries. Returns `undefined` when the codec doesn't render a custom type or when the codec id is unknown.
|
|
@@ -46,6 +46,26 @@ export interface CodecLookup {
|
|
|
46
46
|
targetTypesFor(id: string): readonly string[] | undefined;
|
|
47
47
|
metaFor(id: string): CodecMeta | undefined;
|
|
48
48
|
renderOutputTypeFor(id: string, params: Record<string, unknown>): string | undefined;
|
|
49
|
+
/** Codec-id-keyed `renderInputType` renderer for the `contract.d.ts` input position. Optional so existing lookups need not provide it; returns `undefined` when the codec renders no custom input type or the id is unknown. */
|
|
50
|
+
renderInputTypeFor?(id: string, params: Record<string, unknown>): string | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Full codec registry — the read surface of {@link CodecLookup} plus codec resolution by ref or
|
|
55
|
+
* column coordinate. Built once by `extractCodecLookup` and passed by reference to adapters and
|
|
56
|
+
* other consumers that need to materialise codecs at runtime.
|
|
57
|
+
*
|
|
58
|
+
* - `forCodecRef(ref)` materialises a codec from a {@link CodecRef}. Throws
|
|
59
|
+
* `RUNTIME.CODEC_DESCRIPTOR_MISSING` for unknown ids and `RUNTIME.TYPE_PARAMS_INVALID` on param
|
|
60
|
+
* schema rejection.
|
|
61
|
+
* - `forColumn(namespaceId, table, column)` returns the codec for a specific column coordinate, or
|
|
62
|
+
* `undefined` when no column-to-codec mapping is present. This registry is contract-free so it
|
|
63
|
+
* always returns `undefined` — the method exists so the object structurally satisfies the SQL
|
|
64
|
+
* `ContractCodecRegistry` interface.
|
|
65
|
+
*/
|
|
66
|
+
export interface CodecRegistry extends CodecLookup {
|
|
67
|
+
forCodecRef(ref: CodecRef): Codec;
|
|
68
|
+
forColumn(namespaceId: string, table: string, column: string): Codec | undefined;
|
|
49
69
|
}
|
|
50
70
|
|
|
51
71
|
export const emptyCodecLookup: CodecLookup = {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
11
11
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
12
12
|
import type { Type } from 'arktype';
|
|
13
|
+
import type { CodecLookup } from './codec-types';
|
|
13
14
|
import type { PslBlockParam } from './psl-extension-block';
|
|
14
15
|
|
|
15
16
|
export type AuthoringArgRef = {
|
|
@@ -109,9 +110,30 @@ export type AuthoringFieldNamespace = {
|
|
|
109
110
|
* discover what the factory actually needs to read (codec lookup,
|
|
110
111
|
* namespace registry, …).
|
|
111
112
|
*/
|
|
113
|
+
/**
|
|
114
|
+
* A write-only sink that a factory may push authoring-time diagnostics into.
|
|
115
|
+
* The concrete type pushed must be structurally compatible with whatever the
|
|
116
|
+
* consumer accumulates (typically `ContractSourceDiagnostic[]`); the framework
|
|
117
|
+
* layer deliberately does not depend on that concrete type.
|
|
118
|
+
*/
|
|
119
|
+
export interface AuthoringDiagnosticSink {
|
|
120
|
+
push(d: {
|
|
121
|
+
readonly code: string;
|
|
122
|
+
readonly message: string;
|
|
123
|
+
readonly sourceId: string;
|
|
124
|
+
readonly span?: unknown;
|
|
125
|
+
}): void;
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
export interface AuthoringEntityContext {
|
|
113
129
|
readonly family: string;
|
|
114
130
|
readonly target: string;
|
|
131
|
+
/** Codec registry available to factories that need to validate or decode values. */
|
|
132
|
+
readonly codecLookup?: CodecLookup;
|
|
133
|
+
/** Source file identifier threaded into diagnostics emitted by the factory. */
|
|
134
|
+
readonly sourceId?: string;
|
|
135
|
+
/** Push channel for authoring-time diagnostics emitted by the factory. */
|
|
136
|
+
readonly diagnostics?: AuthoringDiagnosticSink;
|
|
115
137
|
}
|
|
116
138
|
|
|
117
139
|
export interface AuthoringEntityTypeTemplateOutput {
|
|
@@ -186,6 +208,21 @@ export interface AuthoringPslBlockDescriptor {
|
|
|
186
208
|
readonly discriminator: string;
|
|
187
209
|
readonly name: { readonly required: boolean };
|
|
188
210
|
readonly parameters: Record<string, PslBlockParam>;
|
|
211
|
+
/**
|
|
212
|
+
* When `true`, the block body accepts a variadic tail of parameters beyond
|
|
213
|
+
* the declared set. The block body may contain: fields (model-style),
|
|
214
|
+
* `key = value` parameters, and `@@` attributes. With `variadicParameters`,
|
|
215
|
+
* bare identifiers (keys without a `= value`) and undeclared `key = value`
|
|
216
|
+
* pairs flow into the variadic tail — their semantics belong to the
|
|
217
|
+
* lowering, not the parser.
|
|
218
|
+
*
|
|
219
|
+
* A key that IS declared in `parameters` must still be supplied as
|
|
220
|
+
* `key = value`; a bare occurrence of a declared key is a diagnostic.
|
|
221
|
+
*
|
|
222
|
+
* When `false` (default), the validator emits `PSL_EXTENSION_UNKNOWN_PARAMETER`
|
|
223
|
+
* for keys absent from `parameters`.
|
|
224
|
+
*/
|
|
225
|
+
readonly variadicParameters?: boolean;
|
|
189
226
|
}
|
|
190
227
|
|
|
191
228
|
export type AuthoringPslBlockDescriptorNamespace = {
|
|
@@ -329,13 +366,29 @@ export function hasRegisteredFieldNamespace(
|
|
|
329
366
|
return !isAuthoringFieldPresetDescriptor(contributions.field[namespace]);
|
|
330
367
|
}
|
|
331
368
|
|
|
332
|
-
function
|
|
333
|
-
|
|
369
|
+
function isCopyableNamespaceObject(value: unknown): value is Record<string, unknown> {
|
|
370
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
371
|
+
const proto: unknown = Object.getPrototypeOf(value);
|
|
372
|
+
return proto === Object.prototype || proto === null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function deepCopyNamespace(
|
|
376
|
+
source: Record<string, unknown>,
|
|
377
|
+
isLeafDescriptor: (value: unknown) => boolean,
|
|
378
|
+
): Record<string, unknown> {
|
|
379
|
+
const copy: Record<string, unknown> = {};
|
|
380
|
+
for (const [key, value] of Object.entries(source)) {
|
|
381
|
+
copy[key] =
|
|
382
|
+
isCopyableNamespaceObject(value) && !isLeafDescriptor(value)
|
|
383
|
+
? deepCopyNamespace(value, isLeafDescriptor)
|
|
384
|
+
: value;
|
|
385
|
+
}
|
|
386
|
+
return copy;
|
|
334
387
|
}
|
|
335
388
|
|
|
336
389
|
/**
|
|
337
390
|
* Merges `source` into `target` recursively at the descriptor-namespace
|
|
338
|
-
* level. `
|
|
391
|
+
* level. `isLeafDescriptor` decides which values are descriptors (terminal
|
|
339
392
|
* merge points; same-path registrations across components are reported
|
|
340
393
|
* as duplicates) versus sub-namespaces (recursion targets).
|
|
341
394
|
*
|
|
@@ -355,7 +408,7 @@ export function mergeAuthoringNamespaces(
|
|
|
355
408
|
target: Record<string, unknown>,
|
|
356
409
|
source: Record<string, unknown>,
|
|
357
410
|
path: readonly string[],
|
|
358
|
-
|
|
411
|
+
isLeafDescriptor: (value: unknown) => boolean,
|
|
359
412
|
label: string,
|
|
360
413
|
): void {
|
|
361
414
|
const assertSafePath = (currentPath: readonly string[]) => {
|
|
@@ -376,12 +429,19 @@ export function mergeAuthoringNamespaces(
|
|
|
376
429
|
const existingValue = hasExistingValue ? target[key] : undefined;
|
|
377
430
|
|
|
378
431
|
if (!hasExistingValue) {
|
|
379
|
-
|
|
432
|
+
// Deep-copy plain-object sub-namespaces so subsequent merges don't mutate
|
|
433
|
+
// objects owned by source packs. Leaf descriptors and class instances are
|
|
434
|
+
// passed by reference — leaves are identity values; class instances carry
|
|
435
|
+
// prototype getters that spread would destroy.
|
|
436
|
+
target[key] =
|
|
437
|
+
isCopyableNamespaceObject(sourceValue) && !isLeafDescriptor(sourceValue)
|
|
438
|
+
? deepCopyNamespace(sourceValue, isLeafDescriptor)
|
|
439
|
+
: sourceValue;
|
|
380
440
|
continue;
|
|
381
441
|
}
|
|
382
442
|
|
|
383
|
-
const existingIsLeaf =
|
|
384
|
-
const sourceIsLeaf =
|
|
443
|
+
const existingIsLeaf = isLeafDescriptor(existingValue);
|
|
444
|
+
const sourceIsLeaf = isLeafDescriptor(sourceValue);
|
|
385
445
|
|
|
386
446
|
if (existingIsLeaf || sourceIsLeaf) {
|
|
387
447
|
throw new Error(
|
|
@@ -389,17 +449,17 @@ export function mergeAuthoringNamespaces(
|
|
|
389
449
|
);
|
|
390
450
|
}
|
|
391
451
|
|
|
392
|
-
if (!
|
|
452
|
+
if (!isCopyableNamespaceObject(existingValue) || !isCopyableNamespaceObject(sourceValue)) {
|
|
393
453
|
throw new Error(
|
|
394
454
|
`Invalid authoring ${label} helper "${currentPath.join('.')}". Expected a sub-namespace object or a recognized descriptor; received a malformed value.`,
|
|
395
455
|
);
|
|
396
456
|
}
|
|
397
457
|
|
|
398
|
-
mergeAuthoringNamespaces(existingValue, sourceValue, currentPath,
|
|
458
|
+
mergeAuthoringNamespaces(existingValue, sourceValue, currentPath, isLeafDescriptor, label);
|
|
399
459
|
}
|
|
400
460
|
}
|
|
401
461
|
|
|
402
|
-
function
|
|
462
|
+
function collectDescriptorPaths(
|
|
403
463
|
namespace: Readonly<Record<string, unknown>>,
|
|
404
464
|
isLeaf: (value: unknown) => boolean,
|
|
405
465
|
path: readonly string[] = [],
|
|
@@ -413,29 +473,25 @@ function collectAuthoringLeafPaths(
|
|
|
413
473
|
}
|
|
414
474
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
415
475
|
paths.push(
|
|
416
|
-
...
|
|
417
|
-
value as Readonly<Record<string, unknown>>,
|
|
418
|
-
isLeaf,
|
|
419
|
-
currentPath,
|
|
420
|
-
),
|
|
476
|
+
...collectDescriptorPaths(value as Readonly<Record<string, unknown>>, isLeaf, currentPath),
|
|
421
477
|
);
|
|
422
478
|
}
|
|
423
479
|
}
|
|
424
480
|
return paths;
|
|
425
481
|
}
|
|
426
482
|
|
|
427
|
-
interface
|
|
483
|
+
interface DescriptorEntry {
|
|
428
484
|
readonly path: string;
|
|
429
485
|
readonly discriminator: string;
|
|
430
486
|
}
|
|
431
487
|
|
|
432
|
-
function
|
|
488
|
+
function collectDescriptorEntries(
|
|
433
489
|
namespace: Readonly<Record<string, unknown>>,
|
|
434
490
|
isLeaf: (value: unknown) => boolean,
|
|
435
491
|
label: string,
|
|
436
492
|
path: readonly string[] = [],
|
|
437
|
-
):
|
|
438
|
-
const entries:
|
|
493
|
+
): DescriptorEntry[] {
|
|
494
|
+
const entries: DescriptorEntry[] = [];
|
|
439
495
|
for (const [key, value] of Object.entries(namespace)) {
|
|
440
496
|
const currentPath = [...path, key];
|
|
441
497
|
if (isLeaf(value)) {
|
|
@@ -479,7 +535,7 @@ function collectAuthoringLeafDiscriminators(
|
|
|
479
535
|
);
|
|
480
536
|
}
|
|
481
537
|
}
|
|
482
|
-
entries.push(...
|
|
538
|
+
entries.push(...collectDescriptorEntries(record, isLeaf, label, currentPath));
|
|
483
539
|
}
|
|
484
540
|
}
|
|
485
541
|
return entries;
|
|
@@ -491,7 +547,7 @@ function collectAuthoringLeafDiscriminators(
|
|
|
491
547
|
* lowering factory lookup dispatches by discriminator, so one would silently
|
|
492
548
|
* shadow the other. Catch duplicates before building any dispatch map.
|
|
493
549
|
*/
|
|
494
|
-
function assertUniqueDiscriminators(entries: readonly
|
|
550
|
+
function assertUniqueDiscriminators(entries: readonly DescriptorEntry[], label: string): void {
|
|
495
551
|
const seen = new Map<string, string>();
|
|
496
552
|
for (const { path, discriminator } of entries) {
|
|
497
553
|
const existing = seen.get(discriminator);
|
|
@@ -504,23 +560,50 @@ function assertUniqueDiscriminators(entries: readonly AuthoringLeafEntry[], labe
|
|
|
504
560
|
}
|
|
505
561
|
}
|
|
506
562
|
|
|
563
|
+
function collectPslBlockDescriptorEntries(
|
|
564
|
+
namespace: Readonly<Record<string, unknown>>,
|
|
565
|
+
path: readonly string[] = [],
|
|
566
|
+
): DescriptorEntry[] {
|
|
567
|
+
const entries: DescriptorEntry[] = [];
|
|
568
|
+
for (const [key, value] of Object.entries(namespace)) {
|
|
569
|
+
const currentPath = [...path, key];
|
|
570
|
+
if (isAuthoringPslBlockDescriptor(value)) {
|
|
571
|
+
entries.push({
|
|
572
|
+
path: currentPath.join('.'),
|
|
573
|
+
discriminator: value.discriminator,
|
|
574
|
+
});
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
578
|
+
const record = blindCast<
|
|
579
|
+
Readonly<Record<string, unknown>>,
|
|
580
|
+
'walker descends into psl block namespace'
|
|
581
|
+
>(value);
|
|
582
|
+
const hasKind = record['kind'] === 'pslBlock';
|
|
583
|
+
const hasKeyword = typeof record['keyword'] === 'string';
|
|
584
|
+
const hasDiscriminator = typeof record['discriminator'] === 'string';
|
|
585
|
+
if (hasKind || (hasKeyword && hasDiscriminator)) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
`Malformed authoring pslBlock contribution at "${currentPath.join('.')}". The value carries descriptor keys (kind/keyword/discriminator) but does not satisfy the pslBlock descriptor shape. Fix the contribution so it is a complete descriptor, or remove the stray keys if it was meant to be a sub-namespace.`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
entries.push(...collectPslBlockDescriptorEntries(record, currentPath));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return entries;
|
|
594
|
+
}
|
|
595
|
+
|
|
507
596
|
/**
|
|
508
|
-
* Every `pslBlockDescriptors` entry
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* — an `entityTypes` factory may stand alone (e.g. `enum`, reachable from
|
|
512
|
-
* the TypeScript builder without any PSL block).
|
|
597
|
+
* Every `pslBlockDescriptors` entry requires a matching `entityTypes` factory
|
|
598
|
+
* with the same discriminator. An `entityTypes` factory may stand alone (e.g.
|
|
599
|
+
* `enum`, reachable from the TypeScript builder without any PSL block).
|
|
513
600
|
*/
|
|
514
601
|
function assertPslBlocksHaveFactories(
|
|
515
602
|
entityTypeNamespace: AuthoringEntityTypeNamespace,
|
|
516
603
|
pslBlockNamespace: AuthoringPslBlockDescriptorNamespace,
|
|
517
604
|
): void {
|
|
518
|
-
const blockEntries =
|
|
519
|
-
|
|
520
|
-
isAuthoringPslBlockDescriptor,
|
|
521
|
-
'pslBlock',
|
|
522
|
-
);
|
|
523
|
-
const entityEntries = collectAuthoringLeafDiscriminators(
|
|
605
|
+
const blockEntries = collectPslBlockDescriptorEntries(pslBlockNamespace);
|
|
606
|
+
const entityEntries = collectDescriptorEntries(
|
|
524
607
|
entityTypeNamespace,
|
|
525
608
|
isAuthoringEntityTypeDescriptor,
|
|
526
609
|
'entityType',
|
|
@@ -547,13 +630,13 @@ export function assertNoCrossRegistryCollisions(
|
|
|
547
630
|
pslBlockNamespace: AuthoringPslBlockDescriptorNamespace = {},
|
|
548
631
|
): void {
|
|
549
632
|
const typePaths = new Set(
|
|
550
|
-
|
|
633
|
+
collectDescriptorPaths(typeNamespace, isAuthoringTypeConstructorDescriptor),
|
|
551
634
|
);
|
|
552
635
|
const fieldPaths = new Set(
|
|
553
|
-
|
|
636
|
+
collectDescriptorPaths(fieldNamespace, isAuthoringFieldPresetDescriptor),
|
|
554
637
|
);
|
|
555
638
|
const entityPaths = new Set(
|
|
556
|
-
|
|
639
|
+
collectDescriptorPaths(entityTypeNamespace, isAuthoringEntityTypeDescriptor),
|
|
557
640
|
);
|
|
558
641
|
// Within-registry duplicate detection is handled upstream by the merge
|
|
559
642
|
// walker (`mergeAuthoringNamespaces` in control-stack.ts and
|