@three-flatland/bake 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +63 -73
- package/dist/cli.js.map +1 -1
- package/dist/devtimeWarn.d.ts +20 -0
- package/dist/devtimeWarn.d.ts.map +1 -0
- package/dist/devtimeWarn.js +36 -16
- package/dist/devtimeWarn.js.map +1 -1
- package/dist/discovery.d.ts +21 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +106 -98
- package/dist/discovery.js.map +1 -1
- package/dist/index.d.ts +6 -162
- package/dist/index.js +6 -16
- package/dist/node.d.ts +8 -35
- package/dist/node.js +7 -8
- package/dist/sidecar.d.ts +52 -0
- package/dist/sidecar.d.ts.map +1 -0
- package/dist/sidecar.js +141 -96
- package/dist/sidecar.js.map +1 -1
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -1
- package/dist/writeSidecar.d.ts +22 -0
- package/dist/writeSidecar.d.ts.map +1 -0
- package/dist/writeSidecar.js +69 -45
- package/dist/writeSidecar.js.map +1 -1
- package/package.json +24 -5
- package/dist/index.js.map +0 -1
- package/dist/node.js.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,162 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* {
|
|
8
|
-
* "flatland": {
|
|
9
|
-
* "bake": [
|
|
10
|
-
* { "name": "font", "description": "Bake SlugFont", "entry": "./dist/cli.js" }
|
|
11
|
-
* ]
|
|
12
|
-
* }
|
|
13
|
-
* }
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* Entry modules must default-export a `Baker`. The legacy `flatland.bakers`
|
|
17
|
-
* shape is still accepted for one release with a deprecation warning.
|
|
18
|
-
*/
|
|
19
|
-
interface Baker {
|
|
20
|
-
/** Subcommand name used on the CLI: `flatland-bake <name> ...` */
|
|
21
|
-
name: string;
|
|
22
|
-
/** One-line description shown by `flatland-bake --list`. */
|
|
23
|
-
description: string;
|
|
24
|
-
/**
|
|
25
|
-
* Run the baker with the CLI args that follow the subcommand.
|
|
26
|
-
* Resolves to an exit code (0 = success).
|
|
27
|
-
*/
|
|
28
|
-
run(args: string[]): Promise<number>;
|
|
29
|
-
/** Optional multiline usage string for `flatland-bake <name> --help`. */
|
|
30
|
-
usage?(): string;
|
|
31
|
-
}
|
|
32
|
-
interface BakerRegistration {
|
|
33
|
-
name: string;
|
|
34
|
-
description: string;
|
|
35
|
-
entry: string;
|
|
36
|
-
/** Package that declared the baker — used in diagnostics. */
|
|
37
|
-
packageName: string;
|
|
38
|
-
/** Absolute path on disk the `entry` resolves to. */
|
|
39
|
-
resolvedEntry: string;
|
|
40
|
-
}
|
|
41
|
-
interface FlatlandManifestEntry {
|
|
42
|
-
name: string;
|
|
43
|
-
description: string;
|
|
44
|
-
entry: string;
|
|
45
|
-
}
|
|
46
|
-
interface FlatlandManifest {
|
|
47
|
-
/** Current registration shape. */
|
|
48
|
-
bake?: FlatlandManifestEntry[];
|
|
49
|
-
/** @deprecated Legacy shape; use `bake` instead. Accepted for one release. */
|
|
50
|
-
bakers?: FlatlandManifestEntry[];
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Shared option interface for every loader that speaks the
|
|
54
|
-
* "try baked sibling first → fall back to in-memory generation" pattern.
|
|
55
|
-
*
|
|
56
|
-
* Loaders extend this with their asset-specific options:
|
|
57
|
-
*
|
|
58
|
-
* ```ts
|
|
59
|
-
* interface MyLoaderOptions extends BakedAssetLoaderOptions {
|
|
60
|
-
* // asset-specific fields
|
|
61
|
-
* }
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
interface BakedAssetLoaderOptions {
|
|
65
|
-
/**
|
|
66
|
-
* Generate this asset's derived data in the browser on every load
|
|
67
|
-
* instead of loading a pre-baked sidecar. The runtime generator
|
|
68
|
-
* becomes the canonical source — no sidecar probe, no devtime "no
|
|
69
|
-
* baked sibling" warning, just a fresh generate on every load.
|
|
70
|
-
*
|
|
71
|
-
* If you ask for the data (e.g. `normals: true`), you always get it.
|
|
72
|
-
* `forceRuntime` chooses *where* the generation happens — browser vs
|
|
73
|
-
* CI — it does not choose whether you get the data. The default path
|
|
74
|
-
* still produces the data on every miss; this flag just commits to
|
|
75
|
-
* "the browser is always where it's produced for this asset."
|
|
76
|
-
*
|
|
77
|
-
* Use when runtime really is the right home for the generation:
|
|
78
|
-
* procedurally varied content, throwaway prototypes, asset bundles
|
|
79
|
-
* where shipping the sidecar isn't worth the bytes. Not a dev-
|
|
80
|
-
* iteration knob — the default path (probe → generate on miss + warn
|
|
81
|
-
* pointing at `flatland-bake`) already handles iteration.
|
|
82
|
-
*
|
|
83
|
-
* Default `false`. Mirrors `SlugFontLoader.forceRuntime` — one flag
|
|
84
|
-
* across every baked-asset loader in the codebase.
|
|
85
|
-
*/
|
|
86
|
-
forceRuntime?: boolean;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Metadata stamped into a baked PNG's `tEXt` chunk under the key
|
|
90
|
-
* `flatland`. Read back by `probeBakedSibling` to validate the baked
|
|
91
|
-
* file still matches the descriptor a consumer is about to use.
|
|
92
|
-
*/
|
|
93
|
-
interface BakedSidecarMetadata {
|
|
94
|
-
/** Content hash of the descriptor that produced this file. */
|
|
95
|
-
hash: string;
|
|
96
|
-
/** Schema version of the metadata format itself. */
|
|
97
|
-
v: 1;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Derive the sibling URL for a baked asset.
|
|
102
|
-
*
|
|
103
|
-
* @example
|
|
104
|
-
* bakedSiblingURL('/sprites/knight.png', '.normal.png')
|
|
105
|
-
* // → '/sprites/knight.normal.png'
|
|
106
|
-
*
|
|
107
|
-
* Query strings and fragments are preserved:
|
|
108
|
-
* bakedSiblingURL('/a.png?v=2', '.normal.png')
|
|
109
|
-
* // → '/a.normal.png?v=2'
|
|
110
|
-
*/
|
|
111
|
-
declare function bakedSiblingURL(sourceURL: string, suffix: string): string;
|
|
112
|
-
/**
|
|
113
|
-
* Stable content hash of any JSON-serializable value. FNV-1a 64-bit over
|
|
114
|
-
* a canonical stringification (sorted keys). Deterministic across
|
|
115
|
-
* browser and node, no deps, sync.
|
|
116
|
-
*
|
|
117
|
-
* Intended for cache invalidation of baked sidecars — not for any
|
|
118
|
-
* cryptographic purpose.
|
|
119
|
-
*/
|
|
120
|
-
declare function hashDescriptor(value: unknown): string;
|
|
121
|
-
/**
|
|
122
|
-
* HEAD-probe a baked sibling URL and, when `expectedHash` is provided,
|
|
123
|
-
* range-fetch the PNG header to verify the tEXt stamp matches.
|
|
124
|
-
*
|
|
125
|
-
* Returns `{ ok: false }` when the sibling is missing or unreachable.
|
|
126
|
-
* Returns `{ ok: true, hashMatches }` otherwise; callers decide whether
|
|
127
|
-
* to use the baked file or re-generate in-memory.
|
|
128
|
-
*/
|
|
129
|
-
declare function probeBakedSibling(url: string, opts?: {
|
|
130
|
-
expectedHash?: string;
|
|
131
|
-
}): Promise<{
|
|
132
|
-
ok: true;
|
|
133
|
-
hashMatches: boolean;
|
|
134
|
-
url: string;
|
|
135
|
-
} | {
|
|
136
|
-
ok: false;
|
|
137
|
-
}>;
|
|
138
|
-
/**
|
|
139
|
-
* Minimal PNG `tEXt` chunk reader. Walks chunks after the 8-byte
|
|
140
|
-
* signature, stopping when it finds one whose keyword matches `key`.
|
|
141
|
-
*
|
|
142
|
-
* Returns the chunk's Latin-1 text value, or `null` when the keyword is
|
|
143
|
-
* absent / the buffer is not a valid PNG.
|
|
144
|
-
*/
|
|
145
|
-
declare function readPngTextChunk(buffer: ArrayBuffer, key: string): string | null;
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Emit a console warning gated on `NODE_ENV !== 'production'`. Deduped
|
|
149
|
-
* per `(category, url)` — the same warning never fires twice.
|
|
150
|
-
*
|
|
151
|
-
* Shared by every sidecar-using loader so warnings surface uniformly
|
|
152
|
-
* across the ecosystem (normals, fonts, atlases, …).
|
|
153
|
-
*
|
|
154
|
-
* @param category short tag identifying what system warned (e.g. 'normal')
|
|
155
|
-
* @param url absolute URL or path of the asset
|
|
156
|
-
* @param message the message shown to the user
|
|
157
|
-
*/
|
|
158
|
-
declare function devtimeWarn(category: string, url: string, message: string): void;
|
|
159
|
-
/** Clear the devtime-warning dedupe cache. Intended for tests. */
|
|
160
|
-
declare function _resetDevtimeWarnings(): void;
|
|
161
|
-
|
|
162
|
-
export { type BakedAssetLoaderOptions, type BakedSidecarMetadata, type Baker, type BakerRegistration, type FlatlandManifest, type FlatlandManifestEntry, _resetDevtimeWarnings, bakedSiblingURL, devtimeWarn, hashDescriptor, probeBakedSibling, readPngTextChunk };
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { _resetDevtimeWarnings, devtimeWarn } from "./devtimeWarn.js";
|
|
4
|
+
import { BakedAssetLoaderOptions, BakedSidecarMetadata, Baker, BakerRegistration, FlatlandManifest, FlatlandManifestEntry } from "./types.js";
|
|
5
|
+
import { bakedSiblingURL, hashDescriptor, probeBakedSibling, readPngTextChunk } from "./sidecar.js";
|
|
6
|
+
export { type BakedAssetLoaderOptions, type BakedSidecarMetadata, type Baker, type BakerRegistration, type FlatlandManifest, type FlatlandManifestEntry, _resetDevtimeWarnings, bakedSiblingURL, devtimeWarn, hashDescriptor, probeBakedSibling, readPngTextChunk };
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { devtimeWarn, _resetDevtimeWarnings } from "./devtimeWarn.js";
|
|
8
|
-
export {
|
|
9
|
-
_resetDevtimeWarnings,
|
|
10
|
-
bakedSiblingURL,
|
|
11
|
-
devtimeWarn,
|
|
12
|
-
hashDescriptor,
|
|
13
|
-
probeBakedSibling,
|
|
14
|
-
readPngTextChunk
|
|
15
|
-
};
|
|
16
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { _resetDevtimeWarnings, devtimeWarn } from "./devtimeWarn.js";
|
|
5
|
+
import { bakedSiblingURL, hashDescriptor, probeBakedSibling, readPngTextChunk } from "./sidecar.js";
|
|
6
|
+
export { _resetDevtimeWarnings, bakedSiblingURL, devtimeWarn, hashDescriptor, probeBakedSibling, readPngTextChunk };
|
package/dist/node.d.ts
CHANGED
|
@@ -1,35 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
* Conflicts (multiple packages registering the same baker name) are reported;
|
|
11
|
-
* the first match wins and the rest are returned as warnings so the caller
|
|
12
|
-
* can decide whether to fail or log.
|
|
13
|
-
*/
|
|
14
|
-
declare function discoverBakers(cwd?: string): {
|
|
15
|
-
bakers: BakerRegistration[];
|
|
16
|
-
conflicts: string[];
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Write a PNG with a flatland metadata `tEXt` chunk stamped in.
|
|
21
|
-
*
|
|
22
|
-
* The stamp lives under keyword `flatland` and carries the descriptor
|
|
23
|
-
* hash so downstream `probeBakedSibling` calls can invalidate the
|
|
24
|
-
* baked file when the descriptor changes.
|
|
25
|
-
*/
|
|
26
|
-
declare function writeSidecarPng(outputPath: string, pixels: Uint8Array, width: number, height: number, metadata: BakedSidecarMetadata): void;
|
|
27
|
-
/**
|
|
28
|
-
* Write a sidecar descriptor JSON file adjacent to the source asset.
|
|
29
|
-
*
|
|
30
|
-
* Same file format `flatland-bake <subcommand> --descriptor` consumes,
|
|
31
|
-
* so the runtime and CLI agree on one shape.
|
|
32
|
-
*/
|
|
33
|
-
declare function writeSidecarJson(outputPath: string, descriptor: unknown): void;
|
|
34
|
-
|
|
35
|
-
export { BakedSidecarMetadata, BakerRegistration, discoverBakers, writeSidecarJson, writeSidecarPng };
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { _resetDevtimeWarnings, devtimeWarn } from "./devtimeWarn.js";
|
|
4
|
+
import { BakedAssetLoaderOptions, BakedSidecarMetadata, Baker, BakerRegistration, FlatlandManifest, FlatlandManifestEntry } from "./types.js";
|
|
5
|
+
import { discoverBakers } from "./discovery.js";
|
|
6
|
+
import { bakedSiblingURL, hashDescriptor, probeBakedSibling, readPngTextChunk } from "./sidecar.js";
|
|
7
|
+
import { writeSidecarJson, writeSidecarPng } from "./writeSidecar.js";
|
|
8
|
+
export { type BakedAssetLoaderOptions, type BakedSidecarMetadata, type Baker, type BakerRegistration, type FlatlandManifest, type FlatlandManifestEntry, _resetDevtimeWarnings, bakedSiblingURL, devtimeWarn, discoverBakers, hashDescriptor, probeBakedSibling, readPngTextChunk, writeSidecarJson, writeSidecarPng };
|
package/dist/node.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
2
4
|
import { discoverBakers } from "./discovery.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
writeSidecarPng
|
|
8
|
-
};
|
|
9
|
-
//# sourceMappingURL=node.js.map
|
|
5
|
+
import { _resetDevtimeWarnings, devtimeWarn } from "./devtimeWarn.js";
|
|
6
|
+
import { bakedSiblingURL, hashDescriptor, probeBakedSibling, readPngTextChunk } from "./sidecar.js";
|
|
7
|
+
import { writeSidecarJson, writeSidecarPng } from "./writeSidecar.js";
|
|
8
|
+
export { _resetDevtimeWarnings, bakedSiblingURL, devtimeWarn, discoverBakers, hashDescriptor, probeBakedSibling, readPngTextChunk, writeSidecarJson, writeSidecarPng };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
//#region src/sidecar.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Derive the sibling URL for a baked asset.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* bakedSiblingURL('/sprites/knight.png', '.normal.png')
|
|
9
|
+
* // → '/sprites/knight.normal.png'
|
|
10
|
+
*
|
|
11
|
+
* Query strings and fragments are preserved:
|
|
12
|
+
* bakedSiblingURL('/a.png?v=2', '.normal.png')
|
|
13
|
+
* // → '/a.normal.png?v=2'
|
|
14
|
+
*/
|
|
15
|
+
declare function bakedSiblingURL(sourceURL: string, suffix: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Stable content hash of any JSON-serializable value. FNV-1a 64-bit over
|
|
18
|
+
* a canonical stringification (sorted keys). Deterministic across
|
|
19
|
+
* browser and node, no deps, sync.
|
|
20
|
+
*
|
|
21
|
+
* Intended for cache invalidation of baked sidecars — not for any
|
|
22
|
+
* cryptographic purpose.
|
|
23
|
+
*/
|
|
24
|
+
declare function hashDescriptor(value: unknown): string;
|
|
25
|
+
/**
|
|
26
|
+
* HEAD-probe a baked sibling URL and, when `expectedHash` is provided,
|
|
27
|
+
* range-fetch the PNG header to verify the tEXt stamp matches.
|
|
28
|
+
*
|
|
29
|
+
* Returns `{ ok: false }` when the sibling is missing or unreachable.
|
|
30
|
+
* Returns `{ ok: true, hashMatches }` otherwise; callers decide whether
|
|
31
|
+
* to use the baked file or re-generate in-memory.
|
|
32
|
+
*/
|
|
33
|
+
declare function probeBakedSibling(url: string, opts?: {
|
|
34
|
+
expectedHash?: string;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
ok: true;
|
|
37
|
+
hashMatches: boolean;
|
|
38
|
+
url: string;
|
|
39
|
+
} | {
|
|
40
|
+
ok: false;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Minimal PNG `tEXt` chunk reader. Walks chunks after the 8-byte
|
|
44
|
+
* signature, stopping when it finds one whose keyword matches `key`.
|
|
45
|
+
*
|
|
46
|
+
* Returns the chunk's Latin-1 text value, or `null` when the keyword is
|
|
47
|
+
* absent / the buffer is not a valid PNG.
|
|
48
|
+
*/
|
|
49
|
+
declare function readPngTextChunk(buffer: ArrayBuffer, key: string): string | null;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { bakedSiblingURL, hashDescriptor, probeBakedSibling, readPngTextChunk };
|
|
52
|
+
//# sourceMappingURL=sidecar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar.d.ts","names":[],"sources":["../src/sidecar.ts"],"mappings":";;;;;;;;;;;;;;iBAagB,gBAAgB,mBAAmB;;;;;;;;;iBAYnC,eAAe;;;;;;;;;iBAkCT,kBACpB,aACA;EAAS;IACR;EAAU;EAAU;EAAsB;;EAAkB;;;;;;;;;iBA2C/C,iBAAiB,QAAQ,aAAa"}
|
package/dist/sidecar.js
CHANGED
|
@@ -1,109 +1,154 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/sidecar.ts
|
|
5
|
+
/**
|
|
6
|
+
* Derive the sibling URL for a baked asset.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* bakedSiblingURL('/sprites/knight.png', '.normal.png')
|
|
10
|
+
* // → '/sprites/knight.normal.png'
|
|
11
|
+
*
|
|
12
|
+
* Query strings and fragments are preserved:
|
|
13
|
+
* bakedSiblingURL('/a.png?v=2', '.normal.png')
|
|
14
|
+
* // → '/a.normal.png?v=2'
|
|
15
|
+
*/
|
|
1
16
|
function bakedSiblingURL(sourceURL, suffix) {
|
|
2
|
-
|
|
17
|
+
return sourceURL.replace(/\.(\w+)($|[?#])/i, `${suffix}$2`);
|
|
3
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Stable content hash of any JSON-serializable value. FNV-1a 64-bit over
|
|
21
|
+
* a canonical stringification (sorted keys). Deterministic across
|
|
22
|
+
* browser and node, no deps, sync.
|
|
23
|
+
*
|
|
24
|
+
* Intended for cache invalidation of baked sidecars — not for any
|
|
25
|
+
* cryptographic purpose.
|
|
26
|
+
*/
|
|
4
27
|
function hashDescriptor(value) {
|
|
5
|
-
|
|
6
|
-
return fnv1a64(canonical);
|
|
28
|
+
return fnv1a64(stableStringify(value));
|
|
7
29
|
}
|
|
8
30
|
function stableStringify(value) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const obj = value;
|
|
14
|
-
const keys = Object.keys(obj).sort();
|
|
15
|
-
return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
31
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
32
|
+
if (Array.isArray(value)) return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
|
|
33
|
+
const obj = value;
|
|
34
|
+
return "{" + Object.keys(obj).sort().map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
16
35
|
}
|
|
17
36
|
function fnv1a64(str) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
let hash = 14695981039346656037n;
|
|
38
|
+
const prime = 1099511628211n;
|
|
39
|
+
const mask = 18446744073709551615n;
|
|
40
|
+
for (let i = 0; i < str.length; i++) {
|
|
41
|
+
hash = hash ^ BigInt(str.charCodeAt(i));
|
|
42
|
+
hash = hash * prime & mask;
|
|
43
|
+
}
|
|
44
|
+
return hash.toString(16).padStart(16, "0");
|
|
26
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* HEAD-probe a baked sibling URL and, when `expectedHash` is provided,
|
|
48
|
+
* range-fetch the PNG header to verify the tEXt stamp matches.
|
|
49
|
+
*
|
|
50
|
+
* Returns `{ ok: false }` when the sibling is missing or unreachable.
|
|
51
|
+
* Returns `{ ok: true, hashMatches }` otherwise; callers decide whether
|
|
52
|
+
* to use the baked file or re-generate in-memory.
|
|
53
|
+
*/
|
|
27
54
|
async function probeBakedSibling(url, opts) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
let head;
|
|
56
|
+
try {
|
|
57
|
+
head = await fetch(url, { method: "HEAD" });
|
|
58
|
+
} catch {
|
|
59
|
+
return { ok: false };
|
|
60
|
+
}
|
|
61
|
+
if (!head.ok) return { ok: false };
|
|
62
|
+
if (opts?.expectedHash === void 0) return {
|
|
63
|
+
ok: true,
|
|
64
|
+
hashMatches: true,
|
|
65
|
+
url
|
|
66
|
+
};
|
|
67
|
+
let header;
|
|
68
|
+
try {
|
|
69
|
+
header = await fetch(url, { headers: { Range: "bytes=0-4095" } });
|
|
70
|
+
} catch {
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
hashMatches: false,
|
|
74
|
+
url
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (!header.ok) return {
|
|
78
|
+
ok: true,
|
|
79
|
+
hashMatches: false,
|
|
80
|
+
url
|
|
81
|
+
};
|
|
82
|
+
const metaJSON = readPngTextChunk(await header.arrayBuffer(), "flatland");
|
|
83
|
+
if (!metaJSON) return {
|
|
84
|
+
ok: true,
|
|
85
|
+
hashMatches: false,
|
|
86
|
+
url
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
hashMatches: JSON.parse(metaJSON).hash === opts.expectedHash,
|
|
92
|
+
url
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
hashMatches: false,
|
|
98
|
+
url
|
|
99
|
+
};
|
|
100
|
+
}
|
|
56
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Minimal PNG `tEXt` chunk reader. Walks chunks after the 8-byte
|
|
104
|
+
* signature, stopping when it finds one whose keyword matches `key`.
|
|
105
|
+
*
|
|
106
|
+
* Returns the chunk's Latin-1 text value, or `null` when the keyword is
|
|
107
|
+
* absent / the buffer is not a valid PNG.
|
|
108
|
+
*/
|
|
57
109
|
function readPngTextChunk(buffer, key) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (type === "IEND") return null;
|
|
99
|
-
offset = dataEnd + 4;
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
110
|
+
if (buffer.byteLength < 8) return null;
|
|
111
|
+
const view = new DataView(buffer);
|
|
112
|
+
const expectedSig = [
|
|
113
|
+
137,
|
|
114
|
+
80,
|
|
115
|
+
78,
|
|
116
|
+
71,
|
|
117
|
+
13,
|
|
118
|
+
10,
|
|
119
|
+
26,
|
|
120
|
+
10
|
|
121
|
+
];
|
|
122
|
+
for (let i = 0; i < 8; i++) if (view.getUint8(i) !== expectedSig[i]) return null;
|
|
123
|
+
let offset = 8;
|
|
124
|
+
while (offset + 8 <= buffer.byteLength) {
|
|
125
|
+
const length = view.getUint32(offset);
|
|
126
|
+
const type = String.fromCharCode(view.getUint8(offset + 4), view.getUint8(offset + 5), view.getUint8(offset + 6), view.getUint8(offset + 7));
|
|
127
|
+
const dataStart = offset + 8;
|
|
128
|
+
const dataEnd = dataStart + length;
|
|
129
|
+
if (dataEnd + 4 > buffer.byteLength) return null;
|
|
130
|
+
if (type === "tEXt") {
|
|
131
|
+
let sepIdx = -1;
|
|
132
|
+
for (let i = dataStart; i < dataEnd; i++) if (view.getUint8(i) === 0) {
|
|
133
|
+
sepIdx = i;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
if (sepIdx > dataStart) {
|
|
137
|
+
let keyword = "";
|
|
138
|
+
for (let i = dataStart; i < sepIdx; i++) keyword += String.fromCharCode(view.getUint8(i));
|
|
139
|
+
if (keyword === key) {
|
|
140
|
+
let value = "";
|
|
141
|
+
for (let i = sepIdx + 1; i < dataEnd; i++) value += String.fromCharCode(view.getUint8(i));
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (type === "IEND") return null;
|
|
147
|
+
offset = dataEnd + 4;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
102
150
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
probeBakedSibling,
|
|
107
|
-
readPngTextChunk
|
|
108
|
-
};
|
|
151
|
+
//#endregion
|
|
152
|
+
export { bakedSiblingURL, hashDescriptor, probeBakedSibling, readPngTextChunk };
|
|
153
|
+
|
|
109
154
|
//# sourceMappingURL=sidecar.js.map
|
package/dist/sidecar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sidecar.ts"],"sourcesContent":["import type { BakedSidecarMetadata } from './types.js'\n\n/**\n * Derive the sibling URL for a baked asset.\n *\n * @example\n * bakedSiblingURL('/sprites/knight.png', '.normal.png')\n * // → '/sprites/knight.normal.png'\n *\n * Query strings and fragments are preserved:\n * bakedSiblingURL('/a.png?v=2', '.normal.png')\n * // → '/a.normal.png?v=2'\n */\nexport function bakedSiblingURL(sourceURL: string, suffix: string): string {\n return sourceURL.replace(/\\.(\\w+)($|[?#])/i, `${suffix}$2`)\n}\n\n/**\n * Stable content hash of any JSON-serializable value. FNV-1a 64-bit over\n * a canonical stringification (sorted keys). Deterministic across\n * browser and node, no deps, sync.\n *\n * Intended for cache invalidation of baked sidecars — not for any\n * cryptographic purpose.\n */\nexport function hashDescriptor(value: unknown): string {\n const canonical = stableStringify(value)\n return fnv1a64(canonical)\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'\n if (Array.isArray(value)) {\n return '[' + value.map((v) => stableStringify(v)).join(',') + ']'\n }\n const obj = value as Record<string, unknown>\n const keys = Object.keys(obj).sort()\n return
|
|
1
|
+
{"version":3,"file":"sidecar.js","names":[],"sources":["../src/sidecar.ts"],"sourcesContent":["import type { BakedSidecarMetadata } from './types.js'\n\n/**\n * Derive the sibling URL for a baked asset.\n *\n * @example\n * bakedSiblingURL('/sprites/knight.png', '.normal.png')\n * // → '/sprites/knight.normal.png'\n *\n * Query strings and fragments are preserved:\n * bakedSiblingURL('/a.png?v=2', '.normal.png')\n * // → '/a.normal.png?v=2'\n */\nexport function bakedSiblingURL(sourceURL: string, suffix: string): string {\n return sourceURL.replace(/\\.(\\w+)($|[?#])/i, `${suffix}$2`)\n}\n\n/**\n * Stable content hash of any JSON-serializable value. FNV-1a 64-bit over\n * a canonical stringification (sorted keys). Deterministic across\n * browser and node, no deps, sync.\n *\n * Intended for cache invalidation of baked sidecars — not for any\n * cryptographic purpose.\n */\nexport function hashDescriptor(value: unknown): string {\n const canonical = stableStringify(value)\n return fnv1a64(canonical)\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'\n if (Array.isArray(value)) {\n return '[' + value.map((v) => stableStringify(v)).join(',') + ']'\n }\n const obj = value as Record<string, unknown>\n const keys = Object.keys(obj).sort()\n return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k])).join(',') + '}'\n}\n\nfunction fnv1a64(str: string): string {\n let hash = 0xcbf29ce484222325n\n const prime = 0x100000001b3n\n const mask = 0xffffffffffffffffn\n for (let i = 0; i < str.length; i++) {\n hash = hash ^ BigInt(str.charCodeAt(i))\n hash = (hash * prime) & mask\n }\n return hash.toString(16).padStart(16, '0')\n}\n\n/**\n * HEAD-probe a baked sibling URL and, when `expectedHash` is provided,\n * range-fetch the PNG header to verify the tEXt stamp matches.\n *\n * Returns `{ ok: false }` when the sibling is missing or unreachable.\n * Returns `{ ok: true, hashMatches }` otherwise; callers decide whether\n * to use the baked file or re-generate in-memory.\n */\nexport async function probeBakedSibling(\n url: string,\n opts?: { expectedHash?: string }\n): Promise<{ ok: true; hashMatches: boolean; url: string } | { ok: false }> {\n let head: Response\n try {\n head = await fetch(url, { method: 'HEAD' })\n } catch {\n return { ok: false }\n }\n if (!head.ok) return { ok: false }\n\n if (opts?.expectedHash === undefined) {\n return { ok: true, hashMatches: true, url }\n }\n\n // Range-fetch the PNG header to read the tEXt stamp. We don't need the\n // full image; the first ~4 KB comfortably covers signature + IHDR +\n // metadata chunks for every baker we emit.\n let header: Response\n try {\n header = await fetch(url, { headers: { Range: 'bytes=0-4095' } })\n } catch {\n return { ok: true, hashMatches: false, url }\n }\n if (!header.ok) {\n return { ok: true, hashMatches: false, url }\n }\n const buf = await header.arrayBuffer()\n const metaJSON = readPngTextChunk(buf, 'flatland')\n if (!metaJSON) return { ok: true, hashMatches: false, url }\n try {\n const meta = JSON.parse(metaJSON) as BakedSidecarMetadata\n return { ok: true, hashMatches: meta.hash === opts.expectedHash, url }\n } catch {\n return { ok: true, hashMatches: false, url }\n }\n}\n\n/**\n * Minimal PNG `tEXt` chunk reader. Walks chunks after the 8-byte\n * signature, stopping when it finds one whose keyword matches `key`.\n *\n * Returns the chunk's Latin-1 text value, or `null` when the keyword is\n * absent / the buffer is not a valid PNG.\n */\nexport function readPngTextChunk(buffer: ArrayBuffer, key: string): string | null {\n if (buffer.byteLength < 8) return null\n const view = new DataView(buffer)\n const expectedSig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]\n for (let i = 0; i < 8; i++) {\n if (view.getUint8(i) !== expectedSig[i]) return null\n }\n\n let offset = 8\n while (offset + 8 <= buffer.byteLength) {\n const length = view.getUint32(offset)\n const type = String.fromCharCode(\n view.getUint8(offset + 4),\n view.getUint8(offset + 5),\n view.getUint8(offset + 6),\n view.getUint8(offset + 7)\n )\n const dataStart = offset + 8\n const dataEnd = dataStart + length\n if (dataEnd + 4 > buffer.byteLength) return null\n\n if (type === 'tEXt') {\n let sepIdx = -1\n for (let i = dataStart; i < dataEnd; i++) {\n if (view.getUint8(i) === 0) {\n sepIdx = i\n break\n }\n }\n if (sepIdx > dataStart) {\n let keyword = ''\n for (let i = dataStart; i < sepIdx; i++) {\n keyword += String.fromCharCode(view.getUint8(i))\n }\n if (keyword === key) {\n let value = ''\n for (let i = sepIdx + 1; i < dataEnd; i++) {\n value += String.fromCharCode(view.getUint8(i))\n }\n return value\n }\n }\n }\n\n if (type === 'IEND') return null\n offset = dataEnd + 4\n }\n return null\n}\n"],"mappings":"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"}
|