create-three-blocks-starter 0.0.13 → 0.3.0-alpha.176.g8ff8a72e6369

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +39 -270
  3. package/NOTICE +32 -0
  4. package/README.md +39 -60
  5. package/dist/artifacts.d.ts +54 -0
  6. package/dist/artifacts.js +349 -0
  7. package/dist/bin/index.d.ts +2 -0
  8. package/dist/bin/index.js +8 -0
  9. package/dist/catalog.d.ts +5 -0
  10. package/dist/catalog.js +80 -0
  11. package/dist/cli.d.ts +16 -0
  12. package/dist/cli.js +122 -0
  13. package/dist/compose.d.ts +10 -0
  14. package/dist/compose.js +331 -0
  15. package/dist/constants.d.ts +5 -0
  16. package/dist/constants.js +15 -0
  17. package/dist/generate.d.ts +21 -0
  18. package/dist/generate.js +225 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +5 -0
  21. package/dist/layers.d.ts +2 -0
  22. package/dist/layers.js +103 -0
  23. package/dist/shader-inputs.d.ts +6 -0
  24. package/dist/shader-inputs.js +184 -0
  25. package/dist/types.d.ts +125 -0
  26. package/dist/types.js +3 -0
  27. package/dist/ui.d.ts +97 -0
  28. package/dist/ui.js +606 -0
  29. package/package.json +50 -9
  30. package/template-assets/shaders/scene/meta.json +67 -0
  31. package/template-assets/shaders/scene/scene.webgpu.ts +895 -0
  32. package/template-assets/shaders/text/meta.json +67 -0
  33. package/template-assets/shaders/text/text.webgpu.ts +1843 -0
  34. package/template-assets/shaders/website/meta.json +67 -0
  35. package/template-assets/shaders/website/website.webgpu.ts +894 -0
  36. package/template-assets/text/public/fonts/OFL-1.1.txt +92 -0
  37. package/template-assets/text/public/fonts/geist.msdf.json +1 -0
  38. package/template-assets/text/public/fonts/geist.msdf.ktx2 +0 -0
  39. package/template-assets/text/public/fonts/geist.msdf.meta.json +1 -0
  40. package/template-assets/text/public/fonts/geist.woff2 +0 -0
  41. package/template-assets/text/public/fonts/noto-jp.msdf.json +1 -0
  42. package/template-assets/text/public/fonts/noto-jp.msdf.ktx2 +0 -0
  43. package/template-assets/text/public/fonts/noto-jp.msdf.meta.json +1 -0
  44. package/template-assets/text/public/fonts/noto-jp.woff2 +0 -0
  45. package/templates/core/.editorconfig +13 -0
  46. package/templates/core/.gitattributes +1 -0
  47. package/templates/core/.vscode/extensions.json +5 -0
  48. package/templates/core/.vscode/settings.json +8 -0
  49. package/templates/core/NOTICE +20 -0
  50. package/templates/core/eslint.config.js +45 -0
  51. package/templates/core/gitignore +6 -0
  52. package/templates/core/public/icon.svg +8 -0
  53. package/templates/core/src/assets.ts +4 -0
  54. package/templates/core/src/look.ts +54 -0
  55. package/templates/core/src/main.ts +106 -0
  56. package/templates/core/src/protocol.ts +100 -0
  57. package/templates/core/src/render.worker.ts +239 -0
  58. package/templates/core/src/shaders.d.ts +6 -0
  59. package/templates/core/src/style.css +119 -0
  60. package/templates/core/src/vite-env.d.ts +11 -0
  61. package/templates/core/tsconfig.json +23 -0
  62. package/templates/core/vite.config.ts +12 -0
  63. package/templates/scene/index.html +16 -0
  64. package/templates/scene/src/scene.ts +66 -0
  65. package/templates/text/index.html +37 -0
  66. package/templates/text/src/scene.ts +61 -0
  67. package/templates/text/three-blocks.text.ts +31 -0
  68. package/templates/text/vite.config.ts +14 -0
  69. package/templates/website/index.html +30 -0
  70. package/templates/website/src/scene.ts +62 -0
  71. package/bin/index.js +0 -1989
@@ -0,0 +1,349 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { COMPATIBILITY_VERSIONS } from './constants.js';
5
+ import { TEMPLATE_NAMES, } from './types.js';
6
+ import { snapshotGeneratedShaderInputs } from './shader-inputs.js';
7
+ const TEXT_FILES = [
8
+ 'public/fonts/OFL-1.1.txt',
9
+ 'public/fonts/geist.woff2',
10
+ 'public/fonts/geist.msdf.json',
11
+ 'public/fonts/geist.msdf.ktx2',
12
+ 'public/fonts/geist.msdf.meta.json',
13
+ 'public/fonts/noto-jp.woff2',
14
+ 'public/fonts/noto-jp.msdf.json',
15
+ 'public/fonts/noto-jp.msdf.ktx2',
16
+ 'public/fonts/noto-jp.msdf.meta.json',
17
+ ];
18
+ const SHADER_COMPATIBILITY = 'three-webgpu-r185-v1';
19
+ const SHADER_ADDRESS_VERSION = 2;
20
+ const SHADER_RECIPE_VERSION = 1;
21
+ const SHADER_HYDRATION_VERSION = 2;
22
+ const SHADER_TRANSFORM_VERSION = 3;
23
+ const SHA256 = /^[a-f0-9]{64}$/u;
24
+ const SAFE_VERSION = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,127}$/u;
25
+ const MAX_MANIFEST_BYTES = 32 * 1_024 * 1_024;
26
+ const MAX_RECEIPT_BYTES = 2 * 1_024 * 1_024;
27
+ const MAX_INPUTS = 4_096;
28
+ const MAX_ENTRIES = 4_096;
29
+ function isRecord(value) {
30
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
31
+ }
32
+ function bytes(content) {
33
+ return typeof content === 'string' ? Buffer.from(content, 'utf8') : content;
34
+ }
35
+ function text(content, label) {
36
+ try {
37
+ return typeof content === 'string' ? content : new TextDecoder('utf-8', { fatal: true }).decode(content);
38
+ }
39
+ catch {
40
+ throw new Error(`${label} must be valid UTF-8.`);
41
+ }
42
+ }
43
+ function sha256(content) {
44
+ return createHash('sha256').update(bytes(content)).digest('hex');
45
+ }
46
+ function nonNegativeInteger(value, label) {
47
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
48
+ throw new Error(`${label} must be a non-negative integer.`);
49
+ }
50
+ return value;
51
+ }
52
+ function safeRelativePath(value, label) {
53
+ if (typeof value !== 'string' || value.length === 0 || value.includes('\\') || path.posix.isAbsolute(value)
54
+ || path.posix.normalize(value) !== value || value === '..' || value.startsWith('../')
55
+ || /[\u0000-\u001f\u007f]/u.test(value)) {
56
+ throw new Error(`${label} must be a safe project-relative path.`);
57
+ }
58
+ return value;
59
+ }
60
+ function coverage(value, label) {
61
+ if (!isRecord(value))
62
+ throw new Error(`${label} is missing.`);
63
+ return {
64
+ render: nonNegativeInteger(value.render, `${label}.render`),
65
+ vertex: nonNegativeInteger(value.vertex, `${label}.vertex`),
66
+ fragment: nonNegativeInteger(value.fragment, `${label}.fragment`),
67
+ compute: nonNegativeInteger(value.compute, `${label}.compute`),
68
+ };
69
+ }
70
+ function parseInputs(value, template) {
71
+ if (!isRecord(value))
72
+ throw new Error(`Shader receipt inputs for "${template}" are invalid.`);
73
+ const entries = Object.entries(value);
74
+ if (entries.length === 0 || entries.length > MAX_INPUTS) {
75
+ throw new Error(`Shader receipt inputs for "${template}" must contain 1...${MAX_INPUTS} files.`);
76
+ }
77
+ const result = {};
78
+ for (const [inputPath, hash] of entries) {
79
+ safeRelativePath(inputPath, `Shader receipt input for "${template}"`);
80
+ if (typeof hash !== 'string' || !SHA256.test(hash))
81
+ throw new Error(`Shader receipt input hash for "${template}" is invalid.`);
82
+ result[inputPath] = hash;
83
+ }
84
+ return result;
85
+ }
86
+ function parseReceipt(template, content, versions) {
87
+ if (bytes(content).byteLength > MAX_RECEIPT_BYTES)
88
+ throw new Error(`Shader metadata for "${template}" exceeds ${MAX_RECEIPT_BYTES} bytes.`);
89
+ let value;
90
+ try {
91
+ value = JSON.parse(text(content, `Shader metadata for "${template}"`));
92
+ }
93
+ catch (error) {
94
+ if (error instanceof Error && error.message.endsWith('must be valid UTF-8.'))
95
+ throw error;
96
+ throw new Error(`Shader metadata for "${template}" is not valid JSON.`);
97
+ }
98
+ if (!isRecord(value) || value.schemaVersion !== 2 || !isRecord(value.runtime)
99
+ || value.runtime.three !== versions.three || value.runtime.threeBlocks !== versions.threeBlocks) {
100
+ throw new Error(`Shader metadata for "${template}" has an incompatible schema or runtime version.`);
101
+ }
102
+ if (typeof value.runtime.threeBlocksShaderClosure !== 'string' || !SHA256.test(value.runtime.threeBlocksShaderClosure)) {
103
+ throw new Error(`Shader metadata for "${template}" has an invalid three-blocks shader runtime closure.`);
104
+ }
105
+ if (!isRecord(value.tool) || value.tool.name !== '@three-blocks/devtools'
106
+ || typeof value.tool.version !== 'string' || !SAFE_VERSION.test(value.tool.version)
107
+ || value.tool.compatibility !== SHADER_COMPATIBILITY
108
+ || value.tool.transform !== SHADER_TRANSFORM_VERSION || value.tool.address !== SHADER_ADDRESS_VERSION
109
+ || value.tool.recipe !== SHADER_RECIPE_VERSION || value.tool.hydration !== SHADER_HYDRATION_VERSION) {
110
+ throw new Error(`Shader metadata for "${template}" has incompatible public tool metadata.`);
111
+ }
112
+ if (!isRecord(value.scenes) || Object.keys(value.scenes).length !== 1 || !isRecord(value.scenes[template])) {
113
+ throw new Error(`Shader metadata for "${template}" must contain exactly that complete template scene.`);
114
+ }
115
+ const raw = value.scenes[template];
116
+ const expectedArtifact = `${template}.webgpu.ts`;
117
+ if (raw.artifact !== expectedArtifact || typeof raw.artifactHash !== 'string' || !SHA256.test(raw.artifactHash)) {
118
+ throw new Error(`Shader receipt for "${template}" references an invalid artifact.`);
119
+ }
120
+ const selectedCoverage = coverage(raw.coverage, `Shader coverage for "${template}"`);
121
+ if (selectedCoverage.render === 0 || selectedCoverage.vertex === 0 || selectedCoverage.fragment === 0) {
122
+ throw new Error(`Shader capture for "${template}" has no complete render pipeline coverage.`);
123
+ }
124
+ if (!isRecord(raw.skipped))
125
+ throw new Error(`Shader skipped-build counts for "${template}" are missing.`);
126
+ const skipped = {
127
+ render: nonNegativeInteger(raw.skipped.render, `Shader skipped render builds for "${template}"`),
128
+ compute: nonNegativeInteger(raw.skipped.compute, `Shader skipped compute builds for "${template}"`),
129
+ };
130
+ return {
131
+ schemaVersion: 2,
132
+ runtime: {
133
+ three: versions.three,
134
+ threeBlocks: versions.threeBlocks,
135
+ threeBlocksShaderClosure: value.runtime.threeBlocksShaderClosure,
136
+ },
137
+ tool: {
138
+ name: '@three-blocks/devtools',
139
+ version: value.tool.version,
140
+ compatibility: SHADER_COMPATIBILITY,
141
+ transform: SHADER_TRANSFORM_VERSION,
142
+ address: SHADER_ADDRESS_VERSION,
143
+ recipe: SHADER_RECIPE_VERSION,
144
+ hydration: SHADER_HYDRATION_VERSION,
145
+ },
146
+ scenes: {
147
+ [template]: {
148
+ artifact: expectedArtifact,
149
+ artifactHash: raw.artifactHash,
150
+ bytes: nonNegativeInteger(raw.bytes, `Shader artifact bytes for "${template}"`),
151
+ inputs: parseInputs(raw.inputs, template),
152
+ coverage: selectedCoverage,
153
+ skipped,
154
+ },
155
+ },
156
+ };
157
+ }
158
+ function parseManifestObject(template, content) {
159
+ const manifestBytes = bytes(content);
160
+ if (manifestBytes.byteLength === 0 || manifestBytes.byteLength > MAX_MANIFEST_BYTES) {
161
+ throw new Error(`Shader manifest for "${template}" must contain 1...${MAX_MANIFEST_BYTES} bytes.`);
162
+ }
163
+ const source = text(content, `Shader manifest for "${template}"`);
164
+ const exportDefault = source.indexOf('export default');
165
+ const firstBrace = source.indexOf('{', exportDefault);
166
+ const lastBrace = source.lastIndexOf('}');
167
+ if (exportDefault < 0 || firstBrace < 0 || lastBrace <= firstBrace) {
168
+ throw new Error(`Shader manifest for "${template}" is not a generated TypeScript manifest module.`);
169
+ }
170
+ for (const match of source.matchAll(/^\s*import(?:\s+type)?[\s\S]*?\sfrom\s+['"]([^'"]+)['"];?\s*$/gmu)) {
171
+ if (match[1] !== 'three-blocks/shaders')
172
+ throw new Error(`Shader manifest for "${template}" imports a non-runtime module.`);
173
+ }
174
+ let parsed;
175
+ try {
176
+ parsed = JSON.parse(source.slice(firstBrace, lastBrace + 1));
177
+ }
178
+ catch {
179
+ throw new Error(`Shader manifest for "${template}" does not contain the deterministic JSON payload emitted by public devtools.`);
180
+ }
181
+ if (!isRecord(parsed))
182
+ throw new Error(`Shader manifest for "${template}" has no object payload.`);
183
+ return parsed;
184
+ }
185
+ function manifestCoverage(template, content, versions) {
186
+ const manifest = parseManifestObject(template, content);
187
+ if (manifest.version !== 3 || manifest.scene !== template || manifest.three !== versions.three || manifest.threeBlocks !== versions.threeBlocks
188
+ || !isRecord(manifest.runtime) || manifest.runtime.id !== SHADER_COMPATIBILITY
189
+ || manifest.runtime.address !== SHADER_ADDRESS_VERSION || manifest.runtime.recipe !== SHADER_RECIPE_VERSION
190
+ || manifest.runtime.hydration !== SHADER_HYDRATION_VERSION || !isRecord(manifest.entries)) {
191
+ throw new Error(`Shader manifest for "${template}" has an incompatible scene, runtime, or schema.`);
192
+ }
193
+ const entries = Object.entries(manifest.entries);
194
+ if (entries.length === 0 || entries.length > MAX_ENTRIES)
195
+ throw new Error(`Shader manifest for "${template}" must contain 1...${MAX_ENTRIES} entries.`);
196
+ const states = Array.isArray(manifest.states) ? manifest.states : [];
197
+ let render = 0;
198
+ let vertex = 0;
199
+ let fragment = 0;
200
+ let compute = 0;
201
+ for (const [key, stateIndex] of entries) {
202
+ const state = typeof stateIndex === 'number' ? states[stateIndex] : undefined;
203
+ if (!isRecord(state))
204
+ throw new Error(`Shader manifest entry "${key}" for "${template}" references an invalid state.`);
205
+ for (const stage of ['vertex', 'fragment', 'compute']) {
206
+ if (state[stage] !== null && typeof state[stage] !== 'number')
207
+ throw new Error(`Shader manifest entry "${key}" has an invalid ${stage} module reference.`);
208
+ }
209
+ const hasVertex = typeof state.vertex === 'number';
210
+ const hasFragment = typeof state.fragment === 'number';
211
+ const hasCompute = typeof state.compute === 'number';
212
+ if (!hasVertex && !hasFragment && !hasCompute)
213
+ throw new Error(`Shader manifest entry "${key}" has no shader stage.`);
214
+ if (hasVertex || hasFragment)
215
+ render++;
216
+ if (hasVertex)
217
+ vertex++;
218
+ if (hasFragment)
219
+ fragment++;
220
+ if (hasCompute)
221
+ compute++;
222
+ }
223
+ return { render, vertex, fragment, compute };
224
+ }
225
+ function sameCoverage(left, right) {
226
+ return left.render === right.render && left.vertex === right.vertex
227
+ && left.fragment === right.fragment && left.compute === right.compute;
228
+ }
229
+ /** Validate the exact pair emitted by public shader capture before it can become a fresh catalog payload. */
230
+ export function validateShaderArtifactPayload(template, manifest, metadata, versions = COMPATIBILITY_VERSIONS) {
231
+ const receipt = parseReceipt(template, metadata, versions);
232
+ const selected = receipt.scenes[template];
233
+ if (selected === undefined)
234
+ throw new Error(`Shader receipt for "${template}" is missing.`);
235
+ if (selected.artifactHash !== sha256(manifest))
236
+ throw new Error(`Shader manifest checksum for "${template}" does not match metadata.`);
237
+ if (selected.bytes !== bytes(manifest).byteLength)
238
+ throw new Error(`Shader manifest byte count for "${template}" does not match metadata.`);
239
+ if (!sameCoverage(manifestCoverage(template, manifest, versions), selected.coverage)) {
240
+ throw new Error(`Shader manifest coverage for "${template}" does not match metadata.`);
241
+ }
242
+ return receipt;
243
+ }
244
+ export function shaderArtifactPaths(template) {
245
+ return [`.three-blocks/shaders/${template}.webgpu.ts`, '.three-blocks/shaders/meta.json'];
246
+ }
247
+ function loadShaderArtifact(template) {
248
+ const directory = new URL(`../template-assets/shaders/${template}/`, import.meta.url);
249
+ if (!existsSync(directory))
250
+ return {
251
+ status: 'pending',
252
+ files: [],
253
+ note: 'Pending verified shader capture; runtime behavior must not treat this descriptor as fresh.',
254
+ };
255
+ const directoryMetadata = lstatSync(directory);
256
+ if (directoryMetadata.isSymbolicLink() || !directoryMetadata.isDirectory())
257
+ throw new Error(`Shader template asset root for "${template}" must be a real directory.`);
258
+ const expected = [`${template}.webgpu.ts`, 'meta.json'];
259
+ const entries = readdirSync(directory, { withFileTypes: true });
260
+ if (entries.length === 0)
261
+ return {
262
+ status: 'pending',
263
+ files: [],
264
+ note: 'Pending verified shader capture; runtime behavior must not treat this descriptor as fresh.',
265
+ };
266
+ const unexpected = entries.filter((entry) => !expected.includes(entry.name)).map((entry) => entry.name);
267
+ if (unexpected.length > 0)
268
+ throw new Error(`Shader payload for "${template}" contains non-allowlisted files: ${unexpected.join(', ')}.`);
269
+ for (const expectedFile of expected) {
270
+ const entry = entries.find((candidate) => candidate.name === expectedFile);
271
+ if (entry === undefined || entry.isSymbolicLink() || !entry.isFile())
272
+ throw new Error(`Shader payload for "${template}" is incomplete: expected ${expected.join(' and ')}.`);
273
+ }
274
+ const manifest = readFileSync(new URL(`${template}.webgpu.ts`, directory));
275
+ const metadata = readFileSync(new URL('meta.json', directory));
276
+ const metadataValue = JSON.parse(text(metadata, `Shader metadata for "${template}"`));
277
+ if (isRecord(metadataValue) && isRecord(metadataValue.runtime) && metadataValue.runtime.threeBlocksShaderClosure === undefined) {
278
+ return {
279
+ status: 'pending',
280
+ files: [],
281
+ note: 'Existing capture predates shader-relevant closure receipts and must be recaptured.',
282
+ };
283
+ }
284
+ const receipt = validateShaderArtifactPayload(template, manifest, metadata);
285
+ const selected = receipt.scenes[template];
286
+ if (selected === undefined)
287
+ throw new Error(`Shader receipt for "${template}" is missing.`);
288
+ const targets = shaderArtifactPaths(template);
289
+ return {
290
+ status: 'fresh',
291
+ files: [
292
+ { path: targets[0], content: manifest, owner: 'generator' },
293
+ { path: targets[1], content: metadata, owner: 'generator' },
294
+ ],
295
+ note: `Captured complete ${template} template with ${selected.coverage.render} render and ${selected.coverage.compute} compute pipelines.`,
296
+ };
297
+ }
298
+ const SHADER_ARTIFACTS = Object.freeze(Object.fromEntries(TEMPLATE_NAMES.map((template) => [template, loadShaderArtifact(template)])));
299
+ export function shaderArtifactFiles(template) {
300
+ return SHADER_ARTIFACTS[template].files;
301
+ }
302
+ export function shaderArtifactState(template) {
303
+ const artifact = SHADER_ARTIFACTS[template];
304
+ return { status: artifact.status, note: artifact.note };
305
+ }
306
+ /** Prove that a fresh payload was captured from the exact fully composed generated template. */
307
+ export function validateShaderArtifactProject(template, files, versions) {
308
+ const targets = shaderArtifactPaths(template);
309
+ const manifest = files.get(targets[0]);
310
+ const metadata = files.get(targets[1]);
311
+ if (manifest === undefined || metadata === undefined)
312
+ throw new Error(`Fresh shader artifact for "${template}" is missing its allowlisted manifest or metadata.`);
313
+ const receipt = validateShaderArtifactPayload(template, manifest.content, metadata.content, versions);
314
+ const selected = receipt.scenes[template];
315
+ if (selected === undefined)
316
+ throw new Error(`Shader receipt for "${template}" is missing.`);
317
+ const current = snapshotGeneratedShaderInputs(files);
318
+ // Receipts hash the exact composed source bytes. Keep hash-relevant inputs
319
+ // ASCII so character representation stays explicit and stable across editors,
320
+ // archives, and capture runtimes; write other characters as source escapes.
321
+ for (const input of Object.keys(current)) {
322
+ const file = files.get(input);
323
+ if (file === undefined)
324
+ continue;
325
+ const content = typeof file.content === 'string' ? Buffer.from(file.content) : file.content;
326
+ if (content.some((byte) => byte > 0x7f)) {
327
+ throw new Error(`Shader semantic input "${input}" for "${template}" contains non-ASCII bytes; embed them as escape sequences so composition is runtime-independent.`);
328
+ }
329
+ }
330
+ const recorded = selected.inputs;
331
+ const changed = Object.keys(current).filter((input) => recorded[input] !== current[input]);
332
+ const removed = Object.keys(recorded).filter((input) => !Object.hasOwn(current, input));
333
+ if (changed.length > 0 || removed.length > 0) {
334
+ throw new Error(`Fresh shader artifact for "${template}" does not match its composed semantic inputs`
335
+ + ` (${changed.length} added/changed: ${changed.join(', ') || 'none'};`
336
+ + ` ${removed.length} removed: ${removed.join(', ') || 'none'}).`);
337
+ }
338
+ }
339
+ function textPayload(relativePath) {
340
+ return {
341
+ path: relativePath,
342
+ content: readFileSync(new URL(`../template-assets/text/${relativePath}`, import.meta.url)),
343
+ owner: 'generator',
344
+ };
345
+ }
346
+ export function textArtifactFiles() {
347
+ return TEXT_FILES.map(textPayload);
348
+ }
349
+ export const textArtifactPaths = TEXT_FILES;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../cli.js';
3
+ import { fail } from '../ui.js';
4
+ runCli().catch((error) => {
5
+ const message = error instanceof Error ? error.message : String(error);
6
+ fail(message);
7
+ process.exitCode = 1;
8
+ });
@@ -0,0 +1,5 @@
1
+ import { type TemplateCatalog, type TemplateInput, type TemplateName } from './types.js';
2
+ export declare function defineTemplateCatalog(catalog: TemplateCatalog): TemplateCatalog;
3
+ export declare const catalog: TemplateCatalog;
4
+ export declare function resolveTemplateName(input: string | undefined, source?: TemplateCatalog): TemplateName;
5
+ export declare function isTemplateInput(input: string): input is TemplateInput;
@@ -0,0 +1,80 @@
1
+ import { COMPATIBILITY_VERSIONS } from './constants.js';
2
+ import { shaderArtifactPaths, shaderArtifactState, textArtifactPaths } from './artifacts.js';
3
+ import { LEGACY_TEMPLATE_NAMES, TEMPLATE_NAMES, } from './types.js';
4
+ export function defineTemplateCatalog(catalog) {
5
+ return catalog;
6
+ }
7
+ function shaderArtifact(template) {
8
+ const state = shaderArtifactState(template);
9
+ return Object.freeze({
10
+ schemaVersion: 1,
11
+ kind: 'shader',
12
+ template,
13
+ status: state.status,
14
+ capturedWith: {
15
+ three: COMPATIBILITY_VERSIONS.three,
16
+ threeBlocks: COMPATIBILITY_VERSIONS.threeBlocks,
17
+ },
18
+ files: state.status === 'fresh' ? shaderArtifactPaths(template) : [],
19
+ note: state.note,
20
+ });
21
+ }
22
+ function freshTextArtifact() {
23
+ return Object.freeze({
24
+ schemaVersion: 1,
25
+ kind: 'text',
26
+ template: 'text',
27
+ status: 'fresh',
28
+ capturedWith: {
29
+ three: COMPATIBILITY_VERSIONS.three,
30
+ threeBlocks: COMPATIBILITY_VERSIONS.threeBlocks,
31
+ },
32
+ files: textArtifactPaths,
33
+ note: 'Validated Geist and Noto Sans JP browser/MSDF outputs generated from the complete text template.',
34
+ });
35
+ }
36
+ export const catalog = defineTemplateCatalog({
37
+ schemaVersion: 2,
38
+ default: 'scene',
39
+ aliases: {
40
+ starter: 'scene',
41
+ minimal: 'scene',
42
+ },
43
+ versions: COMPATIBILITY_VERSIONS,
44
+ templates: {
45
+ scene: {
46
+ name: 'scene',
47
+ label: 'Minimal',
48
+ description: 'The Three Blocks mark as a pointer-reactive, worker-rendered WebGPU scene',
49
+ layers: ['core', 'scene'],
50
+ shaderArtifact: shaderArtifact('scene'),
51
+ },
52
+ website: {
53
+ name: 'website',
54
+ label: 'Website',
55
+ description: 'A semantic one-page site that proves worker rendering and precompiled shaders',
56
+ layers: ['core', 'website'],
57
+ shaderArtifact: shaderArtifact('website'),
58
+ },
59
+ text: {
60
+ name: 'text',
61
+ label: 'Text website',
62
+ description: 'A bilingual type specimen whose selectable DOM text renders on the GPU',
63
+ layers: ['core', 'website', 'text'],
64
+ shaderArtifact: shaderArtifact('text'),
65
+ textArtifact: freshTextArtifact(),
66
+ },
67
+ },
68
+ });
69
+ export function resolveTemplateName(input, source = catalog) {
70
+ const normalized = (input ?? source.default).trim().toLowerCase();
71
+ if (TEMPLATE_NAMES.includes(normalized))
72
+ return normalized;
73
+ if (LEGACY_TEMPLATE_NAMES.includes(normalized)) {
74
+ return source.aliases[normalized];
75
+ }
76
+ throw new Error(`Unknown template "${input}". Expected: ${TEMPLATE_NAMES.join(', ')}.`);
77
+ }
78
+ export function isTemplateInput(input) {
79
+ return [...TEMPLATE_NAMES, ...LEGACY_TEMPLATE_NAMES].includes(input);
80
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type PackageManager } from './generate.js';
2
+ interface ParsedArguments {
3
+ directory: string | undefined;
4
+ template: string | undefined;
5
+ mode: string | undefined;
6
+ resultFile: string | undefined;
7
+ packageManager: PackageManager | undefined;
8
+ install: boolean | undefined;
9
+ git: boolean | undefined;
10
+ yes: boolean;
11
+ help: boolean;
12
+ version: boolean;
13
+ }
14
+ export declare function parseArguments(argv: readonly string[]): ParsedArguments;
15
+ export declare function runCli(argv?: readonly string[]): Promise<void>;
16
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,122 @@
1
+ import path from 'node:path';
2
+ import { catalog, resolveTemplateName } from './catalog.js';
3
+ import { GENERATOR_VERSION } from './constants.js';
4
+ import { detectPackageManager, generateProject } from './generate.js';
5
+ import { confirm, intro, printCompletion, printHelp, promptProjectDirectory, promptTemplate, step, theme, } from './ui.js';
6
+ function valueAfter(argv, index, option) {
7
+ const value = argv[index + 1];
8
+ if (value === undefined || value.startsWith('-'))
9
+ throw new Error(`${option} requires a value.`);
10
+ return value;
11
+ }
12
+ export function parseArguments(argv) {
13
+ const result = {
14
+ directory: undefined,
15
+ template: undefined,
16
+ mode: undefined,
17
+ resultFile: undefined,
18
+ packageManager: undefined,
19
+ install: undefined,
20
+ git: undefined,
21
+ yes: false,
22
+ help: false,
23
+ version: false,
24
+ };
25
+ for (let index = 0; index < argv.length; index++) {
26
+ const argument = argv[index];
27
+ if (argument === undefined || argument === '--')
28
+ continue;
29
+ if (argument === '--help' || argument === '-h')
30
+ result.help = true;
31
+ else if (argument === '--version' || argument === '-v')
32
+ result.version = true;
33
+ else if (argument === '--yes' || argument === '-y')
34
+ result.yes = true;
35
+ else if (argument === '--install')
36
+ result.install = true;
37
+ else if (argument === '--no-install')
38
+ result.install = false;
39
+ else if (argument === '--git')
40
+ result.git = true;
41
+ else if (argument === '--no-git')
42
+ result.git = false;
43
+ else if (argument === '--template' || argument === '-t')
44
+ result.template = valueAfter(argv, index++, argument);
45
+ else if (argument.startsWith('--template='))
46
+ result.template = argument.slice('--template='.length);
47
+ else if (argument === '--mode' || argument === '-m')
48
+ result.mode = valueAfter(argv, index++, argument);
49
+ else if (argument.startsWith('--mode='))
50
+ result.mode = argument.slice('--mode='.length);
51
+ else if (argument === '--name' || argument === '-n')
52
+ result.directory = valueAfter(argv, index++, argument);
53
+ else if (argument.startsWith('--name='))
54
+ result.directory = argument.slice('--name='.length);
55
+ else if (argument === '--result-file')
56
+ result.resultFile = valueAfter(argv, index++, argument);
57
+ else if (argument.startsWith('--result-file='))
58
+ result.resultFile = argument.slice('--result-file='.length);
59
+ else if (argument === '--package-manager') {
60
+ const manager = valueAfter(argv, index++, argument);
61
+ if (manager !== 'bun' && manager !== 'npm' && manager !== 'pnpm')
62
+ throw new Error(`Unknown package manager "${manager}".`);
63
+ result.packageManager = manager;
64
+ }
65
+ else if (argument.startsWith('--package-manager=')) {
66
+ const manager = argument.slice('--package-manager='.length);
67
+ if (manager !== 'bun' && manager !== 'npm' && manager !== 'pnpm')
68
+ throw new Error(`Unknown package manager "${manager}".`);
69
+ result.packageManager = manager;
70
+ }
71
+ else if (argument.startsWith('-'))
72
+ throw new Error(`Unknown option "${argument}".`);
73
+ else if (result.directory === undefined)
74
+ result.directory = argument;
75
+ else
76
+ throw new Error(`Unexpected argument "${argument}".`);
77
+ }
78
+ if (result.template !== undefined && result.mode !== undefined)
79
+ throw new Error('Use either --template or --mode, not both.');
80
+ return result;
81
+ }
82
+ export async function runCli(argv = process.argv.slice(2)) {
83
+ const args = parseArguments(argv);
84
+ if (args.help) {
85
+ printHelp(GENERATOR_VERSION);
86
+ return;
87
+ }
88
+ if (args.version) {
89
+ process.stdout.write(`${GENERATOR_VERSION}\n`);
90
+ return;
91
+ }
92
+ intro(GENERATOR_VERSION);
93
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !args.yes);
94
+ const directory = args.directory ?? (interactive ? await promptProjectDirectory() : 'my-three-blocks-app');
95
+ const requested = args.template ?? args.mode;
96
+ const template = requested === undefined && interactive ? await promptTemplate() : resolveTemplateName(requested, catalog);
97
+ if (template === null)
98
+ return;
99
+ const install = args.install ?? (interactive ? await confirm('Install dependencies?', true) : true);
100
+ const git = args.git ?? (interactive ? await confirm('Initialize Git with an initial commit?', true) : true);
101
+ const packageManager = args.packageManager ?? detectPackageManager();
102
+ const targetDirectory = path.resolve(process.cwd(), directory);
103
+ step(`Creating ${theme.bold(catalog.templates[template].label)} in ${theme.accent(directory)}`);
104
+ const outcome = await generateProject({
105
+ targetDirectory,
106
+ template,
107
+ install,
108
+ git,
109
+ packageManager,
110
+ ...(args.resultFile === undefined ? {} : { resultFile: args.resultFile }),
111
+ });
112
+ printCompletion({
113
+ projectName: outcome.project.name,
114
+ templateLabel: catalog.templates[template].label,
115
+ directory,
116
+ packageManager,
117
+ installed: install,
118
+ git,
119
+ shaderStatus: outcome.project.artifacts.shader.status,
120
+ ...(outcome.project.artifacts.text === undefined ? {} : { textStatus: outcome.project.artifacts.text.status }),
121
+ });
122
+ }
@@ -0,0 +1,10 @@
1
+ import { type GeneratedMetadata, type LayerDefinition, type LayerName, type PackageManager, type ResolvedProject, type TemplateCatalog } from './types.js';
2
+ export interface ResolveProjectOptions {
3
+ readonly catalog?: TemplateCatalog;
4
+ readonly layers?: Readonly<Record<LayerName, LayerDefinition>>;
5
+ readonly packageManager?: PackageManager;
6
+ }
7
+ export declare function normalizeProjectName(input: string): string;
8
+ export declare function createMetadata(project: Omit<ResolvedProject, 'files'>): GeneratedMetadata;
9
+ export declare function validateResolvedProject(project: ResolvedProject, source?: TemplateCatalog): void;
10
+ export declare function resolveProject(templateInput: string | undefined, projectName: string, options?: ResolveProjectOptions): ResolvedProject;