create-shallot 0.5.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +115 -80
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -3,21 +3,16 @@
|
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
4
4
|
import { dirname, join, resolve } from "path";
|
|
5
5
|
|
|
6
|
-
export interface TemplateOptions {
|
|
7
|
-
/**
|
|
8
|
-
* dependency spec for `@dylanebert/shallot`. `"latest"` for the published `bun create shallot`
|
|
9
|
-
* scaffold; `"workspace:*"` when `bun run build` regenerates examples/templates/starter in-repo.
|
|
10
|
-
*/
|
|
11
|
-
shallot?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
6
|
/**
|
|
15
|
-
* the project files keyed by relative path, with the project name interpolated. single source of
|
|
16
|
-
* truth for
|
|
17
|
-
*
|
|
7
|
+
* the project files keyed by relative path, with the project name interpolated. the single source of
|
|
8
|
+
* truth for `bun create shallot <name>`.
|
|
9
|
+
*
|
|
10
|
+
* the project is pure data — a `shallot.json` manifest + plugin modules + `public/`, no vite
|
|
11
|
+
* boilerplate. the CLI provides every harness over it: `shallot dev` runs it standalone with hot
|
|
12
|
+
* reload, `shallot build` ships it (web + native targets). the emitted CLAUDE.md / AGENTS.md point an
|
|
13
|
+
* agent at the installed engine's contract.
|
|
18
14
|
*/
|
|
19
|
-
export function template(name: string
|
|
20
|
-
const shallot = opts.shallot ?? "latest";
|
|
15
|
+
export function template(name: string): Record<string, string> {
|
|
21
16
|
return {
|
|
22
17
|
"public/icon.svg": ICON,
|
|
23
18
|
".gitignore": "node_modules/\ndist/\nbuild/\n",
|
|
@@ -28,9 +23,8 @@ export function template(name: string, opts: TemplateOptions = {}): Record<strin
|
|
|
28
23
|
version: "0.0.0",
|
|
29
24
|
private: true,
|
|
30
25
|
type: "module",
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
devDependencies: { typescript: "^5.9.3", vite: "^8.0.0" },
|
|
26
|
+
dependencies: { "@dylanebert/shallot": "latest" },
|
|
27
|
+
devDependencies: { typescript: "^7.0.2" },
|
|
34
28
|
},
|
|
35
29
|
null,
|
|
36
30
|
2,
|
|
@@ -43,7 +37,7 @@ export function template(name: string, opts: TemplateOptions = {}): Record<strin
|
|
|
43
37
|
module: "ESNext",
|
|
44
38
|
moduleResolution: "bundler",
|
|
45
39
|
lib: ["ESNext", "DOM", "DOM.Iterable"],
|
|
46
|
-
types: ["@webgpu/types"
|
|
40
|
+
types: ["@webgpu/types"],
|
|
47
41
|
strict: true,
|
|
48
42
|
noEmit: true,
|
|
49
43
|
skipLibCheck: true,
|
|
@@ -53,29 +47,13 @@ export function template(name: string, opts: TemplateOptions = {}): Record<strin
|
|
|
53
47
|
null,
|
|
54
48
|
2,
|
|
55
49
|
) + "\n",
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<meta charset="UTF-8" />
|
|
60
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
61
|
-
<link rel="icon" type="image/svg+xml" href="./icon.svg" />
|
|
62
|
-
<title>${name}</title>
|
|
63
|
-
<style>
|
|
64
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
65
|
-
body { background: #0c0a09; overflow: hidden; }
|
|
66
|
-
canvas { display: block; width: 100vw; height: 100vh; }
|
|
67
|
-
</style>
|
|
68
|
-
</head>
|
|
69
|
-
<body>
|
|
70
|
-
<canvas id="canvas"></canvas>
|
|
71
|
-
<script type="module" src="./src/main.ts"></script>
|
|
72
|
-
</body>
|
|
73
|
-
</html>
|
|
74
|
-
`,
|
|
75
|
-
"vite.config.ts": VITE,
|
|
76
|
-
"src/lib.ts": LIB,
|
|
77
|
-
"src/main.ts": MAIN,
|
|
50
|
+
"shallot.json": MANIFEST,
|
|
51
|
+
"src/env.d.ts": ENV,
|
|
52
|
+
"src/spin.ts": SPIN,
|
|
78
53
|
"public/scenes/scene.scene": SCENE,
|
|
54
|
+
"README.md": readme(name),
|
|
55
|
+
"AGENTS.md": agents(name),
|
|
56
|
+
"CLAUDE.md": agents(name),
|
|
79
57
|
};
|
|
80
58
|
}
|
|
81
59
|
|
|
@@ -108,58 +86,114 @@ const ICON = `<svg id="Shallot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
|
108
86
|
</svg>
|
|
109
87
|
`;
|
|
110
88
|
|
|
111
|
-
const
|
|
89
|
+
const readme = (name: string) => `# ${name}
|
|
112
90
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
91
|
+
A shallot project.
|
|
92
|
+
|
|
93
|
+
## Develop
|
|
94
|
+
|
|
95
|
+
\`\`\`bash
|
|
96
|
+
bun install
|
|
97
|
+
bunx shallot dev
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
\`bun install\` fetches the engine. \`bunx shallot dev\` runs the project with hot
|
|
101
|
+
reload. Edit \`src/spin.ts\` (a plugin) and \`shallot.json\` (the manifest: scene +
|
|
102
|
+
plugin enablement) in your IDE.
|
|
103
|
+
|
|
104
|
+
## Ship
|
|
105
|
+
|
|
106
|
+
\`\`\`bash
|
|
107
|
+
bunx shallot build
|
|
108
|
+
\`\`\`
|
|
109
|
+
|
|
110
|
+
Builds a web bundle to \`dist/\`. For native targets:
|
|
111
|
+
\`bunx shallot build --target windows|mac|linux\` (add \`--release\` for an optimized build).
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
const agents = (name: string) => `# ${name}
|
|
115
|
+
|
|
116
|
+
A WebGPU game built on \`@dylanebert/shallot\`.
|
|
117
|
+
|
|
118
|
+
## Layout
|
|
119
|
+
|
|
120
|
+
- \`shallot.json\` — the manifest: which scene to open + which plugins to enable
|
|
121
|
+
- \`public/scenes/*.scene\` — the world as declarative XML (each \`<a>\` is an entity, each attribute a component)
|
|
122
|
+
- \`src/*.ts\` — your plugins (a plugin is data: components + systems)
|
|
123
|
+
|
|
124
|
+
## Build, run, verify
|
|
125
|
+
|
|
126
|
+
\`\`\`bash
|
|
127
|
+
bunx tsc --noEmit # typecheck — run after every change
|
|
128
|
+
bunx shallot dev # run with hot reload while you work
|
|
129
|
+
bunx shallot build [--target windows|mac|linux] # ship it (web bundle, or a native app)
|
|
130
|
+
bunx shallot verify # prove it: boot headless, check it renders, exit 0/nonzero
|
|
131
|
+
\`\`\`
|
|
132
|
+
|
|
133
|
+
\`shallot verify\` is the verification step — a self-terminating gate that boots the project in a real
|
|
134
|
+
headless browser, waits for a settled frame, and exits nonzero on failure. Nothing left running. \`--json\`
|
|
135
|
+
emits the full result; \`--screenshot <path>\` saves a frame. It drives Playwright, an optional one-time
|
|
136
|
+
install: \`bun add -d playwright && bunx playwright install chromium\` (exit code 3 names this command if
|
|
137
|
+
it's missing). By default it checks the scene rendered; to assert your own pass/fail (entity poses,
|
|
138
|
+
physics state) install \`window.__harness\` via \`installHarness\` from \`@dylanebert/shallot/harness\` — in a
|
|
139
|
+
manifest project like this one, from a plugin's \`initialize(state)\` hook (the engine's AGENTS.md has the
|
|
140
|
+
worked example).
|
|
141
|
+
|
|
142
|
+
## Engine reference
|
|
143
|
+
|
|
144
|
+
The engine is the documentation. Read \`node_modules/@dylanebert/shallot/AGENTS.md\` for the full
|
|
145
|
+
contract (ECS, plugins, scenes, GPU, UI, and the \`shallot verify\` harness), and every public export
|
|
146
|
+
carries JSDoc. The examples index lives at \`node_modules/@dylanebert/shallot/examples/AGENTS.md\` — grep
|
|
147
|
+
it for the problem you have, then read that recipe's source, before writing a pattern from scratch.
|
|
148
|
+
|
|
149
|
+
## Conventions
|
|
150
|
+
|
|
151
|
+
Data-oriented, ECS, declarative. Add components and systems, not methods — a \`Jump\` marker plus a
|
|
152
|
+
system, never \`player.jump()\`. Scenes declare; code transforms. One source of truth: every value has
|
|
153
|
+
one authoritative home; derive, don't duplicate.
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
// The project manifest: `shallot dev` and a shipped `shallot build` both read it. `scene` is the scene
|
|
157
|
+
// to open; `plugins` is enablement — "Orbit": true turns on the orbit camera the scene uses, and
|
|
158
|
+
// "Spin": "./src/spin" declares our own plugin by its module path. The default plugins (render, lit
|
|
159
|
+
// surface) are on unless you set one false.
|
|
160
|
+
const MANIFEST = `{
|
|
161
|
+
"$schema": "./node_modules/@dylanebert/shallot/shallot.schema.json",
|
|
162
|
+
"scene": "scenes/scene.scene",
|
|
163
|
+
"plugins": {
|
|
164
|
+
"Orbit": true,
|
|
165
|
+
"Spin": "./src/spin"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
122
168
|
`;
|
|
123
169
|
|
|
124
|
-
|
|
170
|
+
// Ambient types + the tsconfig anchor. `include: ["src"]` needs at least one matching file, so this
|
|
171
|
+
// keeps `bunx tsc --noEmit` green (no TS18003) even when spin.ts is deleted for a static scene. It's
|
|
172
|
+
// infrastructure, not demo content — don't delete it.
|
|
173
|
+
const ENV = `/// <reference types="@webgpu/types" />
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
const SPIN = `import { type Plugin, type State, type System, Part, quat, Transform } from "@dylanebert/shallot";
|
|
125
177
|
|
|
126
|
-
// A
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// and editable there. Delete it (and the plugin entry below) for a static scene.
|
|
130
|
-
let angle = 0;
|
|
178
|
+
// A plugin is plain data: components + systems the engine runs. This system spins every Part around Y.
|
|
179
|
+
// It runs in the "simulation" group, which plays when the project runs. Delete this file (and its
|
|
180
|
+
// \`shallot.json\` entry) for a static scene.
|
|
131
181
|
const SpinSystem: System = {
|
|
132
182
|
group: "simulation",
|
|
133
183
|
update(state: State) {
|
|
134
|
-
|
|
135
|
-
|
|
184
|
+
// Derive the angle from elapsed time, not a module-level accumulator, so it stays
|
|
185
|
+
// correct after a hot reload or State rebuild rather than carrying stale rotation.
|
|
186
|
+
const q = quat(0, (state.time.elapsed * 45) % 360, 0);
|
|
136
187
|
for (const eid of state.query([Part, Transform])) {
|
|
137
188
|
Transform.rot.set(eid, q.x, q.y, q.z, q.w);
|
|
138
189
|
}
|
|
139
190
|
},
|
|
140
191
|
};
|
|
141
192
|
|
|
142
|
-
// The
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
export const config: Config = {
|
|
147
|
-
plugins: [{ name: "spin", systems: [SpinSystem] }],
|
|
148
|
-
scene: "scenes/scene.scene",
|
|
149
|
-
};
|
|
150
|
-
`;
|
|
151
|
-
|
|
152
|
-
const MAIN = `import { run } from "@dylanebert/shallot";
|
|
153
|
-
import { config } from "./lib";
|
|
154
|
-
|
|
155
|
-
// One config source (src/lib.ts) drives both this standalone build and the editor. The camera
|
|
156
|
-
// auto-binds to the page's <canvas>.
|
|
157
|
-
const { dispose } = await run(config);
|
|
158
|
-
|
|
159
|
-
// HMR re-runs this module — dispose the old State + RAF loop or each reload stacks another.
|
|
160
|
-
if (import.meta.hot) {
|
|
161
|
-
import.meta.hot.dispose(() => dispose());
|
|
162
|
-
}
|
|
193
|
+
// The default export is the plugin — \`shallot.json\` references this file by path and imports
|
|
194
|
+
// its default. The name ("Spin") is how the manifest lists it.
|
|
195
|
+
const SpinPlugin: Plugin = { name: "Spin", systems: [SpinSystem] };
|
|
196
|
+
export default SpinPlugin;
|
|
163
197
|
`;
|
|
164
198
|
|
|
165
199
|
const SCENE = `<scene>
|
|
@@ -175,7 +209,8 @@ const SCENE = `<scene>
|
|
|
175
209
|
`;
|
|
176
210
|
|
|
177
211
|
if (import.meta.main) {
|
|
178
|
-
const
|
|
212
|
+
const args = process.argv.slice(2);
|
|
213
|
+
const name = args.find((a) => !a.startsWith("--"));
|
|
179
214
|
if (!name) {
|
|
180
215
|
console.error("Usage: bun create shallot <project-name>");
|
|
181
216
|
process.exit(1);
|
|
@@ -194,5 +229,5 @@ if (import.meta.main) {
|
|
|
194
229
|
console.log("Next steps:");
|
|
195
230
|
console.log(` cd ${name}`);
|
|
196
231
|
console.log(" bun install");
|
|
197
|
-
console.log("
|
|
232
|
+
console.log(" bunx shallot dev");
|
|
198
233
|
}
|