@three-flatland/bake 0.1.0-alpha.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Justin Walsh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/thejustinwalsh/three-flatland/main/assets/repo-banner.png" alt="three-flatland" width="100%" />
3
+ </p>
4
+
5
+ # @three-flatland/bake
6
+
7
+ Shared bake pipeline infrastructure for [three-flatland](https://www.npmjs.com/package/three-flatland) and Three.js WebGPU. Provides the `flatland-bake` CLI dispatcher, browser-safe loader utilities for assets with offline-baked siblings, and the baker discovery mechanism.
8
+
9
+ > **Alpha Release** — this package is in active development. The API will evolve and breaking changes are expected between releases. Pin your version and check the [changelog](https://github.com/thejustinwalsh/three-flatland/releases) before upgrading.
10
+
11
+ [![npm](https://img.shields.io/npm/v/@three-flatland/bake)](https://www.npmjs.com/package/@three-flatland/bake)
12
+ [![license](https://img.shields.io/npm/l/@three-flatland/bake)](https://github.com/thejustinwalsh/three-flatland/blob/main/LICENSE)
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @three-flatland/bake
18
+ ```
19
+
20
+ Install alongside any package that contributes a baker — e.g. [`@three-flatland/normals`](https://www.npmjs.com/package/@three-flatland/normals) for sprite normal maps.
21
+
22
+ ## Quick Start
23
+
24
+ ### Run a registered baker
25
+
26
+ ```bash
27
+ npx flatland-bake normal public/sprites/knight.png
28
+ npx flatland-bake --list
29
+ ```
30
+
31
+ The CLI walks `node_modules` and picks up any package that declares a `flatland.bake` manifest in its `package.json`. Install a baker package and its subcommand appears in `--list` automatically — no registration step.
32
+
33
+ ### Use the browser-safe utilities
34
+
35
+ Loaders that follow the "try baked sibling → fall back to in-memory" pattern share a small utility surface from the default entry. Importable from any environment (browser, node, workers):
36
+
37
+ ```typescript
38
+ import {
39
+ bakedSiblingURL,
40
+ probeBakedSibling,
41
+ hashDescriptor,
42
+ devtimeWarn,
43
+ type BakedAssetLoaderOptions,
44
+ } from '@three-flatland/bake'
45
+ ```
46
+
47
+ Node-only code (CLI, discovery, sidecar file writers) lives under the `/node` subpath:
48
+
49
+ ```typescript
50
+ import { discoverBakers, writeSidecarPng, writeSidecarJson } from '@three-flatland/bake/node'
51
+ ```
52
+
53
+ ## Authoring a Baker
54
+
55
+ Packages contribute bakers via a `flatland.bake` manifest plus a default-exported `Baker`:
56
+
57
+ ```jsonc
58
+ // package.json
59
+ {
60
+ "flatland": {
61
+ "bake": [
62
+ {
63
+ "name": "normal",
64
+ "description": "Bake a tangent-space normal map from a sprite PNG",
65
+ "entry": "./dist/cli.js"
66
+ }
67
+ ]
68
+ }
69
+ }
70
+ ```
71
+
72
+ ```typescript
73
+ // src/cli.ts
74
+ import type { Baker } from '@three-flatland/bake'
75
+
76
+ const baker: Baker = {
77
+ name: 'normal',
78
+ description: 'Bake a tangent-space normal map from a sprite PNG',
79
+ run: async (args) => {
80
+ // your CLI logic here — return 0 for success
81
+ return 0
82
+ },
83
+ }
84
+ export default baker
85
+ ```
86
+
87
+ [`@three-flatland/normals`](https://github.com/thejustinwalsh/three-flatland/tree/main/packages/normals) is a reference implementation.
88
+
89
+ ## Options
90
+
91
+ ### CLI
92
+
93
+ ```
94
+ flatland-bake <name> [args...] Run a registered baker
95
+ flatland-bake --list List registered bakers
96
+ flatland-bake --help Show usage
97
+ ```
98
+
99
+ ### Shared loader option
100
+
101
+ Every loader that speaks the baked-sibling pattern extends `BakedAssetLoaderOptions`, which adds a single flag:
102
+
103
+ ```typescript
104
+ interface BakedAssetLoaderOptions {
105
+ /** Generate this asset's derived data in the browser on every load
106
+ * instead of loading a pre-baked sidecar. */
107
+ forceRuntime?: boolean
108
+ }
109
+ ```
110
+
111
+ `forceRuntime: true` declares **the browser is where this asset's derived data is produced** — not the CI bake step. You always get the data either way (if you ask for it, you get it); this flag just chooses where the generation runs. Use it for assets that are procedurally varied, throwaway prototypes, or asset bundles where shipping the sidecar isn't worth the bytes. Suppresses the "no baked sibling" warn because the missing sidecar is the architecture, not a mistake.
112
+
113
+ Not a dev-iteration knob — the default path (probe → generate on miss + warn pointing at `flatland-bake`) already handles iteration. Mirrors `SlugFontLoader.forceRuntime`; one name across every baked-asset loader in the ecosystem.
114
+
115
+ ## Using with plain Three.js
116
+
117
+ `@three-flatland/bake` is renderer-agnostic. Run the CLI from any project's `npm scripts` to bake assets, or import the browser-safe helpers to write your own sidecar-aware loaders — no three-flatland dependency required.
118
+
119
+ ## Related
120
+
121
+ - **[three-flatland](https://www.npmjs.com/package/three-flatland)** — the 2D engine. High-level loaders consume this package's helpers internally.
122
+ - **[@three-flatland/normals](https://www.npmjs.com/package/@three-flatland/normals)** — reference baker: sprite / tileset normal map generation.
123
+
124
+ ## Documentation
125
+
126
+ Full docs, interactive examples, and API reference at **[thejustinwalsh.com/three-flatland](https://thejustinwalsh.com/three-flatland/)**
127
+
128
+ ## License
129
+
130
+ [MIT](./LICENSE)
131
+
132
+ ---
133
+
134
+ <sub>This README was created with AI assistance. AI can make mistakes — please verify claims and test code examples. Submit corrections [here](https://github.com/thejustinwalsh/three-flatland/issues).</sub>
package/dist/cli.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
3
+ import { existsSync } from "node:fs";
4
+ import { discoverBakers } from "./discovery.js";
5
+ const USAGE = `flatland-bake \u2014 unified bake entry point
6
+
7
+ Usage:
8
+ flatland-bake <name> [args...] Run a registered baker
9
+ flatland-bake --list List registered bakers
10
+ flatland-bake --help Show this usage
11
+
12
+ Bakers are contributed by packages via a \`flatland.bake\` field in
13
+ package.json. Install a package that provides one (e.g. @three-flatland/slug)
14
+ and its subcommand will appear in --list.`;
15
+ async function main(argv) {
16
+ const [first, ...rest] = argv;
17
+ if (!first || first === "--help" || first === "-h") {
18
+ process.stdout.write(USAGE + "\n");
19
+ return 0;
20
+ }
21
+ const { bakers, conflicts } = discoverBakers();
22
+ for (const warning of conflicts) {
23
+ process.stderr.write(`[flatland-bake] warn: ${warning}
24
+ `);
25
+ }
26
+ if (first === "--list") {
27
+ return printList(bakers);
28
+ }
29
+ const match = bakers.find((b) => b.name === first);
30
+ if (!match) {
31
+ process.stderr.write(
32
+ `[flatland-bake] unknown baker "${first}". Run \`flatland-bake --list\` to see what's available.
33
+ `
34
+ );
35
+ return 1;
36
+ }
37
+ const baker = await loadBaker(match);
38
+ if (!baker) return 1;
39
+ try {
40
+ return await baker.run(rest);
41
+ } catch (err) {
42
+ process.stderr.write(
43
+ `[flatland-bake] baker "${match.name}" threw: ${err instanceof Error ? err.message : String(err)}
44
+ `
45
+ );
46
+ return 1;
47
+ }
48
+ }
49
+ function printList(bakers) {
50
+ if (bakers.length === 0) {
51
+ process.stdout.write(
52
+ "No bakers registered. Install a package that contributes one (e.g. @three-flatland/slug).\n"
53
+ );
54
+ return 0;
55
+ }
56
+ const nameWidth = Math.max(...bakers.map((b) => b.name.length));
57
+ process.stdout.write("Registered bakers:\n");
58
+ for (const b of bakers) {
59
+ const pad = " ".repeat(nameWidth - b.name.length);
60
+ process.stdout.write(` ${b.name}${pad} ${b.description} (${b.packageName})
61
+ `);
62
+ }
63
+ return 0;
64
+ }
65
+ async function loadBaker(reg) {
66
+ if (!existsSync(reg.resolvedEntry)) {
67
+ process.stderr.write(
68
+ `[flatland-bake] baker "${reg.name}" from "${reg.packageName}" points to missing entry: ${reg.resolvedEntry}
69
+ `
70
+ );
71
+ return null;
72
+ }
73
+ const mod = await import(pathToFileURL(reg.resolvedEntry).href);
74
+ const baker = mod.default;
75
+ if (!baker || typeof baker.run !== "function") {
76
+ process.stderr.write(
77
+ `[flatland-bake] baker "${reg.name}" from "${reg.packageName}" did not default-export a Baker.
78
+ `
79
+ );
80
+ return null;
81
+ }
82
+ return baker;
83
+ }
84
+ main(process.argv.slice(2)).then(
85
+ (code) => process.exit(code),
86
+ (err) => {
87
+ process.stderr.write(`[flatland-bake] fatal: ${String(err)}
88
+ `);
89
+ process.exit(1);
90
+ }
91
+ );
92
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `flatland-bake` — the unified bake entry point. Discovers bakers\n * contributed by installed packages (via a `flatland.bake` field in\n * their package.json), then dispatches `flatland-bake <name> [args]` to\n * the matching baker's `run()`. `--list` enumerates available bakers and\n * `--help` prints usage. See {@link discoverBakers} and the {@link Baker}\n * contract for how a package registers a subcommand.\n */\nimport { pathToFileURL } from 'node:url'\nimport { existsSync } from 'node:fs'\nimport { discoverBakers } from './discovery.js'\nimport type { Baker, BakerRegistration } from './types.js'\n\nconst USAGE = `flatland-bake — unified bake entry point\n\nUsage:\n flatland-bake <name> [args...] Run a registered baker\n flatland-bake --list List registered bakers\n flatland-bake --help Show this usage\n\nBakers are contributed by packages via a \\`flatland.bake\\` field in\npackage.json. Install a package that provides one (e.g. @three-flatland/slug)\nand its subcommand will appear in --list.`\n\nasync function main(argv: string[]): Promise<number> {\n const [first, ...rest] = argv\n\n if (!first || first === '--help' || first === '-h') {\n process.stdout.write(USAGE + '\\n')\n return 0\n }\n\n const { bakers, conflicts } = discoverBakers()\n for (const warning of conflicts) {\n process.stderr.write(`[flatland-bake] warn: ${warning}\\n`)\n }\n\n if (first === '--list') {\n return printList(bakers)\n }\n\n const match = bakers.find((b) => b.name === first)\n if (!match) {\n process.stderr.write(\n `[flatland-bake] unknown baker \"${first}\". Run \\`flatland-bake --list\\` to see what's available.\\n`\n )\n return 1\n }\n\n const baker = await loadBaker(match)\n if (!baker) return 1\n\n try {\n return await baker.run(rest)\n } catch (err) {\n process.stderr.write(\n `[flatland-bake] baker \"${match.name}\" threw: ${err instanceof Error ? err.message : String(err)}\\n`\n )\n return 1\n }\n}\n\nfunction printList(bakers: BakerRegistration[]): number {\n if (bakers.length === 0) {\n process.stdout.write(\n 'No bakers registered. Install a package that contributes one (e.g. @three-flatland/slug).\\n'\n )\n return 0\n }\n\n const nameWidth = Math.max(...bakers.map((b) => b.name.length))\n process.stdout.write('Registered bakers:\\n')\n for (const b of bakers) {\n const pad = ' '.repeat(nameWidth - b.name.length)\n process.stdout.write(` ${b.name}${pad} ${b.description} (${b.packageName})\\n`)\n }\n return 0\n}\n\nasync function loadBaker(reg: BakerRegistration): Promise<Baker | null> {\n if (!existsSync(reg.resolvedEntry)) {\n process.stderr.write(\n `[flatland-bake] baker \"${reg.name}\" from \"${reg.packageName}\" points to missing entry: ${reg.resolvedEntry}\\n`\n )\n return null\n }\n\n const mod = (await import(pathToFileURL(reg.resolvedEntry).href)) as {\n default?: Baker\n }\n const baker = mod.default\n if (!baker || typeof baker.run !== 'function') {\n process.stderr.write(\n `[flatland-bake] baker \"${reg.name}\" from \"${reg.packageName}\" did not default-export a Baker.\\n`\n )\n return null\n }\n return baker\n}\n\nmain(process.argv.slice(2)).then(\n (code) => process.exit(code),\n (err) => {\n process.stderr.write(`[flatland-bake] fatal: ${String(err)}\\n`)\n process.exit(1)\n }\n)\n"],"mappings":";AASA,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAG/B,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWd,eAAe,KAAK,MAAiC;AACnD,QAAM,CAAC,OAAO,GAAG,IAAI,IAAI;AAEzB,MAAI,CAAC,SAAS,UAAU,YAAY,UAAU,MAAM;AAClD,YAAQ,OAAO,MAAM,QAAQ,IAAI;AACjC,WAAO;AAAA,EACT;AAEA,QAAM,EAAE,QAAQ,UAAU,IAAI,eAAe;AAC7C,aAAW,WAAW,WAAW;AAC/B,YAAQ,OAAO,MAAM,yBAAyB,OAAO;AAAA,CAAI;AAAA,EAC3D;AAEA,MAAI,UAAU,UAAU;AACtB,WAAO,UAAU,MAAM;AAAA,EACzB;AAEA,QAAM,QAAQ,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK;AACjD,MAAI,CAAC,OAAO;AACV,YAAQ,OAAO;AAAA,MACb,kCAAkC,KAAK;AAAA;AAAA,IACzC;AACA,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,MAAM,UAAU,KAAK;AACnC,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI;AACF,WAAO,MAAM,MAAM,IAAI,IAAI;AAAA,EAC7B,SAAS,KAAK;AACZ,YAAQ,OAAO;AAAA,MACb,0BAA0B,MAAM,IAAI,YAAY,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,IAClG;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,UAAU,QAAqC;AACtD,MAAI,OAAO,WAAW,GAAG;AACvB,YAAQ,OAAO;AAAA,MACb;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,KAAK,IAAI,GAAG,OAAO,IAAI,CAAC,MAAM,EAAE,KAAK,MAAM,CAAC;AAC9D,UAAQ,OAAO,MAAM,sBAAsB;AAC3C,aAAW,KAAK,QAAQ;AACtB,UAAM,MAAM,IAAI,OAAO,YAAY,EAAE,KAAK,MAAM;AAChD,YAAQ,OAAO,MAAM,KAAK,EAAE,IAAI,GAAG,GAAG,KAAK,EAAE,WAAW,MAAM,EAAE,WAAW;AAAA,CAAK;AAAA,EAClF;AACA,SAAO;AACT;AAEA,eAAe,UAAU,KAA+C;AACtE,MAAI,CAAC,WAAW,IAAI,aAAa,GAAG;AAClC,YAAQ,OAAO;AAAA,MACb,0BAA0B,IAAI,IAAI,WAAW,IAAI,WAAW,8BAA8B,IAAI,aAAa;AAAA;AAAA,IAC7G;AACA,WAAO;AAAA,EACT;AAEA,QAAM,MAAO,MAAM,OAAO,cAAc,IAAI,aAAa,EAAE;AAG3D,QAAM,QAAQ,IAAI;AAClB,MAAI,CAAC,SAAS,OAAO,MAAM,QAAQ,YAAY;AAC7C,YAAQ,OAAO;AAAA,MACb,0BAA0B,IAAI,IAAI,WAAW,IAAI,WAAW;AAAA;AAAA,IAC9D;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,EAAE;AAAA,EAC1B,CAAC,SAAS,QAAQ,KAAK,IAAI;AAAA,EAC3B,CAAC,QAAQ;AACP,YAAQ,OAAO,MAAM,0BAA0B,OAAO,GAAG,CAAC;AAAA,CAAI;AAC9D,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
@@ -0,0 +1,24 @@
1
+ const _warned = /* @__PURE__ */ new Map();
2
+ function devtimeWarn(category, url, message) {
3
+ if (isProduction()) return;
4
+ let set = _warned.get(category);
5
+ if (!set) {
6
+ set = /* @__PURE__ */ new Set();
7
+ _warned.set(category, set);
8
+ }
9
+ if (set.has(url)) return;
10
+ set.add(url);
11
+ console.warn(`[${category}] ${message}`);
12
+ }
13
+ function _resetDevtimeWarnings() {
14
+ _warned.clear();
15
+ }
16
+ function isProduction() {
17
+ const proc = globalThis.process;
18
+ return proc?.env?.["NODE_ENV"] === "production";
19
+ }
20
+ export {
21
+ _resetDevtimeWarnings,
22
+ devtimeWarn
23
+ };
24
+ //# sourceMappingURL=devtimeWarn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/devtimeWarn.ts"],"sourcesContent":["const _warned = new Map<string, Set<string>>()\n\n/**\n * Emit a console warning gated on `NODE_ENV !== 'production'`. Deduped\n * per `(category, url)` — the same warning never fires twice.\n *\n * Shared by every sidecar-using loader so warnings surface uniformly\n * across the ecosystem (normals, fonts, atlases, …).\n *\n * @param category short tag identifying what system warned (e.g. 'normal')\n * @param url absolute URL or path of the asset\n * @param message the message shown to the user\n */\nexport function devtimeWarn(category: string, url: string, message: string): void {\n if (isProduction()) return\n let set = _warned.get(category)\n if (!set) {\n set = new Set()\n _warned.set(category, set)\n }\n if (set.has(url)) return\n set.add(url)\n console.warn(`[${category}] ${message}`)\n}\n\n/** Clear the devtime-warning dedupe cache. Intended for tests. */\nexport function _resetDevtimeWarnings(): void {\n _warned.clear()\n}\n\n/**\n * Type-agnostic NODE_ENV check. Reads `globalThis.process.env.NODE_ENV`\n * without forcing consumers to depend on `@types/node` — browser-only\n * packages (e.g. mini-breakout, example apps) inline-consume this\n * module through the source export and would otherwise hit TS2591.\n */\nfunction isProduction(): boolean {\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n return proc?.env?.['NODE_ENV'] === 'production'\n}\n"],"mappings":"AAAA,MAAM,UAAU,oBAAI,IAAyB;AAatC,SAAS,YAAY,UAAkB,KAAa,SAAuB;AAChF,MAAI,aAAa,EAAG;AACpB,MAAI,MAAM,QAAQ,IAAI,QAAQ;AAC9B,MAAI,CAAC,KAAK;AACR,UAAM,oBAAI,IAAI;AACd,YAAQ,IAAI,UAAU,GAAG;AAAA,EAC3B;AACA,MAAI,IAAI,IAAI,GAAG,EAAG;AAClB,MAAI,IAAI,GAAG;AACX,UAAQ,KAAK,IAAI,QAAQ,KAAK,OAAO,EAAE;AACzC;AAGO,SAAS,wBAA8B;AAC5C,UAAQ,MAAM;AAChB;AAQA,SAAS,eAAwB;AAC/B,QAAM,OAAQ,WAA0E;AACxF,SAAO,MAAM,MAAM,UAAU,MAAM;AACrC;","names":[]}
@@ -0,0 +1,111 @@
1
+ import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ function discoverBakers(cwd = process.cwd()) {
4
+ const seenPackages = /* @__PURE__ */ new Set();
5
+ const bakers = /* @__PURE__ */ new Map();
6
+ const conflicts = [];
7
+ const selfDir = findPackageRoot(cwd);
8
+ if (selfDir) {
9
+ readPackage(selfDir, bakers, conflicts, seenPackages);
10
+ }
11
+ for (const nodeModulesDir of findNodeModulesDirs(cwd)) {
12
+ scanNodeModules(nodeModulesDir, bakers, conflicts, seenPackages);
13
+ }
14
+ return { bakers: Array.from(bakers.values()), conflicts };
15
+ }
16
+ function findPackageRoot(start) {
17
+ let dir = resolve(start);
18
+ while (true) {
19
+ if (existsSync(join(dir, "package.json"))) return dir;
20
+ const parent = resolve(dir, "..");
21
+ if (parent === dir) return null;
22
+ dir = parent;
23
+ }
24
+ }
25
+ function findNodeModulesDirs(cwd) {
26
+ const dirs = [];
27
+ let dir = resolve(cwd);
28
+ while (true) {
29
+ const nm = join(dir, "node_modules");
30
+ if (existsSync(nm) && statSync(nm).isDirectory()) {
31
+ dirs.push(nm);
32
+ }
33
+ const parent = resolve(dir, "..");
34
+ if (parent === dir) break;
35
+ dir = parent;
36
+ }
37
+ return dirs;
38
+ }
39
+ function scanNodeModules(nodeModulesDir, bakers, conflicts, seenPackages) {
40
+ let entries;
41
+ try {
42
+ entries = readdirSync(nodeModulesDir);
43
+ } catch {
44
+ return;
45
+ }
46
+ for (const entry of entries) {
47
+ if (entry.startsWith(".")) continue;
48
+ const entryPath = join(nodeModulesDir, entry);
49
+ if (entry.startsWith("@")) {
50
+ let scoped;
51
+ try {
52
+ scoped = readdirSync(entryPath);
53
+ } catch {
54
+ continue;
55
+ }
56
+ for (const scopedName of scoped) {
57
+ readPackage(join(entryPath, scopedName), bakers, conflicts, seenPackages);
58
+ }
59
+ } else {
60
+ readPackage(entryPath, bakers, conflicts, seenPackages);
61
+ }
62
+ }
63
+ }
64
+ function readPackage(packageDir, bakers, conflicts, seenPackages) {
65
+ const pkgPath = join(packageDir, "package.json");
66
+ if (!existsSync(pkgPath)) return;
67
+ let pkg;
68
+ try {
69
+ pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
70
+ } catch {
71
+ return;
72
+ }
73
+ const name = pkg.name ?? packageDir;
74
+ if (seenPackages.has(name)) return;
75
+ seenPackages.add(name);
76
+ const manifest = resolveManifest(pkg.flatland, name, conflicts);
77
+ if (!manifest || manifest.length === 0) return;
78
+ for (const decl of manifest) {
79
+ const resolvedEntry = resolve(packageDir, decl.entry);
80
+ const registration = {
81
+ name: decl.name,
82
+ description: decl.description,
83
+ entry: decl.entry,
84
+ packageName: name,
85
+ resolvedEntry
86
+ };
87
+ const existing = bakers.get(decl.name);
88
+ if (existing) {
89
+ conflicts.push(
90
+ `baker "${decl.name}" is registered by both "${existing.packageName}" and "${name}" \u2014 using "${existing.packageName}"`
91
+ );
92
+ continue;
93
+ }
94
+ bakers.set(decl.name, registration);
95
+ }
96
+ }
97
+ function resolveManifest(manifest, packageName, conflicts) {
98
+ if (!manifest) return void 0;
99
+ if (manifest.bake && manifest.bake.length > 0) return manifest.bake;
100
+ if (manifest.bakers && manifest.bakers.length > 0) {
101
+ conflicts.push(
102
+ `"${packageName}" uses deprecated \`flatland.bakers\` \u2014 rename to \`flatland.bake\` (the legacy key is accepted for one release)`
103
+ );
104
+ return manifest.bakers;
105
+ }
106
+ return void 0;
107
+ }
108
+ export {
109
+ discoverBakers
110
+ };
111
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/discovery.ts"],"sourcesContent":["import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport type {\n BakerRegistration,\n FlatlandManifest,\n FlatlandManifestEntry,\n} from './types.js'\n\ninterface PackageJson {\n name?: string\n flatland?: FlatlandManifest\n}\n\n/**\n * Discover registered bakers by walking `node_modules` near the current\n * working directory. Supports both flat and pnpm-symlinked layouts: every\n * `package.json` that declares a `flatland.bake` (or legacy\n * `flatland.bakers`) field is picked up.\n *\n * Conflicts (multiple packages registering the same baker name) are reported;\n * the first match wins and the rest are returned as warnings so the caller\n * can decide whether to fail or log.\n */\nexport function discoverBakers(cwd: string = process.cwd()): {\n bakers: BakerRegistration[]\n conflicts: string[]\n} {\n const seenPackages = new Set<string>()\n const bakers = new Map<string, BakerRegistration>()\n const conflicts: string[] = []\n\n // CWD-package self-discovery: if the user is iterating inside a package\n // that declares a baker, let them invoke it without symlinking into\n // node_modules first. Registers the baker before node_modules scans so a\n // local version always wins against an older installed copy.\n const selfDir = findPackageRoot(cwd)\n if (selfDir) {\n readPackage(selfDir, bakers, conflicts, seenPackages)\n }\n\n for (const nodeModulesDir of findNodeModulesDirs(cwd)) {\n scanNodeModules(nodeModulesDir, bakers, conflicts, seenPackages)\n }\n\n return { bakers: Array.from(bakers.values()), conflicts }\n}\n\n/** Walk upward from `start` until a directory with package.json is found. */\nfunction findPackageRoot(start: string): string | null {\n let dir = resolve(start)\n while (true) {\n if (existsSync(join(dir, 'package.json'))) return dir\n const parent = resolve(dir, '..')\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/** Walk upward from `cwd` collecting each `node_modules` directory we find. */\nfunction findNodeModulesDirs(cwd: string): string[] {\n const dirs: string[] = []\n let dir = resolve(cwd)\n while (true) {\n const nm = join(dir, 'node_modules')\n if (existsSync(nm) && statSync(nm).isDirectory()) {\n dirs.push(nm)\n }\n const parent = resolve(dir, '..')\n if (parent === dir) break\n dir = parent\n }\n return dirs\n}\n\nfunction scanNodeModules(\n nodeModulesDir: string,\n bakers: Map<string, BakerRegistration>,\n conflicts: string[],\n seenPackages: Set<string>\n): void {\n let entries: string[]\n try {\n entries = readdirSync(nodeModulesDir)\n } catch {\n return\n }\n\n for (const entry of entries) {\n if (entry.startsWith('.')) continue\n const entryPath = join(nodeModulesDir, entry)\n\n if (entry.startsWith('@')) {\n // Scoped packages: @scope/name\n let scoped: string[]\n try {\n scoped = readdirSync(entryPath)\n } catch {\n continue\n }\n for (const scopedName of scoped) {\n readPackage(join(entryPath, scopedName), bakers, conflicts, seenPackages)\n }\n } else {\n readPackage(entryPath, bakers, conflicts, seenPackages)\n }\n }\n}\n\nfunction readPackage(\n packageDir: string,\n bakers: Map<string, BakerRegistration>,\n conflicts: string[],\n seenPackages: Set<string>\n): void {\n const pkgPath = join(packageDir, 'package.json')\n if (!existsSync(pkgPath)) return\n\n let pkg: PackageJson\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as PackageJson\n } catch {\n return\n }\n\n const name = pkg.name ?? packageDir\n if (seenPackages.has(name)) return\n seenPackages.add(name)\n\n const manifest = resolveManifest(pkg.flatland, name, conflicts)\n if (!manifest || manifest.length === 0) return\n\n for (const decl of manifest) {\n const resolvedEntry = resolve(packageDir, decl.entry)\n const registration: BakerRegistration = {\n name: decl.name,\n description: decl.description,\n entry: decl.entry,\n packageName: name,\n resolvedEntry,\n }\n\n const existing = bakers.get(decl.name)\n if (existing) {\n conflicts.push(\n `baker \"${decl.name}\" is registered by both \"${existing.packageName}\" and \"${name}\" — using \"${existing.packageName}\"`\n )\n continue\n }\n bakers.set(decl.name, registration)\n }\n}\n\n/**\n * Prefer `flatland.bake` (current); fall back to `flatland.bakers`\n * (legacy) with a one-time deprecation warning per package.\n */\nfunction resolveManifest(\n manifest: FlatlandManifest | undefined,\n packageName: string,\n conflicts: string[]\n): FlatlandManifestEntry[] | undefined {\n if (!manifest) return undefined\n if (manifest.bake && manifest.bake.length > 0) return manifest.bake\n if (manifest.bakers && manifest.bakers.length > 0) {\n conflicts.push(\n `\"${packageName}\" uses deprecated \\`flatland.bakers\\` — rename to \\`flatland.bake\\` (the legacy key is accepted for one release)`\n )\n return manifest.bakers\n }\n return undefined\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Contract every baker must satisfy.
3
+ *
4
+ * Bakers are registered by package.json via a `flatland.bake` field:
5
+ *
6
+ * ```json
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 };
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import {
2
+ bakedSiblingURL,
3
+ probeBakedSibling,
4
+ readPngTextChunk,
5
+ hashDescriptor
6
+ } from "./sidecar.js";
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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Browser-safe surface of @three-flatland/bake.\n//\n// Types + runtime helpers usable from any environment (browser, node,\n// workers, deno). No `node:*` imports, no filesystem access.\n//\n// Consumers running in node that need the CLI, discovery, or sidecar\n// emission should import from '@three-flatland/bake/node' instead.\nexport type {\n Baker,\n BakerRegistration,\n FlatlandManifest,\n FlatlandManifestEntry,\n BakedAssetLoaderOptions,\n BakedSidecarMetadata,\n} from './types.js'\n\nexport {\n bakedSiblingURL,\n probeBakedSibling,\n readPngTextChunk,\n hashDescriptor,\n} from './sidecar.js'\n\nexport { devtimeWarn, _resetDevtimeWarnings } from './devtimeWarn.js'\n"],"mappings":"AAgBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,aAAa,6BAA6B;","names":[]}
package/dist/node.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { BakerRegistration, BakedSidecarMetadata } from './index.js';
2
+ export { BakedAssetLoaderOptions, Baker, FlatlandManifest, FlatlandManifestEntry, _resetDevtimeWarnings, bakedSiblingURL, devtimeWarn, hashDescriptor, probeBakedSibling, readPngTextChunk } from './index.js';
3
+
4
+ /**
5
+ * Discover registered bakers by walking `node_modules` near the current
6
+ * working directory. Supports both flat and pnpm-symlinked layouts: every
7
+ * `package.json` that declares a `flatland.bake` (or legacy
8
+ * `flatland.bakers`) field is picked up.
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 };
package/dist/node.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./index.js";
2
+ import { discoverBakers } from "./discovery.js";
3
+ import { writeSidecarPng, writeSidecarJson } from "./writeSidecar.js";
4
+ export {
5
+ discoverBakers,
6
+ writeSidecarJson,
7
+ writeSidecarPng
8
+ };
9
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/node.ts"],"sourcesContent":["// Node-only surface of @three-flatland/bake.\n//\n// Re-exports the browser-safe surface plus filesystem helpers and the\n// manifest discovery used by the `flatland-bake` CLI. Importing this\n// entry pulls in `node:fs`, `node:path`, and `pngjs` — avoid it from\n// bundles targeting the browser.\n\nexport * from './index.js'\nexport { discoverBakers } from './discovery.js'\nexport { writeSidecarPng, writeSidecarJson } from './writeSidecar.js'\n"],"mappings":"AAOA,cAAc;AACd,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB,wBAAwB;","names":[]}
@@ -0,0 +1,109 @@
1
+ function bakedSiblingURL(sourceURL, suffix) {
2
+ return sourceURL.replace(/\.(\w+)($|[?#])/i, `${suffix}$2`);
3
+ }
4
+ function hashDescriptor(value) {
5
+ const canonical = stableStringify(value);
6
+ return fnv1a64(canonical);
7
+ }
8
+ function stableStringify(value) {
9
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
10
+ if (Array.isArray(value)) {
11
+ return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
12
+ }
13
+ const obj = value;
14
+ const keys = Object.keys(obj).sort();
15
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
16
+ }
17
+ function fnv1a64(str) {
18
+ let hash = 0xcbf29ce484222325n;
19
+ const prime = 0x100000001b3n;
20
+ const mask = 0xffffffffffffffffn;
21
+ for (let i = 0; i < str.length; i++) {
22
+ hash = hash ^ BigInt(str.charCodeAt(i));
23
+ hash = hash * prime & mask;
24
+ }
25
+ return hash.toString(16).padStart(16, "0");
26
+ }
27
+ async function probeBakedSibling(url, opts) {
28
+ let head;
29
+ try {
30
+ head = await fetch(url, { method: "HEAD" });
31
+ } catch {
32
+ return { ok: false };
33
+ }
34
+ if (!head.ok) return { ok: false };
35
+ if (opts?.expectedHash === void 0) {
36
+ return { ok: true, hashMatches: true, url };
37
+ }
38
+ let header;
39
+ try {
40
+ header = await fetch(url, { headers: { Range: "bytes=0-4095" } });
41
+ } catch {
42
+ return { ok: true, hashMatches: false, url };
43
+ }
44
+ if (!header.ok) {
45
+ return { ok: true, hashMatches: false, url };
46
+ }
47
+ const buf = await header.arrayBuffer();
48
+ const metaJSON = readPngTextChunk(buf, "flatland");
49
+ if (!metaJSON) return { ok: true, hashMatches: false, url };
50
+ try {
51
+ const meta = JSON.parse(metaJSON);
52
+ return { ok: true, hashMatches: meta.hash === opts.expectedHash, url };
53
+ } catch {
54
+ return { ok: true, hashMatches: false, url };
55
+ }
56
+ }
57
+ function readPngTextChunk(buffer, key) {
58
+ if (buffer.byteLength < 8) return null;
59
+ const view = new DataView(buffer);
60
+ const expectedSig = [137, 80, 78, 71, 13, 10, 26, 10];
61
+ for (let i = 0; i < 8; i++) {
62
+ if (view.getUint8(i) !== expectedSig[i]) return null;
63
+ }
64
+ let offset = 8;
65
+ while (offset + 8 <= buffer.byteLength) {
66
+ const length = view.getUint32(offset);
67
+ const type = String.fromCharCode(
68
+ view.getUint8(offset + 4),
69
+ view.getUint8(offset + 5),
70
+ view.getUint8(offset + 6),
71
+ view.getUint8(offset + 7)
72
+ );
73
+ const dataStart = offset + 8;
74
+ const dataEnd = dataStart + length;
75
+ if (dataEnd + 4 > buffer.byteLength) return null;
76
+ if (type === "tEXt") {
77
+ let sepIdx = -1;
78
+ for (let i = dataStart; i < dataEnd; i++) {
79
+ if (view.getUint8(i) === 0) {
80
+ sepIdx = i;
81
+ break;
82
+ }
83
+ }
84
+ if (sepIdx > dataStart) {
85
+ let keyword = "";
86
+ for (let i = dataStart; i < sepIdx; i++) {
87
+ keyword += String.fromCharCode(view.getUint8(i));
88
+ }
89
+ if (keyword === key) {
90
+ let value = "";
91
+ for (let i = sepIdx + 1; i < dataEnd; i++) {
92
+ value += String.fromCharCode(view.getUint8(i));
93
+ }
94
+ return value;
95
+ }
96
+ }
97
+ }
98
+ if (type === "IEND") return null;
99
+ offset = dataEnd + 4;
100
+ }
101
+ return null;
102
+ }
103
+ export {
104
+ bakedSiblingURL,
105
+ hashDescriptor,
106
+ probeBakedSibling,
107
+ readPngTextChunk
108
+ };
109
+ //# sourceMappingURL=sidecar.js.map
@@ -0,0 +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 (\n '{' +\n keys.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k])).join(',') +\n '}'\n )\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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","names":[]}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,60 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { Buffer } from "node:buffer";
3
+ import { PNG } from "pngjs";
4
+ function writeSidecarPng(outputPath, pixels, width, height, metadata) {
5
+ const png = new PNG({ width, height });
6
+ png.data = Buffer.from(pixels.buffer, pixels.byteOffset, pixels.byteLength);
7
+ const buffer = PNG.sync.write(png);
8
+ const stamped = injectTextChunk(buffer, "flatland", JSON.stringify(metadata));
9
+ writeFileSync(outputPath, stamped);
10
+ }
11
+ function writeSidecarJson(outputPath, descriptor) {
12
+ writeFileSync(outputPath, JSON.stringify(descriptor, null, 2) + "\n");
13
+ }
14
+ function injectTextChunk(pngBuffer, keyword, value) {
15
+ const sigLen = 8;
16
+ if (pngBuffer[sigLen + 4] !== 73 || pngBuffer[sigLen + 5] !== 72 || pngBuffer[sigLen + 6] !== 68 || pngBuffer[sigLen + 7] !== 82) {
17
+ return pngBuffer;
18
+ }
19
+ const ihdrLength = pngBuffer.readUInt32BE(sigLen);
20
+ const ihdrEnd = sigLen + 8 + ihdrLength + 4;
21
+ const textData = Buffer.concat([
22
+ Buffer.from(keyword, "latin1"),
23
+ Buffer.from([0]),
24
+ Buffer.from(value, "latin1")
25
+ ]);
26
+ const textType = Buffer.from("tEXt", "latin1");
27
+ const textLen = Buffer.alloc(4);
28
+ textLen.writeUInt32BE(textData.length, 0);
29
+ const textCrc = Buffer.alloc(4);
30
+ textCrc.writeUInt32BE(crc32(Buffer.concat([textType, textData])), 0);
31
+ const textChunk = Buffer.concat([textLen, textType, textData, textCrc]);
32
+ return Buffer.concat([pngBuffer.subarray(0, ihdrEnd), textChunk, pngBuffer.subarray(ihdrEnd)]);
33
+ }
34
+ let _crcTable = null;
35
+ function crcTable() {
36
+ if (_crcTable) return _crcTable;
37
+ const table = new Uint32Array(256);
38
+ for (let n = 0; n < 256; n++) {
39
+ let c = n;
40
+ for (let k = 0; k < 8; k++) {
41
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
42
+ }
43
+ table[n] = c >>> 0;
44
+ }
45
+ _crcTable = table;
46
+ return table;
47
+ }
48
+ function crc32(buffer) {
49
+ const table = crcTable();
50
+ let crc = 4294967295;
51
+ for (let i = 0; i < buffer.length; i++) {
52
+ crc = (table[(crc ^ buffer[i]) & 255] ^ crc >>> 8) >>> 0;
53
+ }
54
+ return (crc ^ 4294967295) >>> 0;
55
+ }
56
+ export {
57
+ writeSidecarJson,
58
+ writeSidecarPng
59
+ };
60
+ //# sourceMappingURL=writeSidecar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/writeSidecar.ts"],"sourcesContent":["import { writeFileSync } from 'node:fs'\nimport { Buffer } from 'node:buffer'\nimport { PNG } from 'pngjs'\nimport type { BakedSidecarMetadata } from './types.js'\n\n/**\n * Write a PNG with a flatland metadata `tEXt` chunk stamped in.\n *\n * The stamp lives under keyword `flatland` and carries the descriptor\n * hash so downstream `probeBakedSibling` calls can invalidate the\n * baked file when the descriptor changes.\n */\nexport function writeSidecarPng(\n outputPath: string,\n pixels: Uint8Array,\n width: number,\n height: number,\n metadata: BakedSidecarMetadata\n): void {\n const png = new PNG({ width, height })\n png.data = Buffer.from(pixels.buffer, pixels.byteOffset, pixels.byteLength)\n const buffer = PNG.sync.write(png)\n const stamped = injectTextChunk(buffer, 'flatland', JSON.stringify(metadata))\n writeFileSync(outputPath, stamped)\n}\n\n/**\n * Write a sidecar descriptor JSON file adjacent to the source asset.\n *\n * Same file format `flatland-bake <subcommand> --descriptor` consumes,\n * so the runtime and CLI agree on one shape.\n */\nexport function writeSidecarJson(outputPath: string, descriptor: unknown): void {\n writeFileSync(outputPath, JSON.stringify(descriptor, null, 2) + '\\n')\n}\n\n/**\n * Inject a `tEXt` chunk immediately after the IHDR so the metadata is\n * near the head of the file — `probeBakedSibling` range-fetches ~4 KB\n * to read the stamp, so placement matters.\n */\nfunction injectTextChunk(pngBuffer: Buffer, keyword: string, value: string): Buffer {\n const sigLen = 8\n // IHDR type starts 4 bytes after the length prefix: [len][I][H][D][R]...\n if (\n pngBuffer[sigLen + 4] !== 0x49 ||\n pngBuffer[sigLen + 5] !== 0x48 ||\n pngBuffer[sigLen + 6] !== 0x44 ||\n pngBuffer[sigLen + 7] !== 0x52\n ) {\n return pngBuffer\n }\n const ihdrLength = pngBuffer.readUInt32BE(sigLen)\n const ihdrEnd = sigLen + 8 + ihdrLength + 4 // length(4) + type(4) + data + CRC(4)\n\n const textData = Buffer.concat([\n Buffer.from(keyword, 'latin1'),\n Buffer.from([0]),\n Buffer.from(value, 'latin1'),\n ])\n const textType = Buffer.from('tEXt', 'latin1')\n const textLen = Buffer.alloc(4)\n textLen.writeUInt32BE(textData.length, 0)\n const textCrc = Buffer.alloc(4)\n textCrc.writeUInt32BE(crc32(Buffer.concat([textType, textData])), 0)\n const textChunk = Buffer.concat([textLen, textType, textData, textCrc])\n\n return Buffer.concat([pngBuffer.subarray(0, ihdrEnd), textChunk, pngBuffer.subarray(ihdrEnd)])\n}\n\nlet _crcTable: Uint32Array | null = null\nfunction crcTable(): Uint32Array {\n if (_crcTable) return _crcTable\n const table = new Uint32Array(256)\n for (let n = 0; n < 256; n++) {\n let c = n\n for (let k = 0; k < 8; k++) {\n c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1\n }\n table[n] = c >>> 0\n }\n _crcTable = table\n return table\n}\n\nfunction crc32(buffer: Buffer): number {\n const table = crcTable()\n let crc = 0xffffffff\n for (let i = 0; i < buffer.length; i++) {\n crc = (table[(crc ^ buffer[i]!) & 0xff]! ^ (crc >>> 8)) >>> 0\n }\n return (crc ^ 0xffffffff) >>> 0\n}\n"],"mappings":"AAAA,SAAS,qBAAqB;AAC9B,SAAS,cAAc;AACvB,SAAS,WAAW;AAUb,SAAS,gBACd,YACA,QACA,OACA,QACA,UACM;AACN,QAAM,MAAM,IAAI,IAAI,EAAE,OAAO,OAAO,CAAC;AACrC,MAAI,OAAO,OAAO,KAAK,OAAO,QAAQ,OAAO,YAAY,OAAO,UAAU;AAC1E,QAAM,SAAS,IAAI,KAAK,MAAM,GAAG;AACjC,QAAM,UAAU,gBAAgB,QAAQ,YAAY,KAAK,UAAU,QAAQ,CAAC;AAC5E,gBAAc,YAAY,OAAO;AACnC;AAQO,SAAS,iBAAiB,YAAoB,YAA2B;AAC9E,gBAAc,YAAY,KAAK,UAAU,YAAY,MAAM,CAAC,IAAI,IAAI;AACtE;AAOA,SAAS,gBAAgB,WAAmB,SAAiB,OAAuB;AAClF,QAAM,SAAS;AAEf,MACE,UAAU,SAAS,CAAC,MAAM,MAC1B,UAAU,SAAS,CAAC,MAAM,MAC1B,UAAU,SAAS,CAAC,MAAM,MAC1B,UAAU,SAAS,CAAC,MAAM,IAC1B;AACA,WAAO;AAAA,EACT;AACA,QAAM,aAAa,UAAU,aAAa,MAAM;AAChD,QAAM,UAAU,SAAS,IAAI,aAAa;AAE1C,QAAM,WAAW,OAAO,OAAO;AAAA,IAC7B,OAAO,KAAK,SAAS,QAAQ;AAAA,IAC7B,OAAO,KAAK,CAAC,CAAC,CAAC;AAAA,IACf,OAAO,KAAK,OAAO,QAAQ;AAAA,EAC7B,CAAC;AACD,QAAM,WAAW,OAAO,KAAK,QAAQ,QAAQ;AAC7C,QAAM,UAAU,OAAO,MAAM,CAAC;AAC9B,UAAQ,cAAc,SAAS,QAAQ,CAAC;AACxC,QAAM,UAAU,OAAO,MAAM,CAAC;AAC9B,UAAQ,cAAc,MAAM,OAAO,OAAO,CAAC,UAAU,QAAQ,CAAC,CAAC,GAAG,CAAC;AACnE,QAAM,YAAY,OAAO,OAAO,CAAC,SAAS,UAAU,UAAU,OAAO,CAAC;AAEtE,SAAO,OAAO,OAAO,CAAC,UAAU,SAAS,GAAG,OAAO,GAAG,WAAW,UAAU,SAAS,OAAO,CAAC,CAAC;AAC/F;AAEA,IAAI,YAAgC;AACpC,SAAS,WAAwB;AAC/B,MAAI,UAAW,QAAO;AACtB,QAAM,QAAQ,IAAI,YAAY,GAAG;AACjC,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,QAAI,IAAI;AACR,aAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,UAAI,IAAI,IAAI,aAAc,MAAM,IAAK,MAAM;AAAA,IAC7C;AACA,UAAM,CAAC,IAAI,MAAM;AAAA,EACnB;AACA,cAAY;AACZ,SAAO;AACT;AAEA,SAAS,MAAM,QAAwB;AACrC,QAAM,QAAQ,SAAS;AACvB,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,WAAO,OAAO,MAAM,OAAO,CAAC,KAAM,GAAI,IAAM,QAAQ,OAAQ;AAAA,EAC9D;AACA,UAAQ,MAAM,gBAAgB;AAChC;","names":[]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@three-flatland/bake",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Shared bake pipeline infrastructure — CLI, discovery, and browser-safe loader utilities for assets with offline-baked siblings",
5
+ "type": "module",
6
+ "bin": {
7
+ "flatland-bake": "./dist/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "source": "./src/index.ts",
12
+ "node": {
13
+ "types": "./dist/node.d.ts",
14
+ "import": "./dist/node.js"
15
+ },
16
+ "browser": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ },
20
+ "default": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "./node": {
26
+ "source": "./src/node.ts",
27
+ "types": "./dist/node.d.ts",
28
+ "import": "./dist/node.js"
29
+ }
30
+ },
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "sideEffects": false,
37
+ "dependencies": {
38
+ "pngjs": "^7.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/pngjs": "^6.0.5"
42
+ },
43
+ "keywords": [
44
+ "three-flatland",
45
+ "bake",
46
+ "cli"
47
+ ],
48
+ "license": "MIT",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/thejustinwalsh/three-flatland.git",
52
+ "directory": "packages/bake"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "dev": "tsup --watch",
57
+ "typecheck": "tsc --noEmit",
58
+ "clean": "rm -rf dist"
59
+ }
60
+ }