@cardanvil/frame-kit 0.2.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/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/authoring/index.d.ts +2 -0
- package/dist/authoring/index.js +2 -0
- package/dist/authoring/layoutDefaults.d.ts +6 -0
- package/dist/authoring/layoutDefaults.js +80 -0
- package/dist/authoring/omit.d.ts +2 -0
- package/dist/authoring/omit.js +14 -0
- package/dist/cli/api.d.ts +34 -0
- package/dist/cli/api.js +188 -0
- package/dist/cli/build.d.ts +36 -0
- package/dist/cli/build.js +222 -0
- package/dist/cli/bundle.d.ts +23 -0
- package/dist/cli/bundle.js +58 -0
- package/dist/cli/devUrl.d.ts +10 -0
- package/dist/cli/devUrl.js +41 -0
- package/dist/cli/discover.d.ts +25 -0
- package/dist/cli/discover.js +116 -0
- package/dist/cli/emitSchemas.d.ts +2 -0
- package/dist/cli/emitSchemas.js +13 -0
- package/dist/cli/errors.d.ts +8 -0
- package/dist/cli/errors.js +11 -0
- package/dist/cli/frameIndex.d.ts +115 -0
- package/dist/cli/frameIndex.js +104 -0
- package/dist/cli/hash.d.ts +14 -0
- package/dist/cli/hash.js +32 -0
- package/dist/cli/imageSize.d.ts +13 -0
- package/dist/cli/imageSize.js +72 -0
- package/dist/cli/index.d.ts +18 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/loadFrame.d.ts +24 -0
- package/dist/cli/loadFrame.js +90 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +228 -0
- package/dist/cli/report.d.ts +35 -0
- package/dist/cli/report.js +74 -0
- package/dist/cli/schema.d.ts +33 -0
- package/dist/cli/schema.js +51 -0
- package/dist/cli/stableJson.d.ts +11 -0
- package/dist/cli/stableJson.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/manifest/contract.d.ts +10503 -0
- package/dist/manifest/contract.js +87 -0
- package/dist/manifest/deserialize.d.ts +19 -0
- package/dist/manifest/deserialize.js +23 -0
- package/dist/manifest/index.d.ts +4 -0
- package/dist/manifest/index.js +4 -0
- package/dist/manifest/serialize.d.ts +35 -0
- package/dist/manifest/serialize.js +44 -0
- package/dist/manifest/testFrame.d.ts +9 -0
- package/dist/manifest/testFrame.js +53 -0
- package/dist/manifest/walk.d.ts +21 -0
- package/dist/manifest/walk.js +111 -0
- package/dist/schema/assetUrl.d.ts +27 -0
- package/dist/schema/assetUrl.js +28 -0
- package/dist/schema/frame.d.ts +26467 -0
- package/dist/schema/frame.js +312 -0
- package/dist/schema/frameAssets.d.ts +539 -0
- package/dist/schema/frameAssets.js +183 -0
- package/dist/schema/frameMeta.d.ts +24 -0
- package/dist/schema/frameMeta.js +48 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/layout.d.ts +101 -0
- package/dist/schema/layout.js +59 -0
- package/package.json +62 -0
- package/schema/frame-index.schema.json +233 -0
- package/schema/frame-meta.schema.json +54 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FrameSchema } from "../schema/frame.js";
|
|
3
|
+
/**
|
|
4
|
+
* Version of the distributable frame format, as `"MAJOR.MINOR"`.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately independent of this package's semver: frame-kit will change for
|
|
7
|
+
* authoring and tooling reasons that do not touch the format on disk.
|
|
8
|
+
*
|
|
9
|
+
* - MAJOR changes when a field is removed, renamed, retyped, or given new
|
|
10
|
+
* required semantics. A loader must refuse a manifest whose major differs.
|
|
11
|
+
* - MINOR changes only ever add optional fields. Older loaders stay compatible
|
|
12
|
+
* because parsing strips unknown keys; they should warn that the frame may
|
|
13
|
+
* render with reduced fidelity.
|
|
14
|
+
*/
|
|
15
|
+
export const CONTRACT_VERSION = "1.0";
|
|
16
|
+
/** The only manifest kind any loader understands today. */
|
|
17
|
+
export const DECLARATIVE_KIND = "declarative";
|
|
18
|
+
export const AssetEntrySchema = z.object({
|
|
19
|
+
/** Path relative to the directory holding `frame.json`. Always POSIX. */
|
|
20
|
+
path: z.string(),
|
|
21
|
+
bytes: z.int().nonnegative().optional(),
|
|
22
|
+
sha256: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
export const FrameManifestSchema = z.object({
|
|
25
|
+
contractVersion: z.string(),
|
|
26
|
+
/**
|
|
27
|
+
* Open on purpose. Future formats (for example a frame that ships code) will
|
|
28
|
+
* introduce new kinds, and a loader must reject those explicitly — see
|
|
29
|
+
* {@link assertDeclarative} — rather than failing schema validation, which
|
|
30
|
+
* would report the wrong problem.
|
|
31
|
+
*/
|
|
32
|
+
kind: z.string(),
|
|
33
|
+
/** Stable identifier, reverse-DNS by convention. */
|
|
34
|
+
id: z.string(),
|
|
35
|
+
/** The frame's own version, independent of `contractVersion`. */
|
|
36
|
+
version: z.string(),
|
|
37
|
+
generator: z.string().optional(),
|
|
38
|
+
author: z.object({ name: z.string(), url: z.string().optional() }).optional(),
|
|
39
|
+
/** The frame itself, with every asset URL rewritten to a relative path. */
|
|
40
|
+
frame: FrameSchema,
|
|
41
|
+
/** Integrity index for the packaged files. Loaders may ignore it. */
|
|
42
|
+
assets: z.array(AssetEntrySchema),
|
|
43
|
+
});
|
|
44
|
+
function parseVersion(version) {
|
|
45
|
+
const match = /^(\d+)\.(\d+)$/.exec(version);
|
|
46
|
+
if (!match?.[1] || !match[2]) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return { major: Number(match[1]), minor: Number(match[2]) };
|
|
50
|
+
}
|
|
51
|
+
/** Whether a manifest's `contractVersion` can be loaded by this build. */
|
|
52
|
+
export function isContractCompatible(contractVersion, loaderVersion = CONTRACT_VERSION) {
|
|
53
|
+
const theirs = parseVersion(contractVersion);
|
|
54
|
+
const ours = parseVersion(loaderVersion);
|
|
55
|
+
if (!theirs || !ours) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
degraded: false,
|
|
59
|
+
reason: `malformed contract version: ${contractVersion}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (theirs.major !== ours.major) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
degraded: false,
|
|
66
|
+
reason: `contract major ${String(theirs.major)} is not supported by this build (expects ${String(ours.major)})`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
degraded: theirs.minor > ours.minor,
|
|
72
|
+
reason: theirs.minor > ours.minor
|
|
73
|
+
? `frame targets contract ${contractVersion}; this build understands ${loaderVersion} and may render it with reduced fidelity`
|
|
74
|
+
: undefined,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Narrows a manifest to one a declarative loader can use.
|
|
79
|
+
*
|
|
80
|
+
* Every loader must call this. A manifest of an unknown kind is a manifest
|
|
81
|
+
* whose `frame` this loader has no business interpreting.
|
|
82
|
+
*/
|
|
83
|
+
export function assertDeclarative(manifest) {
|
|
84
|
+
if (manifest.kind !== DECLARATIVE_KIND) {
|
|
85
|
+
throw new Error(`unsupported frame kind ${JSON.stringify(manifest.kind)}; this build only loads ${JSON.stringify(DECLARATIVE_KIND)} frames`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Frame } from "../schema/frame.js";
|
|
2
|
+
export interface ManifestToFrameOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Turns a package-relative asset path into a URL the renderer can load —
|
|
5
|
+
* an object URL for a zip, an `asset://` URL for a directory on disk, an
|
|
6
|
+
* absolute http(s) URL for a remote package.
|
|
7
|
+
*/
|
|
8
|
+
resolveUrl: (path: string) => string;
|
|
9
|
+
/**
|
|
10
|
+
* Called when the manifest targets a newer minor contract than this build.
|
|
11
|
+
* The frame still loads; some fields may be ignored.
|
|
12
|
+
*/
|
|
13
|
+
onDegraded?: (reason: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validates a manifest and rebuilds a loadable {@link Frame} from it, turning
|
|
17
|
+
* every package-relative asset path back into a URL.
|
|
18
|
+
*/
|
|
19
|
+
export declare function manifestToFrame(manifest: unknown, options: ManifestToFrameOptions): Frame;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { FrameSchema } from "../schema/frame.js";
|
|
2
|
+
import { FrameManifestSchema, assertDeclarative, isContractCompatible, } from "./contract.js";
|
|
3
|
+
import { setAtPath, walkAssets } from "./walk.js";
|
|
4
|
+
/**
|
|
5
|
+
* Validates a manifest and rebuilds a loadable {@link Frame} from it, turning
|
|
6
|
+
* every package-relative asset path back into a URL.
|
|
7
|
+
*/
|
|
8
|
+
export function manifestToFrame(manifest, options) {
|
|
9
|
+
const parsed = FrameManifestSchema.parse(manifest);
|
|
10
|
+
const compatibility = isContractCompatible(parsed.contractVersion);
|
|
11
|
+
if (!compatibility.ok) {
|
|
12
|
+
throw new Error(compatibility.reason ?? "incompatible contract version");
|
|
13
|
+
}
|
|
14
|
+
if (compatibility.degraded && compatibility.reason) {
|
|
15
|
+
options.onDegraded?.(compatibility.reason);
|
|
16
|
+
}
|
|
17
|
+
assertDeclarative(parsed);
|
|
18
|
+
const frame = JSON.parse(JSON.stringify(parsed.frame));
|
|
19
|
+
for (const occurrence of walkAssets(FrameSchema, frame)) {
|
|
20
|
+
setAtPath(frame, occurrence.path, options.resolveUrl(occurrence.url));
|
|
21
|
+
}
|
|
22
|
+
return FrameSchema.parse(frame);
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Frame } from "../schema/frame.js";
|
|
2
|
+
import { type FrameManifest } from "./contract.js";
|
|
3
|
+
/** Where one asset ended up in the package, and what it contains. */
|
|
4
|
+
export interface PackagedAsset {
|
|
5
|
+
/** Path relative to the directory holding `frame.json`. POSIX separators. */
|
|
6
|
+
path: string;
|
|
7
|
+
bytes?: number;
|
|
8
|
+
sha256?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface FrameToManifestOptions {
|
|
11
|
+
id: string;
|
|
12
|
+
version: string;
|
|
13
|
+
generator?: string;
|
|
14
|
+
author?: {
|
|
15
|
+
name: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Maps a build-time asset URL to its packaged location.
|
|
20
|
+
*
|
|
21
|
+
* Supplied by the caller so this module stays free of filesystem access:
|
|
22
|
+
* the packaging CLI hashes and copies files, tests pass an in-memory map.
|
|
23
|
+
* Returning the same path for two URLs is how deduplication happens.
|
|
24
|
+
*/
|
|
25
|
+
resolveAsset: (url: string) => PackagedAsset;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Converts an authored {@link Frame} into a distributable manifest, rewriting
|
|
29
|
+
* every asset URL to a package-relative path.
|
|
30
|
+
*
|
|
31
|
+
* The frame is serialized *after* authoring helpers have run, so a loader never
|
|
32
|
+
* needs them — which is what lets the authoring API evolve without changing the
|
|
33
|
+
* on-disk format.
|
|
34
|
+
*/
|
|
35
|
+
export declare function frameToManifest(frame: Frame, options: FrameToManifestOptions): FrameManifest;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { FrameSchema } from "../schema/frame.js";
|
|
2
|
+
import { CONTRACT_VERSION, DECLARATIVE_KIND, } from "./contract.js";
|
|
3
|
+
import { formatPath, setAtPath, walkAssets } from "./walk.js";
|
|
4
|
+
/**
|
|
5
|
+
* Converts an authored {@link Frame} into a distributable manifest, rewriting
|
|
6
|
+
* every asset URL to a package-relative path.
|
|
7
|
+
*
|
|
8
|
+
* The frame is serialized *after* authoring helpers have run, so a loader never
|
|
9
|
+
* needs them — which is what lets the authoring API evolve without changing the
|
|
10
|
+
* on-disk format.
|
|
11
|
+
*/
|
|
12
|
+
export function frameToManifest(frame, options) {
|
|
13
|
+
// Round-trip through JSON to get a plain, mutable copy. Frame configs wire
|
|
14
|
+
// module namespace objects straight into asset fields (the barrel
|
|
15
|
+
// convention), and those are neither mutable nor plain.
|
|
16
|
+
const plain = JSON.parse(JSON.stringify(frame));
|
|
17
|
+
const assetsByPath = new Map();
|
|
18
|
+
for (const occurrence of walkAssets(FrameSchema, plain)) {
|
|
19
|
+
const packaged = options.resolveAsset(occurrence.url);
|
|
20
|
+
if (!packaged.path) {
|
|
21
|
+
throw new Error(`resolveAsset returned no path for ${occurrence.url} at ${formatPath(occurrence.path)}`);
|
|
22
|
+
}
|
|
23
|
+
setAtPath(plain, occurrence.path, packaged.path);
|
|
24
|
+
assetsByPath.set(packaged.path, {
|
|
25
|
+
path: packaged.path,
|
|
26
|
+
...(packaged.bytes === undefined ? {} : { bytes: packaged.bytes }),
|
|
27
|
+
...(packaged.sha256 === undefined ? {} : { sha256: packaged.sha256 }),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const rewritten = FrameSchema.parse(plain);
|
|
31
|
+
return {
|
|
32
|
+
contractVersion: CONTRACT_VERSION,
|
|
33
|
+
kind: DECLARATIVE_KIND,
|
|
34
|
+
id: options.id,
|
|
35
|
+
version: options.version,
|
|
36
|
+
...(options.generator === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { generator: options.generator }),
|
|
39
|
+
...(options.author === undefined ? {} : { author: options.author }),
|
|
40
|
+
frame: rewritten,
|
|
41
|
+
// Sorted so the manifest is byte-stable across builds and diffs cleanly.
|
|
42
|
+
assets: [...assetsByPath.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Frame } from "../schema/frame.js";
|
|
2
|
+
/**
|
|
3
|
+
* A small but structurally realistic frame for tests: assets in a color set, a
|
|
4
|
+
* nested mask, a preview image, and styling strings that must NOT be treated as
|
|
5
|
+
* assets. `base.w` and `base.u` deliberately share one URL so deduplication is
|
|
6
|
+
* exercised.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SHARED_URL = "/src/frames/shared-border.png";
|
|
9
|
+
export declare const testFrame: Frame;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small but structurally realistic frame for tests: assets in a color set, a
|
|
3
|
+
* nested mask, a preview image, and styling strings that must NOT be treated as
|
|
4
|
+
* assets. `base.w` and `base.u` deliberately share one URL so deduplication is
|
|
5
|
+
* exercised.
|
|
6
|
+
*/
|
|
7
|
+
export const SHARED_URL = "/src/frames/shared-border.png";
|
|
8
|
+
export const testFrame = {
|
|
9
|
+
name: "Test Frame",
|
|
10
|
+
description: "Fixture",
|
|
11
|
+
previewImage: "/src/frames/preview.jpg",
|
|
12
|
+
tags: ["test"],
|
|
13
|
+
templateSettings: {
|
|
14
|
+
useNyxBorder: {
|
|
15
|
+
type: "boolean",
|
|
16
|
+
label: "Use Nyx Border",
|
|
17
|
+
defaultValue: true,
|
|
18
|
+
},
|
|
19
|
+
style: {
|
|
20
|
+
type: "select",
|
|
21
|
+
label: "Style",
|
|
22
|
+
defaultValue: "classic",
|
|
23
|
+
options: ["classic", "modern"],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
config: {
|
|
27
|
+
layouts: {
|
|
28
|
+
normal: {
|
|
29
|
+
boxes: {
|
|
30
|
+
art: { x: 0, y: 0, width: 100, height: 100 },
|
|
31
|
+
mana: { x: 0, y: 0, width: 10, height: 10, fontSize: 12 },
|
|
32
|
+
title: {
|
|
33
|
+
x: 0,
|
|
34
|
+
y: 0,
|
|
35
|
+
width: 10,
|
|
36
|
+
height: 10,
|
|
37
|
+
fontSize: 12,
|
|
38
|
+
color: "white",
|
|
39
|
+
outlineColor: "black",
|
|
40
|
+
fontFamily: "Beleren",
|
|
41
|
+
},
|
|
42
|
+
type: { x: 0, y: 0, width: 10, height: 10, fontSize: 12 },
|
|
43
|
+
setSymbol: { x: 0, y: 0, width: 10, height: 10 },
|
|
44
|
+
},
|
|
45
|
+
frameAssets: {
|
|
46
|
+
base: { w: SHARED_URL, u: SHARED_URL, b: "/src/frames/black.png" },
|
|
47
|
+
black: "/src/frames/crown-bar.png",
|
|
48
|
+
},
|
|
49
|
+
masks: { pinlines: "/src/frames/pinlines.png" },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Location of a value inside a frame, as a property path. */
|
|
3
|
+
export type FramePath = readonly (string | number)[];
|
|
4
|
+
export interface AssetOccurrence {
|
|
5
|
+
readonly path: FramePath;
|
|
6
|
+
readonly url: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Finds every asset URL in `value` by walking `schema` alongside it.
|
|
10
|
+
*
|
|
11
|
+
* Driven by the schema rather than by a hard-coded list of field paths, so a
|
|
12
|
+
* new asset field in the contract is picked up automatically — see
|
|
13
|
+
* {@link isAssetUrlSchema}.
|
|
14
|
+
*/
|
|
15
|
+
export declare function walkAssets(schema: z.ZodType, value: unknown): AssetOccurrence[];
|
|
16
|
+
/** Reads the value at `path`, or `undefined` if any segment is missing. */
|
|
17
|
+
export declare function getAtPath(root: unknown, path: FramePath): unknown;
|
|
18
|
+
/** Writes `value` at `path`. Every parent segment must already exist. */
|
|
19
|
+
export declare function setAtPath(root: unknown, path: FramePath, value: unknown): void;
|
|
20
|
+
/** Human-readable rendering of a path, for error messages. */
|
|
21
|
+
export declare function formatPath(path: FramePath): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { isAssetUrlSchema } from "../schema/assetUrl.js";
|
|
2
|
+
function definition(schema) {
|
|
3
|
+
return schema.def;
|
|
4
|
+
}
|
|
5
|
+
function isPlainRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function visit(schema, value, path, out) {
|
|
9
|
+
const node = definition(schema);
|
|
10
|
+
switch (node.type) {
|
|
11
|
+
// Wrappers: same value, one level in.
|
|
12
|
+
case "optional":
|
|
13
|
+
case "nullable":
|
|
14
|
+
case "default":
|
|
15
|
+
case "prefault":
|
|
16
|
+
case "readonly":
|
|
17
|
+
case "nonoptional": {
|
|
18
|
+
if (node.innerType) {
|
|
19
|
+
visit(node.innerType, value, path, out);
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
case "object": {
|
|
24
|
+
if (!isPlainRecord(value) || !node.shape) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const [key, child] of Object.entries(node.shape)) {
|
|
28
|
+
if (key in value) {
|
|
29
|
+
visit(child, value[key], [...path, key], out);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
case "record": {
|
|
35
|
+
const { valueType } = node;
|
|
36
|
+
if (!isPlainRecord(value) || !valueType) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const [key, child] of Object.entries(value)) {
|
|
40
|
+
visit(valueType, child, [...path, key], out);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
case "array": {
|
|
45
|
+
const { element } = node;
|
|
46
|
+
if (!Array.isArray(value) || !element) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
value.forEach((item, index) => {
|
|
50
|
+
visit(element, item, [...path, index], out);
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
case "union": {
|
|
55
|
+
// Follow only the branch that actually accepts this value, so a union
|
|
56
|
+
// never reports assets belonging to a variant the value is not.
|
|
57
|
+
const match = node.options?.find((option) => option.safeParse(value).success);
|
|
58
|
+
if (match) {
|
|
59
|
+
visit(match, value, path, out);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
default: {
|
|
64
|
+
if (typeof value === "string" && isAssetUrlSchema(schema)) {
|
|
65
|
+
out.push({ path, url: value });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Finds every asset URL in `value` by walking `schema` alongside it.
|
|
72
|
+
*
|
|
73
|
+
* Driven by the schema rather than by a hard-coded list of field paths, so a
|
|
74
|
+
* new asset field in the contract is picked up automatically — see
|
|
75
|
+
* {@link isAssetUrlSchema}.
|
|
76
|
+
*/
|
|
77
|
+
export function walkAssets(schema, value) {
|
|
78
|
+
const found = [];
|
|
79
|
+
visit(schema, value, [], found);
|
|
80
|
+
return found;
|
|
81
|
+
}
|
|
82
|
+
/** Reads the value at `path`, or `undefined` if any segment is missing. */
|
|
83
|
+
export function getAtPath(root, path) {
|
|
84
|
+
let current = root;
|
|
85
|
+
for (const segment of path) {
|
|
86
|
+
if (!isPlainRecord(current) && !Array.isArray(current)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
current = current[segment];
|
|
90
|
+
}
|
|
91
|
+
return current;
|
|
92
|
+
}
|
|
93
|
+
/** Writes `value` at `path`. Every parent segment must already exist. */
|
|
94
|
+
export function setAtPath(root, path, value) {
|
|
95
|
+
if (path.length === 0) {
|
|
96
|
+
throw new Error("setAtPath: empty path");
|
|
97
|
+
}
|
|
98
|
+
const parent = getAtPath(root, path.slice(0, -1));
|
|
99
|
+
if (!isPlainRecord(parent) && !Array.isArray(parent)) {
|
|
100
|
+
throw new Error(`setAtPath: no container at ${formatPath(path)}`);
|
|
101
|
+
}
|
|
102
|
+
const last = path[path.length - 1];
|
|
103
|
+
if (last === undefined) {
|
|
104
|
+
throw new Error("setAtPath: empty path segment");
|
|
105
|
+
}
|
|
106
|
+
parent[last] = value;
|
|
107
|
+
}
|
|
108
|
+
/** Human-readable rendering of a path, for error messages. */
|
|
109
|
+
export function formatPath(path) {
|
|
110
|
+
return path.length === 0 ? "<root>" : path.join(".");
|
|
111
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Metadata key marking a schema position that holds an image URL rather than
|
|
4
|
+
* an arbitrary string. Frame configs assign these from bundler asset imports
|
|
5
|
+
* (`import w from "./w.png"`), so at runtime they are ordinary strings and the
|
|
6
|
+
* inferred type stays `string`.
|
|
7
|
+
*
|
|
8
|
+
* The marker exists so tooling can find every asset in a `Frame` by walking the
|
|
9
|
+
* schema instead of hard-coding a list of field paths that would silently rot
|
|
10
|
+
* as the contract grows. Packaging a frame for distribution rewrites exactly
|
|
11
|
+
* these positions to package-relative paths.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ASSET_URL_META_KEY = "cardAnvilAsset";
|
|
14
|
+
/**
|
|
15
|
+
* An image URL. Identical to `z.string()` at both the type and validation
|
|
16
|
+
* level — the only difference is the metadata marker described above.
|
|
17
|
+
*/
|
|
18
|
+
export declare const AssetUrlSchema: z.ZodString;
|
|
19
|
+
export type AssetUrl = z.infer<typeof AssetUrlSchema>;
|
|
20
|
+
/**
|
|
21
|
+
* Whether a schema position was declared as an asset URL.
|
|
22
|
+
*
|
|
23
|
+
* Typed on the core schema interface so it also accepts the results of
|
|
24
|
+
* unwrapping (`ZodOptional.unwrap()`), which is how callers reach the declared
|
|
25
|
+
* schema behind an optional field.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isAssetUrlSchema(schema: z.core.$ZodType): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Metadata key marking a schema position that holds an image URL rather than
|
|
4
|
+
* an arbitrary string. Frame configs assign these from bundler asset imports
|
|
5
|
+
* (`import w from "./w.png"`), so at runtime they are ordinary strings and the
|
|
6
|
+
* inferred type stays `string`.
|
|
7
|
+
*
|
|
8
|
+
* The marker exists so tooling can find every asset in a `Frame` by walking the
|
|
9
|
+
* schema instead of hard-coding a list of field paths that would silently rot
|
|
10
|
+
* as the contract grows. Packaging a frame for distribution rewrites exactly
|
|
11
|
+
* these positions to package-relative paths.
|
|
12
|
+
*/
|
|
13
|
+
export const ASSET_URL_META_KEY = "cardAnvilAsset";
|
|
14
|
+
/**
|
|
15
|
+
* An image URL. Identical to `z.string()` at both the type and validation
|
|
16
|
+
* level — the only difference is the metadata marker described above.
|
|
17
|
+
*/
|
|
18
|
+
export const AssetUrlSchema = z.string().meta({ [ASSET_URL_META_KEY]: true });
|
|
19
|
+
/**
|
|
20
|
+
* Whether a schema position was declared as an asset URL.
|
|
21
|
+
*
|
|
22
|
+
* Typed on the core schema interface so it also accepts the results of
|
|
23
|
+
* unwrapping (`ZodOptional.unwrap()`), which is how callers reach the declared
|
|
24
|
+
* schema behind an optional field.
|
|
25
|
+
*/
|
|
26
|
+
export function isAssetUrlSchema(schema) {
|
|
27
|
+
return z.globalRegistry.get(schema)?.[ASSET_URL_META_KEY] === true;
|
|
28
|
+
}
|