@bleedingdev/modern-js-surface-resolution 0.0.0 → 3.9.0-ultramodern.6
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/LICENSE +21 -0
- package/dist/cjs/surface-resolution/env-static-provider.js +318 -0
- package/dist/cjs/surface-resolution/index.js +69 -0
- package/dist/cjs/surface-resolution/surface-ref.js +132 -0
- package/dist/cjs/surface-resolution/types.js +18 -0
- package/dist/cjs/surface-resolution/validation.js +202 -0
- package/dist/esm/surface-resolution/env-static-provider.mjs +274 -0
- package/dist/esm/surface-resolution/index.mjs +3 -0
- package/dist/esm/surface-resolution/surface-ref.mjs +88 -0
- package/dist/esm/surface-resolution/types.mjs +0 -0
- package/dist/esm/surface-resolution/validation.mjs +155 -0
- package/dist/esm-node/surface-resolution/env-static-provider.mjs +275 -0
- package/dist/esm-node/surface-resolution/index.mjs +4 -0
- package/dist/esm-node/surface-resolution/surface-ref.mjs +89 -0
- package/dist/esm-node/surface-resolution/types.mjs +1 -0
- package/dist/esm-node/surface-resolution/validation.mjs +156 -0
- package/dist/types/surface-resolution/env-static-provider.d.ts +107 -0
- package/dist/types/surface-resolution/index.d.ts +4 -0
- package/dist/types/surface-resolution/surface-ref.d.ts +72 -0
- package/dist/types/surface-resolution/types.d.ts +102 -0
- package/dist/types/surface-resolution/validation.d.ts +53 -0
- package/package.json +40 -4
- package/src/surface-resolution/env-static-provider.ts +596 -0
- package/src/surface-resolution/index.ts +43 -0
- package/src/surface-resolution/surface-ref.ts +145 -0
- package/src/surface-resolution/types.ts +136 -0
- package/src/surface-resolution/validation.ts +337 -0
- package/README.md +0 -3
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SurfaceRef grammar (MV-G25a).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the EBNF in `packages/toolkit/create/delivery-unit-schema-SPEC.md` §2
|
|
5
|
+
* exactly:
|
|
6
|
+
*
|
|
7
|
+
* ```ebnf
|
|
8
|
+
* SurfaceRef = UnitId , "#" , SurfaceId , [ "@" , Major ] ;
|
|
9
|
+
* UnitId = Segment , { "/" , Segment } ;
|
|
10
|
+
* SurfaceId = Segment ;
|
|
11
|
+
* Segment = SegmentChar , { SegmentChar } ;
|
|
12
|
+
* SegmentChar = letter | digit | "-" | "_" | "." ;
|
|
13
|
+
* Major = "v" , nonzero , { digit } ;
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Canonical form: `unitId#surfaceId` with optional `@vN` external-major suffix
|
|
17
|
+
* (e.g. `acme/checkout#cart`, `acme/checkout#cart@v2`). Universal module:
|
|
18
|
+
* dependency-free, runs in any JavaScript environment.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parsed form of a SurfaceRef. Canonical string form is `unitId#surfaceId`
|
|
23
|
+
* with an optional `@vN` major suffix.
|
|
24
|
+
*/
|
|
25
|
+
export type ParsedSurfaceRef = {
|
|
26
|
+
unitId: string;
|
|
27
|
+
surfaceId: string;
|
|
28
|
+
/** External-major selector. Absent means "the coordinated-zone surface". */
|
|
29
|
+
major?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type SurfaceRefParseError =
|
|
33
|
+
| { code: 'empty' }
|
|
34
|
+
| { code: 'missing-surface-separator' }
|
|
35
|
+
| { code: 'multiple-surface-separators' }
|
|
36
|
+
| { code: 'empty-unit-id' }
|
|
37
|
+
| { code: 'invalid-unit-id'; segment: string }
|
|
38
|
+
| { code: 'empty-surface-id' }
|
|
39
|
+
| { code: 'invalid-surface-id' }
|
|
40
|
+
| { code: 'empty-major' }
|
|
41
|
+
| { code: 'invalid-major'; value: string };
|
|
42
|
+
|
|
43
|
+
export type SurfaceRefParseResult =
|
|
44
|
+
| { ok: true; ref: ParsedSurfaceRef }
|
|
45
|
+
| { ok: false; error: SurfaceRefParseError };
|
|
46
|
+
|
|
47
|
+
const SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
48
|
+
const MAJOR_PATTERN = /^v[1-9][0-9]*$/;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse a canonical SurfaceRef string. Total function: every rejection is a
|
|
52
|
+
* typed {@link SurfaceRefParseError}; never throws.
|
|
53
|
+
*/
|
|
54
|
+
export function parseSurfaceRef(input: string): SurfaceRefParseResult {
|
|
55
|
+
if (input === '') {
|
|
56
|
+
return { ok: false, error: { code: 'empty' } };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const hashCount = countChar(input, '#');
|
|
60
|
+
if (hashCount === 0) {
|
|
61
|
+
return { ok: false, error: { code: 'missing-surface-separator' } };
|
|
62
|
+
}
|
|
63
|
+
if (hashCount > 1) {
|
|
64
|
+
return { ok: false, error: { code: 'multiple-surface-separators' } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hashIndex = input.indexOf('#');
|
|
68
|
+
const unitPart = input.slice(0, hashIndex);
|
|
69
|
+
const rest = input.slice(hashIndex + 1);
|
|
70
|
+
|
|
71
|
+
const atIndex = rest.indexOf('@');
|
|
72
|
+
const surfaceId = atIndex === -1 ? rest : rest.slice(0, atIndex);
|
|
73
|
+
const ref: ParsedSurfaceRef = { unitId: unitPart, surfaceId };
|
|
74
|
+
|
|
75
|
+
if (atIndex !== -1) {
|
|
76
|
+
const majorPart = rest.slice(atIndex + 1);
|
|
77
|
+
if (majorPart === '') {
|
|
78
|
+
return { ok: false, error: { code: 'empty-major' } };
|
|
79
|
+
}
|
|
80
|
+
if (!MAJOR_PATTERN.test(majorPart)) {
|
|
81
|
+
return { ok: false, error: { code: 'invalid-major', value: majorPart } };
|
|
82
|
+
}
|
|
83
|
+
ref.major = Number(majorPart.slice(1));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const error = validateSurfaceRef(ref);
|
|
87
|
+
return error === undefined ? { ok: true, ref } : { ok: false, error };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render a {@link ParsedSurfaceRef} back to its canonical string form.
|
|
92
|
+
*
|
|
93
|
+
* Direct inputs are checked against the same invariant as parsed references,
|
|
94
|
+
* so this formatter cannot emit a string that {@link parseSurfaceRef} rejects.
|
|
95
|
+
* Round-trip: `formatSurfaceRef(parseSurfaceRef(x).ref) === x` for valid `x`.
|
|
96
|
+
*/
|
|
97
|
+
export function formatSurfaceRef(ref: ParsedSurfaceRef): string {
|
|
98
|
+
const error = validateSurfaceRef(ref);
|
|
99
|
+
if (error !== undefined) {
|
|
100
|
+
throw new TypeError(`Cannot format invalid SurfaceRef: ${error.code}.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const base = `${ref.unitId}#${ref.surfaceId}`;
|
|
104
|
+
return ref.major === undefined ? base : `${base}@v${ref.major}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The shared semantic invariant for parsed and directly formatted references. */
|
|
108
|
+
export function validateSurfaceRef(
|
|
109
|
+
ref: ParsedSurfaceRef,
|
|
110
|
+
): SurfaceRefParseError | undefined {
|
|
111
|
+
if (ref.unitId === '') {
|
|
112
|
+
return { code: 'empty-unit-id' };
|
|
113
|
+
}
|
|
114
|
+
for (const segment of ref.unitId.split('/')) {
|
|
115
|
+
if (!SEGMENT_PATTERN.test(segment)) {
|
|
116
|
+
return { code: 'invalid-unit-id', segment };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (ref.surfaceId === '') {
|
|
121
|
+
return { code: 'empty-surface-id' };
|
|
122
|
+
}
|
|
123
|
+
if (!SEGMENT_PATTERN.test(ref.surfaceId)) {
|
|
124
|
+
return { code: 'invalid-surface-id' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
ref.major !== undefined &&
|
|
129
|
+
(!Number.isSafeInteger(ref.major) || ref.major < 1)
|
|
130
|
+
) {
|
|
131
|
+
return { code: 'invalid-major', value: String(ref.major) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function countChar(input: string, char: string): number {
|
|
138
|
+
let count = 0;
|
|
139
|
+
for (const current of input) {
|
|
140
|
+
if (current === char) {
|
|
141
|
+
count += 1;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return count;
|
|
145
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface-resolution record + provider SPI (MV-G25b/c).
|
|
3
|
+
*
|
|
4
|
+
* Contract: `docs/super-app-rfc-adr/RESOLUTION-0001-surface-discovery-record.md`.
|
|
5
|
+
* Discovery answers a SurfaceRef with exactly ONE {@link ResolvedDeliveryUnit}
|
|
6
|
+
* — never a bare URL, never a partial set of locations — or a typed
|
|
7
|
+
* {@link DiscoveryError} (§2.4: discovery errors are expected states, not
|
|
8
|
+
* exceptions).
|
|
9
|
+
*
|
|
10
|
+
* Atomicity invariant (ADR-0019): `buildMarker` / `sourceRevision` /
|
|
11
|
+
* `baselineCohortId` live once on the record; a {@link ResolvedSurface}
|
|
12
|
+
* carries no marker of its own, so mixing locations from two build markers is
|
|
13
|
+
* structurally unrepresentable.
|
|
14
|
+
*/
|
|
15
|
+
import type { DeliveryUnitIdentity } from '@modern-js/backend-federation-contracts';
|
|
16
|
+
import type { ParsedSurfaceRef } from './surface-ref';
|
|
17
|
+
|
|
18
|
+
/* -------------------------------------------------------------------------- */
|
|
19
|
+
/* Locations (RESOLUTION-0001 §2.1) */
|
|
20
|
+
/* -------------------------------------------------------------------------- */
|
|
21
|
+
|
|
22
|
+
export type ResolvedSurfaceLocationPlatform =
|
|
23
|
+
| 'browser-mf-manifest'
|
|
24
|
+
| 'node-mf-manifest'
|
|
25
|
+
| 'http-api'
|
|
26
|
+
| 'cloudflare-service-binding';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One platform address for a surface, discriminated on `platform` so each
|
|
30
|
+
* platform carries only the address shape its execution adapter can load.
|
|
31
|
+
*/
|
|
32
|
+
export type ResolvedSurfaceLocation =
|
|
33
|
+
| { platform: 'browser-mf-manifest'; manifestUrl: string }
|
|
34
|
+
| {
|
|
35
|
+
/** Backend `backend-mf-manifest.json` URL or filesystem path. */
|
|
36
|
+
platform: 'node-mf-manifest';
|
|
37
|
+
manifestRef: string;
|
|
38
|
+
}
|
|
39
|
+
| { platform: 'http-api'; baseUrl: string; prefix: string }
|
|
40
|
+
| {
|
|
41
|
+
platform: 'cloudflare-service-binding';
|
|
42
|
+
serviceBinding: string;
|
|
43
|
+
dispatchNamespace?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/* -------------------------------------------------------------------------- */
|
|
47
|
+
/* Record */
|
|
48
|
+
/* -------------------------------------------------------------------------- */
|
|
49
|
+
|
|
50
|
+
export type ResolvedSurfaceKind = 'component' | 'route' | 'api' | 'backend';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A resolved surface. It carries NO build marker of its own: the marker lives
|
|
54
|
+
* once on the {@link ResolvedDeliveryUnit} (ADR-0019 structural atomicity).
|
|
55
|
+
*/
|
|
56
|
+
export type ResolvedSurface = {
|
|
57
|
+
surfaceId: string;
|
|
58
|
+
kind: ResolvedSurfaceKind;
|
|
59
|
+
locations: ResolvedSurfaceLocation[];
|
|
60
|
+
/**
|
|
61
|
+
* The external major (ADR-0020) this materialization serves. Present exactly
|
|
62
|
+
* when the record answers a versioned SurfaceRef (`…@vN`): the locations are
|
|
63
|
+
* the major-specific materialization, never the unversioned addresses.
|
|
64
|
+
*/
|
|
65
|
+
servedMajor?: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type CompatibilityStatus = 'compatible' | 'incompatible' | 'degraded';
|
|
69
|
+
|
|
70
|
+
/** The resolver's verdict, not the consumer's guess (RESOLUTION-0001 §2.1). */
|
|
71
|
+
export type CompatibilityVerdict = {
|
|
72
|
+
status: CompatibilityStatus;
|
|
73
|
+
/** Baseline cohort id the verdict was computed against. */
|
|
74
|
+
baselineCohortId: string;
|
|
75
|
+
reason?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Atomic resolution result: every platform location for the unit, resolved
|
|
80
|
+
* together against ONE `buildMarker` / `sourceRevision`. There is no partial
|
|
81
|
+
* variant; a resolver returns this whole record or a {@link DiscoveryError}.
|
|
82
|
+
*/
|
|
83
|
+
export type ResolvedDeliveryUnit = DeliveryUnitIdentity & {
|
|
84
|
+
baselineCohortId: string;
|
|
85
|
+
surfaces: ResolvedSurface[];
|
|
86
|
+
compatibility: CompatibilityVerdict;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/* -------------------------------------------------------------------------- */
|
|
90
|
+
/* Failure semantics (RESOLUTION-0001 §2.4) */
|
|
91
|
+
/* -------------------------------------------------------------------------- */
|
|
92
|
+
|
|
93
|
+
export type DiscoveryErrorCode =
|
|
94
|
+
| 'unknown-unit'
|
|
95
|
+
| 'unknown-surface'
|
|
96
|
+
| 'major-not-published'
|
|
97
|
+
| 'identity-mismatch'
|
|
98
|
+
| 'stale-record'
|
|
99
|
+
| 'provider-unavailable';
|
|
100
|
+
|
|
101
|
+
export type DiscoveryError = {
|
|
102
|
+
code: DiscoveryErrorCode;
|
|
103
|
+
/** Canonical string form of the SurfaceRef being resolved. */
|
|
104
|
+
ref: string;
|
|
105
|
+
message: string;
|
|
106
|
+
details?: Record<string, unknown>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type DiscoveryResult =
|
|
110
|
+
| { ok: true; unit: ResolvedDeliveryUnit }
|
|
111
|
+
| { ok: false; error: DiscoveryError };
|
|
112
|
+
|
|
113
|
+
/* -------------------------------------------------------------------------- */
|
|
114
|
+
/* Provider SPI (RESOLUTION-0001 §2.2) */
|
|
115
|
+
/* -------------------------------------------------------------------------- */
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Environment identity. Providers are selected per environment, not per
|
|
119
|
+
* surface: one environment resolves all surfaces of a unit through the same
|
|
120
|
+
* provider chain.
|
|
121
|
+
*/
|
|
122
|
+
export type EnvironmentId = string;
|
|
123
|
+
|
|
124
|
+
export type SurfaceResolutionProvider = {
|
|
125
|
+
/** Stable provider name (e.g. `env-static`), used in error details. */
|
|
126
|
+
name: string;
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a SurfaceRef in an environment to one complete record or one
|
|
129
|
+
* typed error. Must never return a partial record and must never mix
|
|
130
|
+
* locations from different build markers.
|
|
131
|
+
*/
|
|
132
|
+
resolve(
|
|
133
|
+
ref: ParsedSurfaceRef,
|
|
134
|
+
env: EnvironmentId,
|
|
135
|
+
): DiscoveryResult | Promise<DiscoveryResult>;
|
|
136
|
+
};
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity / compatibility validation helpers for resolved delivery units
|
|
3
|
+
* (MV-G25c). Runtime checks for records that crossed a serialization boundary;
|
|
4
|
+
* within TypeScript the atomicity invariant is already structural.
|
|
5
|
+
*/
|
|
6
|
+
import { formatSurfaceRef, type ParsedSurfaceRef } from './surface-ref';
|
|
7
|
+
import type {
|
|
8
|
+
DiscoveryError,
|
|
9
|
+
DiscoveryErrorCode,
|
|
10
|
+
ResolvedDeliveryUnit,
|
|
11
|
+
ResolvedSurface,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
const SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
15
|
+
|
|
16
|
+
export type ResolvedDeliveryUnitIssue = {
|
|
17
|
+
path: string;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ResolvedDeliveryUnitValidationResult = {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
issues: ResolvedDeliveryUnitIssue[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Build a typed {@link DiscoveryError} for a reference. */
|
|
27
|
+
export function createDiscoveryError(
|
|
28
|
+
code: DiscoveryErrorCode,
|
|
29
|
+
ref: ParsedSurfaceRef | string,
|
|
30
|
+
message: string,
|
|
31
|
+
details?: Record<string, unknown>,
|
|
32
|
+
): DiscoveryError {
|
|
33
|
+
const refString = typeof ref === 'string' ? ref : formatSurfaceRef(ref);
|
|
34
|
+
return details === undefined
|
|
35
|
+
? { code, ref: refString, message }
|
|
36
|
+
: { code, ref: refString, message, details };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
40
|
+
return typeof value === 'string' && value.length > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isValidUnitId(unitId: string): boolean {
|
|
44
|
+
return unitId.split('/').every(segment => SEGMENT_PATTERN.test(segment));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
48
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const COMPATIBILITY_STATUSES: readonly string[] = [
|
|
52
|
+
'compatible',
|
|
53
|
+
'incompatible',
|
|
54
|
+
'degraded',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const SURFACE_KINDS: readonly string[] = [
|
|
58
|
+
'component',
|
|
59
|
+
'route',
|
|
60
|
+
'api',
|
|
61
|
+
'backend',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const LOCATION_PLATFORMS: readonly string[] = [
|
|
65
|
+
'browser-mf-manifest',
|
|
66
|
+
'node-mf-manifest',
|
|
67
|
+
'http-api',
|
|
68
|
+
'cloudflare-service-binding',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** Required non-empty string address fields per location platform. */
|
|
72
|
+
const LOCATION_REQUIRED_FIELDS: Record<string, readonly string[]> = {
|
|
73
|
+
'browser-mf-manifest': ['manifestUrl'],
|
|
74
|
+
'node-mf-manifest': ['manifestRef'],
|
|
75
|
+
'http-api': ['baseUrl', 'prefix'],
|
|
76
|
+
'cloudflare-service-binding': ['serviceBinding'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type IssuePush = (path: string, message: string) => void;
|
|
80
|
+
|
|
81
|
+
function validateLocation(
|
|
82
|
+
location: unknown,
|
|
83
|
+
path: string,
|
|
84
|
+
seenPlatforms: Set<string>,
|
|
85
|
+
push: IssuePush,
|
|
86
|
+
): void {
|
|
87
|
+
if (!isRecord(location)) {
|
|
88
|
+
push(path, 'must be an object');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const platform = location.platform;
|
|
93
|
+
if (typeof platform !== 'string' || !LOCATION_PLATFORMS.includes(platform)) {
|
|
94
|
+
push(`${path}.platform`, `must be one of ${LOCATION_PLATFORMS.join(', ')}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (seenPlatforms.has(platform)) {
|
|
99
|
+
push(
|
|
100
|
+
`${path}.platform`,
|
|
101
|
+
`duplicate platform ${platform} within one surface`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
seenPlatforms.add(platform);
|
|
105
|
+
|
|
106
|
+
for (const field of LOCATION_REQUIRED_FIELDS[platform] ?? []) {
|
|
107
|
+
if (!isNonEmptyString(location[field])) {
|
|
108
|
+
push(`${path}.${field}`, 'must be a non-empty string');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (
|
|
112
|
+
platform === 'cloudflare-service-binding' &&
|
|
113
|
+
location.dispatchNamespace !== undefined &&
|
|
114
|
+
!isNonEmptyString(location.dispatchNamespace)
|
|
115
|
+
) {
|
|
116
|
+
push(
|
|
117
|
+
`${path}.dispatchNamespace`,
|
|
118
|
+
'must be a non-empty string when present',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateSurface(
|
|
124
|
+
surface: unknown,
|
|
125
|
+
path: string,
|
|
126
|
+
seenSurfaceIds: Set<string>,
|
|
127
|
+
push: IssuePush,
|
|
128
|
+
): void {
|
|
129
|
+
if (!isRecord(surface)) {
|
|
130
|
+
push(path, 'must be an object');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!isNonEmptyString(surface.surfaceId)) {
|
|
135
|
+
push(`${path}.surfaceId`, 'must be a non-empty string');
|
|
136
|
+
} else if (!SEGMENT_PATTERN.test(surface.surfaceId)) {
|
|
137
|
+
push(`${path}.surfaceId`, 'must match the SurfaceRef SurfaceId grammar');
|
|
138
|
+
} else if (seenSurfaceIds.has(surface.surfaceId)) {
|
|
139
|
+
push(`${path}.surfaceId`, 'must be unique within the record');
|
|
140
|
+
} else {
|
|
141
|
+
seenSurfaceIds.add(surface.surfaceId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
typeof surface.kind !== 'string' ||
|
|
146
|
+
!SURFACE_KINDS.includes(surface.kind)
|
|
147
|
+
) {
|
|
148
|
+
push(`${path}.kind`, `must be one of ${SURFACE_KINDS.join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
surface.servedMajor !== undefined &&
|
|
153
|
+
(!Number.isSafeInteger(surface.servedMajor) ||
|
|
154
|
+
(surface.servedMajor as number) < 1)
|
|
155
|
+
) {
|
|
156
|
+
push(`${path}.servedMajor`, 'must be a positive safe integer when present');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!Array.isArray(surface.locations)) {
|
|
160
|
+
push(`${path}.locations`, 'must be an array of locations');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (surface.locations.length === 0) {
|
|
164
|
+
push(
|
|
165
|
+
`${path}.locations`,
|
|
166
|
+
'must contain at least one location (no partial records)',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const seenPlatforms = new Set<string>();
|
|
170
|
+
surface.locations.forEach((location, locationIndex) => {
|
|
171
|
+
validateLocation(
|
|
172
|
+
location,
|
|
173
|
+
`${path}.locations[${locationIndex}]`,
|
|
174
|
+
seenPlatforms,
|
|
175
|
+
push,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validate the structural invariants of a {@link ResolvedDeliveryUnit}:
|
|
182
|
+
* non-empty identity root, a well-formed compatibility verdict (valid status,
|
|
183
|
+
* computed against the record-level baseline cohort), and per-surface
|
|
184
|
+
* completeness (valid unique surface ids, valid kind, at least one location,
|
|
185
|
+
* no duplicate platform entries, and every discriminant + required address
|
|
186
|
+
* field per location platform). There is no partial-success shape: any issue
|
|
187
|
+
* means the record is not a valid resolution.
|
|
188
|
+
*
|
|
189
|
+
* Total: never throws, even for records that crossed a serialization boundary
|
|
190
|
+
* with missing or malformed nested objects — every defect is a typed issue.
|
|
191
|
+
*/
|
|
192
|
+
export function validateResolvedDeliveryUnit(
|
|
193
|
+
unit: ResolvedDeliveryUnit,
|
|
194
|
+
): ResolvedDeliveryUnitValidationResult {
|
|
195
|
+
const issues: ResolvedDeliveryUnitIssue[] = [];
|
|
196
|
+
const push: IssuePush = (path, message) => {
|
|
197
|
+
issues.push({ path, message });
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (!isRecord(unit)) {
|
|
201
|
+
push('', 'must be an object');
|
|
202
|
+
return { ok: false, issues };
|
|
203
|
+
}
|
|
204
|
+
// From here on treat the input as untrusted wire data.
|
|
205
|
+
const raw = unit as unknown as Record<string, unknown>;
|
|
206
|
+
|
|
207
|
+
if (!isNonEmptyString(raw.unitId)) {
|
|
208
|
+
push('unitId', 'must be a non-empty string');
|
|
209
|
+
} else if (!isValidUnitId(raw.unitId)) {
|
|
210
|
+
push('unitId', 'must match the SurfaceRef UnitId grammar');
|
|
211
|
+
}
|
|
212
|
+
if (!isNonEmptyString(raw.buildMarker)) {
|
|
213
|
+
push('buildMarker', 'must be a non-empty string');
|
|
214
|
+
}
|
|
215
|
+
if (!isNonEmptyString(raw.sourceRevision)) {
|
|
216
|
+
push('sourceRevision', 'must be a non-empty string');
|
|
217
|
+
}
|
|
218
|
+
if (!isNonEmptyString(raw.baselineCohortId)) {
|
|
219
|
+
push('baselineCohortId', 'must be a non-empty string');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const compatibility = raw.compatibility;
|
|
223
|
+
if (!isRecord(compatibility)) {
|
|
224
|
+
push('compatibility', 'must be an object');
|
|
225
|
+
} else {
|
|
226
|
+
if (
|
|
227
|
+
typeof compatibility.status !== 'string' ||
|
|
228
|
+
!COMPATIBILITY_STATUSES.includes(compatibility.status)
|
|
229
|
+
) {
|
|
230
|
+
push(
|
|
231
|
+
'compatibility.status',
|
|
232
|
+
`must be one of ${COMPATIBILITY_STATUSES.join(', ')}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (compatibility.baselineCohortId !== raw.baselineCohortId) {
|
|
236
|
+
push(
|
|
237
|
+
'compatibility.baselineCohortId',
|
|
238
|
+
'must equal the record-level baselineCohortId (one verdict per record)',
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (
|
|
242
|
+
compatibility.reason !== undefined &&
|
|
243
|
+
!isNonEmptyString(compatibility.reason)
|
|
244
|
+
) {
|
|
245
|
+
push('compatibility.reason', 'must be a non-empty string when present');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!Array.isArray(raw.surfaces)) {
|
|
250
|
+
push('surfaces', 'must be an array of surfaces');
|
|
251
|
+
return { ok: false, issues };
|
|
252
|
+
}
|
|
253
|
+
if (raw.surfaces.length === 0) {
|
|
254
|
+
push('surfaces', 'must contain at least one surface');
|
|
255
|
+
}
|
|
256
|
+
const seenSurfaceIds = new Set<string>();
|
|
257
|
+
raw.surfaces.forEach((surface, surfaceIndex) => {
|
|
258
|
+
validateSurface(surface, `surfaces[${surfaceIndex}]`, seenSurfaceIds, push);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return { ok: issues.length === 0, issues };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Select the surface a {@link ParsedSurfaceRef} points at within one record.
|
|
266
|
+
* `unknown-unit` when the record is for a different unit, `unknown-surface`
|
|
267
|
+
* when the unit does not publish the surface.
|
|
268
|
+
*/
|
|
269
|
+
export function selectResolvedSurface(
|
|
270
|
+
unit: ResolvedDeliveryUnit,
|
|
271
|
+
ref: ParsedSurfaceRef,
|
|
272
|
+
):
|
|
273
|
+
| { ok: true; surface: ResolvedSurface }
|
|
274
|
+
| { ok: false; error: DiscoveryError } {
|
|
275
|
+
if (unit.unitId !== ref.unitId) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
error: createDiscoveryError(
|
|
279
|
+
'unknown-unit',
|
|
280
|
+
ref,
|
|
281
|
+
`Resolved record is for unit ${unit.unitId}, not ${ref.unitId}.`,
|
|
282
|
+
{ recordUnitId: unit.unitId },
|
|
283
|
+
),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const surface = unit.surfaces.find(
|
|
288
|
+
candidate => candidate.surfaceId === ref.surfaceId,
|
|
289
|
+
);
|
|
290
|
+
if (surface === undefined) {
|
|
291
|
+
return {
|
|
292
|
+
ok: false,
|
|
293
|
+
error: createDiscoveryError(
|
|
294
|
+
'unknown-surface',
|
|
295
|
+
ref,
|
|
296
|
+
`Unit ${unit.unitId} (buildMarker ${unit.buildMarker}) does not publish surface ${ref.surfaceId}.`,
|
|
297
|
+
{ availableSurfaces: unit.surfaces.map(entry => entry.surfaceId) },
|
|
298
|
+
),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { ok: true, surface };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export type ExpectedDeliveryUnitIdentity = {
|
|
306
|
+
unitId: string;
|
|
307
|
+
buildMarker: string;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Compare a consumer's expected delivery-unit identity against a resolved
|
|
312
|
+
* record. Execution adapters must pass the record's `unitId` + `buildMarker`
|
|
313
|
+
* through to identity validation (RESOLUTION-0001 §2.3); a mismatch is the
|
|
314
|
+
* typed `identity-mismatch` discovery error.
|
|
315
|
+
*/
|
|
316
|
+
export function matchDeliveryUnitIdentity(
|
|
317
|
+
expected: ExpectedDeliveryUnitIdentity,
|
|
318
|
+
unit: Pick<ResolvedDeliveryUnit, 'unitId' | 'buildMarker'>,
|
|
319
|
+
ref: ParsedSurfaceRef | string,
|
|
320
|
+
): DiscoveryError | undefined {
|
|
321
|
+
if (
|
|
322
|
+
expected.unitId === unit.unitId &&
|
|
323
|
+
expected.buildMarker === unit.buildMarker
|
|
324
|
+
) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return createDiscoveryError(
|
|
329
|
+
'identity-mismatch',
|
|
330
|
+
ref,
|
|
331
|
+
`Delivery-unit identity mismatch: expected ${expected.unitId}@${expected.buildMarker}, resolved ${unit.unitId}@${unit.buildMarker}.`,
|
|
332
|
+
{
|
|
333
|
+
expected,
|
|
334
|
+
resolved: { unitId: unit.unitId, buildMarker: unit.buildMarker },
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
}
|
package/README.md
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
# @bleedingdev/modern-js-surface-resolution
|
|
2
|
-
|
|
3
|
-
This 0.0.0 version only registers the package name so npm trusted publishing can be configured. It contains no runtime implementation. Use the subsequent UltraModern.js release, published from the repository workflow with provenance.
|