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.
Files changed (2) hide show
  1. package/index.ts +115 -80
  2. 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 both `bun create shallot <name>` and the in-repo examples/templates/starter example
17
- * (regenerated by `bun run build`).
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, opts: TemplateOptions = {}): Record<string, 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
- scripts: { dev: "vite", build: "vite build", preview: "vite preview" },
32
- dependencies: { "@dylanebert/shallot": shallot },
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", "vite/client"],
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
- "index.html": `<!doctype html>
57
- <html lang="en">
58
- <head>
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 VITE = `import { defineConfig } from "vite";
89
+ const readme = (name: string) => `# ${name}
112
90
 
113
- export default defineConfig({
114
- base: "./",
115
- server: { port: 3000 },
116
- build: {
117
- target: "esnext",
118
- outDir: "dist",
119
- emptyOutDir: true,
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
- const LIB = `import { type Config, Part, quat, type State, type System, Transform } from "@dylanebert/shallot";
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 system is plain data: an \`update\` the engine runs each frame. This one spins
127
- // every Part around Y. It runs in the standalone app but not the editor — \`shallot .\`
128
- // builds in edit mode, where unannotated systems are skipped, so the cube stays put
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
- angle = (angle + state.time.deltaTime * 45) % 360;
135
- const q = quat(0, angle, 0);
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 project definition, shared by the standalone app (src/main.ts) and the shallot
143
- // editor (\`shallot .\`). The default plugins (render, lit surface, orbit camera) stay
144
- // on; the spin plugin adds our one system. The scene lives here too, so main.ts stays
145
- // untouched as the project grows.
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 name = process.argv[2];
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(" bun dev");
232
+ console.log(" bunx shallot dev");
198
233
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-shallot",
3
- "version": "0.5.2",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "bin": "./index.ts",
6
6
  "files": ["index.ts"],