@hops-ops/distributed 4.10.0 → 4.11.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/README.md +46 -3
- package/dist/replica/identity/codec.d.ts +1 -0
- package/dist/replica/identity/codec.js +16 -3
- package/dist/replica/types.d.ts +4 -1
- package/dist/sveltekit/boundary-variables.js +5 -1
- package/dist/sveltekit/index.d.ts +1 -0
- package/dist/sveltekit/index.js +1 -0
- package/dist/sveltekit/island-bindings.d.ts +17 -0
- package/dist/sveltekit/island-bindings.js +58 -0
- package/dist/sveltekit/islands/boundaries.d.ts +5 -2
- package/dist/sveltekit/islands/boundaries.js +63 -9
- package/dist/sveltekit/vite.js +19 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -134,9 +134,52 @@ request-local replica. A single-surface application can omit the second entry.
|
|
|
134
134
|
A component can own a sibling `Component.graphql` island. The adapter walks
|
|
135
135
|
static Svelte imports and promotes `@load` work to the nearest page/layout.
|
|
136
136
|
Route-local `+page.graphql` and `+layout.graphql` remain first-class. When a
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
surface needs its own authorization-specific copy, use a qualified route
|
|
138
|
+
document such as `+layout.public.graphql`; it still belongs to that layout.
|
|
139
|
+
|
|
140
|
+
Put stable values in GraphQL itself:
|
|
141
|
+
|
|
142
|
+
```graphql
|
|
143
|
+
query ChatMessages(
|
|
144
|
+
$limit: Int! = 25
|
|
145
|
+
$offset: Int! = 0
|
|
146
|
+
) @load @live {
|
|
147
|
+
chat_messages(limit: $limit, offset: $offset) { message_id body }
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The generated variable properties are optional, while the compiler-owned codec
|
|
152
|
+
canonicalizes the defaults before cache identity, SSR, live continuation, or
|
|
153
|
+
transport. An explicit `ChatMessages.use({ limit: 50, offset: 0 })` overrides
|
|
154
|
+
them; `ChatMessages.use()` uses the exact defaults.
|
|
155
|
+
|
|
156
|
+
Same-name route parameters are inferred. For external values, add one bounded
|
|
157
|
+
sidecar beside the document rather than putting screen behavior in
|
|
158
|
+
`distributed.config.js`:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
// SearchResults.graphql.bindings.js
|
|
162
|
+
import {
|
|
163
|
+
defineGraphqlIslandBindings,
|
|
164
|
+
forwardedProp,
|
|
165
|
+
searchParam,
|
|
166
|
+
sessionClaim
|
|
167
|
+
} from '@hops-ops/distributed/sveltekit';
|
|
168
|
+
|
|
169
|
+
export default defineGraphqlIslandBindings({
|
|
170
|
+
query: searchParam('q'),
|
|
171
|
+
viewerId: sessionClaim('user', 'id'),
|
|
172
|
+
filters: forwardedProp('filters')
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The helper rejects unsafe shapes, generation checks the keys against the
|
|
177
|
+
operation, and the same binding is reused by SSR, hover prefetch, navigation,
|
|
178
|
+
hydration, and live work. Resolution precedence is explicit call variables,
|
|
179
|
+
then sidecar/route sources, then GraphQL defaults. A non-null variable without
|
|
180
|
+
any of those sources fails generation before transport. Central `boundaries`
|
|
181
|
+
registrations remain an explicit-placement escape hatch and cannot be combined
|
|
182
|
+
with a sidecar for the same operation.
|
|
140
183
|
|
|
141
184
|
The Vite integration runs `distributed client` at startup/build, watches GraphQL
|
|
142
185
|
documents, stages all surfaces, commits a rollback-capable multi-output
|
|
@@ -3,6 +3,7 @@ import type { ReplicaOperationArtifact, ReplicaVariableCodecArtifact, ReplicaVar
|
|
|
3
3
|
export type VariableCodecRegistry = {
|
|
4
4
|
readonly limits: ReplicaVariableCodecLimits;
|
|
5
5
|
readonly variables: ReadonlyMap<string, ReplicaVariableInputRef>;
|
|
6
|
+
readonly defaults: ReadonlyMap<string, ReplicaValue>;
|
|
6
7
|
readonly inputs: ReadonlyMap<string, ReplicaVariableInputDefinition>;
|
|
7
8
|
};
|
|
8
9
|
/**
|
|
@@ -24,6 +24,10 @@ export function canonicalizeOperationVariables(artifact, variables) {
|
|
|
24
24
|
for (const [name, input] of [...registry.variables].sort(([left], [right]) => compareCodeUnits(left, right))) {
|
|
25
25
|
const present = supplied.has(name) && supplied.get(name) !== undefined;
|
|
26
26
|
if (!present) {
|
|
27
|
+
if (registry.defaults.has(name)) {
|
|
28
|
+
canonical.push([name, registry.defaults.get(name)]);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
27
31
|
if (!input.nullable) {
|
|
28
32
|
variableValueInvalid(`variables.${name}`, 'required variable is missing');
|
|
29
33
|
}
|
|
@@ -37,8 +41,8 @@ export function canonicalizeOperationVariables(artifact, variables) {
|
|
|
37
41
|
return freezeRecord(canonical);
|
|
38
42
|
}
|
|
39
43
|
export function validateVariableCodec(codec) {
|
|
40
|
-
const root = artifactRecord(codec, 'artifact.variableCodec', ['version', 'limits', 'variables', 'inputs']);
|
|
41
|
-
if (root.version !==
|
|
44
|
+
const root = artifactRecord(codec, 'artifact.variableCodec', ['version', 'limits', 'variables', 'defaults', 'inputs']);
|
|
45
|
+
if (root.version !== 2)
|
|
42
46
|
variableCodecInvalid('artifact.variableCodec.version');
|
|
43
47
|
const rawLimits = artifactRecord(root.limits, 'artifact.variableCodec.limits', ['maxDepth', 'maxBoolWidth', 'maxInList']);
|
|
44
48
|
const limits = {
|
|
@@ -62,7 +66,16 @@ export function validateVariableCodec(codec) {
|
|
|
62
66
|
for (const [name, definition] of inputs) {
|
|
63
67
|
validateInputDefinition(definition, `artifact.variableCodec.inputs.${name}`, inputs, new Set(), 0);
|
|
64
68
|
}
|
|
65
|
-
|
|
69
|
+
const defaults = new Map();
|
|
70
|
+
const registry = { limits, variables, defaults, inputs };
|
|
71
|
+
for (const [name, value] of artifactRecordEntries(root.defaults, 'artifact.variableCodec.defaults')) {
|
|
72
|
+
const input = variables.get(name);
|
|
73
|
+
if (input === undefined) {
|
|
74
|
+
variableCodecInvalid(`artifact.variableCodec.defaults.${name}`);
|
|
75
|
+
}
|
|
76
|
+
defaults.set(name, canonicalizeInputRef(input, value, registry, `artifact.variableCodec.defaults.${name}`, new Set(), 0));
|
|
77
|
+
}
|
|
78
|
+
return registry;
|
|
66
79
|
}
|
|
67
80
|
export function validateInputRef(value, path, inputs, limits, active, depth) {
|
|
68
81
|
checkCodecDepth(depth, path);
|
package/dist/replica/types.d.ts
CHANGED
|
@@ -150,9 +150,11 @@ export type ReplicaVariableCodecLimits = {
|
|
|
150
150
|
};
|
|
151
151
|
/** Exact variable codec emitted beside a generated operation artifact. */
|
|
152
152
|
export type ReplicaVariableCodecArtifact = {
|
|
153
|
-
readonly version:
|
|
153
|
+
readonly version: 2;
|
|
154
154
|
readonly limits: ReplicaVariableCodecLimits;
|
|
155
155
|
readonly variables: Readonly<Record<string, ReplicaVariableInputRef>>;
|
|
156
|
+
/** Canonical GraphQL defaults, applied before cache identity or transport. */
|
|
157
|
+
readonly defaults: Readonly<Record<string, ReplicaValue>>;
|
|
156
158
|
readonly inputs: Readonly<Record<string, ReplicaVariableInputDefinition>>;
|
|
157
159
|
};
|
|
158
160
|
export type ReplicaFilterOperator = '_eq' | '_neq' | '_gt' | '_gte' | '_lt' | '_lte' | '_in' | '_nin' | '_is_null' | '_like' | '_ilike' | '_contains' | '_contained_in' | '_has_key';
|
|
@@ -463,6 +465,7 @@ export type ReplicaIslandMetadata = {
|
|
|
463
465
|
readonly variables: readonly {
|
|
464
466
|
readonly name: string;
|
|
465
467
|
readonly graphqlType: string;
|
|
468
|
+
readonly defaultValue?: ReplicaValue;
|
|
466
469
|
}[];
|
|
467
470
|
};
|
|
468
471
|
readonly liveCoverage: {
|
|
@@ -96,9 +96,13 @@ function validateSources(artifact, sources) {
|
|
|
96
96
|
throw new TypeError('Distributed boundary variable sources must be an object');
|
|
97
97
|
}
|
|
98
98
|
const definitions = artifact.variableCodec?.variables;
|
|
99
|
+
const defaults = artifact.variableCodec?.defaults;
|
|
99
100
|
if (definitions === null || typeof definitions !== 'object' || Array.isArray(definitions)) {
|
|
100
101
|
throw new TypeError('Distributed boundary artifact has no variable codec');
|
|
101
102
|
}
|
|
103
|
+
if (defaults === null || typeof defaults !== 'object' || Array.isArray(defaults)) {
|
|
104
|
+
throw new TypeError('Distributed boundary artifact has no variable defaults');
|
|
105
|
+
}
|
|
102
106
|
const names = Object.keys(sources);
|
|
103
107
|
if (names.length > MAX_BINDING_VARIABLES) {
|
|
104
108
|
throw new TypeError(`Distributed boundary binding exceeds ${MAX_BINDING_VARIABLES} variables`);
|
|
@@ -116,7 +120,7 @@ function validateSources(artifact, sources) {
|
|
|
116
120
|
typeof definition === 'object' &&
|
|
117
121
|
'nullable' in definition &&
|
|
118
122
|
definition.nullable === false;
|
|
119
|
-
if (required && !Object.hasOwn(sources, name)) {
|
|
123
|
+
if (required && !Object.hasOwn(defaults, name) && !Object.hasOwn(sources, name)) {
|
|
120
124
|
throw new TypeError(`Distributed boundary binding is missing required variable ${name}; use an explicit binding, parent/boundary query, client-only execution, or a better read root`);
|
|
121
125
|
}
|
|
122
126
|
}
|
|
@@ -2,6 +2,7 @@ export { authFromPageData, type PageGraphqlData } from './auth.js';
|
|
|
2
2
|
export { distributedReloadLifecycle, registerDistributedReloadClient, validateDistributedReloadLocation, validateDistributedReloadState, type DistributedReloadLifecycle, type DistributedReloadOptions, type DistributedReloadStateDeclaration } from './lifecycle.js';
|
|
3
3
|
export { parseDistributedGenerationEnvelope, type DistributedGenerationEnvelope } from '../generation.js';
|
|
4
4
|
export { defineDistributedBoundaryBinding, defineDistributedBoundaryOperation, resolveDistributedBoundaryVariables, type DistributedBoundaryBinding, type DistributedBoundaryOperation, type DistributedBoundaryPlan, type DistributedBoundaryVariableContext, type DistributedBoundaryVariableSource, type DistributedBoundaryVariableSources } from './boundary-variables.js';
|
|
5
|
+
export { constant, defineGraphqlIslandBindings, forwardedProp, omitVariable, routeParam, searchParam, sessionClaim } from './island-bindings.js';
|
|
5
6
|
export { DistributedSvelteKitBoundaryController, type DistributedSvelteKitBoundaryInstance, type DistributedSvelteKitBoundaryLocation, type DistributedSvelteKitLocationContext, type SveltekitBoundaryLifecycleDiagnostic, type SveltekitBoundaryRetention } from './boundary-lifecycle.js';
|
|
6
7
|
export { defineDistributedSvelteKitOperation, provideDistributedSvelteKitClient, retainDistributedSvelteKitBoundary, useDistributedSvelteKitClient, useDistributedSvelteKitCommands } from './context.js';
|
|
7
8
|
export { bindSveltekitOperation, createPageDataSessionSource, createDistributedSvelteKit, sessionSourceFromPageData, type CreateDistributedSvelteKitOptions, type DistributedSvelteKitClient, type SveltekitBoundOperation, type SveltekitBoundBoundaryOperation, type SveltekitCommandRuntimeFactory, type SveltekitCommandRuntimeFactoryOptions, type SveltekitCommandRuntimeLike, type SveltekitDistributedPageData, type SveltekitPageDataSessionSource, type SveltekitPageDataSource, type SveltekitQuerySnapshot, type SveltekitQueryStore, type SveltekitReplicaAuthority, type SveltekitReplicaHydration, type SveltekitSessionSource, type UseSveltekitOperationOptions } from './replica.js';
|
package/dist/sveltekit/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { authFromPageData } from './auth.js';
|
|
|
2
2
|
export { distributedReloadLifecycle, registerDistributedReloadClient, validateDistributedReloadLocation, validateDistributedReloadState } from './lifecycle.js';
|
|
3
3
|
export { parseDistributedGenerationEnvelope } from '../generation.js';
|
|
4
4
|
export { defineDistributedBoundaryBinding, defineDistributedBoundaryOperation, resolveDistributedBoundaryVariables } from './boundary-variables.js';
|
|
5
|
+
export { constant, defineGraphqlIslandBindings, forwardedProp, omitVariable, routeParam, searchParam, sessionClaim } from './island-bindings.js';
|
|
5
6
|
export { DistributedSvelteKitBoundaryController } from './boundary-lifecycle.js';
|
|
6
7
|
export { defineDistributedSvelteKitOperation, provideDistributedSvelteKitClient, retainDistributedSvelteKitBoundary, useDistributedSvelteKitClient, useDistributedSvelteKitCommands } from './context.js';
|
|
7
8
|
export { bindSveltekitOperation, createPageDataSessionSource, createDistributedSvelteKit, sessionSourceFromPageData } from './replica.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { GraphqlVariables } from '../types.js';
|
|
2
|
+
import type { DistributedBoundaryVariableSource, DistributedBoundaryVariableSources } from './boundary-variables.js';
|
|
3
|
+
/**
|
|
4
|
+
* Define the exceptional variables for one colocated GraphQL island.
|
|
5
|
+
*
|
|
6
|
+
* Save the default export as `<document>.bindings.js`. Route parameters with
|
|
7
|
+
* the same name and GraphQL variable defaults need no sidecar entry.
|
|
8
|
+
*/
|
|
9
|
+
export declare function defineGraphqlIslandBindings<TVariables extends GraphqlVariables = GraphqlVariables>(sources: DistributedBoundaryVariableSources<TVariables>): DistributedBoundaryVariableSources<TVariables>;
|
|
10
|
+
/** @internal Validate a discovered sidecar without trusting its prototype. */
|
|
11
|
+
export declare function isGraphqlIslandBindings(value: unknown): value is DistributedBoundaryVariableSources<GraphqlVariables>;
|
|
12
|
+
export declare function routeParam(name: string): DistributedBoundaryVariableSource;
|
|
13
|
+
export declare function searchParam(name: string, mode?: 'first' | 'all'): DistributedBoundaryVariableSource;
|
|
14
|
+
export declare function sessionClaim(...path: string[]): DistributedBoundaryVariableSource;
|
|
15
|
+
export declare function forwardedProp(...path: string[]): DistributedBoundaryVariableSource;
|
|
16
|
+
export declare function constant<TValue>(value: TValue): DistributedBoundaryVariableSource<TValue>;
|
|
17
|
+
export declare function omitVariable(): DistributedBoundaryVariableSource;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const GRAPHQL_ISLAND_BINDINGS = Symbol.for('@hops-ops/distributed/graphql-island-bindings');
|
|
2
|
+
const HOSTILE_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
3
|
+
/**
|
|
4
|
+
* Define the exceptional variables for one colocated GraphQL island.
|
|
5
|
+
*
|
|
6
|
+
* Save the default export as `<document>.bindings.js`. Route parameters with
|
|
7
|
+
* the same name and GraphQL variable defaults need no sidecar entry.
|
|
8
|
+
*/
|
|
9
|
+
export function defineGraphqlIslandBindings(sources) {
|
|
10
|
+
if (sources === null ||
|
|
11
|
+
typeof sources !== 'object' ||
|
|
12
|
+
Array.isArray(sources) ||
|
|
13
|
+
(Object.getPrototypeOf(sources) !== Object.prototype &&
|
|
14
|
+
Object.getPrototypeOf(sources) !== null)) {
|
|
15
|
+
throw new TypeError('GraphQL island bindings must be an object');
|
|
16
|
+
}
|
|
17
|
+
const bindings = {};
|
|
18
|
+
for (const key of Object.keys(sources)) {
|
|
19
|
+
if (HOSTILE_KEYS.has(key)) {
|
|
20
|
+
throw new TypeError(`GraphQL island binding name ${key} is unsafe`);
|
|
21
|
+
}
|
|
22
|
+
const descriptor = Object.getOwnPropertyDescriptor(sources, key);
|
|
23
|
+
if (descriptor === undefined || !('value' in descriptor)) {
|
|
24
|
+
throw new TypeError(`GraphQL island binding ${key} must be a data property`);
|
|
25
|
+
}
|
|
26
|
+
bindings[key] = descriptor.value;
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(bindings, GRAPHQL_ISLAND_BINDINGS, {
|
|
29
|
+
value: true,
|
|
30
|
+
enumerable: false
|
|
31
|
+
});
|
|
32
|
+
return Object.freeze(bindings);
|
|
33
|
+
}
|
|
34
|
+
/** @internal Validate a discovered sidecar without trusting its prototype. */
|
|
35
|
+
export function isGraphqlIslandBindings(value) {
|
|
36
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
37
|
+
return false;
|
|
38
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, GRAPHQL_ISLAND_BINDINGS);
|
|
39
|
+
return descriptor !== undefined && 'value' in descriptor && descriptor.value === true;
|
|
40
|
+
}
|
|
41
|
+
export function routeParam(name) {
|
|
42
|
+
return Object.freeze({ kind: 'route_param', name });
|
|
43
|
+
}
|
|
44
|
+
export function searchParam(name, mode = 'first') {
|
|
45
|
+
return Object.freeze({ kind: 'search_param', name, mode });
|
|
46
|
+
}
|
|
47
|
+
export function sessionClaim(...path) {
|
|
48
|
+
return Object.freeze({ kind: 'trusted_session', path: Object.freeze(path) });
|
|
49
|
+
}
|
|
50
|
+
export function forwardedProp(...path) {
|
|
51
|
+
return Object.freeze({ kind: 'forwarded_prop', path: Object.freeze(path) });
|
|
52
|
+
}
|
|
53
|
+
export function constant(value) {
|
|
54
|
+
return Object.freeze({ kind: 'constant', value });
|
|
55
|
+
}
|
|
56
|
+
export function omitVariable() {
|
|
57
|
+
return Object.freeze({ kind: 'omit' });
|
|
58
|
+
}
|
|
@@ -34,6 +34,7 @@ export type DistributedIslandPlanInput = Readonly<{
|
|
|
34
34
|
variables: readonly Readonly<{
|
|
35
35
|
name: string;
|
|
36
36
|
graphqlType: string;
|
|
37
|
+
defaultValue?: unknown;
|
|
37
38
|
}>[];
|
|
38
39
|
}>;
|
|
39
40
|
}>;
|
|
@@ -98,7 +99,9 @@ export type DistributedSvelteKitBoundaryAnalysisOptions = Readonly<{
|
|
|
98
99
|
/** Validate a persisted adapter plan before check/dev treats it as coherent. */
|
|
99
100
|
export declare function validateDistributedSvelteKitBoundaryPlan(value: unknown, module?: string): DistributedSvelteKitBoundaryPlan;
|
|
100
101
|
/**
|
|
101
|
-
* Analyze Svelte component reachability without evaluating application
|
|
102
|
-
*
|
|
102
|
+
* Analyze Svelte component reachability without evaluating application
|
|
103
|
+
* components. Colocated, bounded GraphQL binding sidecars are the only app
|
|
104
|
+
* modules evaluated. The returned plan is deterministic and contains
|
|
105
|
+
* project-relative paths only.
|
|
103
106
|
*/
|
|
104
107
|
export declare function analyzeDistributedSvelteKitBoundaries(options: DistributedSvelteKitBoundaryAnalysisOptions): Promise<readonly DistributedSvelteKitBoundaryPlan[]>;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, readFile, readdir, realpath } from 'node:fs/promises';
|
|
2
3
|
import { dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
3
5
|
import { parse } from 'svelte/compiler';
|
|
6
|
+
import { isGraphqlIslandBindings } from '../island-bindings.js';
|
|
4
7
|
const BOUNDARY_PLAN_VERSION = 1;
|
|
8
|
+
const VARIABLE_CODEC_VERSION = 2;
|
|
5
9
|
const MAX_COMPONENTS = 4_096;
|
|
6
10
|
const MAX_ISLANDS = 4_096;
|
|
7
11
|
const MAX_COMPONENT_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
const MAX_BINDINGS_BYTES = 256 * 1024;
|
|
8
13
|
const MAX_GRAPH_EDGES = 32_768;
|
|
9
14
|
/** Validate a persisted adapter plan before check/dev treats it as coherent. */
|
|
10
15
|
export function validateDistributedSvelteKitBoundaryPlan(value, module) {
|
|
@@ -21,8 +26,10 @@ export function validateDistributedSvelteKitBoundaryPlan(value, module) {
|
|
|
21
26
|
return value;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
|
-
* Analyze Svelte component reachability without evaluating application
|
|
25
|
-
*
|
|
29
|
+
* Analyze Svelte component reachability without evaluating application
|
|
30
|
+
* components. Colocated, bounded GraphQL binding sidecars are the only app
|
|
31
|
+
* modules evaluated. The returned plan is deterministic and contains
|
|
32
|
+
* project-relative paths only.
|
|
26
33
|
*/
|
|
27
34
|
export async function analyzeDistributedSvelteKitBoundaries(options) {
|
|
28
35
|
const cwd = resolve(options.cwd);
|
|
@@ -52,6 +59,7 @@ export async function analyzeDistributedSvelteKitBoundaries(options) {
|
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
const plans = [];
|
|
62
|
+
const colocatedBindings = await loadColocatedBindings(cwd, sourceOwners.keys());
|
|
55
63
|
for (const client of [...options.clients].sort((left, right) => left.module.localeCompare(right.module))) {
|
|
56
64
|
const loadIslands = client.inventory.islands
|
|
57
65
|
.filter((island) => island.directives.load)
|
|
@@ -115,11 +123,11 @@ export async function analyzeDistributedSvelteKitBoundaries(options) {
|
|
|
115
123
|
const explicitEntries = explicitByBoundary.get(root.id) ?? [];
|
|
116
124
|
const explicitAtBoundary = new Set(explicitEntries.map(({ entry }) => entry.island.id));
|
|
117
125
|
for (const { entry, registration } of explicitEntries) {
|
|
118
|
-
occurrences.push(occurrence(cwd, entry, root, root.path, 'explicit', false, registration.variables));
|
|
126
|
+
occurrences.push(occurrence(cwd, entry, root, root.path, 'explicit', false, bindingSources(entry, registration.variables, colocatedBindings)));
|
|
119
127
|
placed.add(entry.island.id);
|
|
120
128
|
}
|
|
121
129
|
for (const entry of routeIslands.get(routeBoundaryKey(root.kind, dirname(root.path))) ?? []) {
|
|
122
|
-
occurrences.push(occurrence(cwd, entry, root, root.path, 'route_document', false, explicitByIdentity.get(`${root.id}\u0000${entry.island.operation}`)?.variables));
|
|
130
|
+
occurrences.push(occurrence(cwd, entry, root, root.path, 'route_document', false, bindingSources(entry, explicitByIdentity.get(`${root.id}\u0000${entry.island.operation}`)?.variables, colocatedBindings)));
|
|
123
131
|
placed.add(entry.island.id);
|
|
124
132
|
}
|
|
125
133
|
const dynamicComponentIslands = new Map([...componentIslands.entries()]
|
|
@@ -131,7 +139,7 @@ export async function analyzeDistributedSvelteKitBoundaries(options) {
|
|
|
131
139
|
const reachable = traverse(root, components, aliases, dynamicComponentIslands, cwd);
|
|
132
140
|
for (const component of reachable) {
|
|
133
141
|
for (const entry of componentIslands.get(component) ?? []) {
|
|
134
|
-
occurrences.push(occurrence(cwd, entry, root, component, 'static_component_import', true, explicitByIdentity.get(`${root.id}\u0000${entry.island.operation}`)?.variables));
|
|
142
|
+
occurrences.push(occurrence(cwd, entry, root, component, 'static_component_import', true, bindingSources(entry, explicitByIdentity.get(`${root.id}\u0000${entry.island.operation}`)?.variables, colocatedBindings)));
|
|
135
143
|
placed.add(entry.island.id);
|
|
136
144
|
}
|
|
137
145
|
}
|
|
@@ -164,6 +172,49 @@ export async function analyzeDistributedSvelteKitBoundaries(options) {
|
|
|
164
172
|
}
|
|
165
173
|
return Object.freeze(plans);
|
|
166
174
|
}
|
|
175
|
+
function bindingSources(entry, explicit, colocated) {
|
|
176
|
+
const sidecar = colocated.get(entry.source);
|
|
177
|
+
if (explicit !== undefined && sidecar !== undefined) {
|
|
178
|
+
throw diagnostic('distributed.island.variable_binding_conflict', entry.source, entry.island.source.line, entry.island.source.column, `operation ${entry.island.operation} has both centralized and colocated variable bindings; remove the screen behavior from distributed.config and keep ${entry.source}.bindings.js`);
|
|
179
|
+
}
|
|
180
|
+
return sidecar ?? explicit;
|
|
181
|
+
}
|
|
182
|
+
async function loadColocatedBindings(cwd, sources) {
|
|
183
|
+
const canonicalRoot = await realpath(cwd);
|
|
184
|
+
const bindings = new Map();
|
|
185
|
+
for (const source of [...new Set(sources)].sort()) {
|
|
186
|
+
const path = contained(cwd, `${source}.bindings.js`, 'GraphQL island bindings');
|
|
187
|
+
let metadata;
|
|
188
|
+
try {
|
|
189
|
+
metadata = await lstat(path);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (isMissing(error))
|
|
193
|
+
continue;
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
197
|
+
throw new Error(`[distributed.island.bindings_invalid] ${source}.bindings.js must be a regular project-local file`);
|
|
198
|
+
}
|
|
199
|
+
if (metadata.size > MAX_BINDINGS_BYTES) {
|
|
200
|
+
throw new Error(`[distributed.island.bindings_too_large] ${source}.bindings.js exceeds ${MAX_BINDINGS_BYTES} bytes`);
|
|
201
|
+
}
|
|
202
|
+
const canonicalPath = await realpath(path);
|
|
203
|
+
if (!isWithin(canonicalRoot, canonicalPath)) {
|
|
204
|
+
throw new Error(`[distributed.island.bindings_invalid] ${source}.bindings.js must stay within the project root`);
|
|
205
|
+
}
|
|
206
|
+
const bytes = await readFile(canonicalPath);
|
|
207
|
+
const url = pathToFileURL(canonicalPath);
|
|
208
|
+
url.searchParams.set('distributed-binding', createHash('sha256').update(bytes).digest('hex'));
|
|
209
|
+
const loaded = await import(url.href);
|
|
210
|
+
const value = ownDataValue(loaded, 'default');
|
|
211
|
+
if (!isGraphqlIslandBindings(value)) {
|
|
212
|
+
throw new Error(`[distributed.island.bindings_invalid] ${source}.bindings.js must default-export defineGraphqlIslandBindings({...})`);
|
|
213
|
+
}
|
|
214
|
+
bindings.set(source, value);
|
|
215
|
+
}
|
|
216
|
+
return bindings;
|
|
217
|
+
}
|
|
167
218
|
function occurrence(cwd, entry, root, component, reason, conservative, explicitSources) {
|
|
168
219
|
if (root.kind === 'layout' &&
|
|
169
220
|
entry.island.directives.live &&
|
|
@@ -222,7 +273,7 @@ function boundaryBinding(entry, root, explicitSources) {
|
|
|
222
273
|
]);
|
|
223
274
|
hasRoute = true;
|
|
224
275
|
}
|
|
225
|
-
else if (!variable.graphqlType.endsWith('!')) {
|
|
276
|
+
else if (Object.hasOwn(variable, 'defaultValue') || !variable.graphqlType.endsWith('!')) {
|
|
226
277
|
sources.push([variable.name, Object.freeze({ kind: 'omit' })]);
|
|
227
278
|
}
|
|
228
279
|
else {
|
|
@@ -546,10 +597,11 @@ function islandOwnerComponent(cwd, source) {
|
|
|
546
597
|
const suffix = extname(absolute);
|
|
547
598
|
const base = absolute.slice(0, -suffix.length);
|
|
548
599
|
const name = posix.basename(portable(base));
|
|
549
|
-
|
|
600
|
+
const routeDocument = /^\+(page|layout)(?:\.[_A-Za-z][_0-9A-Za-z-]*)?$/.exec(name);
|
|
601
|
+
if (routeDocument !== null) {
|
|
550
602
|
return {
|
|
551
603
|
kind: 'route',
|
|
552
|
-
key: routeBoundaryKey(
|
|
604
|
+
key: routeBoundaryKey(routeDocument[1] === 'page' ? 'page' : 'layout', dirname(base))
|
|
553
605
|
};
|
|
554
606
|
}
|
|
555
607
|
return { kind: 'component', path: `${base}.svelte` };
|
|
@@ -590,6 +642,8 @@ function validateInventory(module, inventory) {
|
|
|
590
642
|
Array.isArray(variableSchema) ||
|
|
591
643
|
typeof variableSchema.reference !== 'string' ||
|
|
592
644
|
!Number.isSafeInteger(variableSchema.codecVersion) ||
|
|
645
|
+
variableSchema.codecVersion !== VARIABLE_CODEC_VERSION ||
|
|
646
|
+
!variableSchema.reference.endsWith(`#variable-codec-v${VARIABLE_CODEC_VERSION}`) ||
|
|
593
647
|
!Array.isArray(variableSchema.variables) ||
|
|
594
648
|
variableSchema.variables.some((variable) => variable === null ||
|
|
595
649
|
typeof variable !== 'object' ||
|
package/dist/sveltekit/vite.js
CHANGED
|
@@ -62,7 +62,7 @@ export async function checkDistributedSvelteKit(options) {
|
|
|
62
62
|
* coalesced, abortable, and always invoked without a shell.
|
|
63
63
|
*/
|
|
64
64
|
export function distributedSvelteKit(options) {
|
|
65
|
-
const lifecycleOwnsCompile = process.env.
|
|
65
|
+
const lifecycleOwnsCompile = process.env.DISTRIBUTED_LIFECYCLE_OWNS_CLIENT_COMPILE === '1';
|
|
66
66
|
let resolved;
|
|
67
67
|
let dirty = false;
|
|
68
68
|
let running;
|
|
@@ -135,14 +135,11 @@ export function distributedSvelteKit(options) {
|
|
|
135
135
|
frameworkDist = localFrameworkDist(config.root);
|
|
136
136
|
await validateResolvedPaths(resolved);
|
|
137
137
|
/*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* files, while compiler inputs below are held for the supervisor reload.
|
|
138
|
+
* The lifecycle has already built the active generation before it starts
|
|
139
|
+
* this UI process. The supervisor activates later generations atomically
|
|
140
|
+
* while this server stays available; compiling again here would race that
|
|
141
|
+
* owner and can prevent the UI from reaching its readiness probe.
|
|
143
142
|
*/
|
|
144
|
-
if (lifecycleOwnsCompile)
|
|
145
|
-
return;
|
|
146
143
|
/*
|
|
147
144
|
* SvelteKit post-build analysis loads Vite config in an isolated
|
|
148
145
|
* worker marked with SVELTEKIT_FORK. That pass reads framework
|
|
@@ -151,6 +148,8 @@ export function distributedSvelteKit(options) {
|
|
|
151
148
|
*/
|
|
152
149
|
if (!isMainThread && process.env.SVELTEKIT_FORK === 'true')
|
|
153
150
|
return;
|
|
151
|
+
if (lifecycleOwnsCompile)
|
|
152
|
+
return;
|
|
154
153
|
lock = await acquireCompilerLock(resolved.cwd);
|
|
155
154
|
try {
|
|
156
155
|
await compile('Vite startup', true);
|
|
@@ -229,7 +228,7 @@ export function distributedSvelteKit(options) {
|
|
|
229
228
|
context.server.moduleGraph.invalidateModule(module);
|
|
230
229
|
}
|
|
231
230
|
}
|
|
232
|
-
if (
|
|
231
|
+
if (!lifecycleOwnsCompile) {
|
|
233
232
|
context.server.ws.send({ type: 'full-reload', path: '*' });
|
|
234
233
|
}
|
|
235
234
|
reloadedGeneration = completedGeneration;
|
|
@@ -665,12 +664,15 @@ function isCompilerInput(file, integration) {
|
|
|
665
664
|
}
|
|
666
665
|
function isGraphqlInput(file, integration) {
|
|
667
666
|
const absolute = resolve(integration.cwd, file);
|
|
667
|
+
const isDocument = absolute.endsWith('.graphql') || absolute.endsWith('.gql');
|
|
668
|
+
const isBindings = absolute.endsWith('.graphql.bindings.js') ||
|
|
669
|
+
absolute.endsWith('.gql.bindings.js');
|
|
668
670
|
if (absolute.endsWith('.svelte') &&
|
|
669
671
|
(isWithin(integration.routesDir, absolute) ||
|
|
670
672
|
isWithin(integration.libDir, absolute))) {
|
|
671
673
|
return true;
|
|
672
674
|
}
|
|
673
|
-
if ((!
|
|
675
|
+
if ((!isDocument && !isBindings) ||
|
|
674
676
|
!isWithin(integration.cwd, absolute)) {
|
|
675
677
|
return false;
|
|
676
678
|
}
|
|
@@ -728,7 +730,7 @@ async function compileTransaction(integration, children, signal) {
|
|
|
728
730
|
hadAdapterOutput: await realDirectoryExists(client.adapterOut)
|
|
729
731
|
});
|
|
730
732
|
}
|
|
731
|
-
const plans = await analyzeStagedBoundaries(integration, staged);
|
|
733
|
+
const plans = await analyzeStagedBoundaries(integration, transaction, staged);
|
|
732
734
|
const plansByModule = new Map(plans.map((plan) => [plan.module, plan]));
|
|
733
735
|
for (const item of staged) {
|
|
734
736
|
const plan = plansByModule.get(item.client.module);
|
|
@@ -775,7 +777,7 @@ async function checkTransaction(integration, children, signal) {
|
|
|
775
777
|
await validateGeneratedEntrypoint(integration.cwd, output, client.module);
|
|
776
778
|
staged.push(Object.freeze({ client, output }));
|
|
777
779
|
}
|
|
778
|
-
const plans = await analyzeStagedBoundaries(integration, staged);
|
|
780
|
+
const plans = await analyzeStagedBoundaries(integration, transaction, staged);
|
|
779
781
|
const plansByModule = new Map(plans.map((plan) => [plan.module, plan]));
|
|
780
782
|
for (const [index, client] of integration.clients.entries()) {
|
|
781
783
|
const output = staged[index].output;
|
|
@@ -829,7 +831,7 @@ async function validateAdapterBoundaryPlan(client, plan) {
|
|
|
829
831
|
throw new Error(`Distributed SvelteKit boundary plan for ${client.module} is stale; run generation without check`);
|
|
830
832
|
}
|
|
831
833
|
}
|
|
832
|
-
async function analyzeStagedBoundaries(integration, staged) {
|
|
834
|
+
async function analyzeStagedBoundaries(integration, transactionRoot, staged) {
|
|
833
835
|
return await analyzeDistributedSvelteKitBoundaries({
|
|
834
836
|
cwd: integration.cwd,
|
|
835
837
|
routesDir: portablePath(relative(integration.cwd, integration.routesDir)),
|
|
@@ -837,18 +839,18 @@ async function analyzeStagedBoundaries(integration, staged) {
|
|
|
837
839
|
aliases: integration.aliases,
|
|
838
840
|
clients: await Promise.all(staged.map(async ({ client, output }) => ({
|
|
839
841
|
module: client.module,
|
|
840
|
-
inventory: await readIslandInventory(
|
|
842
|
+
inventory: await readIslandInventory(transactionRoot, output),
|
|
841
843
|
explicitBoundaries: client.boundaries
|
|
842
844
|
})))
|
|
843
845
|
});
|
|
844
846
|
}
|
|
845
|
-
async function readIslandInventory(
|
|
847
|
+
async function readIslandInventory(transactionRoot, output) {
|
|
846
848
|
const path = join(output, 'islands.json');
|
|
847
849
|
const metadata = await lstat(path);
|
|
848
850
|
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
849
|
-
throw new Error(`Distributed island inventory ${portablePath(relative(
|
|
851
|
+
throw new Error(`Distributed island inventory ${portablePath(relative(transactionRoot, path))} must be a regular file`);
|
|
850
852
|
}
|
|
851
|
-
const canonicalRoot = await realpath(
|
|
853
|
+
const canonicalRoot = await realpath(transactionRoot);
|
|
852
854
|
const canonical = await realpath(path);
|
|
853
855
|
if (!isWithin(canonicalRoot, canonical)) {
|
|
854
856
|
throw new Error('Distributed island inventory escaped the project root');
|
package/package.json
CHANGED