create-shallot 0.5.1 → 0.6.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 +157 -157
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -1,27 +1,85 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { existsSync, mkdirSync, writeFileSync } from "fs";
4
- import { join, resolve } from "path";
5
-
6
- const name = process.argv[2];
7
- if (!name) {
8
- console.error("Usage: bun create shallot <project-name>");
9
- process.exit(1);
4
+ import { dirname, join, resolve } from "path";
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/* in-repo.
10
+ */
11
+ shallot?: string;
10
12
  }
11
13
 
12
- const dir = resolve(name);
13
- if (existsSync(dir)) {
14
- console.error(`Directory "${name}" already exists`);
15
- process.exit(1);
14
+ /**
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`).
18
+ *
19
+ * the project is pure data — a `shallot.json` manifest + plugin modules + `public/`, no vite
20
+ * boilerplate. the CLI provides every harness over it: `shallot dev` runs it standalone (`bun dev`),
21
+ * `shallot .` opens it in the editor, `shallot build` ships it (web + native targets).
22
+ */
23
+ export function template(name: string, opts: TemplateOptions = {}): Record<string, string> {
24
+ const shallot = opts.shallot ?? "latest";
25
+
26
+ return {
27
+ "public/icon.svg": ICON,
28
+ ".gitignore": "node_modules/\ndist/\nbuild/\n",
29
+ "package.json":
30
+ JSON.stringify(
31
+ {
32
+ name,
33
+ version: "0.0.0",
34
+ private: true,
35
+ type: "module",
36
+ dependencies: { "@dylanebert/shallot": shallot },
37
+ devDependencies: { typescript: "^5.9.3" },
38
+ },
39
+ null,
40
+ 2,
41
+ ) + "\n",
42
+ "tsconfig.json":
43
+ JSON.stringify(
44
+ {
45
+ compilerOptions: {
46
+ target: "ESNext",
47
+ module: "ESNext",
48
+ moduleResolution: "bundler",
49
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
50
+ types: ["@webgpu/types"],
51
+ strict: true,
52
+ noEmit: true,
53
+ skipLibCheck: true,
54
+ },
55
+ include: ["src"],
56
+ },
57
+ null,
58
+ 2,
59
+ ) + "\n",
60
+ "shallot.json": MANIFEST,
61
+ "src/spin.ts": SPIN,
62
+ "public/scenes/scene.scene": SCENE,
63
+ "README.md": readme(name),
64
+ };
16
65
  }
17
66
 
18
- mkdirSync(dir, { recursive: true });
19
- mkdirSync(join(dir, "src"), { recursive: true });
20
- mkdirSync(join(dir, "public/scenes"), { recursive: true });
67
+ /**
68
+ * the committed in-repo template under examples/templates/<dir>. the source of truth for what
69
+ * `bun run build` regenerates and `bun check` gates for staleness.
70
+ */
71
+ export const TEMPLATES: { dir: string }[] = [{ dir: "starter" }];
72
+
73
+ /** write a template file map under dir, creating parent directories as needed. */
74
+ export function scaffold(dir: string, files: Record<string, string>): void {
75
+ for (const [rel, content] of Object.entries(files)) {
76
+ const path = join(dir, rel);
77
+ mkdirSync(dirname(path), { recursive: true });
78
+ writeFileSync(path, content);
79
+ }
80
+ }
21
81
 
22
- writeFileSync(
23
- join(dir, "public/icon.svg"),
24
- `<svg id="Shallot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
82
+ const ICON = `<svg id="Shallot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
25
83
  <title>shallot icon</title>
26
84
  <defs>
27
85
  <radialGradient id="baseGradient" cx="35%" cy="30%" r="70%" fx="25%" fy="20%">
@@ -39,148 +97,74 @@ writeFileSync(
39
97
  <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"/>
40
98
  </g>
41
99
  </svg>
42
- `,
43
- );
44
-
45
- writeFileSync(
46
- join(dir, ".gitignore"),
47
- `node_modules/
48
- dist/
49
- build/
50
- `,
51
- );
52
-
53
- writeFileSync(
54
- join(dir, "package.json"),
55
- JSON.stringify(
56
- {
57
- name,
58
- version: "0.0.0",
59
- private: true,
60
- type: "module",
61
- scripts: {
62
- dev: "vite",
63
- build: "vite build",
64
- preview: "vite preview",
65
- },
66
- dependencies: { "@dylanebert/shallot": "latest" },
67
- devDependencies: { typescript: "^5.9.3", vite: "^8.0.0" },
68
- },
69
- null,
70
- 2,
71
- ) + "\n",
72
- );
73
-
74
- writeFileSync(
75
- join(dir, "tsconfig.json"),
76
- JSON.stringify(
77
- {
78
- compilerOptions: {
79
- target: "ESNext",
80
- module: "ESNext",
81
- moduleResolution: "bundler",
82
- lib: ["ESNext", "DOM", "DOM.Iterable"],
83
- types: ["@webgpu/types", "vite/client"],
84
- strict: true,
85
- noEmit: true,
86
- skipLibCheck: true,
87
- },
88
- include: ["src"],
89
- },
90
- null,
91
- 2,
92
- ) + "\n",
93
- );
94
-
95
- writeFileSync(
96
- join(dir, "index.html"),
97
- `<!doctype html>
98
- <html lang="en">
99
- <head>
100
- <meta charset="UTF-8" />
101
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
102
- <link rel="icon" type="image/svg+xml" href="./icon.svg" />
103
- <title>${name}</title>
104
- <style>
105
- * { box-sizing: border-box; margin: 0; padding: 0; }
106
- body { background: #0c0a09; overflow: hidden; }
107
- canvas { display: block; width: 100vw; height: 100vh; }
108
- </style>
109
- </head>
110
- <body>
111
- <canvas id="canvas"></canvas>
112
- <script type="module" src="./src/main.ts"></script>
113
- </body>
114
- </html>
115
- `,
116
- );
117
-
118
- writeFileSync(
119
- join(dir, "vite.config.ts"),
120
- `import { defineConfig } from "vite";
121
-
122
- export default defineConfig({
123
- base: "./",
124
- server: { port: 3000 },
125
- build: {
126
- target: "esnext",
127
- outDir: "dist",
128
- emptyOutDir: true,
129
- },
130
- });
131
- `,
132
- );
133
-
134
- writeFileSync(
135
- join(dir, "src/lib.ts"),
136
- `import { type Config, Part, quat, type State, type System, Transform } from "@dylanebert/shallot";
137
-
138
- // A system is plain data: an \`update\` the engine runs each frame. This one spins
139
- // every Part around Y. It runs in the standalone app but not the editor — \`shallot .\`
140
- // builds in edit mode, where unannotated systems are skipped, so the cube stays put
141
- // and editable there. Delete it (and the plugin entry below) for a static scene.
142
- let angle = 0;
100
+ `;
101
+
102
+ const readme = (name: string) => `# ${name}
103
+
104
+ A shallot project.
105
+
106
+ ## Develop
107
+
108
+ \`\`\`bash
109
+ bun install
110
+ bunx shallot dev
111
+ \`\`\`
112
+
113
+ \`bun install\` fetches the engine. \`bunx shallot dev\` runs the project with hot
114
+ reload; \`bunx shallot\` opens it in the editor. Edit \`src/spin.ts\` (a plugin) and
115
+ \`shallot.json\` (the manifest: scene + plugin enablement) in your IDE.
116
+
117
+ ## Ship
118
+
119
+ \`\`\`bash
120
+ bunx shallot build
121
+ \`\`\`
122
+
123
+ Builds a web bundle to \`dist/\`. For native targets:
124
+ \`bunx shallot build --target windows|mac|linux\` (add \`--release\` for an optimized build).
125
+ `;
126
+
127
+ // The project manifest: the editor (\`shallot .\`) and a shipped build both read it. \`scene\`
128
+ // is the scene to open; \`plugins\` is enablement — "Orbit": true turns on the orbit camera the
129
+ // scene uses, and "Spin": "./src/spin" declares our own plugin by its module path. The default
130
+ // plugins (render, lit surface) are on unless you set one false. Toggle these in the editor's
131
+ // plugin menu and it rewrites this file, so the editor and a build always agree on what loads.
132
+ const MANIFEST = `{
133
+ "$schema": "./node_modules/@dylanebert/shallot/shallot.schema.json",
134
+ "scene": "scenes/scene.scene",
135
+ "plugins": {
136
+ "Orbit": true,
137
+ "Spin": "./src/spin"
138
+ }
139
+ }
140
+ `;
141
+
142
+ const SPIN = `import { type Plugin, type State, type System, Part, quat, Transform } from "@dylanebert/shallot";
143
+
144
+ // A plugin is plain data: components + systems the engine runs. This system spins every Part
145
+ // around Y. It runs when the project plays (the editor's preview, or a shipped build) but not
146
+ // while you edit — the editor (\`bunx shallot\`) builds in edit mode, where unannotated systems
147
+ // are skipped, so the cube stays put and editable there. Delete this file (and its
148
+ // \`shallot.json\` entry) for a static scene.
143
149
  const SpinSystem: System = {
144
150
  group: "simulation",
145
151
  update(state: State) {
146
- angle = (angle + state.time.deltaTime * 45) % 360;
147
- const q = quat(0, angle, 0);
152
+ // Derive the angle from elapsed time, not a module-level accumulator, so it stays
153
+ // correct after a hot reload or State rebuild rather than carrying stale rotation.
154
+ const q = quat(0, (state.time.elapsed * 45) % 360, 0);
148
155
  for (const eid of state.query([Part, Transform])) {
149
156
  Transform.rot.set(eid, q.x, q.y, q.z, q.w);
150
157
  }
151
158
  },
152
159
  };
153
160
 
154
- // The project definition, shared by the standalone app (src/main.ts) and the shallot
155
- // editor (\`shallot .\`). The default plugins (render, lit surface, orbit camera) stay
156
- // on; the spin plugin adds our one system. The scene lives here too, so main.ts stays
157
- // untouched as the project grows.
158
- export const config: Config = {
159
- plugins: [{ name: "spin", systems: [SpinSystem] }],
160
- scene: "scenes/scene.scene",
161
- };
162
- `,
163
- );
164
-
165
- writeFileSync(
166
- join(dir, "src/main.ts"),
167
- `import { run } from "@dylanebert/shallot";
168
- import { config } from "./lib";
169
-
170
- // One config source (src/lib.ts) drives both this standalone build and the editor. The camera
171
- // auto-binds to the page's <canvas>.
172
- const { dispose } = await run(config);
161
+ // The default export is the plugin — \`shallot.json\` references this file by path and imports
162
+ // its default. The name ("Spin") is how the plugin menu lists it.
163
+ const SpinPlugin: Plugin = { name: "Spin", systems: [SpinSystem] };
164
+ export default SpinPlugin;
165
+ `;
173
166
 
174
- // HMR re-runs this module — dispose the old State + RAF loop or each reload stacks another.
175
- if (import.meta.hot) {
176
- import.meta.hot.dispose(() => dispose());
177
- }
178
- `,
179
- );
180
-
181
- writeFileSync(
182
- join(dir, "public/scenes/scene.scene"),
183
- `<scene>
167
+ const SCENE = `<scene>
184
168
  <a ambient-light="color: 0xd0dcec; intensity: 0.5" />
185
169
  <a directional-light="direction: -0.4 -1 -0.55; color: 0xfff4e0; intensity: 1.1" />
186
170
 
@@ -190,12 +174,28 @@ writeFileSync(
190
174
  <!-- a Part is the engine's drop-in renderable: a mesh (default "cube") wearing a surface (default "default", lit) -->
191
175
  <a part transform="pos: 0 0 0" color="rgba: 0.85 0.55 0.35 1" />
192
176
  </scene>
193
- `,
194
- );
195
-
196
- console.log(`Created ${name}/`);
197
- console.log();
198
- console.log("Next steps:");
199
- console.log(` cd ${name}`);
200
- console.log(" bun install");
201
- console.log(" bun dev");
177
+ `;
178
+
179
+ if (import.meta.main) {
180
+ const args = process.argv.slice(2);
181
+ const name = args.find((a) => !a.startsWith("--"));
182
+ if (!name) {
183
+ console.error("Usage: bun create shallot <project-name>");
184
+ process.exit(1);
185
+ }
186
+
187
+ const dir = resolve(name);
188
+ if (existsSync(dir)) {
189
+ console.error(`Directory "${name}" already exists`);
190
+ process.exit(1);
191
+ }
192
+
193
+ scaffold(dir, template(name));
194
+
195
+ console.log(`Created ${name}/`);
196
+ console.log();
197
+ console.log("Next steps:");
198
+ console.log(` cd ${name}`);
199
+ console.log(" bun install");
200
+ console.log(" bunx shallot dev");
201
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-shallot",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "bin": "./index.ts",
6
6
  "files": ["index.ts"],