every-plugin 3.0.0-rc.14 → 3.0.0-rc.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/artifact-names.cjs +9 -6
- package/dist/build/artifact-names.cjs.map +1 -1
- package/dist/build/artifact-names.d.cts +7 -5
- package/dist/build/artifact-names.d.cts.map +1 -1
- package/dist/build/artifact-names.d.mts +7 -5
- package/dist/build/artifact-names.d.mts.map +1 -1
- package/dist/build/artifact-names.mjs +8 -5
- package/dist/build/artifact-names.mjs.map +1 -1
- package/dist/build/contract-types.cjs +1 -1
- package/dist/build/rspack/build-report-plugin.cjs +1 -1
- package/dist/build/rspack/chunk-completeness-plugin.cjs +1 -1
- package/dist/build/rspack/compose.cjs +3 -3
- package/dist/build/rspack/generated-config.cjs +2 -2
- package/dist/build/rspack/index.cjs +5 -5
- package/dist/build/rspack/module-federation.cjs +1 -1
- package/dist/build/rspack/plugin.cjs +8 -8
- package/dist/build/rspack/utils.cjs +2 -2
- package/dist/build/ui/factory.cjs +3 -3
- package/dist/build/ui/generated-config.cjs +2 -2
- package/dist/build/ui/hash-artifacts-plugin.cjs +2 -2
- package/dist/build/ui/index.cjs +9 -9
- package/dist/build/ui/manifest-plugin.cjs +1 -1
- package/dist/build/ui/rsbuild-config.cjs +10 -10
- package/dist/build/ui/stubs.cjs +2 -7
- package/dist/build/ui/stubs.cjs.map +1 -1
- package/dist/build/ui/stubs.d.cts.map +1 -1
- package/dist/build/ui/stubs.d.mts.map +1 -1
- package/dist/build/ui/stubs.mjs +2 -7
- package/dist/build/ui/stubs.mjs.map +1 -1
- package/dist/cli.cjs +3 -3
- package/dist/client.cjs +8 -4
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +8 -4
- package/dist/client.mjs.map +1 -1
- package/dist/dev/serve.cjs +9 -9
- package/dist/errors.cjs +12 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +15 -1
- package/dist/errors.d.cts.map +1 -1
- package/dist/errors.d.mts +15 -1
- package/dist/errors.d.mts.map +1 -1
- package/dist/errors.mjs +11 -2
- package/dist/errors.mjs.map +1 -1
- package/dist/index.cjs +8 -7
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/plugin.cjs +1 -1
- package/dist/remote-entry.cjs +1 -1
- package/dist/runtime/index.cjs +3 -3
- package/dist/runtime/mf-config.cjs +2 -2
- package/dist/runtime/mf-config.mjs +1 -1
- package/dist/runtime/services/module-federation.service.cjs +5 -5
- package/dist/runtime/services/normalize.cjs +1 -1
- package/dist/runtime/services/plugin-loader.service.cjs +5 -5
- package/dist/runtime/services/plugin.service.cjs +4 -4
- package/dist/runtime/services/secrets.service.cjs +2 -2
- package/dist/runtime/services/shared-identity.cjs +1 -1
- package/dist/runtime/validation.cjs +1 -1
- package/dist/ui/manifest/construct.cjs +18 -43
- package/dist/ui/manifest/construct.cjs.map +1 -1
- package/dist/ui/manifest/construct.d.cts +4 -13
- package/dist/ui/manifest/construct.d.cts.map +1 -1
- package/dist/ui/manifest/construct.d.mts +4 -13
- package/dist/ui/manifest/construct.d.mts.map +1 -1
- package/dist/ui/manifest/construct.mjs +14 -39
- package/dist/ui/manifest/construct.mjs.map +1 -1
- package/dist/ui/manifest/contract.cjs +1 -1
- package/dist/ui/manifest/descriptor.cjs +2 -2
- package/dist/ui/manifest/descriptor.mjs +1 -1
- package/dist/ui/manifest/generator.cjs +6 -6
- package/dist/ui/manifest/index.cjs +7 -7
- package/dist/ui/manifest/index.d.cts +3 -3
- package/dist/ui/manifest/index.d.mts +3 -3
- package/dist/ui/manifest/manifest-schema.cjs +1 -1
- package/dist/ui/manifest/mount-registry.cjs +6 -13
- package/dist/ui/manifest/mount-registry.cjs.map +1 -1
- package/dist/ui/manifest/mount-registry.d.cts +29 -36
- package/dist/ui/manifest/mount-registry.d.cts.map +1 -1
- package/dist/ui/manifest/mount-registry.d.mts +29 -36
- package/dist/ui/manifest/mount-registry.d.mts.map +1 -1
- package/dist/ui/manifest/mount-registry.mjs +6 -13
- package/dist/ui/manifest/mount-registry.mjs.map +1 -1
- package/dist/version-manifest.cjs +25 -0
- package/dist/version-manifest.cjs.map +1 -1
- package/dist/version-manifest.d.cts +13 -1
- package/dist/version-manifest.d.cts.map +1 -1
- package/dist/version-manifest.d.mts +13 -1
- package/dist/version-manifest.d.mts.map +1 -1
- package/dist/version-manifest.mjs +24 -1
- package/dist/version-manifest.mjs.map +1 -1
- package/package.json +28 -28
- package/skills/plugin-client/SKILL.md +2 -2
- package/src/build/artifact-names.ts +8 -5
- package/src/build/ui/stubs.ts +2 -7
- package/src/client.ts +9 -3
- package/src/errors.ts +10 -0
- package/src/ui/manifest/construct.ts +20 -65
- package/src/ui/manifest/index.ts +0 -2
- package/src/ui/manifest/mount-registry.ts +20 -20
- package/src/version-manifest.ts +27 -0
package/src/build/ui/stubs.ts
CHANGED
|
@@ -65,19 +65,14 @@ export type {
|
|
|
65
65
|
ConstructedTree,
|
|
66
66
|
ConstructInput,
|
|
67
67
|
ConstructPluginRef,
|
|
68
|
-
GateUser,
|
|
69
|
-
HostContext,
|
|
70
68
|
NavManifest,
|
|
71
69
|
PluginManifest,
|
|
72
70
|
ResolvedPlugin,
|
|
73
71
|
RouteConfigModule,
|
|
74
72
|
RouteOptionsBundle,
|
|
75
73
|
} from "everything-dev/ui/manifest";
|
|
76
|
-
export {
|
|
77
|
-
|
|
78
|
-
ComposePayloadSchema,
|
|
79
|
-
constructTree,
|
|
80
|
-
} from "everything-dev/ui/manifest";
|
|
74
|
+
export { CORE_UI_PLUGIN_KEY, ComposePayloadSchema } from "everything-dev/ui/manifest";
|
|
75
|
+
export { constructTree } from "everything-dev/ui/compose";
|
|
81
76
|
`,
|
|
82
77
|
"src/globals.gen.ts": `// GENERATED FILE — do not edit. Regenerated from the installed framework version
|
|
83
78
|
// by \`bos dev\` / \`bos build\` / \`bos typecheck\` (gitignored).
|
package/src/client.ts
CHANGED
|
@@ -33,6 +33,14 @@ async function fetchWithRateLimitRetry(
|
|
|
33
33
|
return response;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function mergeHeaders(base: Headers, overrides?: RequestInit["headers"]): Headers {
|
|
37
|
+
const merged = new Headers(base);
|
|
38
|
+
new Headers(overrides).forEach((value, key) => {
|
|
39
|
+
merged.set(key, value);
|
|
40
|
+
});
|
|
41
|
+
return merged;
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
function createRpcLink(config: RpcClientConfig, headers?: Headers) {
|
|
37
45
|
return new RPCLink({
|
|
38
46
|
origin: config.hostUrl,
|
|
@@ -71,9 +79,7 @@ function createRpcLink(config: RpcClientConfig, headers?: Headers) {
|
|
|
71
79
|
return fetchWithRateLimitRetry(fetchUrl, {
|
|
72
80
|
...options,
|
|
73
81
|
credentials: "include",
|
|
74
|
-
headers: headers
|
|
75
|
-
? { ...Object.fromEntries(headers), ...(options?.headers as Record<string, string>) }
|
|
76
|
-
: options?.headers,
|
|
82
|
+
headers: headers ? mergeHeaders(headers, options?.headers) : options?.headers,
|
|
77
83
|
});
|
|
78
84
|
},
|
|
79
85
|
});
|
package/src/errors.ts
CHANGED
|
@@ -68,6 +68,14 @@ export const NOT_FOUND = {
|
|
|
68
68
|
}),
|
|
69
69
|
} as const;
|
|
70
70
|
|
|
71
|
+
export const CONFLICT = {
|
|
72
|
+
status: 409,
|
|
73
|
+
data: z.object({
|
|
74
|
+
resource: z.string().optional(),
|
|
75
|
+
detail: z.string().optional(),
|
|
76
|
+
}),
|
|
77
|
+
} as const;
|
|
78
|
+
|
|
71
79
|
export const FORBIDDEN = {
|
|
72
80
|
status: 403,
|
|
73
81
|
data: z.object({
|
|
@@ -105,6 +113,7 @@ export const PluginErrors = {
|
|
|
105
113
|
SERVICE_UNAVAILABLE,
|
|
106
114
|
BAD_REQUEST,
|
|
107
115
|
NOT_FOUND,
|
|
116
|
+
CONFLICT,
|
|
108
117
|
FORBIDDEN,
|
|
109
118
|
TIMEOUT,
|
|
110
119
|
CONNECTION_ERROR,
|
|
@@ -123,6 +132,7 @@ export const PLUGIN_ERROR_STATUS_MAP: Record<string, number> = {
|
|
|
123
132
|
...COMMON_ERROR_STATUS_MAP,
|
|
124
133
|
TIMEOUT: 504,
|
|
125
134
|
CONNECTION_ERROR: 502,
|
|
135
|
+
CONFLICT: 409,
|
|
126
136
|
} as const;
|
|
127
137
|
|
|
128
138
|
export type { PluginLoadFailureInfo, PluginWarningInfo } from "./runtime/errors";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { AnyRoute } from "@tanstack/react-router";
|
|
2
|
-
import { createRootRoute, createRoute, Outlet
|
|
2
|
+
import { createRootRoute, createRoute, Outlet } from "@tanstack/react-router";
|
|
3
3
|
import { CORE_UI_PLUGIN_KEY } from "./contract";
|
|
4
4
|
import { digestOf } from "./descriptor";
|
|
5
5
|
import type { PluginManifest, RouteRecord } from "./manifest-schema";
|
|
6
|
-
import { MOUNT_REGISTRY, type
|
|
6
|
+
import { MOUNT_REGISTRY, type MountId, resolveMountSegment } from "./mount-registry";
|
|
7
7
|
import { compareNavItems, type NavDeclaration, type NavItem, type NavManifest } from "./nav";
|
|
8
8
|
import { ROUTE_OPTION_KEYS, type RouteConfigModule, type RouteOptionsBundle } from "./route-config";
|
|
9
9
|
|
|
@@ -11,9 +11,10 @@ import { ROUTE_OPTION_KEYS, type RouteConfigModule, type RouteOptionsBundle } fr
|
|
|
11
11
|
* Host-side route-graph construction (ADR 0008 §1): manifests + route
|
|
12
12
|
* configs in → host-built tree out. The host constructs ITS OWN Route
|
|
13
13
|
* objects from validated manifest data via public `createRoute` APIs; no
|
|
14
|
-
* foreign route object is ever mutated.
|
|
15
|
-
*
|
|
16
|
-
* mount's
|
|
14
|
+
* foreign route object is ever mutated. Mount guards are ordinary
|
|
15
|
+
* beforeLoads attached HOST-side from ConstructInput.guards — plugins
|
|
16
|
+
* cannot ship a route that escapes its mount's guard because plugins never
|
|
17
|
+
* construct route objects.
|
|
17
18
|
*
|
|
18
19
|
* Mounts are the ONLY cross-source parenting points: a root-level
|
|
19
20
|
* pathless layout file (`_public.tsx`) declares the mount for the whole
|
|
@@ -34,57 +35,6 @@ function toAnyRoute(route: unknown): AnyRoute {
|
|
|
34
35
|
return route as AnyRoute;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export interface GateUser {
|
|
38
|
-
id: string;
|
|
39
|
-
name?: string;
|
|
40
|
-
/** Better Auth user role — "admin" gates the admin mount. */
|
|
41
|
-
role?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface HostContext {
|
|
45
|
-
user?: GateUser;
|
|
46
|
-
session?: { user?: GateUser };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type GateFn = (args: {
|
|
50
|
-
context: HostContext;
|
|
51
|
-
location: { pathname: string; searchStr: string };
|
|
52
|
-
}) => void;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Gate redirects are absolute: gates attach to root-level mount routes, and
|
|
56
|
-
* relatives would silently shift if a mount ever moved deeper in the tree.
|
|
57
|
-
* The login surface is plugin-owned, never core-known.
|
|
58
|
-
*/
|
|
59
|
-
const LOGIN_TO: string = "/login";
|
|
60
|
-
const HOME_TO: string = "/";
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Gate implementations by mount gate kind. Only `session` and `admin` are
|
|
64
|
-
* implemented (ADR 0008 §3); unimplemented mounts are rejected during mount
|
|
65
|
-
* declaration before any gate lookup, and gates are optional by type — a
|
|
66
|
-
* mount without an entry (or `none`) renders ungated.
|
|
67
|
-
*/
|
|
68
|
-
const GATES: Partial<Record<MountDef["gate"], GateFn>> = {
|
|
69
|
-
session: ({ context, location }) => {
|
|
70
|
-
const user = userOf(context);
|
|
71
|
-
if (!user) {
|
|
72
|
-
throw redirect({
|
|
73
|
-
to: LOGIN_TO,
|
|
74
|
-
search: { redirect: location.pathname + location.searchStr } as never,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
admin: ({ context }) => {
|
|
79
|
-
const user = userOf(context);
|
|
80
|
-
if (!user) throw redirect({ to: LOGIN_TO });
|
|
81
|
-
if (user.role !== "admin") throw redirect({ to: HOME_TO });
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const userOf = (context: HostContext | undefined): GateUser | undefined =>
|
|
86
|
-
context?.user ?? context?.session?.user;
|
|
87
|
-
|
|
88
38
|
export interface ConstructPluginRef {
|
|
89
39
|
/** composition identity — plugin key; participates in the digest */
|
|
90
40
|
key: string;
|
|
@@ -104,6 +54,8 @@ export interface ConstructInput {
|
|
|
104
54
|
resolve: (ref: ConstructPluginRef) => Promise<ResolvedPlugin>;
|
|
105
55
|
/** core ui `__root` options — component/error/notFound + head baseline */
|
|
106
56
|
rootOptions?: RouteOptionsBundle;
|
|
57
|
+
/** Guard per mount id — beforeLoads supplied by the composing framework. */
|
|
58
|
+
guards?: Partial<Record<MountId, RouteOptionsBundle["beforeLoad"]>>;
|
|
107
59
|
}
|
|
108
60
|
|
|
109
61
|
export interface ConstructedTree {
|
|
@@ -139,17 +91,17 @@ const navOf = (options: RouteOptionsBundle): NavDeclaration | undefined => {
|
|
|
139
91
|
};
|
|
140
92
|
|
|
141
93
|
/** Layout routes (mount or pathless) pass their authored options through,
|
|
142
|
-
* like every other route kind. On mount routes the
|
|
94
|
+
* like every other route kind. On mount routes the mount's guard composes
|
|
143
95
|
* BEFORE the declaration's own beforeLoad so a source can never escape its
|
|
144
|
-
* mount's
|
|
96
|
+
* mount's guard (ADR 0008 §3), while the declaration's return value still
|
|
145
97
|
* flows to children as route context. */
|
|
146
98
|
function layoutBeforeLoad(
|
|
147
|
-
|
|
99
|
+
guard: RouteOptionsBundle["beforeLoad"],
|
|
148
100
|
declared: RouteOptionsBundle["beforeLoad"],
|
|
149
101
|
): RouteOptionsBundle["beforeLoad"] {
|
|
150
|
-
if (!
|
|
151
|
-
return (args) => {
|
|
152
|
-
|
|
102
|
+
if (!guard) return declared;
|
|
103
|
+
return async (args) => {
|
|
104
|
+
await guard(args);
|
|
153
105
|
return declared?.(args);
|
|
154
106
|
};
|
|
155
107
|
}
|
|
@@ -270,13 +222,16 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
|
|
|
270
222
|
const declaration = declarations.get(mountId);
|
|
271
223
|
if (!declaration) continue;
|
|
272
224
|
const def = MOUNT_REGISTRY[mountId]!;
|
|
273
|
-
const
|
|
225
|
+
const guard = input.guards?.[mountId];
|
|
226
|
+
if (def.guarded && !guard) {
|
|
227
|
+
throw new Error(`mount "${mountId}" requires a guard — add one to ConstructInput.guards`);
|
|
228
|
+
}
|
|
274
229
|
const options = declaration.options;
|
|
275
230
|
const route = createRoute({
|
|
276
231
|
id: `__mount_${mountId}`,
|
|
277
232
|
getParentRoute: () => rootRoute,
|
|
278
|
-
...(
|
|
279
|
-
? { beforeLoad: layoutBeforeLoad(
|
|
233
|
+
...(guard || options.beforeLoad
|
|
234
|
+
? { beforeLoad: layoutBeforeLoad(guard, options.beforeLoad) }
|
|
280
235
|
: {}),
|
|
281
236
|
...authoredOptions(options),
|
|
282
237
|
component: options.component ?? Outlet,
|
package/src/ui/manifest/index.ts
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mount registry v2 —
|
|
2
|
+
* Mount registry v2 — shape-free (ADR 0008 §3).
|
|
3
3
|
*
|
|
4
|
-
* A mount is a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* construction, so a plugin cannot ship a
|
|
11
|
-
*
|
|
4
|
+
* A mount is a host-owned pathless layout position. It carries no product
|
|
5
|
+
* semantics (no "dashboard", no shell, no chrome) — plugins bring their own
|
|
6
|
+
* layouts and route freely inside their subtree. A mount is declared by a
|
|
7
|
+
* ROOT-level pathless layout file whose `_`-prefixed segment names the mount
|
|
8
|
+
* (`_public.tsx` → `public`); the generator derives it and validates it
|
|
9
|
+
* against this registry; guards are ordinary beforeLoads attached HOST-side
|
|
10
|
+
* at construction from ConstructInput.guards, so a plugin cannot ship a
|
|
11
|
+
* route that escapes its mount's guard (plugins never construct route
|
|
12
|
+
* objects).
|
|
12
13
|
*
|
|
13
|
-
* `org`/`team` are declared vocabulary (parameterized membership
|
|
14
|
+
* `org`/`team` are declared vocabulary (parameterized membership) but
|
|
14
15
|
* unimplemented — construction rejects them until they land.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
export interface MountDef {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
parameterized?: boolean;
|
|
19
|
+
/** The composing framework supplies this mount's guard via ConstructInput.guards. */
|
|
20
|
+
guarded: boolean;
|
|
21
21
|
implemented: boolean;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export const MOUNT_REGISTRY = {
|
|
25
|
-
public: {
|
|
26
|
-
authenticated: {
|
|
27
|
-
admin: {
|
|
28
|
-
org: {
|
|
29
|
-
team: {
|
|
30
|
-
} satisfies Record<string, MountDef>;
|
|
25
|
+
public: { guarded: false, implemented: true },
|
|
26
|
+
authenticated: { guarded: true, implemented: true },
|
|
27
|
+
admin: { guarded: true, implemented: true },
|
|
28
|
+
org: { guarded: true, implemented: false },
|
|
29
|
+
team: { guarded: true, implemented: false },
|
|
30
|
+
} as const satisfies Record<string, MountDef>;
|
|
31
31
|
|
|
32
32
|
export type MountId = Extract<keyof typeof MOUNT_REGISTRY, string>;
|
|
33
33
|
|
|
34
34
|
export const MOUNTS = Object.keys(MOUNT_REGISTRY) as MountId[];
|
|
35
35
|
|
|
36
36
|
/** Bump when the registry shape changes — invalidates all compose digests. */
|
|
37
|
-
export const MOUNT_REGISTRY_VERSION =
|
|
37
|
+
export const MOUNT_REGISTRY_VERSION = 6;
|
|
38
38
|
|
|
39
39
|
/** Migration aliases: legacy `_`-segment names → canonical mounts. */
|
|
40
40
|
export const MOUNT_ALIASES: Record<string, MountId> = {
|
package/src/version-manifest.ts
CHANGED
|
@@ -12,10 +12,24 @@ import { z } from "zod";
|
|
|
12
12
|
|
|
13
13
|
const SRI = z.string().regex(/^sha384-[A-Za-z0-9+/=]+$/);
|
|
14
14
|
|
|
15
|
+
/** Slot-relative directory holding one deploy's immutable file copies (#826). */
|
|
16
|
+
export const VERSION_DIR_PATTERN = /^versions\/[0-9a-f]{16}$/;
|
|
17
|
+
|
|
15
18
|
export const WorkspaceVersionManifestSchema = z.object({
|
|
16
19
|
version: z.string().regex(/^[0-9a-f]{16}$/),
|
|
17
20
|
builtAt: z.string().optional(),
|
|
18
21
|
gitSha: z.string().optional(),
|
|
22
|
+
/**
|
|
23
|
+
* Slot-relative directory (`versions/<dirId>`) holding this deploy's
|
|
24
|
+
* byte-immutable copies of every dist file (#826). When present, all
|
|
25
|
+
* artifact coordinates below resolve against `<slotBase>/<dir>/` — so a
|
|
26
|
+
* pinned child's entry fetches its expose chunk and chunks from the same
|
|
27
|
+
* frozen directory instead of the mutable slot root. Absent on legacy
|
|
28
|
+
* manifests: coordinates resolve against the slot root exactly as before.
|
|
29
|
+
* Derived from the `files` map (see `versionDirPath`), so it participates
|
|
30
|
+
* in the version hash without circularity.
|
|
31
|
+
*/
|
|
32
|
+
dir: z.string().regex(VERSION_DIR_PATTERN).optional(),
|
|
19
33
|
entry: z.string().min(1),
|
|
20
34
|
entryIntegrity: SRI,
|
|
21
35
|
ssr: z
|
|
@@ -67,6 +81,7 @@ export interface VersionManifestInput {
|
|
|
67
81
|
* staged copy and the CDN's immutability for republished versions. */
|
|
68
82
|
builtAt?: string;
|
|
69
83
|
gitSha?: string;
|
|
84
|
+
dir?: string;
|
|
70
85
|
entry: string;
|
|
71
86
|
entryIntegrity: string;
|
|
72
87
|
ssr?: { entry: string; integrity: string };
|
|
@@ -93,6 +108,18 @@ export function composeVersionManifest(input: VersionManifestInput): WorkspaceVe
|
|
|
93
108
|
return WorkspaceVersionManifestSchema.parse({ ...input, version });
|
|
94
109
|
}
|
|
95
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The content-addressed directory name for one dist's file set (#826): a
|
|
113
|
+
* 16-hex hash over the canonical files map. Identical bytes → identical
|
|
114
|
+
* directory → re-deploying unchanged content is a server-side no-op, and a
|
|
115
|
+
* published pin's directory can never be rewritten by a later deploy
|
|
116
|
+
* (the upload service refuses conflicting writes under `versions/`).
|
|
117
|
+
*/
|
|
118
|
+
export function versionDirPath(files: Record<string, string>): string {
|
|
119
|
+
const id = createHash("sha256").update(stableStringify(files)).digest("hex").slice(0, 16);
|
|
120
|
+
return `versions/${id}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
96
123
|
function stableStringify(value: unknown): string {
|
|
97
124
|
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
98
125
|
if (value && typeof value === "object") {
|