create-shallot 0.9.5 → 0.10.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/package.json +27 -8
- package/src/index.ts +23 -0
- package/index.ts +0 -261
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dylan Ebert
|
|
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
package/package.json
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-shallot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0-next.10",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"bin": "./index.ts",
|
|
6
|
-
"files": [
|
|
7
|
-
|
|
5
|
+
"bin": "./src/index.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"!src/**/*.test.ts"
|
|
9
|
+
],
|
|
10
|
+
"description": "Shim for bun create shallot",
|
|
8
11
|
"license": "MIT",
|
|
12
|
+
"author": "Dylan Ebert <dylan.ebert@gmail.com>",
|
|
13
|
+
"packageManager": "bun@1.4.3",
|
|
14
|
+
"engines": {
|
|
15
|
+
"bun": ">=1.4.3"
|
|
16
|
+
},
|
|
9
17
|
"repository": {
|
|
10
18
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/dylanebert/shallot.git"
|
|
12
|
-
|
|
19
|
+
"url": "https://github.com/dylanebert/create-shallot.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/dylanebert/create-shallot",
|
|
22
|
+
"bugs": "https://github.com/dylanebert/create-shallot/issues",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"lint": "biome check",
|
|
25
|
+
"test": "bun test --timeout=250"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@dylanebert/shallot": "0.10.0-next.10"
|
|
13
29
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@biomejs/biome": "^2.5.13",
|
|
32
|
+
"@types/bun": "^1.4.3",
|
|
33
|
+
"@types/node": "^26.2.0"
|
|
34
|
+
}
|
|
16
35
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const engineEntry = fileURLToPath(import.meta.resolve("@dylanebert/shallot"));
|
|
9
|
+
const engineRoot = resolve(dirname(engineEntry), "..");
|
|
10
|
+
const enginePackage = JSON.parse(readFileSync(resolve(engineRoot, "package.json"), "utf8")) as {
|
|
11
|
+
bin: { shallot: string };
|
|
12
|
+
};
|
|
13
|
+
const engineCli = resolve(engineRoot, enginePackage.bin.shallot);
|
|
14
|
+
const result = spawnSync(process.execPath, [engineCli, "create", ...process.argv.slice(2)], {
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (result.error) {
|
|
19
|
+
console.error(result.error);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
if (result.signal) process.kill(process.pid, result.signal);
|
|
23
|
+
else process.exit(result.status ?? 1);
|
package/index.ts
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
-
import { dirname, join, resolve } from "path";
|
|
5
|
-
|
|
6
|
-
// the scaffold pins @dylanebert/shallot to the scaffold's own version — lockstep-gated by
|
|
7
|
-
// check-versions.ts — so a stale scaffold can't install a newer engine whose peer ranges the
|
|
8
|
-
// typegpu/unplugin-typegpu pins no longer satisfy. tilde matches the sibling pins' intent (patch-only
|
|
9
|
-
// for 0.x) and shallot's 0.x cadence where each minor may break peers.
|
|
10
|
-
const shallotRange = `~${(JSON.parse(readFileSync(join(import.meta.dir, "package.json"), "utf8")) as { version: string }).version}`;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* the project files keyed by relative path, with the project name interpolated. the single source of
|
|
14
|
-
* truth for `bun create shallot <name>`.
|
|
15
|
-
*
|
|
16
|
-
* the project is pure data — a `shallot.json` manifest + plugin modules + `public/`, no vite
|
|
17
|
-
* boilerplate. the CLI provides every harness over it: `shallot dev` runs it standalone with hot
|
|
18
|
-
* reload, `shallot build` ships it (web + native targets). the emitted AGENTS.md points an agent at
|
|
19
|
-
* the installed engine's contract, and CLAUDE.md imports it.
|
|
20
|
-
*/
|
|
21
|
-
export function template(name: string): Record<string, string> {
|
|
22
|
-
return {
|
|
23
|
-
"public/icon.svg": ICON,
|
|
24
|
-
".gitignore": "node_modules/\ndist/\nbuild/\n",
|
|
25
|
-
"package.json":
|
|
26
|
-
JSON.stringify(
|
|
27
|
-
{
|
|
28
|
-
name,
|
|
29
|
-
version: "0.0.0",
|
|
30
|
-
private: true,
|
|
31
|
-
type: "module",
|
|
32
|
-
dependencies: { "@dylanebert/shallot": shallotRange, typegpu: "~0.12.0" },
|
|
33
|
-
devDependencies: { "unplugin-typegpu": "~0.12.1", typescript: "^7.0.2" },
|
|
34
|
-
},
|
|
35
|
-
null,
|
|
36
|
-
2,
|
|
37
|
-
) + "\n",
|
|
38
|
-
"tsconfig.json":
|
|
39
|
-
JSON.stringify(
|
|
40
|
-
{
|
|
41
|
-
compilerOptions: {
|
|
42
|
-
target: "ESNext",
|
|
43
|
-
module: "ESNext",
|
|
44
|
-
moduleResolution: "bundler",
|
|
45
|
-
lib: ["ESNext", "DOM", "DOM.Iterable"],
|
|
46
|
-
types: ["@webgpu/types"],
|
|
47
|
-
strict: true,
|
|
48
|
-
noEmit: true,
|
|
49
|
-
skipLibCheck: true,
|
|
50
|
-
},
|
|
51
|
-
include: ["src"],
|
|
52
|
-
},
|
|
53
|
-
null,
|
|
54
|
-
2,
|
|
55
|
-
) + "\n",
|
|
56
|
-
"shallot.json": MANIFEST,
|
|
57
|
-
"src/env.d.ts": ENV,
|
|
58
|
-
"src/spin.ts": SPIN,
|
|
59
|
-
"public/scenes/scene.scene": SCENE,
|
|
60
|
-
"README.md": readme(name),
|
|
61
|
-
"AGENTS.md": agents(name),
|
|
62
|
-
"CLAUDE.md": CLAUDE_IMPORT,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// one contract, two entrypoints: Codex reads AGENTS.md, Claude Code reads CLAUDE.md and expands the
|
|
67
|
-
// `@`-import. An import, not a symlink — a Windows checkout without developer mode gets a literal
|
|
68
|
-
// text file from a symlink. The trailing sentence is the cost of that choice: the import expands only
|
|
69
|
-
// for a session rooted in this file's own directory, so opened from a parent the line is literal text
|
|
70
|
-
// and the prose pointer is all the reader gets. Kept identical to the copy-out's stanza in
|
|
71
|
-
// `packages/shallot/bin/scaffold.ts`.
|
|
72
|
-
const CLAUDE_IMPORT = `@AGENTS.md
|
|
73
|
-
|
|
74
|
-
If the import line above is showing as literal text, this file was loaded from a parent directory; read the AGENTS.md next to this file before working here.
|
|
75
|
-
`;
|
|
76
|
-
|
|
77
|
-
/** write a template file map under dir, creating parent directories as needed. */
|
|
78
|
-
export function scaffold(dir: string, files: Record<string, string>): void {
|
|
79
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
80
|
-
const path = join(dir, rel);
|
|
81
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
82
|
-
writeFileSync(path, content);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const ICON = `<svg id="Shallot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
|
|
87
|
-
<title>shallot icon</title>
|
|
88
|
-
<defs>
|
|
89
|
-
<radialGradient id="baseGradient" cx="35%" cy="30%" r="70%" fx="25%" fy="20%">
|
|
90
|
-
<stop offset="0%" stop-color="#F5D4B8"/>
|
|
91
|
-
<stop offset="45%" stop-color="#E8A86B"/>
|
|
92
|
-
<stop offset="100%" stop-color="#B87654"/>
|
|
93
|
-
</radialGradient>
|
|
94
|
-
</defs>
|
|
95
|
-
<g transform="rotate(35 40.0 40.0)">
|
|
96
|
-
<path d="M40,2 C44,10 66,28 66,46 C66,60 48,70 40,78 C32,70 14,60 14,46 C14,28 36,10 40,2 Z" fill="#E8A86B"/>
|
|
97
|
-
<path d="M40,6 C37,14 22,28 20,44 C20,52 28,62 36,70 C34,58 26,46 26,38 C26,26 38,12 40,6 Z" fill="#D49560"/>
|
|
98
|
-
<path d="M40,6 C43,14 58,28 60,44 C60,52 52,62 44,70 C46,58 54,46 54,38 C54,26 42,12 40,6 Z" fill="#D49560"/>
|
|
99
|
-
<path d="M40,8 C40,20 40,50 40,72" stroke="#6B4230" stroke-width="1" stroke-opacity="0.4" fill="none" stroke-linecap="round"/>
|
|
100
|
-
<path d="M40,78 C48,70 66,60 66,46 C61,58 44,70 40,73 Z" fill="#D49560"/>
|
|
101
|
-
<path d="M40,2 C44,10 66,28 66,46 C66,60 48,70 40,78 C32,70 14,60 14,46 C14,28 36,10 40,2 Z" fill="none" stroke="#6B4230" stroke-width="2"/>
|
|
102
|
-
</g>
|
|
103
|
-
</svg>
|
|
104
|
-
`;
|
|
105
|
-
|
|
106
|
-
const readme = (name: string) => `# ${name}
|
|
107
|
-
|
|
108
|
-
A shallot project.
|
|
109
|
-
|
|
110
|
-
## Develop
|
|
111
|
-
|
|
112
|
-
\`\`\`bash
|
|
113
|
-
bun install
|
|
114
|
-
bunx shallot dev
|
|
115
|
-
\`\`\`
|
|
116
|
-
|
|
117
|
-
\`bun install\` fetches the engine. \`bunx shallot dev\` runs the project with hot
|
|
118
|
-
reload. Edit \`src/spin.ts\` (a plugin) and \`shallot.json\` (the manifest: scene +
|
|
119
|
-
plugin enablement) in your IDE.
|
|
120
|
-
|
|
121
|
-
## Ship
|
|
122
|
-
|
|
123
|
-
\`\`\`bash
|
|
124
|
-
bunx shallot build
|
|
125
|
-
\`\`\`
|
|
126
|
-
|
|
127
|
-
Builds a web bundle to \`dist/\`. Native targets
|
|
128
|
-
(\`--target windows|mac|linux\`) download a prebuilt release shell when one exists
|
|
129
|
-
for your installed version, and otherwise fall back to compiling from source,
|
|
130
|
-
which needs the Rust toolchain and target system dependencies; see the repo
|
|
131
|
-
README for the per-target table.
|
|
132
|
-
`;
|
|
133
|
-
|
|
134
|
-
const agents = (name: string) => `# ${name}
|
|
135
|
-
|
|
136
|
-
A WebGPU game built on \`@dylanebert/shallot\`.
|
|
137
|
-
|
|
138
|
-
## Layout
|
|
139
|
-
|
|
140
|
-
- \`shallot.json\` — the manifest: which scene to open + which plugins to enable
|
|
141
|
-
- \`public/scenes/*.scene\` — the world as declarative XML (each \`<a>\` is an entity, each attribute a component)
|
|
142
|
-
- \`src/*.ts\` — your plugins (a plugin is data: components + systems)
|
|
143
|
-
|
|
144
|
-
## Build, run, verify
|
|
145
|
-
|
|
146
|
-
\`\`\`bash
|
|
147
|
-
bunx tsc --noEmit # typecheck — run after every change
|
|
148
|
-
bunx shallot dev # run with hot reload while you work
|
|
149
|
-
bunx shallot build # ship it (web bundle to dist/)
|
|
150
|
-
bunx shallot verify # prove it: boot headless, check it renders, exit 0/nonzero
|
|
151
|
-
\`\`\`
|
|
152
|
-
|
|
153
|
-
\`shallot verify\` is the verification step — a self-terminating gate that boots the project in a real
|
|
154
|
-
headless browser, waits for a settled frame, and exits nonzero on failure. Nothing left running. \`--json\`
|
|
155
|
-
emits the full result; \`--screenshot <path>\` saves a frame. It drives Playwright, an optional one-time
|
|
156
|
-
install: \`bun add -d playwright && bunx playwright install chromium\` (exit code 3 names this command if
|
|
157
|
-
it's missing). By default it checks the scene rendered; to assert your own pass/fail (entity poses,
|
|
158
|
-
physics state) install \`window.__harness\` via \`installHarness\` from \`@dylanebert/shallot/harness\` — in a
|
|
159
|
-
manifest project like this one, from a plugin's \`initialize(state)\` hook (the engine's AGENTS.md has the
|
|
160
|
-
worked example).
|
|
161
|
-
|
|
162
|
-
## Engine reference
|
|
163
|
-
|
|
164
|
-
The engine is the documentation. Read \`node_modules/@dylanebert/shallot/AGENTS.md\` for the full
|
|
165
|
-
contract (ECS, plugins, scenes, GPU, UI, and the \`shallot verify\` harness), and every public export
|
|
166
|
-
carries JSDoc. The examples index lives at \`node_modules/@dylanebert/shallot/examples/AGENTS.md\` — grep
|
|
167
|
-
it for the problem you have, then read that recipe's source, before writing a pattern from scratch.
|
|
168
|
-
\`bunx shallot recipe <name> [dir]\` copies a recipe out of the installed package into a runnable project
|
|
169
|
-
(bare: lists them).
|
|
170
|
-
|
|
171
|
-
## Conventions
|
|
172
|
-
|
|
173
|
-
Data-oriented, ECS, declarative. Add components and systems, not methods — a \`Jump\` marker plus a
|
|
174
|
-
system, never \`player.jump()\`. Scenes declare; code transforms. One source of truth: every value has
|
|
175
|
-
one authoritative home; derive, don't duplicate.
|
|
176
|
-
`;
|
|
177
|
-
|
|
178
|
-
// The project manifest: `shallot dev` and a shipped `shallot build` both read it. `scene` is the scene
|
|
179
|
-
// to open; `plugins` is enablement — "Orbit": true turns on the orbit camera the scene uses, and
|
|
180
|
-
// "Spin": "./src/spin" declares our own plugin by its module path. The default plugins (render, lit
|
|
181
|
-
// surface) are on unless you set one false.
|
|
182
|
-
const MANIFEST = `{
|
|
183
|
-
"$schema": "./node_modules/@dylanebert/shallot/shallot.schema.json",
|
|
184
|
-
"scene": "scenes/scene.scene",
|
|
185
|
-
"plugins": {
|
|
186
|
-
"Orbit": true,
|
|
187
|
-
"Spin": "./src/spin"
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
`;
|
|
191
|
-
|
|
192
|
-
// Ambient types + the tsconfig anchor. `include: ["src"]` needs at least one matching file, so this
|
|
193
|
-
// keeps `bunx tsc --noEmit` green (no TS18003) even when spin.ts is deleted for a static scene. It's
|
|
194
|
-
// infrastructure, not demo content — don't delete it.
|
|
195
|
-
const ENV = `/// <reference types="@webgpu/types" />
|
|
196
|
-
`;
|
|
197
|
-
|
|
198
|
-
const SPIN = `import { type Plugin, type State, type System, Part, quat, Transform } from "@dylanebert/shallot";
|
|
199
|
-
|
|
200
|
-
// A plugin is plain data: components + systems the engine runs. This system spins every Part around Y.
|
|
201
|
-
// It runs in the "simulation" group, which plays when the project runs. Delete this file (and its
|
|
202
|
-
// \`shallot.json\` entry) for a static scene.
|
|
203
|
-
const SpinSystem: System = {
|
|
204
|
-
group: "simulation",
|
|
205
|
-
update(state: State) {
|
|
206
|
-
// Derive the angle from elapsed time, not a module-level accumulator, so it stays
|
|
207
|
-
// correct after a hot reload or State rebuild rather than carrying stale rotation.
|
|
208
|
-
const q = quat(0, (state.time.elapsed * 45) % 360, 0);
|
|
209
|
-
for (const eid of state.query([Part, Transform])) {
|
|
210
|
-
Transform.rot.set(eid, q.x, q.y, q.z, q.w);
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// The default export is the plugin — \`shallot.json\` references this file by path and imports
|
|
216
|
-
// its default. The name ("Spin") is how the manifest lists it.
|
|
217
|
-
const SpinPlugin: Plugin = { name: "Spin", systems: [SpinSystem] };
|
|
218
|
-
export default SpinPlugin;
|
|
219
|
-
`;
|
|
220
|
-
|
|
221
|
-
const SCENE = `<scene>
|
|
222
|
-
<a ambient-light="color: 0xd0dcec; intensity: 0.5" />
|
|
223
|
-
<a directional-light="direction: -0.4 -1 -0.55; color: 0xfff4e0; intensity: 1.1" />
|
|
224
|
-
|
|
225
|
-
<!-- the camera auto-binds to the page's <canvas>; drag to orbit, scroll to zoom -->
|
|
226
|
-
<a camera sear orbit="distance: 5; yaw: 0.6; pitch: 0.25" transform />
|
|
227
|
-
|
|
228
|
-
<!-- a Part is the engine's drop-in renderable: a mesh (default "cube") wearing a surface (default "default", lit) -->
|
|
229
|
-
<a part transform="pos: 0 0 0" color="rgba: 0.85 0.55 0.35 1" />
|
|
230
|
-
</scene>
|
|
231
|
-
`;
|
|
232
|
-
|
|
233
|
-
/** `bun create shallot <project-name>` — parse argv, guard the target dir, scaffold, report next steps.
|
|
234
|
-
* Returns the process exit code rather than calling `process.exit` itself, so it's callable directly. */
|
|
235
|
-
export function main(argv: string[]): number {
|
|
236
|
-
const name = argv.find((a) => !a.startsWith("--"));
|
|
237
|
-
if (!name) {
|
|
238
|
-
console.error("Usage: bun create shallot <project-name>");
|
|
239
|
-
return 1;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const dir = resolve(name);
|
|
243
|
-
if (existsSync(dir)) {
|
|
244
|
-
console.error(`Directory "${name}" already exists`);
|
|
245
|
-
return 1;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
scaffold(dir, template(name));
|
|
249
|
-
|
|
250
|
-
console.log(`Created ${name}/`);
|
|
251
|
-
console.log();
|
|
252
|
-
console.log("Next steps:");
|
|
253
|
-
console.log(` cd ${name}`);
|
|
254
|
-
console.log(" bun install");
|
|
255
|
-
console.log(" bunx shallot dev");
|
|
256
|
-
return 0;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (import.meta.main) {
|
|
260
|
-
process.exit(main(process.argv.slice(2)));
|
|
261
|
-
}
|