@gamedevland/vite 0.1.2 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 — 2026-11-07
4
+
5
+ - Add `gamedevland create <directory>` to generate a blank SDK consumer project and install
6
+ its dependencies, with `--no-install` for offline/custom installation and `--name` for
7
+ an explicit npm package name.
8
+ - Ship the template inside the npm package: strict TypeScript, Vite, ESLint, an empty
9
+ FSM scene with DI/layout registration, manifest-based JSON assets, and audio volume config.
10
+ - Include web and CrazyGames development/build scripts with relative asset paths.
11
+ - Reject nonempty destinations, symlink destinations and invalid names; never overwrite
12
+ existing project files. Invoke dependency installation without interpolating user paths.
13
+ - Update the engine dependency to 0.0.11 and test generated project structure and CLI behavior.
14
+
3
15
  ## 0.1.2 — 2026-11-04
4
16
 
5
17
  - Release the package and its source under MIT.
@@ -19,3 +31,19 @@
19
31
  - Use the official pinned Poki CLI, per-game private credentials or `POKI_UPLOAD_TOKEN`, and verified upload receipts.
20
32
  - Keep authentication out of check/build subprocesses and reject source maps, private files, symlinks and leaked upload tokens in build output.
21
33
  - Preserve existing Vite configuration exports and browser SDK injection.
34
+
35
+ ## 0.0.4
36
+
37
+ - Update the engine dependency to 0.0.5.
38
+
39
+ ## 0.0.3
40
+
41
+ - Update package links and the engine dependency to 0.0.4.
42
+
43
+ ## 0.0.2
44
+
45
+ - Update repository metadata and the engine dependency to 0.0.3.
46
+
47
+ ## 0.0.1
48
+
49
+ - First npm release of the Vite tooling.
package/README.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @gamedevland/vite
2
2
 
3
+ ## Create a blank game
4
+
5
+ ```sh
6
+ npx --yes --package @gamedevland/vite@0.2.0 gamedevland create my-game
7
+ cd my-game
8
+ npm run dev
9
+ ```
10
+
11
+ When installed locally, run `npx gamedevland create my-game`. The CLI generates a
12
+ strict TypeScript/PixiJS project with an empty FSM scene, scene/global DI, JSON
13
+ layout and audio config, and web/CrazyGames build scripts. Dependencies are installed
14
+ by default. `--no-install` skips installation; `--name my-game` sets the npm name
15
+ when the destination directory has a different name. New or empty directories only;
16
+ nonempty directories and symlink destinations are rejected without overwriting files.
17
+
3
18
  Vite build tooling for HTML5 games built with `@gamedevland/engine`.
4
19
 
5
20
  ## Install
@@ -2,19 +2,26 @@ import { parseArgs } from 'node:util';
2
2
  import { PokiArchive } from '../poki/PokiArchive.js';
3
3
  import { PokiProject } from '../poki/PokiProject.js';
4
4
  import { PokiUpload } from '../poki/PokiUpload.js';
5
+ import { ProjectCreator } from '../project/ProjectCreator.js';
5
6
  /** Public command-line interface; no dependency on a particular game layout. */
6
7
  export class GamedevlandCli {
7
8
  static async run(argv, cwd) {
8
9
  const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: {
9
10
  help: { type: 'boolean', short: 'h' }, root: { type: 'string' },
10
11
  name: { type: 'string' }, notes: { type: 'string' },
12
+ 'no-install': { type: 'boolean', default: false },
11
13
  } });
12
14
  if (values.help || positionals.length === 0) {
13
- process.stdout.write('Usage: gamedevland poki <pack|upload> [--root <project>] [--name <version>] [--notes <text>]\npack validates/packages an existing build; upload runs configured checks and a fresh build before uploading.\n');
15
+ process.stdout.write('Usage: gamedevland create <directory> [--name <name>] [--no-install]\n gamedevland poki <pack|upload> [--root <project>] [--name <version>] [--notes <text>]\ncreate generates a blank project and installs dependencies.\npack validates/packages an existing build; upload runs configured checks and a fresh build before uploading.\n');
16
+ return;
17
+ }
18
+ if (positionals[0] === 'create' && positionals.length === 2 && positionals[1]) {
19
+ await ProjectCreator.create({ cwd, directory: positionals[1], install: !values['no-install'],
20
+ ...(values.name === undefined ? {} : { name: values.name }) });
14
21
  return;
15
22
  }
16
23
  if (positionals.length !== 2 || positionals[0] !== 'poki' || !['pack', 'upload'].includes(positionals[1] ?? '')) {
17
- throw new Error('[gamedevland] Expected poki pack or poki upload; use --help');
24
+ throw new Error('[gamedevland] Expected create <directory>, poki pack or poki upload; use --help');
18
25
  }
19
26
  const project = PokiProject.load(values.root ?? cwd);
20
27
  if (positionals[1] === 'pack') {
@@ -0,0 +1,13 @@
1
+ interface CreateProjectOptions {
2
+ readonly cwd: string;
3
+ readonly directory: string;
4
+ readonly name?: string;
5
+ readonly install: boolean;
6
+ }
7
+ /** Creates a blank consumer project without overwriting existing files. */
8
+ export declare class ProjectCreator {
9
+ static create(options: CreateProjectOptions): Promise<string>;
10
+ private static copy;
11
+ private static install;
12
+ }
13
+ export {};
@@ -0,0 +1,65 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { lstat, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { SchemaDecoder as S } from '@gamedevland/engine/validation';
6
+ /** Creates a blank consumer project without overwriting existing files. */
7
+ export class ProjectCreator {
8
+ static async create(options) {
9
+ const root = path.resolve(options.cwd, options.directory);
10
+ const name = options.name ?? path.basename(root);
11
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name) || name.length > 214) {
12
+ throw new Error('[create] Use a lowercase npm project name (letters, digits, dots, hyphens).');
13
+ }
14
+ const info = await lstat(root).catch((error) => {
15
+ if (error.code !== 'ENOENT')
16
+ throw error;
17
+ return undefined;
18
+ });
19
+ if (info && (!info.isDirectory() || info.isSymbolicLink() || (await readdir(root)).length > 0)) {
20
+ throw new Error('[create] Destination must be a new or empty directory. Existing files are never overwritten.');
21
+ }
22
+ const pkg = S.decode(JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8')), S.object({ version: S.string({ nonEmpty: true }), dependencies: S.object({
23
+ '@gamedevland/engine': S.string({ nonEmpty: true }),
24
+ }) }), 'tooling/package.json');
25
+ const replacements = new Map([
26
+ ['__PROJECT_NAME__', name], ['__ENGINE_VERSION__', pkg.dependencies['@gamedevland/engine']],
27
+ ['__TOOLING_VERSION__', pkg.version],
28
+ ]);
29
+ await mkdir(root, { recursive: true });
30
+ await this.copy(fileURLToPath(new URL('../../templates/empty/', import.meta.url)), root, replacements);
31
+ if (options.install)
32
+ await this.install(root);
33
+ process.stdout.write(`[create] Created ${root}\nNext: cd ${JSON.stringify(options.directory)}${options.install ? '' : ' && npm install'} && npm run dev\n`);
34
+ return root;
35
+ }
36
+ static async copy(source, target, replacements) {
37
+ for (const entry of await readdir(source, { withFileTypes: true })) {
38
+ const destination = path.join(target, entry.name === 'gitignore' ? '.gitignore' : entry.name);
39
+ if (entry.isDirectory()) {
40
+ await mkdir(destination);
41
+ await this.copy(path.join(source, entry.name), destination, replacements);
42
+ }
43
+ else if (entry.isFile()) {
44
+ let content = await readFile(path.join(source, entry.name), 'utf8');
45
+ for (const [token, value] of replacements)
46
+ content = content.replaceAll(token, value);
47
+ await writeFile(destination, content, { flag: 'wx' });
48
+ }
49
+ else
50
+ throw new Error('[create] Invalid template entry');
51
+ }
52
+ }
53
+ static async install(root) {
54
+ const npmEntry = process.env.npm_execpath;
55
+ const command = npmEntry ? process.execPath : 'npm';
56
+ const args = npmEntry ? [npmEntry, 'install'] : ['install'];
57
+ await new Promise((resolve, reject) => {
58
+ const child = spawn(command, args, { cwd: root, stdio: 'inherit',
59
+ shell: process.platform === 'win32' && !npmEntry });
60
+ child.once('error', reject);
61
+ child.once('close', (code) => code === 0 ? resolve()
62
+ : reject(new Error('[create] Dependency installation failed; retry npm install in the generated directory.')));
63
+ });
64
+ }
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamedevland/vite",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Vite build tooling for Gamedev Land HTML5 games.",
6
6
  "license": "MIT",
@@ -25,7 +25,8 @@
25
25
  "dist",
26
26
  "README.md",
27
27
  "CHANGELOG.md",
28
- "LICENSE"
28
+ "LICENSE",
29
+ "templates"
29
30
  ],
30
31
  "publishConfig": {
31
32
  "access": "public"
@@ -44,7 +45,7 @@
44
45
  "build": "npm run clean && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json --resolve-full-paths"
45
46
  },
46
47
  "dependencies": {
47
- "@gamedevland/engine": "0.0.8",
48
+ "@gamedevland/engine": "0.0.11",
48
49
  "@poki/cli": "0.1.19",
49
50
  "fflate": "0.8.3"
50
51
  },
@@ -0,0 +1 @@
1
+ VITE_PLATFORM=crazygames
@@ -0,0 +1,10 @@
1
+ # Project instructions
2
+
3
+ - Use public `@gamedevland/engine` subsystem exports; do not deep-import internals.
4
+ - Keep flow in FSM → Command → Service. Components/actions own node visuals and input.
5
+ - Keep static hierarchies and authored visual values in JSON layouts/prefabs/configs.
6
+ - Keep every music/SFX channel and per-asset volume in the audio JSON config.
7
+ - Use SDK autoplay recovery and platform-owned data where available.
8
+ - Use strict TypeScript; avoid unsafe casts and non-null assertions.
9
+ - Run `npm run typecheck`, `npm run lint`, and `npm run build` after changes.
10
+ - Do not claim real-browser/device verification from simulated tests.
@@ -0,0 +1,24 @@
1
+ # __PROJECT_NAME__
2
+
3
+ A blank TypeScript/PixiJS game using the Gamedev Land SDK. No demo gameplay or media assets.
4
+
5
+ ```sh
6
+ npm run dev
7
+ npm run typecheck
8
+ npm run lint
9
+ npm run build
10
+ ```
11
+
12
+ The CLI installs dependencies by default. If created with `--no-install`, run `npm install` first.
13
+ Node.js 22.12+ is required.
14
+
15
+ Entry: `game/src/main.ts`. Configuration: `game/src/config/GameConfiguration.ts`.
16
+ The empty scene uses the SDK's FSM, scene DI, asset manifest and JSON layout.
17
+ Add gameplay through FSM → Command → Service; put visuals in components/actions.
18
+
19
+ All music/SFX channel and per-file levels belong in
20
+ `game/assets/json/configs/audio/sounds-volumes.json`.
21
+
22
+ `npm run dev:crazygames` enables the official CrazyGames v3 SDK on localhost.
23
+ `npm run build:crazygames` produces `dist/crazygames/` with relative asset paths.
24
+ Report gameplay start/stop when actual gameplay is added; a blank scene is not gameplay.
@@ -0,0 +1,8 @@
1
+ import js from '@eslint/js';
2
+ import ts from 'typescript-eslint';
3
+
4
+ export default [
5
+ { ignores: ['dist/**', 'node_modules/**'] },
6
+ js.configs.recommended,
7
+ ...ts.configs.recommended,
8
+ ];
@@ -0,0 +1 @@
1
+ { "channel.music": 1, "channel.sfx": 1 }
@@ -0,0 +1 @@
1
+ { "root": { "type": "Container", "children": {} } }
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
+ <link rel="icon" href="data:," />
7
+ <title>__PROJECT_NAME__</title>
8
+ <style>
9
+ html, body, #app { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #07111f; }
10
+ body { overscroll-behavior: none; user-select: none; -webkit-user-select: none; }
11
+ canvas { display: block; }
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <div id="app"></div>
16
+ <script type="module" src="./src/main.ts"></script>
17
+ </body>
18
+ </html>
@@ -0,0 +1,29 @@
1
+ import { AssetManifestBuilder } from '@gamedevland/engine/assets';
2
+ import type { GameConfig } from '@gamedevland/engine/core';
3
+ import { MainSceneDefinition } from '../scenes/main/MainSceneDefinition';
4
+ import { ProjectDi } from './ProjectDi';
5
+
6
+ export class GameConfiguration {
7
+ static readonly config: GameConfig = {
8
+ system: {
9
+ canvasBackground: '#07111f',
10
+ screen: {
11
+ land: { width: 960, height: 540, fit: 'height' },
12
+ port: { width: 540, height: 960, fit: 'height' },
13
+ dpr: 'native', maxDpr: 2, render: { maxCanvasPixels: 1500000 },
14
+ },
15
+ boot: { configs: { engineStartBundles: ['configs'] } },
16
+ audio: { volumeConfigId: 'json/configs/audio/sounds-volumes.json' },
17
+ },
18
+ assets: {
19
+ bundles: new AssetManifestBuilder({
20
+ configsJson: import.meta.glob<string>('../../assets/json/configs/**/*.json', { eager: true, query: '?url', import: 'default' }),
21
+ layoutsJson: import.meta.glob<string>('../../assets/json/layouts/**/*.json', { eager: true, query: '?url', import: 'default' }),
22
+ audio: {}, fonts: {}, images: {}, localesJson: {}, prefabsJson: {},
23
+ }).build(),
24
+ preload: ['layouts'],
25
+ },
26
+ scenes: { initial: 'main', map: { main: MainSceneDefinition.config } },
27
+ di: new ProjectDi(),
28
+ };
29
+ }
@@ -0,0 +1,8 @@
1
+ import { GameDiDefinition } from '@gamedevland/engine/di';
2
+ import { MainSceneDi } from '../scenes/main/MainSceneDi';
3
+ import { ProjectGlobalDi } from './ProjectGlobalDi';
4
+
5
+ export class ProjectDi extends GameDiDefinition {
6
+ protected override createGlobalDefinition() { return new ProjectGlobalDi(); }
7
+ protected override createSceneDefinitions() { return [new MainSceneDi()]; }
8
+ }
@@ -0,0 +1,12 @@
1
+ import { GlobalDiDefinition } from '@gamedevland/engine/di';
2
+ import type { ElementFactoriesGroup } from '@gamedevland/engine/di';
3
+ import { Container, Graphics, Text } from '@gamedevland/engine/layouts';
4
+
5
+ export class ProjectGlobalDi extends GlobalDiDefinition {
6
+ protected override registerElements(group: ElementFactoriesGroup): void {
7
+ group.add('Container', Container).add('Graphics', Graphics).add('Text', Text);
8
+ }
9
+ protected override registerComponents(): void {}
10
+ protected override registerCommands(): void {}
11
+ protected override registerServices(): void {}
12
+ }
@@ -0,0 +1,4 @@
1
+ import { Engine } from '@gamedevland/engine';
2
+ import { GameConfiguration } from './config/GameConfiguration';
3
+
4
+ await Engine.run('#app', GameConfiguration.config);
@@ -0,0 +1,3 @@
1
+ import { FsmDrivenScene } from '@gamedevland/engine/scenes';
2
+
3
+ export class MainScene extends FsmDrivenScene {}
@@ -0,0 +1,10 @@
1
+ import type { SceneConfig } from '@gamedevland/engine/core';
2
+ import { MainScene } from './MainScene';
3
+
4
+ export class MainSceneDefinition {
5
+ static readonly config: SceneConfig = {
6
+ key: 'main', useClass: MainScene,
7
+ layout: 'json/layouts/scenes/main/layout.json',
8
+ fsm: { initial: 'ready', states: { ready: {} } },
9
+ };
10
+ }
@@ -0,0 +1,7 @@
1
+ import { SceneDiDefinition } from '@gamedevland/engine/di';
2
+
3
+ export class MainSceneDi extends SceneDiDefinition {
4
+ override getSceneKey(): string { return 'main'; }
5
+ protected override registerProviders(): void {}
6
+ protected override registerServices(): void {}
7
+ }
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ dist/
3
+ *.log
4
+ .DS_Store
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": { "node": ">=22.12.0" },
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "dev:crazygames": "vite --mode crazygames",
10
+ "build": "vite build",
11
+ "build:crazygames": "vite build --mode crazygames --outDir ../dist/crazygames",
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "eslint . --max-warnings=0",
14
+ "preview": "vite preview"
15
+ },
16
+ "dependencies": { "@gamedevland/engine": "__ENGINE_VERSION__", "pixi.js": "^8.16.0" },
17
+ "devDependencies": {
18
+ "@gamedevland/vite": "__TOOLING_VERSION__", "@types/node": "^24.7.0",
19
+ "typescript": "^5.9.3", "vite": "^8.0.16", "eslint": "^9.37.0",
20
+ "@eslint/js": "^9.37.0", "typescript-eslint": "^8.46.0"
21
+ }
22
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"],
4
+ "module": "ESNext", "moduleResolution": "Bundler", "strict": true,
5
+ "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true,
6
+ "noImplicitOverride": true, "resolveJsonModule": true, "isolatedModules": true,
7
+ "skipLibCheck": true, "noEmit": true, "types": ["node", "vite/client"]
8
+ },
9
+ "include": ["game", "vite.config.ts"], "exclude": ["dist", "node_modules"]
10
+ }
@@ -0,0 +1,7 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { GameViteConfig } from '@gamedevland/vite';
3
+
4
+ export default ({ mode }: { mode: string }) => ({
5
+ ...GameViteConfig.create({ mode, repoRoot: fileURLToPath(new URL('.', import.meta.url)) }),
6
+ server: { open: false },
7
+ });