@hops-ops/distributed 4.9.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 +89 -8
- package/dist/replica/distributed-replica/impl-protocol.d.ts +1 -0
- package/dist/replica/distributed-replica/impl-protocol.js +1 -0
- package/dist/replica/distributed-replica/impl.js +11 -0
- package/dist/replica/distributed-replica/watch.js +13 -1
- package/dist/replica/identity/codec.d.ts +1 -0
- package/dist/replica/identity/codec.js +16 -3
- package/dist/replica/index.d.ts +1 -1
- package/dist/replica/types.d.ts +42 -1
- package/dist/sveltekit/boundary-lifecycle.d.ts +37 -0
- package/dist/sveltekit/boundary-lifecycle.js +355 -0
- package/dist/sveltekit/boundary-variables.d.ts +57 -0
- package/dist/sveltekit/boundary-variables.js +294 -0
- package/dist/sveltekit/context.d.ts +3 -0
- package/dist/sveltekit/context.js +8 -0
- package/dist/sveltekit/index.d.ts +6 -3
- package/dist/sveltekit/index.js +5 -2
- package/dist/sveltekit/island-bindings.d.ts +17 -0
- package/dist/sveltekit/island-bindings.js +58 -0
- package/dist/sveltekit/islands/boundaries.d.ts +107 -0
- package/dist/sveltekit/islands/boundaries.js +788 -0
- package/dist/sveltekit/operation-identity.d.ts +4 -0
- package/dist/sveltekit/operation-identity.js +10 -0
- package/dist/sveltekit/replica.d.ts +24 -0
- package/dist/sveltekit/replica.js +72 -5
- package/dist/sveltekit/server-replica.d.ts +10 -26
- package/dist/sveltekit/server-replica.js +159 -132
- package/dist/sveltekit/vite.d.ts +10 -3
- package/dist/sveltekit/vite.js +310 -39
- package/package.json +2 -2
package/dist/sveltekit/vite.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type DistributedSvelteKitBoundaryRegistration } from './islands/boundaries.js';
|
|
2
|
+
export { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan, type DistributedIslandInventory, type DistributedIslandPlanInput, type DistributedSvelteKitBoundary, type DistributedSvelteKitBoundaryAnalysisClient, type DistributedSvelteKitBoundaryAnalysisOptions, type DistributedSvelteKitBoundaryOccurrence, type DistributedSvelteKitBoundaryPlan, type DistributedSvelteKitBoundaryRegistration } from './islands/boundaries.js';
|
|
1
3
|
export type DistributedGraphqlProxyOptions = {
|
|
2
4
|
/** Absolute Distributed API origin, for example `http://127.0.0.1:8791`. */
|
|
3
5
|
target: string;
|
|
@@ -28,8 +30,8 @@ export type DistributedSvelteKitClientCompiler = Readonly<{
|
|
|
28
30
|
surface?: string;
|
|
29
31
|
/** GraphQL globs passed verbatim as repeated `distributed client --documents`. */
|
|
30
32
|
documents: readonly string[];
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
+
/** Typed fallback when static component ownership cannot be proven. */
|
|
34
|
+
boundaries?: readonly DistributedSvelteKitBoundaryRegistration[];
|
|
33
35
|
/** Compiler-owned artifact directory, relative to `cwd` by default. */
|
|
34
36
|
out: string;
|
|
35
37
|
}>;
|
|
@@ -40,6 +42,12 @@ export type DistributedSvelteKitViteOptions = Readonly<{
|
|
|
40
42
|
command?: string;
|
|
41
43
|
/** Prefix argv, e.g. `cargo run ... --`; never interpreted by a shell. */
|
|
42
44
|
commandArgs?: readonly string[];
|
|
45
|
+
/** SvelteKit route source root. Defaults to `src/routes`. */
|
|
46
|
+
routesDir?: string;
|
|
47
|
+
/** SvelteKit library source root. Defaults to `src/lib`. */
|
|
48
|
+
libDir?: string;
|
|
49
|
+
/** Additional project-local `$name` aliases used by static component imports. */
|
|
50
|
+
aliases?: Readonly<Record<string, string>>;
|
|
43
51
|
clients: readonly DistributedSvelteKitClientCompiler[];
|
|
44
52
|
}>;
|
|
45
53
|
type ViteWebSocketLike = Readonly<{
|
|
@@ -136,4 +144,3 @@ export declare function distributedSvelteKit(options: DistributedSvelteKitViteOp
|
|
|
136
144
|
* generation and is safe to call from `svelte.config.js`.
|
|
137
145
|
*/
|
|
138
146
|
export declare function distributedSvelteKitAliases(options: Pick<DistributedSvelteKitViteOptions, 'cwd' | 'clients'>): Readonly<Record<string, string>>;
|
|
139
|
-
export {};
|
package/dist/sveltekit/vite.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
|
-
import {
|
|
4
|
+
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { isMainThread } from 'node:worker_threads';
|
|
7
|
+
import { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan } from './islands/boundaries.js';
|
|
8
|
+
export { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan } from './islands/boundaries.js';
|
|
7
9
|
const GENERATED_SVELTEKIT_MODULE = 'sveltekit.ts';
|
|
10
|
+
const GENERATED_BOUNDARIES_MODULE = 'boundaries.ts';
|
|
8
11
|
const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
9
12
|
const MAX_GENERATED_COMPARE_FILES = 20_000;
|
|
10
13
|
const MAX_GENERATED_COMPARE_BYTES = 128 * 1024 * 1024;
|
|
@@ -59,7 +62,7 @@ export async function checkDistributedSvelteKit(options) {
|
|
|
59
62
|
* coalesced, abortable, and always invoked without a shell.
|
|
60
63
|
*/
|
|
61
64
|
export function distributedSvelteKit(options) {
|
|
62
|
-
const lifecycleOwnsCompile = process.env.
|
|
65
|
+
const lifecycleOwnsCompile = process.env.DISTRIBUTED_LIFECYCLE_OWNS_CLIENT_COMPILE === '1';
|
|
63
66
|
let resolved;
|
|
64
67
|
let dirty = false;
|
|
65
68
|
let running;
|
|
@@ -132,14 +135,11 @@ export function distributedSvelteKit(options) {
|
|
|
132
135
|
frameworkDist = localFrameworkDist(config.root);
|
|
133
136
|
await validateResolvedPaths(resolved);
|
|
134
137
|
/*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* 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.
|
|
140
142
|
*/
|
|
141
|
-
if (lifecycleOwnsCompile)
|
|
142
|
-
return;
|
|
143
143
|
/*
|
|
144
144
|
* SvelteKit post-build analysis loads Vite config in an isolated
|
|
145
145
|
* worker marked with SVELTEKIT_FORK. That pass reads framework
|
|
@@ -148,6 +148,8 @@ export function distributedSvelteKit(options) {
|
|
|
148
148
|
*/
|
|
149
149
|
if (!isMainThread && process.env.SVELTEKIT_FORK === 'true')
|
|
150
150
|
return;
|
|
151
|
+
if (lifecycleOwnsCompile)
|
|
152
|
+
return;
|
|
151
153
|
lock = await acquireCompilerLock(resolved.cwd);
|
|
152
154
|
try {
|
|
153
155
|
await compile('Vite startup', true);
|
|
@@ -162,10 +164,14 @@ export function distributedSvelteKit(options) {
|
|
|
162
164
|
configureLifecycleServer(server);
|
|
163
165
|
const integration = requireResolved(resolved);
|
|
164
166
|
if (!lifecycleOwnsCompile) {
|
|
165
|
-
const roots =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
const roots = [
|
|
168
|
+
integration.routesDir,
|
|
169
|
+
integration.libDir,
|
|
170
|
+
...integration.clients.flatMap((client) => [
|
|
171
|
+
...client.watchRoots,
|
|
172
|
+
...client.manifestWatchRoots
|
|
173
|
+
])
|
|
174
|
+
];
|
|
169
175
|
if (roots.length > 0)
|
|
170
176
|
server.watcher.add(roots);
|
|
171
177
|
}
|
|
@@ -177,6 +183,8 @@ export function distributedSvelteKit(options) {
|
|
|
177
183
|
const integration = requireResolved(resolved);
|
|
178
184
|
if (lifecycleOwnsCompile)
|
|
179
185
|
return;
|
|
186
|
+
this.addWatchFile(integration.routesDir);
|
|
187
|
+
this.addWatchFile(integration.libDir);
|
|
180
188
|
for (const client of integration.clients) {
|
|
181
189
|
for (const root of [...client.watchRoots, ...client.manifestWatchRoots])
|
|
182
190
|
this.addWatchFile(root);
|
|
@@ -203,7 +211,7 @@ export function distributedSvelteKit(options) {
|
|
|
203
211
|
if (lifecycleOwnsCompile)
|
|
204
212
|
return [];
|
|
205
213
|
try {
|
|
206
|
-
await compile(`GraphQL change ${context.file}`);
|
|
214
|
+
await compile(`GraphQL/Svelte change ${context.file}`);
|
|
207
215
|
}
|
|
208
216
|
catch (error) {
|
|
209
217
|
context.server.ws.send({
|
|
@@ -220,7 +228,7 @@ export function distributedSvelteKit(options) {
|
|
|
220
228
|
context.server.moduleGraph.invalidateModule(module);
|
|
221
229
|
}
|
|
222
230
|
}
|
|
223
|
-
if (
|
|
231
|
+
if (!lifecycleOwnsCompile) {
|
|
224
232
|
context.server.ws.send({ type: 'full-reload', path: '*' });
|
|
225
233
|
}
|
|
226
234
|
reloadedGeneration = completedGeneration;
|
|
@@ -231,7 +239,7 @@ export function distributedSvelteKit(options) {
|
|
|
231
239
|
if (lifecycleOwnsCompile)
|
|
232
240
|
return;
|
|
233
241
|
if (isCompilerInput(id, integration)) {
|
|
234
|
-
await compile(`GraphQL watch change ${id}`);
|
|
242
|
+
await compile(`GraphQL/Svelte watch change ${id}`);
|
|
235
243
|
}
|
|
236
244
|
},
|
|
237
245
|
closeBundle: stop
|
|
@@ -488,6 +496,14 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
488
496
|
}
|
|
489
497
|
const modules = new Set();
|
|
490
498
|
const outputs = [];
|
|
499
|
+
const routesDir = containedPath(cwd, options.routesDir ?? 'src/routes', 'routesDir');
|
|
500
|
+
const libDir = containedPath(cwd, options.libDir ?? 'src/lib', 'libDir');
|
|
501
|
+
const aliases = Object.freeze(Object.fromEntries(Object.entries(options.aliases ?? {}).map(([key, value]) => {
|
|
502
|
+
if (!/^\$[A-Za-z0-9_-]+$/.test(key)) {
|
|
503
|
+
throw new TypeError(`Distributed SvelteKit alias \`${key}\` must be a single $name segment`);
|
|
504
|
+
}
|
|
505
|
+
return [key, portablePath(relative(cwd, containedPath(cwd, value, `alias ${key}`)))];
|
|
506
|
+
})));
|
|
491
507
|
const clients = options.clients.map((client, index) => {
|
|
492
508
|
if (client === null || typeof client !== 'object') {
|
|
493
509
|
throw new TypeError(`Distributed client[${index}] must be an object`);
|
|
@@ -515,7 +531,21 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
515
531
|
throw new TypeError(`Distributed client \`${client.module}\` requires at least one GraphQL document glob`);
|
|
516
532
|
}
|
|
517
533
|
const documents = client.documents.map((document, documentIndex) => nonempty(document, `${client.module} documents[${documentIndex}]`));
|
|
518
|
-
const
|
|
534
|
+
const boundaries = (client.boundaries ?? []).map((boundary, boundaryIndex) => {
|
|
535
|
+
if (boundary === null ||
|
|
536
|
+
typeof boundary !== 'object' ||
|
|
537
|
+
(boundary.kind !== 'page' && boundary.kind !== 'layout')) {
|
|
538
|
+
throw new TypeError(`${client.module} boundaries[${boundaryIndex}] is invalid`);
|
|
539
|
+
}
|
|
540
|
+
return Object.freeze({
|
|
541
|
+
operation: nonempty(boundary.operation, `${client.module} boundaries[${boundaryIndex}].operation`),
|
|
542
|
+
route: nonempty(boundary.route, `${client.module} boundaries[${boundaryIndex}].route`),
|
|
543
|
+
kind: boundary.kind,
|
|
544
|
+
...(boundary.variables === undefined
|
|
545
|
+
? {}
|
|
546
|
+
: { variables: boundary.variables })
|
|
547
|
+
});
|
|
548
|
+
});
|
|
519
549
|
validateManifestSource(client.module, client.manifest);
|
|
520
550
|
const out = containedPath(cwd, client.out, `${client.module} generated output`);
|
|
521
551
|
if (out === cwd) {
|
|
@@ -527,14 +557,16 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
527
557
|
}
|
|
528
558
|
}
|
|
529
559
|
outputs.push(out);
|
|
560
|
+
const adapterOut = join(cwd, '.svelte-kit', 'distributed', 'clients', Buffer.from(client.module).toString('base64url'));
|
|
530
561
|
return Object.freeze({
|
|
531
562
|
module: client.module,
|
|
532
563
|
manifest: client.manifest,
|
|
533
564
|
selector,
|
|
534
565
|
documents: Object.freeze(documents),
|
|
535
|
-
|
|
566
|
+
boundaries: Object.freeze(boundaries),
|
|
536
567
|
out,
|
|
537
568
|
entry: join(out, GENERATED_SVELTEKIT_MODULE),
|
|
569
|
+
adapterOut,
|
|
538
570
|
watchRoots: Object.freeze(documentWatchRoots(cwd, documents, out)),
|
|
539
571
|
manifestWatchRoots: Object.freeze(manifestWatchRoots(cwd, client.manifest))
|
|
540
572
|
});
|
|
@@ -548,6 +580,9 @@ function resolveIntegration(options, fallbackCwd) {
|
|
|
548
580
|
}
|
|
549
581
|
return argument;
|
|
550
582
|
})),
|
|
583
|
+
routesDir,
|
|
584
|
+
libDir,
|
|
585
|
+
aliases,
|
|
551
586
|
clients: Object.freeze(clients)
|
|
552
587
|
});
|
|
553
588
|
}
|
|
@@ -629,7 +664,15 @@ function isCompilerInput(file, integration) {
|
|
|
629
664
|
}
|
|
630
665
|
function isGraphqlInput(file, integration) {
|
|
631
666
|
const absolute = resolve(integration.cwd, file);
|
|
632
|
-
|
|
667
|
+
const isDocument = absolute.endsWith('.graphql') || absolute.endsWith('.gql');
|
|
668
|
+
const isBindings = absolute.endsWith('.graphql.bindings.js') ||
|
|
669
|
+
absolute.endsWith('.gql.bindings.js');
|
|
670
|
+
if (absolute.endsWith('.svelte') &&
|
|
671
|
+
(isWithin(integration.routesDir, absolute) ||
|
|
672
|
+
isWithin(integration.libDir, absolute))) {
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
if ((!isDocument && !isBindings) ||
|
|
633
676
|
!isWithin(integration.cwd, absolute)) {
|
|
634
677
|
return false;
|
|
635
678
|
}
|
|
@@ -656,17 +699,12 @@ async function compileTransaction(integration, children, signal) {
|
|
|
656
699
|
throw new Error(`generated output ${client.out} must be a real directory`);
|
|
657
700
|
}
|
|
658
701
|
hadOutput = true;
|
|
659
|
-
await cp(client.out, output, {
|
|
660
|
-
recursive: true,
|
|
661
|
-
errorOnExist: true,
|
|
662
|
-
force: false,
|
|
663
|
-
dereference: false
|
|
664
|
-
});
|
|
665
702
|
}
|
|
666
703
|
catch (error) {
|
|
667
704
|
if (!isMissing(error))
|
|
668
705
|
throw error;
|
|
669
706
|
}
|
|
707
|
+
await mkdir(output, { recursive: true });
|
|
670
708
|
const args = [
|
|
671
709
|
'client',
|
|
672
710
|
'--manifest',
|
|
@@ -677,7 +715,6 @@ async function compileTransaction(integration, children, signal) {
|
|
|
677
715
|
'--documents',
|
|
678
716
|
document
|
|
679
717
|
]),
|
|
680
|
-
...client.routes.flatMap((route) => ['--route', route]),
|
|
681
718
|
'--out',
|
|
682
719
|
output
|
|
683
720
|
];
|
|
@@ -687,9 +724,24 @@ async function compileTransaction(integration, children, signal) {
|
|
|
687
724
|
client,
|
|
688
725
|
output,
|
|
689
726
|
backup: join(transaction, `backup-${index}`),
|
|
690
|
-
hadOutput
|
|
727
|
+
hadOutput,
|
|
728
|
+
adapterOutput: join(transaction, `adapter-${index}`),
|
|
729
|
+
adapterBackup: join(transaction, `adapter-backup-${index}`),
|
|
730
|
+
hadAdapterOutput: await realDirectoryExists(client.adapterOut)
|
|
691
731
|
});
|
|
692
732
|
}
|
|
733
|
+
const plans = await analyzeStagedBoundaries(integration, transaction, staged);
|
|
734
|
+
const plansByModule = new Map(plans.map((plan) => [plan.module, plan]));
|
|
735
|
+
for (const item of staged) {
|
|
736
|
+
const plan = plansByModule.get(item.client.module);
|
|
737
|
+
if (plan === undefined) {
|
|
738
|
+
throw new Error(`Distributed SvelteKit boundary analysis returned no plan for ${item.client.module}`);
|
|
739
|
+
}
|
|
740
|
+
await writeFile(join(item.output, GENERATED_BOUNDARIES_MODULE), boundaryModuleSource(plan), { encoding: 'utf8', flag: 'wx' });
|
|
741
|
+
await exposeBoundaryModule(item.output);
|
|
742
|
+
await mkdir(item.adapterOutput, { recursive: true });
|
|
743
|
+
await writeFile(join(item.adapterOutput, 'boundaries.json'), boundaryPlanSource(plan), { encoding: 'utf8', flag: 'wx' });
|
|
744
|
+
}
|
|
693
745
|
throwIfAborted(signal);
|
|
694
746
|
await commitOutputs(integration, staged, signal);
|
|
695
747
|
}
|
|
@@ -703,12 +755,14 @@ async function checkTransaction(integration, children, signal) {
|
|
|
703
755
|
const transactionRoot = await compilerTransactionRoot(integration);
|
|
704
756
|
const transaction = await mkdtemp(join(transactionRoot, '.distributed-sveltekit-check-'));
|
|
705
757
|
try {
|
|
758
|
+
const staged = [];
|
|
706
759
|
for (const [index, client] of integration.clients.entries()) {
|
|
707
760
|
throwIfAborted(signal);
|
|
708
761
|
const manifest = await materializeManifest(integration, client, transaction, index, children, signal);
|
|
762
|
+
const output = join(transaction, `output-${index}`);
|
|
763
|
+
await mkdir(output, { recursive: true });
|
|
709
764
|
await runCommand(integration, [
|
|
710
765
|
'client',
|
|
711
|
-
'--check',
|
|
712
766
|
'--manifest',
|
|
713
767
|
manifest,
|
|
714
768
|
client.selector[0],
|
|
@@ -717,10 +771,24 @@ async function checkTransaction(integration, children, signal) {
|
|
|
717
771
|
'--documents',
|
|
718
772
|
document
|
|
719
773
|
]),
|
|
720
|
-
...client.routes.flatMap((route) => ['--route', route]),
|
|
721
774
|
'--out',
|
|
722
|
-
|
|
775
|
+
output
|
|
723
776
|
], children, signal);
|
|
777
|
+
await validateGeneratedEntrypoint(integration.cwd, output, client.module);
|
|
778
|
+
staged.push(Object.freeze({ client, output }));
|
|
779
|
+
}
|
|
780
|
+
const plans = await analyzeStagedBoundaries(integration, transaction, staged);
|
|
781
|
+
const plansByModule = new Map(plans.map((plan) => [plan.module, plan]));
|
|
782
|
+
for (const [index, client] of integration.clients.entries()) {
|
|
783
|
+
const output = staged[index].output;
|
|
784
|
+
const plan = plansByModule.get(client.module);
|
|
785
|
+
if (plan === undefined) {
|
|
786
|
+
throw new Error(`Distributed SvelteKit boundary analysis returned no plan for ${client.module}`);
|
|
787
|
+
}
|
|
788
|
+
await writeFile(join(output, GENERATED_BOUNDARIES_MODULE), boundaryModuleSource(plan), { encoding: 'utf8', flag: 'wx' });
|
|
789
|
+
await exposeBoundaryModule(output);
|
|
790
|
+
await compareGeneratedTrees(client.out, output, client.module);
|
|
791
|
+
await validateAdapterBoundaryPlan(client, plan);
|
|
724
792
|
}
|
|
725
793
|
}
|
|
726
794
|
finally {
|
|
@@ -735,6 +803,195 @@ async function compilerTransactionRoot(integration) {
|
|
|
735
803
|
await mkdir(root, { recursive: true });
|
|
736
804
|
return root;
|
|
737
805
|
}
|
|
806
|
+
async function validateAdapterBoundaryPlan(client, plan) {
|
|
807
|
+
let actual;
|
|
808
|
+
try {
|
|
809
|
+
actual = await readFile(join(client.adapterOut, 'boundaries.json'), 'utf8');
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
/*
|
|
813
|
+
* `.svelte-kit` is SvelteKit-owned build state. A production build may
|
|
814
|
+
* replace it after our Vite startup generation, while the durable client
|
|
815
|
+
* tree (including boundaries.ts) remains current and was checked above.
|
|
816
|
+
*/
|
|
817
|
+
if (isMissing(error))
|
|
818
|
+
return;
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
821
|
+
let persisted;
|
|
822
|
+
try {
|
|
823
|
+
persisted = JSON.parse(actual);
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
throw new Error(`[distributed.island.boundary_plan_invalid] ${client.module} boundaries.json is not valid JSON`);
|
|
827
|
+
}
|
|
828
|
+
validateDistributedSvelteKitBoundaryPlan(persisted, client.module);
|
|
829
|
+
const expected = boundaryPlanSource(plan);
|
|
830
|
+
if (actual !== expected) {
|
|
831
|
+
throw new Error(`Distributed SvelteKit boundary plan for ${client.module} is stale; run generation without check`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async function analyzeStagedBoundaries(integration, transactionRoot, staged) {
|
|
835
|
+
return await analyzeDistributedSvelteKitBoundaries({
|
|
836
|
+
cwd: integration.cwd,
|
|
837
|
+
routesDir: portablePath(relative(integration.cwd, integration.routesDir)),
|
|
838
|
+
libDir: portablePath(relative(integration.cwd, integration.libDir)),
|
|
839
|
+
aliases: integration.aliases,
|
|
840
|
+
clients: await Promise.all(staged.map(async ({ client, output }) => ({
|
|
841
|
+
module: client.module,
|
|
842
|
+
inventory: await readIslandInventory(transactionRoot, output),
|
|
843
|
+
explicitBoundaries: client.boundaries
|
|
844
|
+
})))
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
async function readIslandInventory(transactionRoot, output) {
|
|
848
|
+
const path = join(output, 'islands.json');
|
|
849
|
+
const metadata = await lstat(path);
|
|
850
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
851
|
+
throw new Error(`Distributed island inventory ${portablePath(relative(transactionRoot, path))} must be a regular file`);
|
|
852
|
+
}
|
|
853
|
+
const canonicalRoot = await realpath(transactionRoot);
|
|
854
|
+
const canonical = await realpath(path);
|
|
855
|
+
if (!isWithin(canonicalRoot, canonical)) {
|
|
856
|
+
throw new Error('Distributed island inventory escaped the project root');
|
|
857
|
+
}
|
|
858
|
+
return JSON.parse(await readFile(canonical, 'utf8'));
|
|
859
|
+
}
|
|
860
|
+
function boundaryPlanSource(plan) {
|
|
861
|
+
return `${JSON.stringify(plan, null, 2)}\n`;
|
|
862
|
+
}
|
|
863
|
+
function boundaryModuleSource(plan) {
|
|
864
|
+
validateDistributedSvelteKitBoundaryPlan(plan, plan.module);
|
|
865
|
+
const occurrences = plan.boundaries.flatMap((boundary) => boundary.islands.map((island) => ({ boundary, island })));
|
|
866
|
+
const artifacts = new Map();
|
|
867
|
+
for (const { island } of occurrences) {
|
|
868
|
+
if (!/^operations\/[A-Za-z0-9._-]+\.ts$/.test(island.modulePath) ||
|
|
869
|
+
!/^[_A-Za-z][_0-9A-Za-z]*$/.test(island.exportName)) {
|
|
870
|
+
throw new Error(`[distributed.island.boundary_plan_invalid] ${island.graphqlSource} has an unsafe generated artifact reference`);
|
|
871
|
+
}
|
|
872
|
+
const key = `${island.modulePath}\u0000${island.exportName}`;
|
|
873
|
+
if (!artifacts.has(key)) {
|
|
874
|
+
artifacts.set(key, Object.freeze({
|
|
875
|
+
alias: `DistributedBoundaryArtifact_${artifacts.size}`,
|
|
876
|
+
module: island.modulePath.slice(0, -3),
|
|
877
|
+
exportName: island.exportName
|
|
878
|
+
}));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const imports = [...artifacts.values()].map(({ alias, module, exportName }) => `import { ${exportName} as ${alias} } from './${module}.js';`);
|
|
882
|
+
const definitions = occurrences.map(({ boundary, island }, index) => {
|
|
883
|
+
const artifact = artifacts.get(`${island.modulePath}\u0000${island.exportName}`);
|
|
884
|
+
const discovery = island.reason === 'static_component_import' ? 'component' : island.reason;
|
|
885
|
+
return [
|
|
886
|
+
`const DistributedBoundaryBinding_${index} = defineDistributedBoundaryBinding(`,
|
|
887
|
+
` ${artifact.alias},`,
|
|
888
|
+
` ${JSON.stringify(island.binding.sources, null, 2)} as const`,
|
|
889
|
+
`);`,
|
|
890
|
+
`const DistributedBoundaryOperation_${index} = defineDistributedBoundaryOperation(`,
|
|
891
|
+
` ${JSON.stringify({
|
|
892
|
+
operation: island.operation,
|
|
893
|
+
route: boundary.route,
|
|
894
|
+
kind: boundary.kind,
|
|
895
|
+
sourcePath: island.graphqlSource,
|
|
896
|
+
discovery
|
|
897
|
+
}, null, 2)} as const,`,
|
|
898
|
+
` ${artifact.alias},`,
|
|
899
|
+
` DistributedBoundaryBinding_${index}`,
|
|
900
|
+
`);`
|
|
901
|
+
].join('\n');
|
|
902
|
+
});
|
|
903
|
+
const operations = occurrences.map((_, index) => ` DistributedBoundaryOperation_${index}`);
|
|
904
|
+
return [
|
|
905
|
+
'/** GENERATED by the Distributed SvelteKit boundary planner. Do not edit. */',
|
|
906
|
+
"import { defineDistributedBoundaryBinding, defineDistributedBoundaryOperation } from '@hops-ops/distributed/sveltekit';",
|
|
907
|
+
...imports,
|
|
908
|
+
'',
|
|
909
|
+
`export const DISTRIBUTED_BOUNDARY_PLAN = ${JSON.stringify(plan, null, 2)} as const;`,
|
|
910
|
+
'',
|
|
911
|
+
...definitions,
|
|
912
|
+
'',
|
|
913
|
+
'/** Executable SSR/browser ownership assembled from the same boundary plan. */',
|
|
914
|
+
`export const DISTRIBUTED_BOUNDARY_OPERATIONS = ${operations.length === 0 ? '[]' : `[\n${operations.join(',\n')}\n]`} as const;`,
|
|
915
|
+
''
|
|
916
|
+
].join('\n');
|
|
917
|
+
}
|
|
918
|
+
async function exposeBoundaryModule(output) {
|
|
919
|
+
const path = join(output, GENERATED_SVELTEKIT_MODULE);
|
|
920
|
+
const source = await readFile(path, 'utf8');
|
|
921
|
+
if (!source.startsWith('/** GENERATED by distributed client. Do not edit. */')) {
|
|
922
|
+
throw new Error('generated SvelteKit entrypoint is missing its ownership marker');
|
|
923
|
+
}
|
|
924
|
+
await writeFile(path, `${source.trimEnd()}\n\nexport { DISTRIBUTED_BOUNDARY_OPERATIONS, DISTRIBUTED_BOUNDARY_PLAN } from './boundaries.js';\n`, 'utf8');
|
|
925
|
+
}
|
|
926
|
+
async function compareGeneratedTrees(actualRoot, expectedRoot, module) {
|
|
927
|
+
const [actual, expected] = await Promise.all([
|
|
928
|
+
readGeneratedTree(actualRoot),
|
|
929
|
+
readGeneratedTree(expectedRoot)
|
|
930
|
+
]);
|
|
931
|
+
const drift = [];
|
|
932
|
+
for (const [path, contents] of expected) {
|
|
933
|
+
const current = actual.get(path);
|
|
934
|
+
if (current === undefined)
|
|
935
|
+
drift.push(`missing ${path}`);
|
|
936
|
+
else if (current !== contents)
|
|
937
|
+
drift.push(`changed ${path}`);
|
|
938
|
+
}
|
|
939
|
+
for (const path of actual.keys()) {
|
|
940
|
+
if (!expected.has(path))
|
|
941
|
+
drift.push(`unexpected ${path}`);
|
|
942
|
+
}
|
|
943
|
+
if (drift.length > 0) {
|
|
944
|
+
throw new Error(`Distributed SvelteKit client ${module} is stale:\n ${drift.sort().join('\n ')}\nrun generation without check`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
async function readGeneratedTree(root) {
|
|
948
|
+
const rootMetadata = await lstat(root);
|
|
949
|
+
if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) {
|
|
950
|
+
throw new Error(`generated output ${root} must be a real directory`);
|
|
951
|
+
}
|
|
952
|
+
const files = new Map();
|
|
953
|
+
const pending = [
|
|
954
|
+
{ absolute: root, relative: '' }
|
|
955
|
+
];
|
|
956
|
+
while (pending.length > 0) {
|
|
957
|
+
const directory = pending.pop();
|
|
958
|
+
for (const entry of await readdir(directory.absolute, { withFileTypes: true })) {
|
|
959
|
+
const relativePath = directory.relative.length === 0
|
|
960
|
+
? entry.name
|
|
961
|
+
: `${directory.relative}/${entry.name}`;
|
|
962
|
+
const absolutePath = join(directory.absolute, entry.name);
|
|
963
|
+
if (entry.isSymbolicLink()) {
|
|
964
|
+
throw new Error(`generated output contains unsupported symlink ${relativePath}`);
|
|
965
|
+
}
|
|
966
|
+
if (entry.isDirectory()) {
|
|
967
|
+
pending.push({ absolute: absolutePath, relative: relativePath });
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (!entry.isFile()) {
|
|
971
|
+
throw new Error(`generated output contains unsupported entry ${relativePath}`);
|
|
972
|
+
}
|
|
973
|
+
files.set(relativePath, await readFile(absolutePath, 'utf8'));
|
|
974
|
+
if (files.size > 8_192) {
|
|
975
|
+
throw new Error('generated output exceeds 8192 files');
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return files;
|
|
980
|
+
}
|
|
981
|
+
async function realDirectoryExists(path) {
|
|
982
|
+
try {
|
|
983
|
+
const metadata = await lstat(path);
|
|
984
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
985
|
+
throw new Error(`Distributed SvelteKit adapter output ${path} must be a real directory`);
|
|
986
|
+
}
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
if (isMissing(error))
|
|
991
|
+
return false;
|
|
992
|
+
throw error;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
738
995
|
async function materializeManifest(integration, client, transaction, index, children, signal) {
|
|
739
996
|
throwIfAborted(signal);
|
|
740
997
|
if (typeof client.manifest === 'string') {
|
|
@@ -778,23 +1035,37 @@ async function validateGeneratedEntrypoint(cwd, output, module) {
|
|
|
778
1035
|
async function commitOutputs(integration, staged, signal) {
|
|
779
1036
|
throwIfAborted(signal);
|
|
780
1037
|
await validateResolvedPaths(integration);
|
|
1038
|
+
const outputs = staged.flatMap((item) => [
|
|
1039
|
+
{
|
|
1040
|
+
target: item.client.out,
|
|
1041
|
+
output: item.output,
|
|
1042
|
+
backup: item.backup,
|
|
1043
|
+
hadOutput: item.hadOutput
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
target: item.client.adapterOut,
|
|
1047
|
+
output: item.adapterOutput,
|
|
1048
|
+
backup: item.adapterBackup,
|
|
1049
|
+
hadOutput: item.hadAdapterOutput
|
|
1050
|
+
}
|
|
1051
|
+
]);
|
|
781
1052
|
const applied = [];
|
|
782
1053
|
try {
|
|
783
|
-
for (const item of
|
|
1054
|
+
for (const item of outputs) {
|
|
784
1055
|
throwIfAborted(signal);
|
|
785
|
-
await mkdir(dirname(item.
|
|
786
|
-
await validateNearestExistingParent(integration.cwd, item.
|
|
787
|
-
if (item.hadOutput && await generatedTreesEqual(item.
|
|
1056
|
+
await mkdir(dirname(item.target), { recursive: true });
|
|
1057
|
+
await validateNearestExistingParent(integration.cwd, item.target);
|
|
1058
|
+
if (item.hadOutput && await generatedTreesEqual(item.target, item.output)) {
|
|
788
1059
|
continue;
|
|
789
1060
|
}
|
|
790
1061
|
if (item.hadOutput)
|
|
791
|
-
await rename(item.
|
|
1062
|
+
await rename(item.target, item.backup);
|
|
792
1063
|
try {
|
|
793
|
-
await rename(item.output, item.
|
|
1064
|
+
await rename(item.output, item.target);
|
|
794
1065
|
}
|
|
795
1066
|
catch (error) {
|
|
796
1067
|
if (item.hadOutput)
|
|
797
|
-
await rename(item.backup, item.
|
|
1068
|
+
await rename(item.backup, item.target);
|
|
798
1069
|
throw error;
|
|
799
1070
|
}
|
|
800
1071
|
applied.push(item);
|
|
@@ -803,9 +1074,9 @@ async function commitOutputs(integration, staged, signal) {
|
|
|
803
1074
|
}
|
|
804
1075
|
catch (error) {
|
|
805
1076
|
for (const item of [...applied].reverse()) {
|
|
806
|
-
await rm(item.
|
|
1077
|
+
await rm(item.target, { recursive: true, force: true });
|
|
807
1078
|
if (item.hadOutput)
|
|
808
|
-
await rename(item.backup, item.
|
|
1079
|
+
await rename(item.backup, item.target);
|
|
809
1080
|
}
|
|
810
1081
|
throw error;
|
|
811
1082
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hops-ops/distributed",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.11.0",
|
|
4
4
|
"description": "Typed GraphQL client, causal replica, command runtime, and framework adapters for Distributed services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"typescript": "5.9.3"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
|
-
"node": ">=20.
|
|
82
|
+
"node": ">=20.3.0"
|
|
83
83
|
},
|
|
84
84
|
"publishConfig": {
|
|
85
85
|
"access": "public"
|