@flare-engine/create 0.4.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 (55) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +2 -0
  3. package/README.md +43 -0
  4. package/dist/bin.js +10 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/bin.mjs +12 -0
  7. package/dist/bin.mjs.map +1 -0
  8. package/dist/cli-BGXZ73ie.mjs +189 -0
  9. package/dist/cli-BGXZ73ie.mjs.map +1 -0
  10. package/dist/cli-JzwtwZhm.js +230 -0
  11. package/dist/cli-JzwtwZhm.js.map +1 -0
  12. package/dist/index.d.mts +101 -0
  13. package/dist/index.d.mts.map +1 -0
  14. package/dist/index.d.ts +101 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +9 -0
  17. package/dist/index.mjs +2 -0
  18. package/package.json +61 -0
  19. package/templates/blank/README.md +59 -0
  20. package/templates/blank/app.json +10 -0
  21. package/templates/blank/babel.config.js +7 -0
  22. package/templates/blank/gitignore +8 -0
  23. package/templates/blank/index.ts +4 -0
  24. package/templates/blank/index.web.ts +17 -0
  25. package/templates/blank/metro.config.js +3 -0
  26. package/templates/blank/package.json +36 -0
  27. package/templates/blank/scripts/copy-canvaskit.mjs +15 -0
  28. package/templates/blank/src/App.tsx +45 -0
  29. package/templates/blank/tsconfig.json +15 -0
  30. package/templates/shmup/README.md +59 -0
  31. package/templates/shmup/app.json +10 -0
  32. package/templates/shmup/babel.config.js +7 -0
  33. package/templates/shmup/gitignore +8 -0
  34. package/templates/shmup/index.ts +4 -0
  35. package/templates/shmup/index.web.ts +17 -0
  36. package/templates/shmup/metro.config.js +3 -0
  37. package/templates/shmup/package.json +39 -0
  38. package/templates/shmup/scripts/copy-canvaskit.mjs +15 -0
  39. package/templates/shmup/src/App.tsx +171 -0
  40. package/templates/shmup/src/logic.ts +356 -0
  41. package/templates/shmup/tests/logic.test.ts +235 -0
  42. package/templates/shmup/tsconfig.json +15 -0
  43. package/templates/topdown/README.md +79 -0
  44. package/templates/topdown/app.json +10 -0
  45. package/templates/topdown/babel.config.js +7 -0
  46. package/templates/topdown/gitignore +8 -0
  47. package/templates/topdown/index.ts +4 -0
  48. package/templates/topdown/index.web.ts +17 -0
  49. package/templates/topdown/metro.config.js +3 -0
  50. package/templates/topdown/package.json +39 -0
  51. package/templates/topdown/scripts/copy-canvaskit.mjs +15 -0
  52. package/templates/topdown/src/App.tsx +138 -0
  53. package/templates/topdown/src/map.ts +126 -0
  54. package/templates/topdown/tests/tilemap.test.ts +201 -0
  55. package/templates/topdown/tsconfig.json +15 -0
@@ -0,0 +1,230 @@
1
+ let commander = require("commander");
2
+ let node_child_process = require("node:child_process");
3
+ let node_fs_promises = require("node:fs/promises");
4
+ let node_path = require("node:path");
5
+ let node_url = require("node:url");
6
+ //#region package.json
7
+ var version = "0.4.0";
8
+ //#endregion
9
+ //#region src/manifest.ts
10
+ /** Built-in templates shipped inside `@flare-engine/create`. */
11
+ const TEMPLATES = {
12
+ blank: {
13
+ id: "blank",
14
+ displayName: "Blank",
15
+ description: "Empty scene with a single sprite — smallest starting point.",
16
+ engines: { node: ">=20" },
17
+ postInstall: "bun run start"
18
+ },
19
+ topdown: {
20
+ id: "topdown",
21
+ displayName: "Top-down",
22
+ description: "Analog player movement with tilemap collision + camera follow.",
23
+ engines: { node: ">=20" },
24
+ postInstall: "bun run start"
25
+ },
26
+ shmup: {
27
+ id: "shmup",
28
+ displayName: "Shoot-em-up",
29
+ description: "Auto-fire player, enemy spawner, HUD score — mirrors a minimum vertical-slice shmup.",
30
+ engines: { node: ">=20" },
31
+ postInstall: "bun run start"
32
+ }
33
+ };
34
+ function isTemplateId(value) {
35
+ return Object.hasOwn(TEMPLATES, value);
36
+ }
37
+ //#endregion
38
+ //#region src/scaffold.ts
39
+ const DEFAULT_TEMPLATES_DIR = (0, node_path.resolve)((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "..", "templates");
40
+ const VALID_APP_NAME = /^[a-z0-9@/_-]+$/i;
41
+ /** Returns true when the name is safe to use as an app name and shell argument. */
42
+ function isValidAppName(name) {
43
+ return VALID_APP_NAME.test(name);
44
+ }
45
+ /** Detect the package manager from `--package-manager` > `npm_config_user_agent` > `bun`. */
46
+ function detectPackageManager(explicit) {
47
+ if (explicit === "bun" || explicit === "npm" || explicit === "pnpm") return explicit;
48
+ const agent = process.env.npm_config_user_agent ?? "";
49
+ if (agent.startsWith("bun")) return "bun";
50
+ if (agent.startsWith("pnpm")) return "pnpm";
51
+ if (agent.startsWith("npm") || agent.startsWith("yarn")) return "npm";
52
+ return "bun";
53
+ }
54
+ async function pathExists(p) {
55
+ try {
56
+ await (0, node_fs_promises.stat)(p);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ const TOKEN_FILE_EXTS = /* @__PURE__ */ new Set([
63
+ ".ts",
64
+ ".tsx",
65
+ ".js",
66
+ ".jsx",
67
+ ".json",
68
+ ".md",
69
+ ".txt",
70
+ ".html",
71
+ ".css"
72
+ ]);
73
+ function shouldApplyTokens(path) {
74
+ const dot = path.lastIndexOf(".");
75
+ if (dot < 0) return false;
76
+ return TOKEN_FILE_EXTS.has(path.slice(dot).toLowerCase());
77
+ }
78
+ function replaceTokens(content, tokens) {
79
+ let out = content;
80
+ for (const [k, v] of Object.entries(tokens)) out = out.split(k).join(v);
81
+ return out;
82
+ }
83
+ async function walkFiles(dir, base, out) {
84
+ const entries = await (0, node_fs_promises.readdir)(dir, { withFileTypes: true });
85
+ for (const e of entries) {
86
+ const full = (0, node_path.join)(dir, e.name);
87
+ const rel = full.slice(base.length + 1);
88
+ if (e.isDirectory()) await walkFiles(full, base, out);
89
+ else if (e.isFile()) out.push(rel);
90
+ }
91
+ }
92
+ /** Returns the spawn configuration for the given package manager on the given platform. */
93
+ function installSpawnSpec(pm, platform) {
94
+ return {
95
+ cmd: pm,
96
+ shell: platform === "win32"
97
+ };
98
+ }
99
+ function runInstall(pm, cwd) {
100
+ return new Promise((resolvePromise) => {
101
+ const { cmd, shell } = installSpawnSpec(pm, process.platform);
102
+ const child = (0, node_child_process.spawn)(cmd, ["install"], {
103
+ cwd,
104
+ stdio: "inherit",
105
+ shell
106
+ });
107
+ child.on("exit", (code) => resolvePromise(code ?? 1));
108
+ child.on("error", () => resolvePromise(1));
109
+ });
110
+ }
111
+ /**
112
+ * Copy the chosen template into `targetDir`, apply token replacements, and
113
+ * optionally run the package manager's install. Fails if the target exists
114
+ * and is non-empty.
115
+ */
116
+ async function scaffoldApp(opts) {
117
+ if (!isValidAppName(opts.name)) throw new Error(`[flare-create] invalid app name: ${opts.name}`);
118
+ const templateId = opts.template ?? "blank";
119
+ if (!isTemplateId(templateId)) throw new Error(`[flare-create] unknown template: ${templateId}`);
120
+ if (!TEMPLATES[templateId]) throw new Error(`[flare-create] missing manifest for ${templateId}`);
121
+ const cwd = process.cwd();
122
+ const target = opts.targetDir ? (0, node_path.isAbsolute)(opts.targetDir) ? opts.targetDir : (0, node_path.resolve)(cwd, opts.targetDir) : (0, node_path.resolve)(cwd, opts.name);
123
+ if (await pathExists(target)) {
124
+ if ((await (0, node_fs_promises.readdir)(target)).length > 0) throw new Error(`[flare-create] target ${target} already exists and is not empty`);
125
+ } else await (0, node_fs_promises.mkdir)(target, { recursive: true });
126
+ const src = (0, node_path.join)(opts.templatesDir ?? DEFAULT_TEMPLATES_DIR, templateId);
127
+ if (!await pathExists(src)) throw new Error(`[flare-create] template directory not found: ${src}`);
128
+ await (0, node_fs_promises.cp)(src, target, { recursive: true });
129
+ const gi = (0, node_path.join)(target, "gitignore");
130
+ if (await pathExists(gi)) await (0, node_fs_promises.rename)(gi, (0, node_path.join)(target, ".gitignore"));
131
+ const tokens = {
132
+ "{{APP_NAME}}": opts.name,
133
+ "{{FLARE_VERSION}}": opts.flareVersion ?? `^${version}`,
134
+ "{{TEMPLATE_ID}}": templateId
135
+ };
136
+ const files = [];
137
+ await walkFiles(target, target, files);
138
+ for (const rel of files) {
139
+ if (!shouldApplyTokens(rel)) continue;
140
+ const p = (0, node_path.join)(target, rel);
141
+ const content = await (0, node_fs_promises.readFile)(p, "utf8");
142
+ const next = replaceTokens(content, tokens);
143
+ if (next !== content) await (0, node_fs_promises.writeFile)(p, next, "utf8");
144
+ }
145
+ const pm = detectPackageManager(opts.packageManager);
146
+ let installed = false;
147
+ if (!opts.skipInstall) {
148
+ const code = await runInstall(pm, target);
149
+ if (code !== 0) throw new Error(`[flare-create] ${pm} install exited with code ${code}`);
150
+ installed = true;
151
+ }
152
+ return {
153
+ targetDir: target,
154
+ template: templateId,
155
+ packageManager: pm,
156
+ installed
157
+ };
158
+ }
159
+ //#endregion
160
+ //#region src/cli.ts
161
+ function parseTemplateArg(value) {
162
+ if (value !== "blank" && value !== "topdown" && value !== "shmup") throw new Error(`invalid --template: ${value} (expected blank|topdown|shmup)`);
163
+ return value;
164
+ }
165
+ function parsePmArg(value) {
166
+ if (value !== "bun" && value !== "npm" && value !== "pnpm") throw new Error(`invalid --package-manager: ${value} (expected bun|npm|pnpm)`);
167
+ return value;
168
+ }
169
+ /** Build the `flare-create` CLI program. Exposed for tests. */
170
+ function buildProgram() {
171
+ const program = new commander.Command();
172
+ program.name("flare-create").description("Scaffold a new Flare engine app").version(version).argument("<name>", "directory name / package name for the new app").option("-t, --template <name>", "template (blank|topdown|shmup)", parseTemplateArg, "blank").option("-p, --package-manager <pm>", "package manager (bun|npm|pnpm)", parsePmArg).option("--skip-install", "skip running install after scaffolding", false).action(async (name, opts) => {
173
+ const result = await scaffoldApp({
174
+ name,
175
+ template: opts.template,
176
+ packageManager: opts.packageManager,
177
+ skipInstall: opts.skipInstall === true
178
+ });
179
+ const manifest = TEMPLATES[result.template];
180
+ process.stdout.write(`[flare-create] scaffolded ${manifest?.displayName ?? result.template} at ${result.targetDir}\n`);
181
+ if (!result.installed) process.stdout.write(`[flare-create] skipped install — run \`cd ${name} && ${result.packageManager} install\` when ready\n`);
182
+ else if (manifest?.postInstall) process.stdout.write(`[flare-create] next: cd ${name} && ${manifest.postInstall}\n`);
183
+ });
184
+ return program;
185
+ }
186
+ //#endregion
187
+ Object.defineProperty(exports, "TEMPLATES", {
188
+ enumerable: true,
189
+ get: function() {
190
+ return TEMPLATES;
191
+ }
192
+ });
193
+ Object.defineProperty(exports, "buildProgram", {
194
+ enumerable: true,
195
+ get: function() {
196
+ return buildProgram;
197
+ }
198
+ });
199
+ Object.defineProperty(exports, "detectPackageManager", {
200
+ enumerable: true,
201
+ get: function() {
202
+ return detectPackageManager;
203
+ }
204
+ });
205
+ Object.defineProperty(exports, "installSpawnSpec", {
206
+ enumerable: true,
207
+ get: function() {
208
+ return installSpawnSpec;
209
+ }
210
+ });
211
+ Object.defineProperty(exports, "isTemplateId", {
212
+ enumerable: true,
213
+ get: function() {
214
+ return isTemplateId;
215
+ }
216
+ });
217
+ Object.defineProperty(exports, "isValidAppName", {
218
+ enumerable: true,
219
+ get: function() {
220
+ return isValidAppName;
221
+ }
222
+ });
223
+ Object.defineProperty(exports, "scaffoldApp", {
224
+ enumerable: true,
225
+ get: function() {
226
+ return scaffoldApp;
227
+ }
228
+ });
229
+
230
+ //# sourceMappingURL=cli-JzwtwZhm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-JzwtwZhm.js","names":["Command"],"sources":["../package.json","../src/manifest.ts","../src/scaffold.ts","../src/cli.ts"],"sourcesContent":["","/** Declared metadata for a `@flare-engine/create` template. */\nexport interface TemplateManifest {\n /** Template directory name (`blank`, `topdown`, `shmup`, ...). */\n id: string;\n /** Human-readable name shown in logs. */\n displayName: string;\n /** One-line description of what the template produces. */\n description: string;\n /** Engines required — mirrors Node/npm engines field in scaffolded package.json. */\n engines: { node: string };\n /** Optional post-install instructions printed after scaffold completes. */\n postInstall?: string;\n}\n\n/** Built-in templates shipped inside `@flare-engine/create`. */\nexport const TEMPLATES = {\n blank: {\n id: 'blank',\n displayName: 'Blank',\n description: 'Empty scene with a single sprite — smallest starting point.',\n engines: { node: '>=20' },\n postInstall: 'bun run start',\n },\n topdown: {\n id: 'topdown',\n displayName: 'Top-down',\n description: 'Analog player movement with tilemap collision + camera follow.',\n engines: { node: '>=20' },\n postInstall: 'bun run start',\n },\n shmup: {\n id: 'shmup',\n displayName: 'Shoot-em-up',\n description:\n 'Auto-fire player, enemy spawner, HUD score — mirrors a minimum vertical-slice shmup.',\n engines: { node: '>=20' },\n postInstall: 'bun run start',\n },\n} satisfies Record<string, TemplateManifest>;\n\nexport type TemplateId = keyof typeof TEMPLATES;\n\nexport function isTemplateId(value: string): value is TemplateId {\n return Object.hasOwn(TEMPLATES, value);\n}\n","import { spawn } from 'node:child_process';\nimport { cp, mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { version } from '../package.json';\nimport { isTemplateId, TEMPLATES, type TemplateId } from './manifest.js';\n\n/** Options for `scaffoldApp`. */\nexport interface ScaffoldOptions {\n /** App name — also used as the target directory and `name` in package.json. */\n name: string;\n /** Template id (default: `blank`). */\n template?: string;\n /** Destination directory (default: `<cwd>/<name>`). */\n targetDir?: string;\n /** Package manager used for `engines` + post-install command. */\n packageManager?: 'bun' | 'npm' | 'pnpm';\n /** Skip `install` after copying (default: false). */\n skipInstall?: boolean;\n /** Pinned Flare version for template {{FLARE_VERSION}} token (default: current package version prefixed with ^). */\n flareVersion?: string;\n /** Override templates root — used by tests. */\n templatesDir?: string;\n}\n\n/** Result returned by `scaffoldApp`. */\nexport interface ScaffoldResult {\n /** Absolute path of the scaffolded app. */\n targetDir: string;\n /** Resolved template id. */\n template: TemplateId;\n /** Package manager used. */\n packageManager: 'bun' | 'npm' | 'pnpm';\n /** Whether install was run. */\n installed: boolean;\n}\n\nconst FILENAME = fileURLToPath(import.meta.url);\nconst DEFAULT_TEMPLATES_DIR = resolve(dirname(FILENAME), '..', 'templates');\n\nconst VALID_APP_NAME = /^[a-z0-9@/_-]+$/i;\n\n/** Returns true when the name is safe to use as an app name and shell argument. */\nexport function isValidAppName(name: string): boolean {\n return VALID_APP_NAME.test(name);\n}\n\n/** Detect the package manager from `--package-manager` > `npm_config_user_agent` > `bun`. */\nexport function detectPackageManager(explicit: string | undefined): 'bun' | 'npm' | 'pnpm' {\n if (explicit === 'bun' || explicit === 'npm' || explicit === 'pnpm') return explicit;\n const agent = process.env.npm_config_user_agent ?? '';\n if (agent.startsWith('bun')) return 'bun';\n if (agent.startsWith('pnpm')) return 'pnpm';\n if (agent.startsWith('npm') || agent.startsWith('yarn')) return 'npm';\n return 'bun';\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await stat(p);\n return true;\n } catch {\n return false;\n }\n}\n\nconst TOKEN_FILE_EXTS = new Set([\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n '.json',\n '.md',\n '.txt',\n '.html',\n '.css',\n]);\n\nfunction shouldApplyTokens(path: string): boolean {\n const dot = path.lastIndexOf('.');\n if (dot < 0) return false;\n return TOKEN_FILE_EXTS.has(path.slice(dot).toLowerCase());\n}\n\nfunction replaceTokens(content: string, tokens: Record<string, string>): string {\n let out = content;\n for (const [k, v] of Object.entries(tokens)) out = out.split(k).join(v);\n return out;\n}\n\nasync function walkFiles(dir: string, base: string, out: string[]): Promise<void> {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const e of entries) {\n const full = join(dir, e.name);\n const rel = full.slice(base.length + 1);\n if (e.isDirectory()) {\n await walkFiles(full, base, out);\n } else if (e.isFile()) {\n out.push(rel);\n }\n }\n}\n\n/** Returns the spawn configuration for the given package manager on the given platform. */\nexport function installSpawnSpec(\n pm: 'bun' | 'npm' | 'pnpm',\n platform: NodeJS.Platform,\n): { cmd: string; shell: boolean } {\n return { cmd: pm, shell: platform === 'win32' };\n}\n\nfunction runInstall(pm: 'bun' | 'npm' | 'pnpm', cwd: string): Promise<number> {\n return new Promise((resolvePromise) => {\n const { cmd, shell } = installSpawnSpec(pm, process.platform);\n const child = spawn(cmd, ['install'], {\n cwd,\n stdio: 'inherit',\n shell,\n });\n child.on('exit', (code) => resolvePromise(code ?? 1));\n child.on('error', () => resolvePromise(1));\n });\n}\n\n/**\n * Copy the chosen template into `targetDir`, apply token replacements, and\n * optionally run the package manager's install. Fails if the target exists\n * and is non-empty.\n */\nexport async function scaffoldApp(opts: ScaffoldOptions): Promise<ScaffoldResult> {\n if (!isValidAppName(opts.name)) {\n throw new Error(`[flare-create] invalid app name: ${opts.name}`);\n }\n const templateId = opts.template ?? 'blank';\n if (!isTemplateId(templateId)) {\n throw new Error(`[flare-create] unknown template: ${templateId}`);\n }\n const manifest = TEMPLATES[templateId];\n if (!manifest) throw new Error(`[flare-create] missing manifest for ${templateId}`);\n\n const cwd = process.cwd();\n const target = opts.targetDir\n ? isAbsolute(opts.targetDir)\n ? opts.targetDir\n : resolve(cwd, opts.targetDir)\n : resolve(cwd, opts.name);\n\n if (await pathExists(target)) {\n const entries = await readdir(target);\n if (entries.length > 0) {\n throw new Error(`[flare-create] target ${target} already exists and is not empty`);\n }\n } else {\n await mkdir(target, { recursive: true });\n }\n\n const templatesRoot = opts.templatesDir ?? DEFAULT_TEMPLATES_DIR;\n const src = join(templatesRoot, templateId);\n if (!(await pathExists(src))) {\n throw new Error(`[flare-create] template directory not found: ${src}`);\n }\n\n await cp(src, target, { recursive: true });\n\n const gi = join(target, 'gitignore');\n if (await pathExists(gi)) {\n await rename(gi, join(target, '.gitignore'));\n }\n\n const tokens: Record<string, string> = {\n '{{APP_NAME}}': opts.name,\n '{{FLARE_VERSION}}': opts.flareVersion ?? `^${version}`,\n '{{TEMPLATE_ID}}': templateId,\n };\n\n const files: string[] = [];\n await walkFiles(target, target, files);\n for (const rel of files) {\n if (!shouldApplyTokens(rel)) continue;\n const p = join(target, rel);\n const content = await readFile(p, 'utf8');\n const next = replaceTokens(content, tokens);\n if (next !== content) await writeFile(p, next, 'utf8');\n }\n\n const pm = detectPackageManager(opts.packageManager);\n let installed = false;\n if (!opts.skipInstall) {\n const code = await runInstall(pm, target);\n if (code !== 0) {\n throw new Error(`[flare-create] ${pm} install exited with code ${code}`);\n }\n installed = true;\n }\n\n return {\n targetDir: target,\n template: templateId,\n packageManager: pm,\n installed,\n };\n}\n","import { Command } from 'commander';\nimport { version } from '../package.json';\nimport { TEMPLATES } from './manifest.js';\nimport { scaffoldApp } from './scaffold.js';\n\nfunction parseTemplateArg(value: string): 'blank' | 'topdown' | 'shmup' {\n if (value !== 'blank' && value !== 'topdown' && value !== 'shmup') {\n throw new Error(`invalid --template: ${value} (expected blank|topdown|shmup)`);\n }\n return value;\n}\n\nfunction parsePmArg(value: string): 'bun' | 'npm' | 'pnpm' {\n if (value !== 'bun' && value !== 'npm' && value !== 'pnpm') {\n throw new Error(`invalid --package-manager: ${value} (expected bun|npm|pnpm)`);\n }\n return value;\n}\n\n/** Build the `flare-create` CLI program. Exposed for tests. */\nexport function buildProgram(): Command {\n const program = new Command();\n program\n .name('flare-create')\n .description('Scaffold a new Flare engine app')\n .version(version)\n .argument('<name>', 'directory name / package name for the new app')\n .option('-t, --template <name>', 'template (blank|topdown|shmup)', parseTemplateArg, 'blank')\n .option('-p, --package-manager <pm>', 'package manager (bun|npm|pnpm)', parsePmArg)\n .option('--skip-install', 'skip running install after scaffolding', false)\n .action(async (name: string, opts) => {\n const result = await scaffoldApp({\n name,\n template: opts.template,\n packageManager: opts.packageManager,\n skipInstall: opts.skipInstall === true,\n });\n const manifest = TEMPLATES[result.template];\n process.stdout.write(\n `[flare-create] scaffolded ${\n manifest?.displayName ?? result.template\n } at ${result.targetDir}\\n`,\n );\n if (!result.installed) {\n process.stdout.write(\n `[flare-create] skipped install — run \\`cd ${name} && ${result.packageManager} install\\` when ready\\n`,\n );\n } else if (manifest?.postInstall) {\n process.stdout.write(`[flare-create] next: cd ${name} && ${manifest.postInstall}\\n`);\n }\n });\n return program;\n}\n"],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ import { Command } from "commander";
2
+ //#region src/cli.d.ts
3
+ /** Build the `flare-create` CLI program. Exposed for tests. */
4
+ declare function buildProgram(): Command;
5
+ //#endregion
6
+ //#region src/manifest.d.ts
7
+ /** Declared metadata for a `@flare-engine/create` template. */
8
+ interface TemplateManifest {
9
+ /** Template directory name (`blank`, `topdown`, `shmup`, ...). */
10
+ id: string;
11
+ /** Human-readable name shown in logs. */
12
+ displayName: string;
13
+ /** One-line description of what the template produces. */
14
+ description: string;
15
+ /** Engines required — mirrors Node/npm engines field in scaffolded package.json. */
16
+ engines: {
17
+ node: string;
18
+ };
19
+ /** Optional post-install instructions printed after scaffold completes. */
20
+ postInstall?: string;
21
+ }
22
+ /** Built-in templates shipped inside `@flare-engine/create`. */
23
+ declare const TEMPLATES: {
24
+ blank: {
25
+ id: string;
26
+ displayName: string;
27
+ description: string;
28
+ engines: {
29
+ node: string;
30
+ };
31
+ postInstall: string;
32
+ };
33
+ topdown: {
34
+ id: string;
35
+ displayName: string;
36
+ description: string;
37
+ engines: {
38
+ node: string;
39
+ };
40
+ postInstall: string;
41
+ };
42
+ shmup: {
43
+ id: string;
44
+ displayName: string;
45
+ description: string;
46
+ engines: {
47
+ node: string;
48
+ };
49
+ postInstall: string;
50
+ };
51
+ };
52
+ type TemplateId = keyof typeof TEMPLATES;
53
+ declare function isTemplateId(value: string): value is TemplateId;
54
+ //#endregion
55
+ //#region src/scaffold.d.ts
56
+ /** Options for `scaffoldApp`. */
57
+ interface ScaffoldOptions {
58
+ /** App name — also used as the target directory and `name` in package.json. */
59
+ name: string;
60
+ /** Template id (default: `blank`). */
61
+ template?: string;
62
+ /** Destination directory (default: `<cwd>/<name>`). */
63
+ targetDir?: string;
64
+ /** Package manager used for `engines` + post-install command. */
65
+ packageManager?: 'bun' | 'npm' | 'pnpm';
66
+ /** Skip `install` after copying (default: false). */
67
+ skipInstall?: boolean;
68
+ /** Pinned Flare version for template {{FLARE_VERSION}} token (default: current package version prefixed with ^). */
69
+ flareVersion?: string;
70
+ /** Override templates root — used by tests. */
71
+ templatesDir?: string;
72
+ }
73
+ /** Result returned by `scaffoldApp`. */
74
+ interface ScaffoldResult {
75
+ /** Absolute path of the scaffolded app. */
76
+ targetDir: string;
77
+ /** Resolved template id. */
78
+ template: TemplateId;
79
+ /** Package manager used. */
80
+ packageManager: 'bun' | 'npm' | 'pnpm';
81
+ /** Whether install was run. */
82
+ installed: boolean;
83
+ }
84
+ /** Returns true when the name is safe to use as an app name and shell argument. */
85
+ declare function isValidAppName(name: string): boolean;
86
+ /** Detect the package manager from `--package-manager` > `npm_config_user_agent` > `bun`. */
87
+ declare function detectPackageManager(explicit: string | undefined): 'bun' | 'npm' | 'pnpm';
88
+ /** Returns the spawn configuration for the given package manager on the given platform. */
89
+ declare function installSpawnSpec(pm: 'bun' | 'npm' | 'pnpm', platform: NodeJS.Platform): {
90
+ cmd: string;
91
+ shell: boolean;
92
+ };
93
+ /**
94
+ * Copy the chosen template into `targetDir`, apply token replacements, and
95
+ * optionally run the package manager's install. Fails if the target exists
96
+ * and is non-empty.
97
+ */
98
+ declare function scaffoldApp(opts: ScaffoldOptions): Promise<ScaffoldResult>;
99
+ //#endregion
100
+ export { type ScaffoldOptions, type ScaffoldResult, TEMPLATES, type TemplateId, type TemplateManifest, buildProgram, detectPackageManager, installSpawnSpec, isTemplateId, isValidAppName, scaffoldApp };
101
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/cli.ts","../src/manifest.ts","../src/scaffold.ts"],"mappings":";;;iBAoBgB,gBAAgB;;;;UCnBf;;EAEf;;EAEA;;EAEA;;EAEA;IAAW;;;EAEX;;;cAIW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAyBD,0BAA0B;iBAEtB,aAAa,gBAAgB,SAAS;;;;UClCrC;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIe;;EAEf;;EAEA,UAAU;;EAEV;;EAEA;;;iBASc,eAAe;;iBAKf,qBAAqB;;iBAwDrB,iBACd,4BACA,UAAU,OAAO;EACd;EAAa;;;;;;;iBAsBI,YAAY,MAAM,kBAAkB,QAAQ"}
@@ -0,0 +1,101 @@
1
+ import { Command } from "commander";
2
+ //#region src/cli.d.ts
3
+ /** Build the `flare-create` CLI program. Exposed for tests. */
4
+ declare function buildProgram(): Command;
5
+ //#endregion
6
+ //#region src/manifest.d.ts
7
+ /** Declared metadata for a `@flare-engine/create` template. */
8
+ interface TemplateManifest {
9
+ /** Template directory name (`blank`, `topdown`, `shmup`, ...). */
10
+ id: string;
11
+ /** Human-readable name shown in logs. */
12
+ displayName: string;
13
+ /** One-line description of what the template produces. */
14
+ description: string;
15
+ /** Engines required — mirrors Node/npm engines field in scaffolded package.json. */
16
+ engines: {
17
+ node: string;
18
+ };
19
+ /** Optional post-install instructions printed after scaffold completes. */
20
+ postInstall?: string;
21
+ }
22
+ /** Built-in templates shipped inside `@flare-engine/create`. */
23
+ declare const TEMPLATES: {
24
+ blank: {
25
+ id: string;
26
+ displayName: string;
27
+ description: string;
28
+ engines: {
29
+ node: string;
30
+ };
31
+ postInstall: string;
32
+ };
33
+ topdown: {
34
+ id: string;
35
+ displayName: string;
36
+ description: string;
37
+ engines: {
38
+ node: string;
39
+ };
40
+ postInstall: string;
41
+ };
42
+ shmup: {
43
+ id: string;
44
+ displayName: string;
45
+ description: string;
46
+ engines: {
47
+ node: string;
48
+ };
49
+ postInstall: string;
50
+ };
51
+ };
52
+ type TemplateId = keyof typeof TEMPLATES;
53
+ declare function isTemplateId(value: string): value is TemplateId;
54
+ //#endregion
55
+ //#region src/scaffold.d.ts
56
+ /** Options for `scaffoldApp`. */
57
+ interface ScaffoldOptions {
58
+ /** App name — also used as the target directory and `name` in package.json. */
59
+ name: string;
60
+ /** Template id (default: `blank`). */
61
+ template?: string;
62
+ /** Destination directory (default: `<cwd>/<name>`). */
63
+ targetDir?: string;
64
+ /** Package manager used for `engines` + post-install command. */
65
+ packageManager?: 'bun' | 'npm' | 'pnpm';
66
+ /** Skip `install` after copying (default: false). */
67
+ skipInstall?: boolean;
68
+ /** Pinned Flare version for template {{FLARE_VERSION}} token (default: current package version prefixed with ^). */
69
+ flareVersion?: string;
70
+ /** Override templates root — used by tests. */
71
+ templatesDir?: string;
72
+ }
73
+ /** Result returned by `scaffoldApp`. */
74
+ interface ScaffoldResult {
75
+ /** Absolute path of the scaffolded app. */
76
+ targetDir: string;
77
+ /** Resolved template id. */
78
+ template: TemplateId;
79
+ /** Package manager used. */
80
+ packageManager: 'bun' | 'npm' | 'pnpm';
81
+ /** Whether install was run. */
82
+ installed: boolean;
83
+ }
84
+ /** Returns true when the name is safe to use as an app name and shell argument. */
85
+ declare function isValidAppName(name: string): boolean;
86
+ /** Detect the package manager from `--package-manager` > `npm_config_user_agent` > `bun`. */
87
+ declare function detectPackageManager(explicit: string | undefined): 'bun' | 'npm' | 'pnpm';
88
+ /** Returns the spawn configuration for the given package manager on the given platform. */
89
+ declare function installSpawnSpec(pm: 'bun' | 'npm' | 'pnpm', platform: NodeJS.Platform): {
90
+ cmd: string;
91
+ shell: boolean;
92
+ };
93
+ /**
94
+ * Copy the chosen template into `targetDir`, apply token replacements, and
95
+ * optionally run the package manager's install. Fails if the target exists
96
+ * and is non-empty.
97
+ */
98
+ declare function scaffoldApp(opts: ScaffoldOptions): Promise<ScaffoldResult>;
99
+ //#endregion
100
+ export { type ScaffoldOptions, type ScaffoldResult, TEMPLATES, type TemplateId, type TemplateManifest, buildProgram, detectPackageManager, installSpawnSpec, isTemplateId, isValidAppName, scaffoldApp };
101
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/cli.ts","../src/manifest.ts","../src/scaffold.ts"],"mappings":";;;iBAoBgB,gBAAgB;;;;UCnBf;;EAEf;;EAEA;;EAEA;;EAEA;IAAW;;;EAEX;;;cAIW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAyBD,0BAA0B;iBAEtB,aAAa,gBAAgB,SAAS;;;;UClCrC;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIe;;EAEf;;EAEA,UAAU;;EAEV;;EAEA;;;iBASc,eAAe;;iBAKf,qBAAqB;;iBAwDrB,iBACd,4BACA,UAAU,OAAO;EACd;EAAa;;;;;;;iBAsBI,YAAY,MAAM,kBAAkB,QAAQ"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_cli = require("./cli-JzwtwZhm.js");
3
+ exports.TEMPLATES = require_cli.TEMPLATES;
4
+ exports.buildProgram = require_cli.buildProgram;
5
+ exports.detectPackageManager = require_cli.detectPackageManager;
6
+ exports.installSpawnSpec = require_cli.installSpawnSpec;
7
+ exports.isTemplateId = require_cli.isTemplateId;
8
+ exports.isValidAppName = require_cli.isValidAppName;
9
+ exports.scaffoldApp = require_cli.scaffoldApp;
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { a as scaffoldApp, i as isValidAppName, n as detectPackageManager, o as TEMPLATES, r as installSpawnSpec, s as isTemplateId, t as buildProgram } from "./cli-BGXZ73ie.mjs";
2
+ export { TEMPLATES, buildProgram, detectPackageManager, installSpawnSpec, isTemplateId, isValidAppName, scaffoldApp };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@flare-engine/create",
3
+ "version": "0.4.0",
4
+ "description": "One-shot scaffolder for Flare engine apps — `npm create @flare-engine`",
5
+ "keywords": ["flare-engine", "scaffold", "create-app", "expo", "react-native"],
6
+ "license": "Apache-2.0",
7
+ "author": "Grzegorz Otto",
8
+ "homepage": "https://github.com/grzott/flare-engine#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/grzott/flare-engine/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/grzott/flare-engine.git",
15
+ "directory": "packages/create"
16
+ },
17
+ "main": "dist/index.js",
18
+ "module": "dist/index.mjs",
19
+ "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/index.d.mts",
24
+ "default": "./dist/index.mjs"
25
+ },
26
+ "require": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ }
30
+ }
31
+ },
32
+ "bin": {
33
+ "flare-create": "dist/bin.js"
34
+ },
35
+ "files": ["dist", "templates", "README.md", "LICENSE", "NOTICE"],
36
+ "scripts": {
37
+ "build": "tsdown",
38
+ "test": "bun test",
39
+ "lint": "biome check src/",
40
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.templates.json"
41
+ },
42
+ "dependencies": {
43
+ "commander": "^13.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "@flare-engine/ecs": "^0.4.0",
47
+ "@flare-engine/engine": "^0.4.0",
48
+ "@flare-engine/physics": "^0.4.0",
49
+ "@flare-engine/react": "^0.4.0",
50
+ "@flare-engine/render": "^0.4.0",
51
+ "@flare-engine/tilemap": "^0.4.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=20"
55
+ },
56
+ "publishConfig": {
57
+ "access": "restricted",
58
+ "registry": "https://registry.npmjs.org/"
59
+ },
60
+ "sideEffects": false
61
+ }
@@ -0,0 +1,59 @@
1
+ # {{APP_NAME}}
2
+
3
+ A Flare engine app scaffolded from the `{{TEMPLATE_ID}}` template.
4
+
5
+ ## Develop
6
+
7
+ ```bash
8
+ bun install
9
+ bun run android # or: bun run ios
10
+ ```
11
+
12
+ `@shopify/react-native-skia` is a **native** module, so this template needs a
13
+ dev build — it cannot run in Expo Go. `bun run start` then pressing `a`/`i`
14
+ opens Expo Go and red-screens with a missing-native-module error. `bun run
15
+ android` builds and installs a dev client instead; after that first build,
16
+ `bun run start` attaches to it normally.
17
+
18
+ The first Android build takes a few minutes and needs the Android SDK plus a
19
+ JDK. Two things that will otherwise cost you an afternoon:
20
+
21
+ - **Build from a short path.** Skia's CMake step generates paths ~120
22
+ characters longer than your project directory, and Windows still fails at 260
23
+ (`CreateProcess error=2`). `C:\src\my-app` is fine; a deep temp path is not.
24
+ - **Do not remove `trustedDependencies` from `package.json`.** Bun blocks
25
+ install scripts it does not trust, and Skia's `postinstall` is what unpacks
26
+ its prebuilt binaries. Without it the install still succeeds and the Android
27
+ build fails minutes later with `Could not find libskia.a`.
28
+
29
+ ## Run in a browser
30
+
31
+ ```bash
32
+ bun run web
33
+ ```
34
+
35
+ The `web` script copies CanvasKit's wasm binary into `public/` before starting
36
+ Metro, because Skia on web is CanvasKit and it has to be served alongside the
37
+ bundle. Starting the dev server any other way — `bun run start` then pressing
38
+ `w` — skips that copy and the canvas stays blank.
39
+
40
+ The binary is ~8 MB and is `.gitignore`d: it is a copy of
41
+ `canvaskit-wasm/bin/full/canvaskit.wasm`, reproduced by the script on demand.
42
+ It must be the `full` build; the smaller default build is ABI-incompatible with
43
+ the loader.
44
+
45
+ ## Layout
46
+
47
+ - `src/App.tsx` — root component; mounts `<GameView>` and configures the Flare game loop.
48
+ - `index.ts` — Expo entry that registers `App` as the root component (native).
49
+ - `index.web.ts` — web entry; awaits `LoadSkiaWeb()` **before** importing `App`.
50
+ `package.json`'s `main` is extensionless so Metro resolves the right entry per
51
+ platform. The split is required: RN-Skia's web module reads `global.CanvasKit`
52
+ when it is first imported, so importing `App` any earlier leaves Skia hollow.
53
+ - `scripts/copy-canvaskit.mjs` — copies the CanvasKit wasm into `public/` for the
54
+ web build.
55
+ - `app.json` — Expo configuration.
56
+
57
+ ## Docs
58
+
59
+ - [Flare engine](https://github.com/grzott/flare-engine)
@@ -0,0 +1,10 @@
1
+ {
2
+ "expo": {
3
+ "name": "{{APP_NAME}}",
4
+ "slug": "{{APP_NAME}}",
5
+ "version": "0.1.0",
6
+ "orientation": "portrait",
7
+ "platforms": ["android", "ios", "web"],
8
+ "newArchEnabled": true
9
+ }
10
+ }
@@ -0,0 +1,7 @@
1
+ module.exports = function (api) {
2
+ api.cache(true);
3
+ return {
4
+ presets: ["babel-preset-expo"],
5
+ plugins: ["react-native-worklets/plugin"],
6
+ };
7
+ };
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ public/canvaskit.wasm
3
+ .expo
4
+ dist
5
+ *.log
6
+ .DS_Store
7
+ android
8
+ ios
@@ -0,0 +1,4 @@
1
+ import { registerRootComponent } from "expo";
2
+ import { App } from "./src/App";
3
+
4
+ registerRootComponent(App);
@@ -0,0 +1,17 @@
1
+ import { LoadSkiaWeb } from "@shopify/react-native-skia/lib/module/web";
2
+ import { registerRootComponent } from "expo";
3
+
4
+ // Metro picks this file over index.ts on web because package.json's `main` is
5
+ // extensionless. Keeping the web entry separate is not cosmetic: RN-Skia's web
6
+ // module builds its Skia object from global.CanvasKit at module-eval time, so
7
+ // anything that imports Skia must be loaded only after LoadSkiaWeb resolves.
8
+ // A static `import { App }` here would leave Skia permanently hollow and
9
+ // GameView would throw on Skia.Paint.
10
+ //
11
+ // `bun run web` copies canvaskit.wasm into public/, which Expo serves at the
12
+ // web root — that is what locateFile points at.
13
+ void (async () => {
14
+ await LoadSkiaWeb({ locateFile: (file: string) => `/${file}` });
15
+ const { App } = await import("./src/App");
16
+ registerRootComponent(App);
17
+ })();