@nanoforge-dev/schematics 2.3.1 → 3.0.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 +13 -0
- package/dist/collection.json +5 -0
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/libs/component/component.factory.d.ts +1 -4
- package/dist/libs/component/component.factory.d.ts.map +1 -1
- package/dist/libs/component/component.factory.js +2 -4
- package/dist/libs/component/component.factory.js.map +1 -1
- package/dist/libs/plugin/files/README.md +23 -0
- package/dist/libs/plugin/files/__dot__gitignore +2 -0
- package/dist/libs/plugin/files/nanoforge.manifest.json.template +49 -0
- package/dist/libs/plugin/files/package.json.template +21 -0
- package/dist/libs/plugin/files/src/Panel.svelte +35 -0
- package/dist/libs/plugin/files/src/counter.ts +4 -0
- package/dist/libs/plugin/files/src/index.ts +52 -0
- package/dist/libs/plugin/files/svelte.config.js +2 -0
- package/dist/libs/plugin/files/tsconfig.json +14 -0
- package/dist/libs/plugin/files/vite.config.ts +12 -0
- package/dist/libs/plugin/plugin.factory.d.ts +18 -0
- package/dist/libs/plugin/plugin.factory.d.ts.map +1 -0
- package/dist/libs/plugin/plugin.factory.js +111 -0
- package/dist/libs/plugin/plugin.factory.js.map +1 -0
- package/dist/libs/plugin/schema.json +25 -0
- package/dist/libs/plugin/theme-files/README.md +21 -0
- package/dist/libs/plugin/theme-files/__dot__gitignore +2 -0
- package/dist/libs/plugin/theme-files/build.mjs +29 -0
- package/dist/libs/plugin/theme-files/icons/leaf.svg +1 -0
- package/dist/libs/plugin/theme-files/nanoforge.manifest.json.template +30 -0
- package/dist/libs/plugin/theme-files/package.json.template +11 -0
- package/dist/libs/project/files/js/AGENTS.md.template +126 -0
- package/dist/libs/project/files/js/CLAUDE.md.template +1 -0
- package/dist/libs/project/files/js/__dot__gitignore +4 -0
- package/dist/libs/project/files/ts/AGENTS.md.template +114 -0
- package/dist/libs/project/files/ts/CLAUDE.md.template +1 -0
- package/dist/libs/project/files/ts/__dot__gitignore +4 -0
- package/dist/libs/project/project.factory.d.ts +5 -4
- package/dist/libs/project/project.factory.d.ts.map +1 -1
- package/dist/libs/project/project.factory.js +5 -6
- package/dist/libs/project/project.factory.js.map +1 -1
- package/dist/libs/project/schema.json +5 -0
- package/dist/libs/system/system.factory.d.ts +1 -4
- package/dist/libs/system/system.factory.d.ts.map +1 -1
- package/dist/libs/system/system.factory.js +2 -4
- package/dist/libs/system/system.factory.js.map +1 -1
- package/dist/libs/workspace/files/js/AGENTS.md.template +49 -0
- package/dist/libs/workspace/files/js/CLAUDE.md.template +1 -0
- package/dist/libs/workspace/files/js/__dot__gitignore +4 -0
- package/dist/libs/workspace/files/ts/AGENTS.md.template +49 -0
- package/dist/libs/workspace/files/ts/CLAUDE.md.template +1 -0
- package/dist/libs/workspace/files/ts/__dot__gitignore +4 -0
- package/dist/libs/workspace/schema.json +5 -0
- package/dist/libs/workspace/workspace.factory.d.ts +5 -4
- package/dist/libs/workspace/workspace.factory.d.ts.map +1 -1
- package/dist/libs/workspace/workspace.factory.js +5 -6
- package/dist/libs/workspace/workspace.factory.js.map +1 -1
- package/package.json +15 -16
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The theme has no code: its build copies the manifest and the icons into dist/, the folder the
|
|
3
|
+
* editor loads. With --watch, it copies them again on every change (the editor then reloads it).
|
|
4
|
+
*/
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, rmSync, watch } from "node:fs";
|
|
6
|
+
|
|
7
|
+
const build = () => {
|
|
8
|
+
rmSync("dist", { recursive: true, force: true });
|
|
9
|
+
mkdirSync("dist");
|
|
10
|
+
cpSync("nanoforge.manifest.json", "dist/nanoforge.manifest.json");
|
|
11
|
+
if (existsSync("icons")) cpSync("icons", "dist/icons", { recursive: true });
|
|
12
|
+
console.log("Built dist/");
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// An editor saving mid-rename makes cpSync throw ENOENT: log it and wait for the next change
|
|
16
|
+
// rather than killing the watcher.
|
|
17
|
+
const rebuild = () => {
|
|
18
|
+
try {
|
|
19
|
+
build();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`Build failed: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
build();
|
|
26
|
+
if (process.argv.includes("--watch")) {
|
|
27
|
+
for (const target of ["nanoforge.manifest.json", "icons"])
|
|
28
|
+
if (existsSync(target)) watch(target, { recursive: true }, rebuild);
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21c0-9 6-15 16-16-1 10-7 16-16 16Z"/><path d="M5 21 14 12"/></svg>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "plugin",
|
|
3
|
+
"name": "<%= name %>",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"displayName": "<%= title %>",
|
|
6
|
+
"description": "A color theme for the NanoForge editor.",
|
|
7
|
+
"keywords": ["theme"],
|
|
8
|
+
"engines": {
|
|
9
|
+
"editor": ">=0.1.0"
|
|
10
|
+
},
|
|
11
|
+
"contributes": {
|
|
12
|
+
"themes": [
|
|
13
|
+
{
|
|
14
|
+
"id": "<%= short %>",
|
|
15
|
+
"label": "<%= title %>",
|
|
16
|
+
"extends": "nanoforge-dark",
|
|
17
|
+
"colors": {
|
|
18
|
+
"accent": "#2f7d6d",
|
|
19
|
+
"accent-hover": "#286a5d",
|
|
20
|
+
"accent-text": "#7fd6c2"
|
|
21
|
+
},
|
|
22
|
+
"syntax": {
|
|
23
|
+
"keyword": "#7fd6c2",
|
|
24
|
+
"string": "#e6c07b"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"icons": [{ "id": "<%= short %>-leaf", "path": "icons/leaf.svg" }]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<% var nf = { npm: "npm exec --no -- nf", pnpm: "pnpm exec nf", yarn: "yarn nf", bun: "bun run nf" }[packageManager]; var add = { npm: "npm install", pnpm: "pnpm add", yarn: "yarn add", bun: "bun add" }[packageManager]; var networked = part === "server" || hasServer; %># <%= packageName %>
|
|
2
|
+
|
|
3
|
+
Notes for AI coding agents. This is a NanoForge <%= part %> app, written in JavaScript: ES
|
|
4
|
+
modules, with types in JSDoc comments. NanoForge is a game engine built on an entity component
|
|
5
|
+
system (ECS).
|
|
6
|
+
<% if (part === "client") { %>The game runs in the browser.<% } else { %>This app runs in Bun: it has no graphics, input or DOM.<% } %>
|
|
7
|
+
<% if (workspace) { %>
|
|
8
|
+
This app is part of a workspace: read `../../AGENTS.md` first.
|
|
9
|
+
<% } %>
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
- `nanoforge.config.js`: the app config that `nf` reads (`type: "<%= part %>"`). Keep its
|
|
13
|
+
`entryFile`, `editor.entryFile` and `language: "js"`: the defaults are for TypeScript. Fields
|
|
14
|
+
and defaults: https://docs.nanoforge.eu/docs/cli/configuration/configuration-file
|
|
15
|
+
- `src/main.js`: the entry file. The loader calls its exported `main(options)` from the bundled
|
|
16
|
+
`main.js`, so keep the file name and the export.
|
|
17
|
+
- `src/components/`, `src/systems/`: one component or system per file,
|
|
18
|
+
`<kebab-name>.component.js` and `<kebab-name>.system.js`.
|
|
19
|
+
- `assets/`: copied to the root of `dist/` on each build, so `assets/hero.png` is served as
|
|
20
|
+
`/hero.png`. Read it with `ctx.assets.getAsset("/hero.png")`, an `NfFile` whose `path` is its URL.
|
|
21
|
+
- `dist/` is the build output, emptied on every build. `.nanoforge/` belongs to the NanoForge
|
|
22
|
+
tools: don't edit it.
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
`nf` comes from the `@nanoforge-dev/cli` dev dependency: run it as `<%= nf %> <command>`.
|
|
27
|
+
<% if (workspace) { %>
|
|
28
|
+
Run `nf dev` and `nf start` from the workspace root: they run this app with the other one, and
|
|
29
|
+
they read the `.env` of their `-d` folder (the current folder by default). To build this app
|
|
30
|
+
alone, run `<%= nf %> build -d apps/<%= part %>` at the root.
|
|
31
|
+
<% } else { %>
|
|
32
|
+
- `nf dev`: runs `nf build --watch` and `nf start --watch` together.<% if (part === "client") { %> The game is on
|
|
33
|
+
http://localhost:3000 (`port` in the config).<% } %>
|
|
34
|
+
- `nf build`: bundles `src/main.js` with Bun into `dist/`. It does not check the JSDoc types.
|
|
35
|
+
- `nf start`: <% if (part === "client") { %>serves<% } else { %>runs<% } %> the last build.
|
|
36
|
+
<% } %>
|
|
37
|
+
<% if (workspace && packageManager === "npm") { %>Add a package with `npm install <package> -w apps/<%= part %>` at the workspace root.<% } else { %>Add a package with `<%= add %> <package>` in this folder.<% } %>
|
|
38
|
+
|
|
39
|
+
The `@nanoforge-dev/*` packages are on `^2`. The online docs follow the engine's development
|
|
40
|
+
branch and can describe APIs that the installed release lacks: the installed types
|
|
41
|
+
(`@nanoforge-dev/<package>/dist` in `node_modules`) are the reference.
|
|
42
|
+
|
|
43
|
+
## Game code
|
|
44
|
+
|
|
45
|
+
- `main` creates the app with `NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>`, registers each library with
|
|
46
|
+
`app.use(...)`, then calls `await app.init(options)`. `app.use` throws after `init`, and
|
|
47
|
+
`ecs.registry` throws before it. `await app.run()` starts the tick loop and returns.
|
|
48
|
+
- Components are plain classes. The registry keys them by their `name` field: give each class a
|
|
49
|
+
`name` equal to its class name (the generated ones use `this.constructor.name`), and never store
|
|
50
|
+
game data in `name`. Names must be unique in the app, and `entity` and `id` are reserved. An
|
|
51
|
+
entity has one component per class: adding a second one replaces the first.
|
|
52
|
+
- Systems are `(registry, ctx) => void` functions. They run once per tick, in the order of the
|
|
53
|
+
`registry.addSystem` calls. Their result is ignored, so an async system is not awaited.
|
|
54
|
+
- `registry.getZipper([A, B])` returns one object per entity that has both, keyed by component
|
|
55
|
+
name: `({ A, B }) => …`. The values are the live components: change them in place.
|
|
56
|
+
`getIndexedZipper` adds the entity `id`, and `registry.entityFromIndex(id)` turns it back into an
|
|
57
|
+
entity for `killEntity` or `removeComponent`.
|
|
58
|
+
- `ctx.app.delta` is the time since the last tick, in milliseconds. `tickRate` in `main` is the
|
|
59
|
+
number of ticks per second. `ctx.app.requestStop()` stops the game.<% if (part === "client") { %>
|
|
60
|
+
- Graphics use Konva: `@nanoforge-dev/graphics-2d` re-exports its shapes (`Circle`, `Rect`,
|
|
61
|
+
`Layer`…), and `ctx.graphics.baseLayer` is a layer already on the stage. Coordinates are in a
|
|
62
|
+
1920×1080 design space scaled to the window (the `viewport` option of `createClient`).
|
|
63
|
+
- Keys: `ctx.input.isKeyPressed(InputEnum.KeyW)`, with `InputEnum` from `@nanoforge-dev/input`. Its
|
|
64
|
+
values are `KeyboardEvent.code` names.<% } %>
|
|
65
|
+
<% if (networked) { %>
|
|
66
|
+
## Network
|
|
67
|
+
|
|
68
|
+
- The client and the server connect over TCP, a WebSocket, and over UDP, a WebRTC data channel
|
|
69
|
+
set up through a WebSocket. Their address and ports are `NANOFORGE_*` variables, which `nf start`
|
|
70
|
+
reads from the `.env` of <%= workspace ? "the workspace root" : "its `-d` folder" %>. It gives
|
|
71
|
+
`NANOFORGE_CLIENT_X` to the client and `NANOFORGE_SERVER_X` to the server, as `X` in
|
|
72
|
+
`options.env`. Everything the client gets reaches the browser: never put a secret there.
|
|
73
|
+
https://docs.nanoforge.eu/docs/cli/configuration/environment-variables
|
|
74
|
+
- The generated systems use `ctx.network.tcp` and encode JSON with `TextEncoder`. Check the
|
|
75
|
+
installed `@nanoforge-dev/network` types before using another network API.
|
|
76
|
+
<% } %>
|
|
77
|
+
## Example
|
|
78
|
+
|
|
79
|
+
A component and a system, as the rules above want them:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
// src/components/velocity-2d.component.js
|
|
83
|
+
/** Speed of an entity, in pixels per second. */
|
|
84
|
+
export class Velocity2D {
|
|
85
|
+
name = "Velocity2D";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {number} [x]
|
|
89
|
+
* @param {number} [y]
|
|
90
|
+
*/
|
|
91
|
+
constructor(x = 0, y = 0) {
|
|
92
|
+
this.x = x;
|
|
93
|
+
this.y = y;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/systems/apply-velocity-2d.system.js
|
|
98
|
+
import { Position2D } from "../components/position-2d.component";
|
|
99
|
+
import { Velocity2D } from "../components/velocity-2d.component";
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {import("@nanoforge-dev/common").Context} Context
|
|
103
|
+
* @typedef {import("@nanoforge-dev/ecs").Registry} Registry
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Moves every entity that has a position and a velocity.
|
|
108
|
+
* @param {Registry} registry
|
|
109
|
+
* @param {Context} ctx
|
|
110
|
+
*/
|
|
111
|
+
export const applyVelocity2D = (registry, ctx) => {
|
|
112
|
+
const seconds = ctx.app.delta / 1000;
|
|
113
|
+
|
|
114
|
+
registry.getZipper([Position2D, Velocity2D]).forEach(({ Position2D, Velocity2D }) => {
|
|
115
|
+
Position2D.x += Velocity2D.x * seconds;
|
|
116
|
+
Position2D.y += Velocity2D.y * seconds;
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Register it in `main` with `registry.addSystem(applyVelocity2D)`.
|
|
122
|
+
|
|
123
|
+
## Docs
|
|
124
|
+
|
|
125
|
+
- `nf` commands: https://docs.nanoforge.eu/docs/cli/commands/overview
|
|
126
|
+
- Everything else: https://docs.nanoforge.eu
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -82,6 +82,10 @@ build/Release
|
|
|
82
82
|
|
|
83
83
|
# Dependency directories
|
|
84
84
|
node_modules/
|
|
85
|
+
|
|
86
|
+
# NanoForge registry packages (nf install restores them from nanoforge.packages.lock.json)
|
|
87
|
+
nf_modules/
|
|
88
|
+
|
|
85
89
|
jspm_packages/
|
|
86
90
|
|
|
87
91
|
# Snowpack dependency directory (https://snowpack.dev/)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<% var nf = { npm: "npm exec --no -- nf", pnpm: "pnpm exec nf", yarn: "yarn nf", bun: "bun run nf" }[packageManager]; var add = { npm: "npm install", pnpm: "pnpm add", yarn: "yarn add", bun: "bun add" }[packageManager]; var networked = part === "server" || hasServer; %># <%= packageName %>
|
|
2
|
+
|
|
3
|
+
Notes for AI coding agents. This is a NanoForge <%= part %> app, written in TypeScript.
|
|
4
|
+
NanoForge is a game engine built on an entity component system (ECS).
|
|
5
|
+
<% if (part === "client") { %>The game runs in the browser.<% } else { %>This app runs in Bun: it has no graphics, input or DOM.<% } %>
|
|
6
|
+
<% if (workspace) { %>
|
|
7
|
+
This app is part of a workspace: read `../../AGENTS.md` first.
|
|
8
|
+
<% } %>
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
- `nanoforge.config.ts`: the app config that `nf` reads (`type: "<%= part %>"`). Fields and defaults:
|
|
12
|
+
https://docs.nanoforge.eu/docs/cli/configuration/configuration-file
|
|
13
|
+
- `src/main.ts`: the entry file. The loader calls its exported `main(options)` from the bundled
|
|
14
|
+
`main.js`, so keep the file name and the export.
|
|
15
|
+
- `src/components/`, `src/systems/`: one component or system per file,
|
|
16
|
+
`<kebab-name>.component.ts` and `<kebab-name>.system.ts`.
|
|
17
|
+
- `assets/`: copied to the root of `dist/` on each build, so `assets/hero.png` is served as
|
|
18
|
+
`/hero.png`. Read it with `ctx.assets.getAsset("/hero.png")`, an `NfFile` whose `path` is its URL.
|
|
19
|
+
- `dist/` is the build output, emptied on every build. `.nanoforge/` belongs to the NanoForge
|
|
20
|
+
tools: don't edit it.
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
`nf` comes from the `@nanoforge-dev/cli` dev dependency: run it as `<%= nf %> <command>`.
|
|
25
|
+
<% if (workspace) { %>
|
|
26
|
+
Run `nf dev` and `nf start` from the workspace root: they run this app with the other one, and
|
|
27
|
+
they read the `.env` of their `-d` folder (the current folder by default). To build this app
|
|
28
|
+
alone, run `<%= nf %> build -d apps/<%= part %>` at the root.
|
|
29
|
+
<% } else { %>
|
|
30
|
+
- `nf dev`: runs `nf build --watch` and `nf start --watch` together.<% if (part === "client") { %> The game is on
|
|
31
|
+
http://localhost:3000 (`port` in the config).<% } %>
|
|
32
|
+
- `nf build`: bundles `src/main.ts` with Bun into `dist/`. It does not type-check.
|
|
33
|
+
- `nf start`: <% if (part === "client") { %>serves<% } else { %>runs<% } %> the last build.
|
|
34
|
+
<% } %>
|
|
35
|
+
<% if (workspace && packageManager === "npm") { %>Add a package with `npm install <package> -w apps/<%= part %>` at the workspace root.<% } else { %>Add a package with `<%= add %> <package>` in this folder.<% } %>
|
|
36
|
+
|
|
37
|
+
The `@nanoforge-dev/*` packages are on `^2`. The online docs follow the engine's development
|
|
38
|
+
branch and can describe APIs that the installed release lacks: the installed types
|
|
39
|
+
(`@nanoforge-dev/<package>/dist` in `node_modules`) are the reference.
|
|
40
|
+
|
|
41
|
+
## Game code
|
|
42
|
+
|
|
43
|
+
- `main` creates the app with `NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>`, registers each library with
|
|
44
|
+
`app.use(...)`, then calls `await app.init(options)`. `app.use` throws after `init`, and
|
|
45
|
+
`ecs.registry` throws before it. `await app.run()` starts the tick loop and returns.
|
|
46
|
+
- Components are plain classes. The registry keys them by their `name` field: give each class a
|
|
47
|
+
`name` equal to its class name (the generated ones use `this.constructor.name`), and never store
|
|
48
|
+
game data in `name`. Names must be unique in the app, and `entity` and `id` are reserved. An
|
|
49
|
+
entity has one component per class: adding a second one replaces the first.
|
|
50
|
+
- Systems are `(registry, ctx) => void` functions. They run once per tick, in the order of the
|
|
51
|
+
`registry.addSystem` calls. Their result is ignored, so an async system is not awaited.
|
|
52
|
+
- `registry.getZipper([A, B])` returns one object per entity that has both, keyed by component
|
|
53
|
+
name: `({ A, B }) => …`. The values are the live components: change them in place.
|
|
54
|
+
`getIndexedZipper` adds the entity `id`, and `registry.entityFromIndex(id)` turns it back into an
|
|
55
|
+
entity for `killEntity` or `removeComponent`.
|
|
56
|
+
- `ctx.app.delta` is the time since the last tick, in milliseconds. `tickRate` in `main` is the
|
|
57
|
+
number of ticks per second. `ctx.app.requestStop()` stops the game.<% if (part === "client") { %>
|
|
58
|
+
- Graphics use Konva: `@nanoforge-dev/graphics-2d` re-exports its shapes (`Circle`, `Rect`,
|
|
59
|
+
`Layer`…), and `ctx.graphics.baseLayer` is a layer already on the stage. Coordinates are in a
|
|
60
|
+
1920×1080 design space scaled to the window (the `viewport` option of `createClient`).
|
|
61
|
+
- Keys: `ctx.input.isKeyPressed(InputEnum.KeyW)`, with `InputEnum` from `@nanoforge-dev/input`. Its
|
|
62
|
+
values are `KeyboardEvent.code` names.<% } %>
|
|
63
|
+
<% if (networked) { %>
|
|
64
|
+
## Network
|
|
65
|
+
|
|
66
|
+
- The client and the server connect over TCP, a WebSocket, and over UDP, a WebRTC data channel
|
|
67
|
+
set up through a WebSocket. Their address and ports are `NANOFORGE_*` variables, which `nf start`
|
|
68
|
+
reads from the `.env` of <%= workspace ? "the workspace root" : "its `-d` folder" %>. It gives
|
|
69
|
+
`NANOFORGE_CLIENT_X` to the client and `NANOFORGE_SERVER_X` to the server, as `X` in
|
|
70
|
+
`options.env`. Everything the client gets reaches the browser: never put a secret there.
|
|
71
|
+
https://docs.nanoforge.eu/docs/cli/configuration/environment-variables
|
|
72
|
+
- The generated systems use `ctx.network.tcp` and encode JSON with `TextEncoder`. Check the
|
|
73
|
+
installed `@nanoforge-dev/network` types before using another network API.
|
|
74
|
+
<% } %>
|
|
75
|
+
## Example
|
|
76
|
+
|
|
77
|
+
A component and a system, as the rules above want them:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// src/components/velocity-2d.component.ts
|
|
81
|
+
/** Speed of an entity, in pixels per second. */
|
|
82
|
+
export class Velocity2D {
|
|
83
|
+
name = "Velocity2D";
|
|
84
|
+
|
|
85
|
+
constructor(
|
|
86
|
+
public x = 0,
|
|
87
|
+
public y = 0,
|
|
88
|
+
) {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/systems/apply-velocity-2d.system.ts
|
|
92
|
+
import { type Context } from "@nanoforge-dev/common";
|
|
93
|
+
import { type Registry } from "@nanoforge-dev/ecs";
|
|
94
|
+
|
|
95
|
+
import { Position2D } from "../components/position-2d.component";
|
|
96
|
+
import { Velocity2D } from "../components/velocity-2d.component";
|
|
97
|
+
|
|
98
|
+
/** Moves every entity that has a position and a velocity. */
|
|
99
|
+
export const applyVelocity2D = (registry: Registry, ctx: Context): void => {
|
|
100
|
+
const seconds = ctx.app.delta / 1000;
|
|
101
|
+
|
|
102
|
+
registry.getZipper([Position2D, Velocity2D]).forEach(({ Position2D, Velocity2D }) => {
|
|
103
|
+
Position2D.x += Velocity2D.x * seconds;
|
|
104
|
+
Position2D.y += Velocity2D.y * seconds;
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Register it in `main` with `registry.addSystem(applyVelocity2D)`.
|
|
110
|
+
|
|
111
|
+
## Docs
|
|
112
|
+
|
|
113
|
+
- `nf` commands: https://docs.nanoforge.eu/docs/cli/commands/overview
|
|
114
|
+
- Everything else: https://docs.nanoforge.eu
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -82,6 +82,10 @@ build/Release
|
|
|
82
82
|
|
|
83
83
|
# Dependency directories
|
|
84
84
|
node_modules/
|
|
85
|
+
|
|
86
|
+
# NanoForge registry packages (nf install restores them from nanoforge.packages.lock.json)
|
|
87
|
+
nf_modules/
|
|
88
|
+
|
|
85
89
|
jspm_packages/
|
|
86
90
|
|
|
87
91
|
# Snowpack dependency directory (https://snowpack.dev/)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Rule } from "@angular-devkit/schematics";
|
|
2
|
-
//#region src/libs/project/project.schema.d.ts
|
|
3
2
|
interface ProjectSchema {
|
|
4
3
|
/**
|
|
5
4
|
* The type of project to generate
|
|
@@ -56,9 +55,11 @@ interface ProjectSchema {
|
|
|
56
55
|
* Add editor dependencies
|
|
57
56
|
*/
|
|
58
57
|
editor: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Generate an AGENTS.md file documenting the project for AI coding agents, and a CLAUDE.md that imports it
|
|
60
|
+
*/
|
|
61
|
+
agents: boolean;
|
|
59
62
|
}
|
|
60
|
-
//#endregion
|
|
61
|
-
//#region src/libs/project/project.factory.d.ts
|
|
62
63
|
export declare const main: (schema: ProjectSchema) => Rule;
|
|
63
|
-
|
|
64
|
+
export {};
|
|
64
65
|
//# sourceMappingURL=project.factory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.factory.d.ts","names":[],"sources":["../../../src/libs/project/project.schema.d.ts","../../../src/libs/project/project.factory.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"project.factory.d.ts","names":[],"sources":["../../../src/libs/project/project.schema.d.ts","../../../src/libs/project/project.factory.ts"],"mappings":";UAAiB;;;;EAIf;;;;;;EAOA;;;;;;;EAQA;;;;;;EAOA;;;;EAKA;;;;EAKA;;;;EAKA;;;;EAKA;;;;;EAMA;;;;;;;EAQA;;;;EAKA;;;;EAKA;;qBCoEW,OAAQ,QAAQ,kBAAgB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, normalize, strings } from "@angular-devkit/core";
|
|
2
2
|
import { apply, filter, mergeWith, move, renameTemplateFiles, template, url } from "@angular-devkit/schematics";
|
|
3
|
-
|
|
3
|
+
// src/utils/formatting.ts
|
|
4
4
|
const toWords = (str) => {
|
|
5
5
|
return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().split(/\s+/).filter(Boolean);
|
|
6
6
|
};
|
|
@@ -9,8 +9,7 @@ const toKebabCase = (str) => {
|
|
|
9
9
|
};
|
|
10
10
|
/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */
|
|
11
11
|
const DEFAULT_CLI_DEPENDENCY_VERSION = "latest";
|
|
12
|
-
|
|
13
|
-
//#region src/utils/registry.ts
|
|
12
|
+
// src/utils/registry.ts
|
|
14
13
|
const REGISTRY_URL = "https://registry.npmjs.org";
|
|
15
14
|
const DEFAULT_TIMEOUT_MS = 3e3;
|
|
16
15
|
const RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
@@ -48,8 +47,7 @@ const fetchTrustedVersion = async (packageName, fallback, options = {}) => {
|
|
|
48
47
|
clearTimeout(timeout);
|
|
49
48
|
}
|
|
50
49
|
};
|
|
51
|
-
|
|
52
|
-
//#region src/libs/project/project.factory.ts
|
|
50
|
+
// src/libs/project/project.factory.ts
|
|
53
51
|
const resolveDepVersions = async (part) => {
|
|
54
52
|
const isClient = part === "client";
|
|
55
53
|
const [engine, cli, ecs, network, graphics2d, input] = await Promise.all([
|
|
@@ -92,6 +90,7 @@ const transform = async (schema) => {
|
|
|
92
90
|
hasServer: schema.hasServer,
|
|
93
91
|
docker: schema.docker ?? false,
|
|
94
92
|
editor: schema.editor ?? false,
|
|
93
|
+
agents: schema.agents ?? true,
|
|
95
94
|
allowBuilds: workspace ? [] : ["bun"]
|
|
96
95
|
};
|
|
97
96
|
};
|
|
@@ -104,6 +103,7 @@ const generate = (options, path) => {
|
|
|
104
103
|
}),
|
|
105
104
|
filter((path) => options.docker && !options.workspace || !["Dockerfile", ".dockerignore"].includes(path.split("/").at(-1) ?? "")),
|
|
106
105
|
filter((path) => options.packageManager === "pnpm" && options.allowBuilds.length > 0 || !(path.split("/").at(-1) ?? "").startsWith("pnpm-workspace.yaml")),
|
|
106
|
+
filter((path) => options.agents || !["AGENTS.md", "CLAUDE.md"].some((name) => (path.split("/").at(-1) ?? "").startsWith(name))),
|
|
107
107
|
filter((path) => {
|
|
108
108
|
const filename = path.split("/").at(-1) ?? "";
|
|
109
109
|
if (filename.startsWith("drawable-circle-2d.component") || filename.startsWith("draw-2d.system")) return options.part === "client";
|
|
@@ -121,7 +121,6 @@ const main = (schema) => {
|
|
|
121
121
|
return mergeWith(generate(options, schema.directory));
|
|
122
122
|
};
|
|
123
123
|
};
|
|
124
|
-
//#endregion
|
|
125
124
|
export { main };
|
|
126
125
|
|
|
127
126
|
//# sourceMappingURL=project.factory.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/defaults.ts","../../../src/utils/registry.ts","../../../src/libs/project/project.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","export const DEFAULT_APP_NAME = \"nanoforge-app\";\nexport const DEFAULT_LANGUAGE = \"ts\";\nexport const DEFAULT_PACKAGE_MANAGER = \"npm\";\nexport const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = \"^2\";\nexport const DEFAULT_ENGINE_VERSION = \"^2\";\n\n/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */\nexport const DEFAULT_CLI_DEPENDENCY_VERSION = \"latest\";\n\n/**\n * Fallback used when the registry lookup for `typescript` fails. A specific\n * version, not a range — non-nanoforge packages are pinned exactly.\n */\nexport const DEFAULT_TYPESCRIPT_VERSION = \"6.0.3\";\n\n/**\n * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when\n * resolving a pinned dependency version at generation time, only consider\n * versions published at least this long ago, so a freshly (and potentially\n * maliciously) published release isn't picked up immediately.\n */\nexport const MIN_RELEASE_AGE_HOURS = 48;\n","import { MIN_RELEASE_AGE_HOURS } from \"~/defaults\";\n\nconst REGISTRY_URL = \"https://registry.npmjs.org\";\nconst DEFAULT_TIMEOUT_MS = 3000;\n\nconst RELEASE_VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/;\n\nconst compareVersions = (a: string, b: string): number => {\n const partsA = a.split(\".\").map(Number);\n const partsB = b.split(\".\").map(Number);\n for (let i = 0; i < 3; i++) {\n const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);\n if (diff !== 0) {\n return diff;\n }\n }\n return 0;\n};\n\nexport interface FetchTrustedVersionOptions {\n /**\n * Only consider versions published at least this many hours ago, mirroring\n * pnpm's `minimumReleaseAge` supply-chain protection for a freshly\n * published (and potentially compromised) release.\n * @default MIN_RELEASE_AGE_HOURS\n */\n minAgeHours?: number;\n\n /**\n * Restrict candidates to this major version (e.g. `6` only considers\n * `6.x.x`). Useful to avoid silently jumping a generated project onto a\n * new major release.\n */\n major?: number;\n\n /** Network timeout in milliseconds. @default 3000 */\n timeoutMs?: number;\n}\n\n/**\n * Resolves the newest published version of an npm package that is at least\n * `minAgeHours` old, so freshly (and potentially maliciously) published\n * versions aren't picked up immediately. Falls back to `fallback` on any\n * network error, timeout, missing package, or when no version satisfies the\n * constraints, so callers never have to handle a rejected promise.\n */\nexport const fetchTrustedVersion = async (\n packageName: string,\n fallback: string,\n options: FetchTrustedVersionOptions = {},\n): Promise<string> => {\n const { minAgeHours = MIN_RELEASE_AGE_HOURS, major, timeoutMs = DEFAULT_TIMEOUT_MS } = options;\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(`${REGISTRY_URL}/${encodeURIComponent(packageName)}`, {\n signal: controller.signal,\n });\n if (!response.ok) {\n return fallback;\n }\n\n const data = (await response.json()) as { time?: Record<string, string> };\n const time = data.time;\n if (!time) {\n return fallback;\n }\n\n const cutoff = Date.now() - minAgeHours * 60 * 60 * 1000;\n const eligible = Object.entries(time)\n .filter(([version]) => RELEASE_VERSION_PATTERN.test(version))\n .filter(([version]) => major === undefined || Number(version.split(\".\")[0]) === major)\n .filter(([, publishedAt]) => new Date(publishedAt).getTime() <= cutoff)\n .map(([version]) => version)\n .sort(compareVersions);\n\n return eligible.at(-1) ? `^${eligible.at(-1)}` : fallback;\n } catch {\n return fallback;\n } finally {\n clearTimeout(timeout);\n }\n};\n\n/**\n * Resolves several packages' trusted versions in parallel. Keys of `specs`\n * are the package name to query the registry for; values are the fallback\n * to use if that lookup fails. Returns a map from the *same* keys to the\n * resolved (or fallback) version — typed so callers don't get `| undefined`\n * back for a key they just passed in.\n */\nexport const fetchTrustedVersions = async <\n T extends Record<string, { fallback: string; major?: number }>,\n>(\n specs: T,\n): Promise<{ [K in keyof T]: string }> => {\n const entries = await Promise.all(\n Object.entries(specs).map(async ([packageName, { fallback, major }]) => {\n const version = await fetchTrustedVersion(packageName, fallback, { major });\n return [packageName, version] as const;\n }),\n );\n return Object.fromEntries(entries) as { [K in keyof T]: string };\n};\n","import { type Path, join, normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n type Source,\n apply,\n filter,\n mergeWith,\n move,\n renameTemplateFiles,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toKebabCase } from \"@utils/formatting\";\nimport { fetchTrustedVersion } from \"@utils/registry\";\n\nimport {\n DEFAULT_APP_NAME,\n DEFAULT_CLI_DEPENDENCY_VERSION,\n DEFAULT_ENGINE_VERSION,\n DEFAULT_LANGUAGE,\n DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n DEFAULT_PACKAGE_MANAGER,\n} from \"~/defaults\";\n\nimport { type ProjectOptions } from \"./project.options\";\nimport { type ProjectSchema } from \"./project.schema\";\n\ninterface DepVersions {\n ecs: string;\n graphics2d: string;\n input: string;\n network: string;\n engine: string;\n cli: string;\n}\n\nconst resolveDepVersions = async (part: ProjectSchema[\"part\"]): Promise<DepVersions> => {\n const isClient = part === \"client\";\n\n const [engine, cli, ecs, network, graphics2d, input] = await Promise.all([\n fetchTrustedVersion(\"@nanoforge-dev/core\", DEFAULT_ENGINE_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/cli\", DEFAULT_CLI_DEPENDENCY_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/ecs\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/network\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),\n isClient\n ? fetchTrustedVersion(\"@nanoforge-dev/graphics-2d\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION)\n : DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n isClient\n ? fetchTrustedVersion(\"@nanoforge-dev/input\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION)\n : DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n ]);\n\n return {\n ecs,\n graphics2d,\n input,\n network,\n engine,\n cli,\n };\n};\n\nconst transform = async (schema: ProjectSchema): Promise<ProjectOptions> => {\n const packageManager = schema.packageManager ?? DEFAULT_PACKAGE_MANAGER;\n const workspace = schema.workspace ?? true;\n const depVersions = await resolveDepVersions(schema.part);\n\n const name = schema.name ?? schema.part;\n const packageName = schema.workspaceName\n ? `${toKebabCase(schema.workspaceName)}-${toKebabCase(name)}`\n : toKebabCase(schema.name ?? DEFAULT_APP_NAME);\n\n return {\n part: schema.part,\n appClass: schema.part === \"client\" ? \"NanoforgeClient\" : \"NanoforgeServer\",\n packageName,\n language: schema.language ?? DEFAULT_LANGUAGE,\n strict: schema.strict ?? true,\n packageManager,\n workspace,\n ecsVersion: depVersions.ecs,\n graphics2dVersion: depVersions.graphics2d,\n inputVersion: depVersions.input,\n networkVersion: depVersions.network,\n engineVersion: depVersions.engine,\n cliVersion: depVersions.cli,\n hasServer: schema.hasServer,\n docker: schema.docker ?? false,\n editor: schema.editor ?? false,\n allowBuilds: workspace ? [] : [\"bun\"],\n };\n};\n\nconst generate = (options: ProjectOptions, path: string): Source => {\n return apply(url(join(\"./files\" as Path, options.language)), [\n template({\n dot: \".\",\n ...strings,\n ...options,\n }),\n filter(\n (path) =>\n (options.docker && !options.workspace) ||\n ![\"Dockerfile\", \".dockerignore\"].includes(path.split(\"/\").at(-1) ?? \"\"),\n ),\n filter(\n (path) =>\n (options.packageManager === \"pnpm\" && options.allowBuilds.length > 0) ||\n !(path.split(\"/\").at(-1) ?? \"\").startsWith(\"pnpm-workspace.yaml\"),\n ),\n filter((path) => {\n const filename = path.split(\"/\").at(-1) ?? \"\";\n if (\n filename.startsWith(\"drawable-circle-2d.component\") ||\n filename.startsWith(\"draw-2d.system\")\n ) {\n return options.part === \"client\";\n }\n if (filename.startsWith(\"move-2d.system\")) {\n return options.part === \"server\";\n }\n if (filename.startsWith(\"position-sync.system\")) {\n return options.part === \"client\" && options.hasServer;\n }\n return true;\n }),\n renameTemplateFiles(),\n move(normalize(path)),\n ]);\n};\n\nexport const main = (schema: ProjectSchema): Rule => {\n return async () => {\n const options = await transform(schema);\n\n return mergeWith(generate(options, schema.directory));\n };\n};\n"],"mappings":";;;AAAA,MAAM,WAAW,QAA0B;CACzC,OAAO,IACJ,QAAQ,mBAAmB,OAAO,CAAC,CACnC,QAAQ,yBAAyB,OAAO,CAAC,CACzC,QAAQ,UAAU,GAAG,CAAC,CACtB,KAAK,CAAC,CACN,MAAM,KAAK,CAAC,CACZ,OAAO,OAAO;AACnB;AAEA,MAAa,eAAe,QAAwB;CAClD,OAAO,QAAQ,GAAG,CAAC,CAChB,KAAK,SAAS,KAAK,YAAY,CAAC,CAAC,CACjC,KAAK,GAAG;AACb;;ACPA,MAAa,iCAAiC;;;ACL9C,MAAM,eAAe;AACrB,MAAM,qBAAqB;AAE3B,MAAM,0BAA0B;AAEhC,MAAM,mBAAmB,GAAW,MAAsB;CACxD,MAAM,SAAS,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;CACtC,MAAM,SAAS,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;CACtC,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;EAC1B,MAAM,QAAQ,OAAO,MAAM,MAAM,OAAO,MAAM;EAC9C,IAAI,SAAS,GACX,OAAO;CAEX;CACA,OAAO;AACT;;;;;;;;AA6BA,MAAa,sBAAsB,OACjC,aACA,UACA,UAAsC,CAAC,MACnB;CACpB,MAAM,EAAE,cAAA,IAAqC,OAAO,YAAY,uBAAuB;CAEvF,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,UAAU,iBAAiB,WAAW,MAAM,GAAG,SAAS;CAE9D,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,GAAG,aAAa,GAAG,mBAAmB,WAAW,KAAK,EACjF,QAAQ,WAAW,OACrB,CAAC;EACD,IAAI,CAAC,SAAS,IACZ,OAAO;EAIT,MAAM,QAAO,MADO,SAAS,KAAK,EACrB,CAAK;EAClB,IAAI,CAAC,MACH,OAAO;EAGT,MAAM,SAAS,KAAK,IAAI,IAAI,cAAc,KAAK,KAAK;EACpD,MAAM,WAAW,OAAO,QAAQ,IAAI,CAAC,CAClC,QAAQ,CAAC,aAAa,wBAAwB,KAAK,OAAO,CAAC,CAAC,CAC5D,QAAQ,CAAC,aAAa,UAAU,UAAa,OAAO,QAAQ,MAAM,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,CAAC,CACrF,QAAQ,GAAG,iBAAiB,IAAI,KAAK,WAAW,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CACtE,KAAK,CAAC,aAAa,OAAO,CAAC,CAC3B,KAAK,eAAe;EAEvB,OAAO,SAAS,GAAG,EAAE,IAAI,IAAI,SAAS,GAAG,EAAE,MAAM;CACnD,QAAQ;EACN,OAAO;CACT,UAAU;EACR,aAAa,OAAO;CACtB;AACF;;;AC/CA,MAAM,qBAAqB,OAAO,SAAsD;CACtF,MAAM,WAAW,SAAS;CAE1B,MAAM,CAAC,QAAQ,KAAK,KAAK,SAAS,YAAY,SAAS,MAAM,QAAQ,IAAI;EACvE,oBAAoB,uBAAA,IAA6C;EACjE,oBAAoB,sBAAsB,8BAA8B;EACxE,oBAAoB,sBAAA,IAA0D;EAC9E,oBAAoB,0BAAA,IAA8D;EAClF,WACI,oBAAoB,8BAAA,IAAkE,IAAA;EAE1F,WACI,oBAAoB,wBAAA,IAA4D,IAAA;CAEtF,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;CACF;AACF;AAEA,MAAM,YAAY,OAAO,WAAmD;CAC1E,MAAM,iBAAiB,OAAO,kBAAA;CAC9B,MAAM,YAAY,OAAO,aAAa;CACtC,MAAM,cAAc,MAAM,mBAAmB,OAAO,IAAI;CAExD,MAAM,OAAO,OAAO,QAAQ,OAAO;CACnC,MAAM,cAAc,OAAO,gBACvB,GAAG,YAAY,OAAO,aAAa,EAAE,GAAG,YAAY,IAAI,MACxD,YAAY,OAAO,QAAA,eAAwB;CAE/C,OAAO;EACL,MAAM,OAAO;EACb,UAAU,OAAO,SAAS,WAAW,oBAAoB;EACzD;EACA,UAAU,OAAO,YAAA;EACjB,QAAQ,OAAO,UAAU;EACzB;EACA;EACA,YAAY,YAAY;EACxB,mBAAmB,YAAY;EAC/B,cAAc,YAAY;EAC1B,gBAAgB,YAAY;EAC5B,eAAe,YAAY;EAC3B,YAAY,YAAY;EACxB,WAAW,OAAO;EAClB,QAAQ,OAAO,UAAU;EACzB,QAAQ,OAAO,UAAU;EACzB,aAAa,YAAY,CAAC,IAAI,CAAC,KAAK;CACtC;AACF;AAEA,MAAM,YAAY,SAAyB,SAAyB;CAClE,OAAO,MAAM,IAAI,KAAK,WAAmB,QAAQ,QAAQ,CAAC,GAAG;EAC3D,SAAS;GACP,KAAK;GACL,GAAG;GACH,GAAG;EACL,CAAC;EACD,QACG,SACE,QAAQ,UAAU,CAAC,QAAQ,aAC5B,CAAC,CAAC,cAAc,eAAe,CAAC,CAAC,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,CAC1E;EACA,QACG,SACE,QAAQ,mBAAmB,UAAU,QAAQ,YAAY,SAAS,KACnE,EAAE,KAAK,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,GAAA,CAAI,WAAW,qBAAqB,CACpE;EACA,QAAQ,SAAS;GACf,MAAM,WAAW,KAAK,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK;GAC3C,IACE,SAAS,WAAW,8BAA8B,KAClD,SAAS,WAAW,gBAAgB,GAEpC,OAAO,QAAQ,SAAS;GAE1B,IAAI,SAAS,WAAW,gBAAgB,GACtC,OAAO,QAAQ,SAAS;GAE1B,IAAI,SAAS,WAAW,sBAAsB,GAC5C,OAAO,QAAQ,SAAS,YAAY,QAAQ;GAE9C,OAAO;EACT,CAAC;EACD,oBAAoB;EACpB,KAAK,UAAU,IAAI,CAAC;CACtB,CAAC;AACH;AAEA,MAAa,QAAQ,WAAgC;CACnD,OAAO,YAAY;EACjB,MAAM,UAAU,MAAM,UAAU,MAAM;EAEtC,OAAO,UAAU,SAAS,SAAS,OAAO,SAAS,CAAC;CACtD;AACF"}
|
|
1
|
+
{"version":3,"file":"project.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/defaults.ts","../../../src/utils/registry.ts","../../../src/libs/project/project.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","export const DEFAULT_APP_NAME = \"nanoforge-app\";\nexport const DEFAULT_LANGUAGE = \"ts\";\nexport const DEFAULT_PACKAGE_MANAGER = \"npm\";\nexport const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = \"^2\";\nexport const DEFAULT_ENGINE_VERSION = \"^2\";\n\n/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */\nexport const DEFAULT_CLI_DEPENDENCY_VERSION = \"latest\";\n\n/**\n * Fallback used when the registry lookup for `typescript` fails. A specific\n * version, not a range — non-nanoforge packages are pinned exactly.\n */\nexport const DEFAULT_TYPESCRIPT_VERSION = \"6.0.3\";\n\n/**\n * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when\n * resolving a pinned dependency version at generation time, only consider\n * versions published at least this long ago, so a freshly (and potentially\n * maliciously) published release isn't picked up immediately.\n */\nexport const MIN_RELEASE_AGE_HOURS = 48;\n\n/** Fallback for the editor packages (`@nanoforge-dev/editor-*`), which share one version line. */\nexport const DEFAULT_EDITOR_VERSION = \"^1.0.0\";\n","import { MIN_RELEASE_AGE_HOURS } from \"~/defaults\";\n\nconst REGISTRY_URL = \"https://registry.npmjs.org\";\nconst DEFAULT_TIMEOUT_MS = 3000;\n\nconst RELEASE_VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/;\n\nconst compareVersions = (a: string, b: string): number => {\n const partsA = a.split(\".\").map(Number);\n const partsB = b.split(\".\").map(Number);\n for (let i = 0; i < 3; i++) {\n const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);\n if (diff !== 0) {\n return diff;\n }\n }\n return 0;\n};\n\nexport interface FetchTrustedVersionOptions {\n /**\n * Only consider versions published at least this many hours ago, mirroring\n * pnpm's `minimumReleaseAge` supply-chain protection for a freshly\n * published (and potentially compromised) release.\n * @default MIN_RELEASE_AGE_HOURS\n */\n minAgeHours?: number;\n\n /**\n * Restrict candidates to this major version (e.g. `6` only considers\n * `6.x.x`). Useful to avoid silently jumping a generated project onto a\n * new major release.\n */\n major?: number;\n\n /** Network timeout in milliseconds. @default 3000 */\n timeoutMs?: number;\n}\n\n/**\n * Resolves the newest published version of an npm package that is at least\n * `minAgeHours` old, so freshly (and potentially maliciously) published\n * versions aren't picked up immediately. Falls back to `fallback` on any\n * network error, timeout, missing package, or when no version satisfies the\n * constraints, so callers never have to handle a rejected promise.\n */\nexport const fetchTrustedVersion = async (\n packageName: string,\n fallback: string,\n options: FetchTrustedVersionOptions = {},\n): Promise<string> => {\n const { minAgeHours = MIN_RELEASE_AGE_HOURS, major, timeoutMs = DEFAULT_TIMEOUT_MS } = options;\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(`${REGISTRY_URL}/${encodeURIComponent(packageName)}`, {\n signal: controller.signal,\n });\n if (!response.ok) {\n return fallback;\n }\n\n const data = (await response.json()) as { time?: Record<string, string> };\n const time = data.time;\n if (!time) {\n return fallback;\n }\n\n const cutoff = Date.now() - minAgeHours * 60 * 60 * 1000;\n const eligible = Object.entries(time)\n .filter(([version]) => RELEASE_VERSION_PATTERN.test(version))\n .filter(([version]) => major === undefined || Number(version.split(\".\")[0]) === major)\n .filter(([, publishedAt]) => new Date(publishedAt).getTime() <= cutoff)\n .map(([version]) => version)\n .sort(compareVersions);\n\n return eligible.at(-1) ? `^${eligible.at(-1)}` : fallback;\n } catch {\n return fallback;\n } finally {\n clearTimeout(timeout);\n }\n};\n\n/**\n * Resolves several packages' trusted versions in parallel. Keys of `specs`\n * are the package name to query the registry for; values are the fallback\n * to use if that lookup fails. Returns a map from the *same* keys to the\n * resolved (or fallback) version — typed so callers don't get `| undefined`\n * back for a key they just passed in.\n */\nexport const fetchTrustedVersions = async <\n T extends Record<string, { fallback: string; major?: number }>,\n>(\n specs: T,\n): Promise<{ [K in keyof T]: string }> => {\n const entries = await Promise.all(\n Object.entries(specs).map(async ([packageName, { fallback, major }]) => {\n const version = await fetchTrustedVersion(packageName, fallback, { major });\n return [packageName, version] as const;\n }),\n );\n return Object.fromEntries(entries) as { [K in keyof T]: string };\n};\n","import { type Path, join, normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n type Source,\n apply,\n filter,\n mergeWith,\n move,\n renameTemplateFiles,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toKebabCase } from \"@utils/formatting\";\nimport { fetchTrustedVersion } from \"@utils/registry\";\n\nimport {\n DEFAULT_APP_NAME,\n DEFAULT_CLI_DEPENDENCY_VERSION,\n DEFAULT_ENGINE_VERSION,\n DEFAULT_LANGUAGE,\n DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n DEFAULT_PACKAGE_MANAGER,\n} from \"~/defaults\";\n\nimport { type ProjectOptions } from \"./project.options\";\nimport { type ProjectSchema } from \"./project.schema\";\n\ninterface DepVersions {\n ecs: string;\n graphics2d: string;\n input: string;\n network: string;\n engine: string;\n cli: string;\n}\n\nconst resolveDepVersions = async (part: ProjectSchema[\"part\"]): Promise<DepVersions> => {\n const isClient = part === \"client\";\n\n const [engine, cli, ecs, network, graphics2d, input] = await Promise.all([\n fetchTrustedVersion(\"@nanoforge-dev/core\", DEFAULT_ENGINE_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/cli\", DEFAULT_CLI_DEPENDENCY_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/ecs\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/network\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),\n isClient\n ? fetchTrustedVersion(\"@nanoforge-dev/graphics-2d\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION)\n : DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n isClient\n ? fetchTrustedVersion(\"@nanoforge-dev/input\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION)\n : DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n ]);\n\n return {\n ecs,\n graphics2d,\n input,\n network,\n engine,\n cli,\n };\n};\n\nconst transform = async (schema: ProjectSchema): Promise<ProjectOptions> => {\n const packageManager = schema.packageManager ?? DEFAULT_PACKAGE_MANAGER;\n const workspace = schema.workspace ?? true;\n const depVersions = await resolveDepVersions(schema.part);\n\n const name = schema.name ?? schema.part;\n const packageName = schema.workspaceName\n ? `${toKebabCase(schema.workspaceName)}-${toKebabCase(name)}`\n : toKebabCase(schema.name ?? DEFAULT_APP_NAME);\n\n return {\n part: schema.part,\n appClass: schema.part === \"client\" ? \"NanoforgeClient\" : \"NanoforgeServer\",\n packageName,\n language: schema.language ?? DEFAULT_LANGUAGE,\n strict: schema.strict ?? true,\n packageManager,\n workspace,\n ecsVersion: depVersions.ecs,\n graphics2dVersion: depVersions.graphics2d,\n inputVersion: depVersions.input,\n networkVersion: depVersions.network,\n engineVersion: depVersions.engine,\n cliVersion: depVersions.cli,\n hasServer: schema.hasServer,\n docker: schema.docker ?? false,\n editor: schema.editor ?? false,\n agents: schema.agents ?? true,\n allowBuilds: workspace ? [] : [\"bun\"],\n };\n};\n\nconst generate = (options: ProjectOptions, path: string): Source => {\n return apply(url(join(\"./files\" as Path, options.language)), [\n template({\n dot: \".\",\n ...strings,\n ...options,\n }),\n filter(\n (path) =>\n (options.docker && !options.workspace) ||\n ![\"Dockerfile\", \".dockerignore\"].includes(path.split(\"/\").at(-1) ?? \"\"),\n ),\n filter(\n (path) =>\n (options.packageManager === \"pnpm\" && options.allowBuilds.length > 0) ||\n !(path.split(\"/\").at(-1) ?? \"\").startsWith(\"pnpm-workspace.yaml\"),\n ),\n filter(\n (path) =>\n options.agents ||\n ![\"AGENTS.md\", \"CLAUDE.md\"].some((name) => (path.split(\"/\").at(-1) ?? \"\").startsWith(name)),\n ),\n filter((path) => {\n const filename = path.split(\"/\").at(-1) ?? \"\";\n if (\n filename.startsWith(\"drawable-circle-2d.component\") ||\n filename.startsWith(\"draw-2d.system\")\n ) {\n return options.part === \"client\";\n }\n if (filename.startsWith(\"move-2d.system\")) {\n return options.part === \"server\";\n }\n if (filename.startsWith(\"position-sync.system\")) {\n return options.part === \"client\" && options.hasServer;\n }\n return true;\n }),\n renameTemplateFiles(),\n move(normalize(path)),\n ]);\n};\n\nexport const main = (schema: ProjectSchema): Rule => {\n return async () => {\n const options = await transform(schema);\n\n return mergeWith(generate(options, schema.directory));\n };\n};\n"],"mappings":"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"}
|
|
@@ -66,6 +66,11 @@
|
|
|
66
66
|
"description": "Add editor dependencies",
|
|
67
67
|
"default": false
|
|
68
68
|
},
|
|
69
|
+
"agents": {
|
|
70
|
+
"type": "boolean",
|
|
71
|
+
"description": "Generate an AGENTS.md file documenting the project for AI coding agents, and a CLAUDE.md that imports it",
|
|
72
|
+
"default": true
|
|
73
|
+
},
|
|
69
74
|
"libs": {
|
|
70
75
|
"type": "array",
|
|
71
76
|
"items": { "type": "string" },
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Rule } from "@angular-devkit/schematics";
|
|
2
|
-
//#region src/libs/system/system.schema.d.ts
|
|
3
2
|
interface SystemSchema {
|
|
4
3
|
/**
|
|
5
4
|
* System destination directory
|
|
@@ -18,8 +17,6 @@ interface SystemSchema {
|
|
|
18
17
|
*/
|
|
19
18
|
language: "js" | "ts";
|
|
20
19
|
}
|
|
21
|
-
//#endregion
|
|
22
|
-
//#region src/libs/system/system.factory.d.ts
|
|
23
20
|
export declare const main: (schema: SystemSchema) => Rule;
|
|
24
|
-
|
|
21
|
+
export {};
|
|
25
22
|
//# sourceMappingURL=system.factory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system.factory.d.ts","names":[],"sources":["../../../src/libs/system/system.schema.d.ts","../../../src/libs/system/system.factory.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"system.factory.d.ts","names":[],"sources":["../../../src/libs/system/system.schema.d.ts","../../../src/libs/system/system.factory.ts"],"mappings":";UAAiB;;;;EAIf;;;;EAKA;;;;EAKA;;;;EAKA;;qBCiBW,OAAQ,QAAQ,iBAAe"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, normalize, strings } from "@angular-devkit/core";
|
|
2
2
|
import { apply, mergeWith, move, template, url } from "@angular-devkit/schematics";
|
|
3
|
-
|
|
3
|
+
// src/utils/formatting.ts
|
|
4
4
|
const toWords = (str) => {
|
|
5
5
|
return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().split(/\s+/).filter(Boolean);
|
|
6
6
|
};
|
|
@@ -10,8 +10,7 @@ const toKebabCase = (str) => {
|
|
|
10
10
|
const toCamelCase = (str) => {
|
|
11
11
|
return toWords(str).map((word, i) => i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
12
12
|
};
|
|
13
|
-
|
|
14
|
-
//#region src/libs/system/system.factory.ts
|
|
13
|
+
// src/libs/system/system.factory.ts
|
|
15
14
|
const transform = (schema) => {
|
|
16
15
|
return {
|
|
17
16
|
part: schema.part,
|
|
@@ -30,7 +29,6 @@ const main = (schema) => {
|
|
|
30
29
|
const options = transform(schema);
|
|
31
30
|
return mergeWith(generate(options, schema.directory, schema.language));
|
|
32
31
|
};
|
|
33
|
-
//#endregion
|
|
34
32
|
export { main };
|
|
35
33
|
|
|
36
34
|
//# sourceMappingURL=system.factory.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/libs/system/system.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","import { type Path, join, normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n type Source,\n apply,\n mergeWith,\n move,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toCamelCase, toKebabCase } from \"@utils/formatting\";\n\nimport { type SystemOptions } from \"./system.options\";\nimport { type SystemSchema } from \"./system.schema\";\n\nconst transform = (schema: SystemSchema): SystemOptions => {\n return {\n part: schema.part,\n functionName: toCamelCase(schema.name),\n fileName: toKebabCase(schema.name),\n };\n};\n\nconst generate = (options: SystemOptions, path: string, language: \"ts\" | \"js\"): Source => {\n const rules = [\n template({\n ...strings,\n ...options,\n }),\n move(normalize(path)),\n ];\n\n return apply(url(join(\"./files\" as Path, language)), rules);\n};\n\nexport const main = (schema: SystemSchema): Rule => {\n const options = transform(schema);\n\n return mergeWith(generate(options, schema.directory, schema.language));\n};\n"],"mappings":";;;AAAA,MAAM,WAAW,QAA0B;CACzC,OAAO,IACJ,QAAQ,mBAAmB,OAAO,CAAC,CACnC,QAAQ,yBAAyB,OAAO,CAAC,CACzC,QAAQ,UAAU,GAAG,CAAC,CACtB,KAAK,CAAC,CACN,MAAM,KAAK,CAAC,CACZ,OAAO,OAAO;AACnB;AAEA,MAAa,eAAe,QAAwB;CAClD,OAAO,QAAQ,GAAG,CAAC,CAChB,KAAK,SAAS,KAAK,YAAY,CAAC,CAAC,CACjC,KAAK,GAAG;AACb;AAQA,MAAa,eAAe,QAAwB;CAElD,OADc,QAAQ,GACf,CAAA,CACJ,KAAK,MAAM,MACV,MAAM,IAAI,KAAK,YAAY,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAC1F,CAAC,CACA,KAAK,EAAE;AACZ
|
|
1
|
+
{"version":3,"file":"system.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/libs/system/system.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","import { type Path, join, normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n type Source,\n apply,\n mergeWith,\n move,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toCamelCase, toKebabCase } from \"@utils/formatting\";\n\nimport { type SystemOptions } from \"./system.options\";\nimport { type SystemSchema } from \"./system.schema\";\n\nconst transform = (schema: SystemSchema): SystemOptions => {\n return {\n part: schema.part,\n functionName: toCamelCase(schema.name),\n fileName: toKebabCase(schema.name),\n };\n};\n\nconst generate = (options: SystemOptions, path: string, language: \"ts\" | \"js\"): Source => {\n const rules = [\n template({\n ...strings,\n ...options,\n }),\n move(normalize(path)),\n ];\n\n return apply(url(join(\"./files\" as Path, language)), rules);\n};\n\nexport const main = (schema: SystemSchema): Rule => {\n const options = transform(schema);\n\n return mergeWith(generate(options, schema.directory, schema.language));\n};\n"],"mappings":";;;AAAA,MAAM,WAAW,QAA0B;CACzC,OAAO,IACJ,QAAQ,mBAAmB,OAAO,CAAC,CACnC,QAAQ,yBAAyB,OAAO,CAAC,CACzC,QAAQ,UAAU,GAAG,CAAC,CACtB,KAAK,CAAC,CACN,MAAM,KAAK,CAAC,CACZ,OAAO,OAAO;AACnB;AAEA,MAAa,eAAe,QAAwB;CAClD,OAAO,QAAQ,GAAG,CAAC,CAChB,KAAK,SAAS,KAAK,YAAY,CAAC,CAAC,CACjC,KAAK,GAAG;AACb;AAQA,MAAa,eAAe,QAAwB;CAElD,OADc,QAAQ,GACf,CAAA,CACJ,KAAK,MAAM,MACV,MAAM,IAAI,KAAK,YAAY,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAC1F,CAAC,CACA,KAAK,EAAE;AACZ;;ACbA,MAAM,aAAa,WAAwC;CACzD,OAAO;EACL,MAAM,OAAO;EACb,cAAc,YAAY,OAAO,IAAI;EACrC,UAAU,YAAY,OAAO,IAAI;CACnC;AACF;AAEA,MAAM,YAAY,SAAwB,MAAc,aAAkC;CACxF,MAAM,QAAQ,CACZ,SAAS;EACP,GAAG;EACH,GAAG;CACL,CAAC,GACD,KAAK,UAAU,IAAI,CAAC,CACtB;CAEA,OAAO,MAAM,IAAI,KAAK,WAAmB,QAAQ,CAAC,GAAG,KAAK;AAC5D;AAEA,MAAa,QAAQ,WAA+B;CAClD,MAAM,UAAU,UAAU,MAAM;CAEhC,OAAO,UAAU,SAAS,SAAS,OAAO,WAAW,OAAO,QAAQ,CAAC;AACvE"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<% var nf = { npm: "npm exec --no -- nf", pnpm: "pnpm exec nf", yarn: "yarn nf", bun: "bun run nf" }[packageManager]; %># <%= name %>
|
|
2
|
+
|
|
3
|
+
Notes for AI coding agents. This is a NanoForge game workspace, written in JavaScript (ES
|
|
4
|
+
modules, with types in JSDoc comments): a client app that runs in the browser and a server app
|
|
5
|
+
that runs in Bun. NanoForge is a game engine built on an entity component system (ECS).
|
|
6
|
+
|
|
7
|
+
## Files
|
|
8
|
+
|
|
9
|
+
- `nanoforge.config.js`: `type: "workspace"`, with `packages: ["apps/*"]`. Each folder it matches
|
|
10
|
+
is an app with its own `nanoforge.config.js`.
|
|
11
|
+
- `apps/client/`, `apps/server/`: the two apps. Each has its own `AGENTS.md`: read the one of the
|
|
12
|
+
app you change.
|
|
13
|
+
- `.env`: the `NANOFORGE_*` variables that connect the client to the server (address, TCP and UDP
|
|
14
|
+
ports). Change both sides together. Git ignores it.
|
|
15
|
+
- `jsconfig.json`: the options your code editor reads the JSDoc types with, extended by each app.
|
|
16
|
+
- `package.json`<% if (packageManager === "pnpm") { %> and `pnpm-workspace.yaml`<% } %>: the <%= packageManager %> workspace. Install every app with
|
|
17
|
+
`<%= packageManager %> install` here.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
`nf` comes from the `@nanoforge-dev/cli` dev dependency. Run it from this folder, as
|
|
22
|
+
`<%= nf %> <command>`.
|
|
23
|
+
|
|
24
|
+
- `nf dev`: runs `nf build --watch` and `nf start --watch` together. The game is on
|
|
25
|
+
http://localhost:3000, with the server running.
|
|
26
|
+
- `nf build`: bundles every app with Bun into its `dist/`. It does not check the JSDoc
|
|
27
|
+
types. `-d apps/client` or `-d apps/server` builds one app.
|
|
28
|
+
- `nf start`: runs the last build of both apps.
|
|
29
|
+
|
|
30
|
+
`nf dev` and `nf start` read the `.env` of their `-d` folder (the current folder by default), so
|
|
31
|
+
run them here.
|
|
32
|
+
|
|
33
|
+
## Client and server
|
|
34
|
+
|
|
35
|
+
- They connect over TCP, a WebSocket, and over UDP, a WebRTC data channel set up through a
|
|
36
|
+
WebSocket. `nf start` gives `NANOFORGE_CLIENT_X` to the client and `NANOFORGE_SERVER_X` to the
|
|
37
|
+
server, as `X` in the `options.env` of `main`. A plain `NANOFORGE_X` goes to both. Everything
|
|
38
|
+
the client gets reaches the browser: never put a secret there.
|
|
39
|
+
https://docs.nanoforge.eu/docs/cli/configuration/environment-variables
|
|
40
|
+
- In the generated game, the server moves the entity and sends its position to every client
|
|
41
|
+
(`apps/server/src/systems/move-2d.system.js`), and the client sets the position it receives
|
|
42
|
+
(`apps/client/src/systems/position-sync.system.js`).
|
|
43
|
+
- The apps share no code: each defines its own `Position2D`.
|
|
44
|
+
|
|
45
|
+
## Docs
|
|
46
|
+
|
|
47
|
+
- `nf` commands: https://docs.nanoforge.eu/docs/cli/commands/overview
|
|
48
|
+
- The workspace config: https://docs.nanoforge.eu/docs/cli/configuration/configuration-file
|
|
49
|
+
- Everything else: https://docs.nanoforge.eu
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -82,6 +82,10 @@ build/Release
|
|
|
82
82
|
|
|
83
83
|
# Dependency directories
|
|
84
84
|
node_modules/
|
|
85
|
+
|
|
86
|
+
# NanoForge registry packages (nf install restores them from nanoforge.packages.lock.json)
|
|
87
|
+
nf_modules/
|
|
88
|
+
|
|
85
89
|
jspm_packages/
|
|
86
90
|
|
|
87
91
|
# Snowpack dependency directory (https://snowpack.dev/)
|