diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75d8737..2d618f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ Thanks for your interest in contributing to oncyberio! This guide will help you ### Setup ```bash -git clone +git clone https://github.com/oncyberio/awe.git cd awe pnpm install ``` diff --git a/README.md b/README.md index bbc21f2..e0f6170 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ This scaffolds a full Next.js project with the oncyberio engine pre-configured, ### Monorepo Development ```bash -git clone +git clone https://github.com/oncyberio/awe.git cd awe pnpm install ``` diff --git a/package.json b/package.json index bfe7320..4f3a223 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "awe", "private": true, + "packageManager": "pnpm@10.10.0", "engines": { "node": ">=20.9.0" }, diff --git a/packages/create-oncyber-app/README.md b/packages/create-oncyber-app/README.md index 14b67da..9b11fdc 100644 --- a/packages/create-oncyber-app/README.md +++ b/packages/create-oncyber-app/README.md @@ -7,7 +7,7 @@ Scaffold a new 3D game powered by the oncyberio engine. ```bash npx create-oncyber-app my-game cd my-game -npm run dev +pnpm dev ``` Open [http://localhost:3000](http://localhost:3000) to see your game, or [http://localhost:3000/_studio](http://localhost:3000/_studio) for the visual editor. @@ -18,34 +18,33 @@ Open [http://localhost:3000](http://localhost:3000) to see your game, or [http:/ npx create-oncyber-app [project-name] [options] ``` -When run without a project name, the CLI starts in interactive mode and prompts for a name and package manager. +When run without a project name, the CLI starts in interactive mode and prompts for a name and template. The generated project uses pnpm. ## Options | Flag | Description | | --- | --- | -| `--use-npm` | Use npm as the package manager | -| `--use-pnpm` | Use pnpm as the package manager | -| `--use-yarn` | Use yarn as the package manager | | `--skip-install` | Skip automatic dependency installation | | `--skip-git` | Skip git repository initialization | | `--help` | Show the help message | | `--version` | Show the CLI version | +The CLI uses pnpm automatically. If pnpm is missing, it will try to enable it with Corepack. + ## Examples ```bash -# Interactive mode — prompts for name and package manager +# Interactive mode — prompts for name and template npx create-oncyber-app # Create a project with a specific name npx create-oncyber-app my-game -# Use pnpm and skip git init -npx create-oncyber-app my-game --use-pnpm --skip-git +# Skip git init +npx create-oncyber-app my-game --skip-git # Scaffold only — no install, no git -npx create-oncyber-app my-game --use-npm --skip-install --skip-git +npx create-oncyber-app my-game --skip-install --skip-git ``` ## What's Included @@ -102,7 +101,7 @@ my-game/ ## Next Steps -1. Run `npm run dev` to start the dev server +1. Run `pnpm dev` to start the dev server 2. Open [http://localhost:3000](http://localhost:3000) to see your game 3. Open [http://localhost:3000/_studio](http://localhost:3000/_studio) to edit the scene visually 4. Edit `src/components/game-script.tsx` to add game logic diff --git a/packages/create-oncyber-app/src/__tests__/cli-integration.test.ts b/packages/create-oncyber-app/src/__tests__/cli-integration.test.ts index 4c5cf71..37f8e07 100644 --- a/packages/create-oncyber-app/src/__tests__/cli-integration.test.ts +++ b/packages/create-oncyber-app/src/__tests__/cli-integration.test.ts @@ -115,6 +115,23 @@ describe("CLI integration", { timeout: 30000 }, () => { expect(rootPkg.scripts["dev"]).toBeDefined(); }); + it("writes pnpm workspace scripts for generated projects", () => { + runCli("pnpm-script-test --template starter --skip-install --skip-git", tmpDir); + + const rootPkg = JSON.parse( + fs.readFileSync(path.join(tmpDir, "pnpm-script-test", "package.json"), "utf-8"), + ); + + expect(rootPkg.packageManager).toBe("pnpm@10.10.0"); + expect(rootPkg.workspaces).toEqual(["packages/*", "apps/*"]); + expect(rootPkg.scripts["dev"]).toBe( + "pnpm --filter pnpm-script-test dev", + ); + expect(rootPkg.scripts["build"]).toBe( + "pnpm --filter pnpm-script-test build", + ); + }); + it("removes examples/* from pnpm-workspace.yaml and keeps apps/*", () => { runCli("workspace-test --template starter --use-pnpm --skip-install --skip-git", tmpDir); @@ -147,6 +164,26 @@ describe("CLI integration", { timeout: 30000 }, () => { expect(stdout).toContain("nonexistent"); }); + it("fails fast when --use-npm is passed", () => { + const { exitCode, stdout } = runCli( + "bad-package-manager --use-npm --skip-install --skip-git", + tmpDir, + ); + + expect(exitCode).not.toBe(0); + expect(stdout).toContain("pnpm only"); + }); + + it("fails fast when --use-yarn is passed", () => { + const { exitCode, stdout } = runCli( + "bad-package-manager --use-yarn --skip-install --skip-git", + tmpDir, + ); + + expect(exitCode).not.toBe(0); + expect(stdout).toContain("pnpm only"); + }); + it("initializes git repo when --skip-git is not passed", () => { runCli("git-test --template starter --use-pnpm --skip-install", tmpDir); @@ -184,11 +221,11 @@ describe("CLI integration", { timeout: 30000 }, () => { expect(exitCode).toBe(0); expect(stdout).toContain("Usage:"); expect(stdout).toContain("--template"); - expect(stdout).toContain("--use-npm"); - expect(stdout).toContain("--use-pnpm"); - expect(stdout).toContain("--use-yarn"); expect(stdout).toContain("--skip-install"); expect(stdout).toContain("--skip-git"); + expect(stdout).toContain("Uses pnpm automatically"); + expect(stdout).not.toContain("--use-npm"); + expect(stdout).not.toContain("--use-yarn"); expect(stdout).toContain("update"); expect(stdout).toContain("Templates:"); expect(stdout).toContain("starter"); diff --git a/packages/create-oncyber-app/src/__tests__/detect-package-manager.test.ts b/packages/create-oncyber-app/src/__tests__/detect-package-manager.test.ts deleted file mode 100644 index 313e8a9..0000000 --- a/packages/create-oncyber-app/src/__tests__/detect-package-manager.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, it, expect, afterEach } from "vitest"; -import { detectPackageManager } from "../detect-package-manager"; - -describe("detectPackageManager", () => { - const originalAgent = process.env.npm_config_user_agent; - - afterEach(() => { - if (originalAgent === undefined) { - delete process.env.npm_config_user_agent; - } else { - process.env.npm_config_user_agent = originalAgent; - } - }); - - it("detects pnpm from user agent", () => { - process.env.npm_config_user_agent = - "pnpm/9.0.0 npm/? node/v20.0.0 darwin arm64"; - expect(detectPackageManager()).toBe("pnpm"); - }); - - it("detects yarn from user agent", () => { - process.env.npm_config_user_agent = "yarn/1.22.0 npm/? node/v20.0.0"; - expect(detectPackageManager()).toBe("yarn"); - }); - - it("detects npm from user agent", () => { - process.env.npm_config_user_agent = - "npm/10.0.0 node/v20.0.0 darwin arm64"; - expect(detectPackageManager()).toBe("npm"); - }); - - it("falls back to npm when no user agent is set", () => { - delete process.env.npm_config_user_agent; - expect(detectPackageManager()).toBe("npm"); - }); - - it("falls back to npm for unknown agent", () => { - process.env.npm_config_user_agent = "bun/1.0.0"; - expect(detectPackageManager()).toBe("npm"); - }); -}); diff --git a/packages/create-oncyber-app/src/__tests__/parse-args.test.ts b/packages/create-oncyber-app/src/__tests__/parse-args.test.ts index 48c5652..ec1f78d 100644 --- a/packages/create-oncyber-app/src/__tests__/parse-args.test.ts +++ b/packages/create-oncyber-app/src/__tests__/parse-args.test.ts @@ -25,21 +25,11 @@ describe("parseArgs", () => { expect(result.projectName).toBe("my-game"); }); - it("parses --use-npm flag", () => { - const result = parseArgs(["node", "create-oncyber-app", "--use-npm"]); - expect(result.packageManager).toBe("npm"); - }); - it("parses --use-pnpm flag", () => { const result = parseArgs(["node", "create-oncyber-app", "--use-pnpm"]); expect(result.packageManager).toBe("pnpm"); }); - it("parses --use-yarn flag", () => { - const result = parseArgs(["node", "create-oncyber-app", "--use-yarn"]); - expect(result.packageManager).toBe("yarn"); - }); - it("parses --skip-install flag", () => { const result = parseArgs(["node", "create-oncyber-app", "--skip-install"]); expect(result.skipInstall).toBe(true); @@ -117,11 +107,11 @@ describe("parseArgs", () => { "create-oncyber-app", "--skip-git", "my-game", - "--use-npm", + "--use-pnpm", ]); expect(result.projectName).toBe("my-game"); expect(result.skipGit).toBe(true); - expect(result.packageManager).toBe("npm"); + expect(result.packageManager).toBe("pnpm"); }); it("parses --local flag", () => { diff --git a/packages/create-oncyber-app/src/detect-package-manager.ts b/packages/create-oncyber-app/src/detect-package-manager.ts deleted file mode 100644 index a2a7eb3..0000000 --- a/packages/create-oncyber-app/src/detect-package-manager.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { PackageManager } from "./index"; - -export function detectPackageManager(): PackageManager { - const agent = process.env.npm_config_user_agent; - if (!agent) return "npm"; - - const name = agent.split("/")[0]; - if (name === "pnpm") return "pnpm"; - if (name === "yarn") return "yarn"; - return "npm"; -} diff --git a/packages/create-oncyber-app/src/index.ts b/packages/create-oncyber-app/src/index.ts index b449b09..6455455 100644 --- a/packages/create-oncyber-app/src/index.ts +++ b/packages/create-oncyber-app/src/index.ts @@ -5,12 +5,15 @@ import path from "path"; import { execSync } from "child_process"; import { input, select } from "@inquirer/prompts"; import pc from "picocolors"; -import { detectPackageManager } from "./detect-package-manager"; import { createSpinner, createSteps } from "./spinner"; const REPO_URL = - process.env.AWE_REPO_URL || "https://github.com/runthefun/awe-dev.git"; + process.env.AWE_REPO_URL || "https://github.com/oncyberio/awe.git"; const REPO_BRANCH = process.env.AWE_REPO_BRANCH || "main"; +const PNPM_VERSION = "10.10.0"; +const PNPM_PACKAGE_MANAGER = `pnpm@${PNPM_VERSION}`; +const PNPM_BOOTSTRAP_COMMAND = + `corepack enable && corepack prepare ${PNPM_PACKAGE_MANAGER} --activate`; const TEMPLATES = [ { name: "starter", description: "Minimal game setup with player controls" }, @@ -109,6 +112,49 @@ function sparseClone(repoUrl: string, dest: string, template: string) { ]); } +function getWorkspaceScriptCommand(workspaceName: string, scriptName: string) { + return `pnpm --filter ${workspaceName} ${scriptName}`; +} + +function canRunCommand(command: string) { + try { + execSync(command, { stdio: "pipe" }); + return true; + } catch { + return false; + } +} + +function ensurePnpmAvailable() { + if (canRunCommand("pnpm --version")) { + return; + } + + console.log(pc.dim("pnpm was not found. Trying to set it up with Corepack...")); + + if (!canRunCommand("corepack --version")) { + console.error("Error: pnpm is required to work with this project."); + console.error(`Run:\n ${PNPM_BOOTSTRAP_COMMAND}`); + process.exit(1); + } + + try { + execSync("corepack enable", { stdio: "pipe" }); + execSync(`corepack prepare ${PNPM_PACKAGE_MANAGER} --activate`, { + stdio: "pipe", + }); + execSync("pnpm --version", { stdio: "pipe" }); + console.log(pc.dim("pnpm is ready.")); + } catch (err: any) { + console.error("Failed to set up pnpm automatically."); + console.error(`Run:\n ${PNPM_BOOTSTRAP_COMMAND}`); + if (err.stderr) { + console.error(err.stderr.toString()); + } + process.exit(1); + } +} + function cleanupScaffold( projectDir: string, projectName: string, @@ -148,11 +194,23 @@ function cleanupScaffold( writeJson(gamePkgPath, gamePkg); } - // Update root package.json — set name + // Update root package.json — set name and generated-project scripts const rootPkgPath = path.join(projectDir, "package.json"); if (fs.existsSync(rootPkgPath)) { const rootPkg = readJson(rootPkgPath); rootPkg.name = projectName; + rootPkg.packageManager = PNPM_PACKAGE_MANAGER; + rootPkg.workspaces = ["packages/*", "apps/*"]; + rootPkg.scripts = { + ...rootPkg.scripts, + dev: getWorkspaceScriptCommand(projectName, "dev"), + build: getWorkspaceScriptCommand(projectName, "build"), + start: getWorkspaceScriptCommand(projectName, "start"), + check: getWorkspaceScriptCommand(projectName, "check"), + }; + delete rootPkg.scripts["create-game"]; + delete rootPkg.scripts["template:dev"]; + delete rootPkg.scripts["template:check"]; writeJson(rootPkgPath, rootPkg); } @@ -190,15 +248,15 @@ ${pc.bold("Commands:")} ${pc.bold("Options:")} --template NAME Choose a project template (default: ${DEFAULT_TEMPLATE}) - --use-npm Use npm for dependency installation - --use-pnpm Use pnpm for dependency installation (default) - --use-yarn Use yarn for dependency installation --skip-install Skip automatic dependency installation --skip-git Skip git repository initialization --local Create app inside the monorepo's apps/ directory --help Show this help message --version Show the CLI version +${pc.bold("Package Manager:")} + Uses pnpm automatically. If pnpm is missing, the CLI will try to enable it with Corepack. + ${pc.bold("Templates:")} ${templateList} @@ -212,8 +270,8 @@ ${pc.bold("Examples:")} ${pc.dim("# Create a multiplayer project")} npx create-oncyber-app my-game --template multiplayer - ${pc.dim("# Use pnpm and skip git init")} - npx create-oncyber-app my-game --use-pnpm --skip-git + ${pc.dim("# Skip git init")} + npx create-oncyber-app my-game --skip-git ${pc.dim("# Create an app inside the monorepo")} create-oncyber-app my-game --local @@ -237,6 +295,17 @@ function printVersion() { console.log(pkg.version); } +function validatePackageManager(options: CliOptions) { + if (!options.packageManager || options.packageManager === "pnpm") { + return; + } + + console.error("Error: create-oncyber-app currently supports pnpm only."); + console.error("Remove the package manager flag and run the command again."); + console.error(`If pnpm is missing, run:\n ${PNPM_BOOTSTRAP_COMMAND}`); + process.exit(1); +} + export function parseArgs(argv: string[]): CliOptions { const args = argv.slice(2); const options: CliOptions = { @@ -449,12 +518,12 @@ async function update(options: CliOptions) { updateSpinner.stop("Packages updated."); // Install deps - const packageManager = options.packageManager || "pnpm"; if (!options.skipInstall) { + ensurePnpmAvailable(); const installSpinner = createSpinner("Installing dependencies..."); installSpinner.start(); try { - execSync(`${packageManager} install`, { + execSync("pnpm install", { cwd: projectDir, stdio: "pipe", }); @@ -587,6 +656,7 @@ async function scaffoldLocal(options: CliOptions, monorepoRoot: string) { // Install deps if (!options.skipInstall) { + ensurePnpmAvailable(); steps.startStep(); try { execSync("pnpm install", { @@ -654,22 +724,6 @@ async function scaffold(options: CliOptions) { }); } - let packageManager: PackageManager; - if (options.packageManager) { - packageManager = options.packageManager; - } else { - const detected = detectPackageManager(); - packageManager = await select({ - message: "Which package manager would you like to use?", - choices: [ - { name: "npm", value: "npm" as const }, - { name: "pnpm", value: "pnpm" as const }, - { name: "yarn", value: "yarn" as const }, - ], - default: detected, - }); - } - const projectDir = path.resolve(process.cwd(), projectName); // Register SIGINT handler to clean up partial project directory @@ -726,10 +780,11 @@ async function scaffold(options: CliOptions) { // Dependency installation let depsInstalled = false; if (!options.skipInstall) { + ensurePnpmAvailable(); const installSpinner = createSpinner("Installing dependencies..."); installSpinner.start(); try { - execSync(`${packageManager} install`, { + execSync("pnpm install", { cwd: projectDir, stdio: "pipe", }); @@ -755,14 +810,15 @@ async function scaffold(options: CliOptions) { console.log("Next steps:"); console.log(` ${pc.cyan(`cd ${projectName}`)}`); if (!depsInstalled) { - console.log(` ${pc.cyan(`${packageManager} install`)}`); + console.log(` ${pc.cyan("pnpm install")}`); } - console.log(` ${pc.cyan(`${packageManager} dev`)}`); + console.log(` ${pc.cyan("pnpm dev")}`); console.log(); } async function main() { const options = parseArgs(process.argv); + validatePackageManager(options); if (options.projectName === "update") { options.projectName = undefined; @@ -785,4 +841,18 @@ async function main() { await scaffold(options); } -main(); +function isDirectExecution() { + const entryPath = process.argv[1]; + if (!entryPath) { + return false; + } + + return path.resolve(entryPath) === __filename; +} + +if (isDirectExecution()) { + void main().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/packages/engine-edit/src/editors/index.ts b/packages/engine-edit/src/editors/index.ts index c062d81..e10e124 100644 --- a/packages/engine-edit/src/editors/index.ts +++ b/packages/engine-edit/src/editors/index.ts @@ -20,7 +20,6 @@ import { MeshComponentEditor } from "./mesh/editor"; import { ModelEditor } from "./model/editor"; import { NavmeshComponentEditor } from "./navmesh/editor"; import { ObjectEditor } from "./object/editor"; -import { ParticlesEditor } from "./particles/editor"; import { PostProcessingEditor } from "./postprocessing/editor"; import { QuarksComponentEditor } from "./quarks/editor"; import { ReflectorEditor } from "./reflector/editor"; @@ -53,7 +52,6 @@ export function registerAllEditors() { registerEditor("model", ModelEditor); registerEditor("navmesh", NavmeshComponentEditor); registerEditor("object", ObjectEditor); - registerEditor("particles", ParticlesEditor); registerEditor("postprocessing", PostProcessingEditor); registerEditor("quarks", QuarksComponentEditor); registerEditor("reflector", ReflectorEditor); diff --git a/packages/engine/src/space/abstract/component-info.ts b/packages/engine/src/space/abstract/component-info.ts index 3b943e5..cc0e701 100644 --- a/packages/engine/src/space/abstract/component-info.ts +++ b/packages/engine/src/space/abstract/component-info.ts @@ -25,6 +25,7 @@ export interface ComponentInfo { studioTab?: string; singleton?: boolean; required?: boolean; + hidden?: boolean; draggable?: boolean; priority?: number; custom?: boolean; diff --git a/packages/engine/src/space/components/particles/index.ts b/packages/engine/src/space/components/particles/index.ts index 2dd410b..2bbbbb7 100644 --- a/packages/engine/src/space/components/particles/index.ts +++ b/packages/engine/src/space/components/particles/index.ts @@ -16,6 +16,7 @@ export class ParticlesComponentFactory extends DefaultComponentFactory { - return !exludedComponents[factory.info.type]; + return !exludedComponents[factory.info.type] && !factory.info.hidden; }); // }, [options]);