@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.
@@ -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
- /** Explicit `OPERATION=/route` fallbacks. */
32
- routes?: readonly string[];
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 {};
@@ -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 { cp, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
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.DISTRIBUTED_LIFECYCLE_DIR !== undefined;
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
- * `distributed dev` has already staged this generation and owns every
136
- * compiler input through its application watcher. Recompiling here would
137
- * race the API process for Cargo and mutate generated output outside the
138
- * lifecycle transaction. The virtual modules still resolve the staged
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 = integration.clients.flatMap((client) => [
166
- ...client.watchRoots,
167
- ...client.manifestWatchRoots
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 (process.env.DISTRIBUTED_LIFECYCLE_DIR === undefined) {
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 routes = (client.routes ?? []).map((route, routeIndex) => nonempty(route, `${client.module} routes[${routeIndex}]`));
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
- routes: Object.freeze(routes),
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
- if ((!absolute.endsWith('.graphql') && !absolute.endsWith('.gql')) ||
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
- client.out
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 staged) {
1054
+ for (const item of outputs) {
784
1055
  throwIfAborted(signal);
785
- await mkdir(dirname(item.client.out), { recursive: true });
786
- await validateNearestExistingParent(integration.cwd, item.client.out);
787
- if (item.hadOutput && await generatedTreesEqual(item.client.out, item.output)) {
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.client.out, item.backup);
1062
+ await rename(item.target, item.backup);
792
1063
  try {
793
- await rename(item.output, item.client.out);
1064
+ await rename(item.output, item.target);
794
1065
  }
795
1066
  catch (error) {
796
1067
  if (item.hadOutput)
797
- await rename(item.backup, item.client.out);
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.client.out, { recursive: true, force: true });
1077
+ await rm(item.target, { recursive: true, force: true });
807
1078
  if (item.hadOutput)
808
- await rename(item.backup, item.client.out);
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.9.0",
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.0.0"
82
+ "node": ">=20.3.0"
83
83
  },
84
84
  "publishConfig": {
85
85
  "access": "public"