Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ Used to create a new nanoforge project.
- `--docker` generate docker files.
- `--no-docker` do not generate docker files.
- `--no-lint` do not generate lint files.
- `--no-agents` do not generate `AGENTS.md` files for AI coding agents.
- `--editor` add editor dependencies.
- `--git` generate a git repository.
- `--no-git` do not generate a git repository.
Expand Down
1 change: 1 addition & 0 deletions docs/docs/commands/2-commands/1-new.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ nf new
| `--skip-install` / `--no-skip-install` | Skip installing dependencies after scaffolding. |
| `--docker` / `--no-docker` | Generate Docker files. |
| `--no-lint` | Skip generating lint configuration files. |
| `--no-agents` | Skip generating the `AGENTS.md` files for AI coding agents. |
| `--editor` | Add editor dependencies. |
| `--git` / `--no-git` | Initialize a git repository. |
| `--git-remote <gitRemote>` / `--no-git-remote` | Set up a git remote (requires `--git`). |
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/schematics/2-schematics/1-workspace.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ schematics @nanoforge-dev/schematics:workspace --name=my-game
| `strict` | `boolean` | `true` | <Tooltip tip="Enables strict mode in tsconfig.json for TypeScript, or strict JSDoc checking in compatible editors for JavaScript.">Enable strict mode</Tooltip> |
| `packageManager` | `"npm" \| "yarn" \| "pnpm" \| "bun"` | `npm` | <Tooltip tip="pnpm gets a pnpm-workspace.yaml and workspace:* dependency ranges; npm, yarn and bun get a package.json 'workspaces' field and pinned ranges.">Package manager to target</Tooltip> |
| `allowBuilds` | `string[]` | `[]` | <Tooltip tip="`bun` is always included (@nanoforge-dev/cli depends on it and it has its own install script). Written to pnpm-workspace.yaml's allowBuilds map (pnpm), package.json's allowScripts (npm) or trustedDependencies (bun). Ignored for yarn.">Packages allowed to run install/build scripts, in addition to `bun`</Tooltip> |
| `agents` | `boolean` | `true` | <Tooltip tip="Documents the workspace layout and commands for AI coding agents; each app gets its own AGENTS.md from the project schematic.">Generate an AGENTS.md file</Tooltip> |

## Generated structure

Expand All @@ -40,6 +41,7 @@ my-game/
├── nanoforge.config.ts # (or .js)
├── .gitignore
├── README.md
├── AGENTS.md # if agents: true
└── apps/ # generated with the `project` schematic
```

Expand Down
2 changes: 2 additions & 0 deletions docs/docs/schematics/2-schematics/2-project.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ schematics @nanoforge-dev/schematics:project --part=client --workspaceName=my-ga
| `editor` | `boolean` | `false` | Reserved for future editor build support |
| `libs` | `string[]` | `[]` | Relative paths to shared libs this project depends on, written into `nanoforge.config.ts` |
| `allowBuilds` | `string[]` | `[]` | <Tooltip tip="Only applies when workspace: false — a project inside a workspace relies on the workspace root's own allowBuilds instead.">Packages allowed to run install/build scripts (standalone projects only)</Tooltip> |
| `agents` | `boolean` | `true` | <Tooltip tip="Documents the project layout, commands and ECS conventions for AI coding agents (Codex, Cursor, GitHub Copilot, Gemini CLI, Claude Code, ...).">Generate an AGENTS.md file</Tooltip> |

## Generated structure

Expand All @@ -48,6 +49,7 @@ schematics @nanoforge-dev/schematics:project --part=client --workspaceName=my-ga
├── pnpm-workspace.yaml # if !workspace && packageManager: pnpm && allowBuilds.length > 0
├── Dockerfile # if docker: true
├── .dockerignore # if docker: true
├── AGENTS.md # if agents: true
├── src/
│ ├── main.ts
│ ├── components/
Expand Down
55 changes: 55 additions & 0 deletions libs/schematics/e2e/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,4 +428,59 @@ describe("project schematic", () => {
expect(bunPackageJson.trustedDependencies).toEqual(["bun"]);
});
});

describe("AGENTS.md", () => {
it("is generated by default with the project specifics", async () => {
const tree = await runner.runSchematic("project", {
part: "client",
workspaceName: "my-workspace",
directory: "apps/client",
editor: true,
});
expect(tree.files).toContain("/apps/client/AGENTS.md");
const content = tree.readContent("/apps/client/AGENTS.md");
expect(content).toContain(
"`my-workspace-client` is a [NanoForge](https://nanoforge.eu) **client** app",
);
expect(content).toContain("read the workspace root `AGENTS.md` first");
expect(content).toContain("NanoforgeFactory.createClient");
expect(content).toContain("`nf editor .`");
expect(content).not.toContain("<%");
});

it("describes a standalone server project without workspace references", async () => {
const tree = await runner.runSchematic("project", {
part: "server",
name: "solo",
directory: "solo",
workspace: false,
});
const content = tree.readContent("/solo/AGENTS.md");
expect(content).toContain("**server** app");
expect(content).toContain("NanoforgeFactory.createServer");
expect(content).not.toContain("workspace root");
expect(content).not.toContain("`nf editor .`");
});

it("uses JavaScript conventions for a JavaScript project", async () => {
const tree = await runner.runSchematic("project", {
part: "client",
directory: "apps/client",
language: "js",
});
const content = tree.readContent("/apps/client/AGENTS.md");
expect(content).toContain("JavaScript (ES modules, typed with JSDoc");
expect(content).toContain("src/main.js");
expect(content).not.toContain("<%");
});

it("is not generated when agents is false", async () => {
const tree = await runner.runSchematic("project", {
part: "client",
directory: "apps/client",
agents: false,
});
expect(tree.files).not.toContain("/apps/client/AGENTS.md");
});
});
});
34 changes: 34 additions & 0 deletions libs/schematics/e2e/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,38 @@ describe("workspace schematic", () => {
},
);
});

describe("AGENTS.md", () => {
it("is generated by default at the workspace root", async () => {
const tree = await runner.runSchematic("workspace", {
name: "my-workspace",
packageManager: "pnpm",
});
expect(tree.files).toContain("/my-workspace/AGENTS.md");
const content = tree.readContent("/my-workspace/AGENTS.md");
expect(content).toContain(
"`my-workspace` is a [NanoForge](https://nanoforge.eu) multiplayer game",
);
expect(content).toContain("`pnpm run build`");
expect(content).not.toContain("<%");
});

it("references the JavaScript config files for a JavaScript workspace", async () => {
const tree = await runner.runSchematic("workspace", {
name: "js-workspace",
language: "js",
});
const content = tree.readContent("/js-workspace/AGENTS.md");
expect(content).toContain("nanoforge.config.js");
expect(content).toContain("jsconfig.json");
});

it("is not generated when agents is false", async () => {
const tree = await runner.runSchematic("workspace", {
name: "no-agents",
agents: false,
});
expect(tree.files).not.toContain("/no-agents/AGENTS.md");
});
});
});
155 changes: 155 additions & 0 deletions libs/schematics/src/libs/project/files/js/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# AGENTS.md

Instructions for AI coding agents (Codex, Cursor, GitHub Copilot, Gemini CLI, Claude Code, ...)
working on this project. Human-oriented documentation lives in `README.md`.

## Project overview

`<%= packageName %>` is a [NanoForge](https://nanoforge.eu) **<%= part %>** app written in
JavaScript (ES modules, typed with JSDoc<% if (strict) { %>, strict checks enabled in `jsconfig.json`<% } %>),
using **<%= packageManager %>**.<% if (workspace) { %>
It is one app of a NanoForge workspace: read the workspace root `AGENTS.md` first.<% } %>

NanoForge is a web game engine built on an ECS (Entity Component System):

- **Entities** are plain ids spawned from the registry.
- **Components** are classes that only hold data, attached to entities.
- **Systems** are functions run every tick on the entities that have a given set of components.
- **Libraries** (graphics, input, network, ...) are plugged into the app with `app.use(...)`.

## Commands

| Task | Command |
| ---------------------------- | ------------------------------------------------ |
| Install dependencies | `<%= packageManager %> install` |
| Develop with hot reload | `nf dev` |
| Build for production | `nf build` (output in `dist/`) |
| Run the built app | `nf start` |
| Scaffold a component | `nf create component -n <Name>` |
| Scaffold a system | `nf create system -n <name>` |
| Install a registry component | `nf install <name>` |<% if (editor) { %>
| Open the visual editor | `nf editor .` |<% } %>

<% if (workspace) { %>Install dependencies from the workspace root. <% } %>There is no test runner configured. After a change, run `nf build` to check that the project
still compiles.

## Project structure

```
.
├── assets/ # Static assets (images, sounds, ...)
├── src/
│ ├── main.js # Entry point: creates the app, registers libraries, entities and systems
│ ├── components/ # ECS components (*.component.js)
│ └── systems/ # ECS systems (*.system.js)
├── nanoforge.config.js # NanoForge project configuration (type: "<%= part %>")
├── package.json
└── jsconfig.json
```

Do not edit generated or installed files: `dist/`, `.nanoforge/` and `nf_modules/`.

## Conventions

### Components

- One component per file in `src/components/`, named `<kebab-name>.component.js`.
- A component is a class with a `name = this.constructor.name;` field and its data assigned
in the constructor. Document parameter types with JSDoc.
- Keep `export default <Component>.name;`: it is required by the code generation.
- Keep `EDITOR_COMPONENT_MANIFEST` in sync with the constructor parameters (same names, order
and types): it is used by the editor.

```js
/**
* @typedef {import("@nanoforge-dev/ecs").EditorComponentManifest} EditorComponentManifest
*/

export class Velocity2D {
name = this.constructor.name;

/**
* @param {number} x
* @param {number} y
*/
constructor(x, y) {
this.x = x;
this.y = y;
}
}

export default Velocity2D.name;

/** @type {EditorComponentManifest} */
export const EDITOR_COMPONENT_MANIFEST = {
name: "Velocity2D",
description: "Velocity of an entity in pixel per tick",
params: [
{ type: "number", name: "x", description: "Horizontal velocity", example: 1, default: 0 },
{ type: "number", name: "y", description: "Vertical velocity", example: 1, default: 0 },
],
};
```

### Systems

- One system per file in `src/systems/`, named `<kebab-name>.system.js`.
- A system is a function `(registry, ctx) => void`. Query entities with
`registry.getZipper([ComponentA, ComponentB])`.
- Keep `export default <system>.name;` and `EDITOR_SYSTEM_MANIFEST`, whose `dependencies` lists
the names of the components the system reads.

```js
/**
* @typedef {import("@nanoforge-dev/common").Context} Context
* @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest
* @typedef {import("@nanoforge-dev/ecs").Registry} Registry
*/

import { Position2D } from "../components/position-2d.component";
import { Velocity2D } from "../components/velocity-2d.component";

/**
* @param {Registry} registry
* @param {Context} _ctx
*/
export function applyVelocity(registry, _ctx) {
registry.getZipper([Position2D, Velocity2D]).forEach(({ Position2D, Velocity2D }) => {
Position2D.x += Velocity2D.x;
Position2D.y += Velocity2D.y;
});
}

export default applyVelocity.name;

/** @type {EditorSystemManifest} */
export const EDITOR_SYSTEM_MANIFEST = {
name: "applyVelocity",
description: "Moves entities according to their velocity",
dependencies: ["Position2D", "Velocity2D"],
};
```

### Entry point

`src/main.js` exports `main(options)`, which:

1. creates the app with `NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>(...)`,
2. registers the libraries with `app.use(...)`, then calls `await app.init(options)`,
3. spawns entities with `registry.spawnEntity()` and `registry.addComponent(...)`,
4. registers systems with `registry.addSystem(...)`, then calls `await app.run()`.

Scaffolding a component or a system does not register it: add the system to `src/main.js`
yourself.

### Imports

- Types (JSDoc `import(...)` only): `@nanoforge-dev/common` (`Context`) and `@nanoforge-dev/ecs`
(`Registry`, manifests).
- Libraries: `@nanoforge-dev/ecs/<%= part %>`, `@nanoforge-dev/network/<%= part %>`<% if (part === "client") { %>,
`@nanoforge-dev/graphics-2d` and `@nanoforge-dev/input`<% } %>.

## Documentation

- NanoForge documentation: <https://docs.nanoforge.eu>
- CLI reference: `nf --help` and `nf <command> --help`
Loading
Loading