From 3d924b3b8da294028f2f612f1ed4960847d31a86 Mon Sep 17 00:00:00 2001
From: Exelo
Date: Wed, 2 Sep 2026 02:50:05 +0900
Subject: [PATCH 01/15] feat: add config lib and monorepo config files (#197)
* feat: add config lib and monorepo config files
* refactor: update tsdown config
---
.gitignore | 4 +-
.nvmrc | 2 +-
.prettierignore | 2 +
cliff.toml | 2 +-
eslint.config.js | 7 +-
libs/config/.cliff-jumperrc.json | 8 +
libs/config/.gitignore | 234 ++
libs/config/.prettierignore | 11 +
libs/config/CHANGELOG.md | 3 +
libs/config/LICENSE | 21 +
libs/config/README.md | 83 +
libs/config/cliff.toml | 79 +
libs/config/eslint.config.js | 3 +
libs/config/package.json | 87 +
libs/config/prettier.config.js | 3 +
libs/config/src/defaults/default-configs.ts | 54 +
libs/config/src/define-config.ts | 7 +
libs/config/src/index.ts | 25 +
libs/config/src/merge/deep-merge.ts | 35 +
libs/config/src/merge/resolve-config.ts | 51 +
libs/config/src/types/base-config.type.ts | 6 +
libs/config/src/types/client-config.type.ts | 5 +
libs/config/src/types/index.ts | 14 +
libs/config/src/types/lib-config.type.ts | 15 +
libs/config/src/types/mixins.type.ts | 25 +
libs/config/src/types/server-config.type.ts | 5 +
.../config/src/types/workspace-config.type.ts | 5 +
libs/config/test/define-config.spec.ts | 20 +
libs/config/test/resolve-config.spec.ts | 76 +
libs/config/tsconfig.json | 15 +
libs/config/tsconfig.spec.json | 10 +
libs/config/tsdown.config.ts | 24 +
package.json | 24 +-
pnpm-lock.yaml | 2547 +++++++++--------
pnpm-workspace.yaml | 13 +-
tsdown.config.ts | 2 +-
turbo.json | 39 +
37 files changed, 2414 insertions(+), 1152 deletions(-)
create mode 100644 libs/config/.cliff-jumperrc.json
create mode 100644 libs/config/.gitignore
create mode 100644 libs/config/.prettierignore
create mode 100644 libs/config/CHANGELOG.md
create mode 100644 libs/config/LICENSE
create mode 100644 libs/config/README.md
create mode 100644 libs/config/cliff.toml
create mode 100644 libs/config/eslint.config.js
create mode 100644 libs/config/package.json
create mode 100644 libs/config/prettier.config.js
create mode 100644 libs/config/src/defaults/default-configs.ts
create mode 100644 libs/config/src/define-config.ts
create mode 100644 libs/config/src/index.ts
create mode 100644 libs/config/src/merge/deep-merge.ts
create mode 100644 libs/config/src/merge/resolve-config.ts
create mode 100644 libs/config/src/types/base-config.type.ts
create mode 100644 libs/config/src/types/client-config.type.ts
create mode 100644 libs/config/src/types/index.ts
create mode 100644 libs/config/src/types/lib-config.type.ts
create mode 100644 libs/config/src/types/mixins.type.ts
create mode 100644 libs/config/src/types/server-config.type.ts
create mode 100644 libs/config/src/types/workspace-config.type.ts
create mode 100644 libs/config/test/define-config.spec.ts
create mode 100644 libs/config/test/resolve-config.spec.ts
create mode 100644 libs/config/tsconfig.json
create mode 100644 libs/config/tsconfig.spec.json
create mode 100644 libs/config/tsdown.config.ts
create mode 100644 turbo.json
diff --git a/.gitignore b/.gitignore
index d1eb980..af461f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -229,4 +229,6 @@ src/**/*.d.ts
.tmp-e2e-*/
-.direnv
\ No newline at end of file
+.direnv
+
+.turbo/
\ No newline at end of file
diff --git a/.nvmrc b/.nvmrc
index 7273c0f..4aa0e0a 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-25
+v26
diff --git a/.prettierignore b/.prettierignore
index 921482e..d416fc3 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -12,3 +12,5 @@ CHANGELOG.md
coverage/
.tmp-e2e-*/
+
+libs/
diff --git a/cliff.toml b/cliff.toml
index 9151523..831f4e2 100644
--- a/cliff.toml
+++ b/cliff.toml
@@ -76,4 +76,4 @@ sort_commits = "newest"
[remote.github]
owner = "NanoForge-dev"
-repo = "cli"
+repo = "CLI"
diff --git a/eslint.config.js b/eslint.config.js
index b0da256..0f0f63c 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,3 +1,8 @@
import eslintConfig from "@nanoforge-dev/utils-eslint-config";
-export default eslintConfig;
+export default [
+ ...eslintConfig,
+ {
+ ignores: ["libs/**"],
+ },
+];
diff --git a/libs/config/.cliff-jumperrc.json b/libs/config/.cliff-jumperrc.json
new file mode 100644
index 0000000..ae950d4
--- /dev/null
+++ b/libs/config/.cliff-jumperrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
+ "name": "config",
+ "org": "nanoforge-dev",
+ "packagePath": "libs/config",
+ "tagTemplate": "{{new-version}}",
+ "identifierBase": false
+}
diff --git a/libs/config/.gitignore b/libs/config/.gitignore
new file mode 100644
index 0000000..88ef62e
--- /dev/null
+++ b/libs/config/.gitignore
@@ -0,0 +1,234 @@
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# Turbo
+.turbo/
+
+# Compiled files
+src/**/*.js
+src/**/*.d.ts
+
+# mint-tsdocs cache
+docs/.tsdocs/
diff --git a/libs/config/.prettierignore b/libs/config/.prettierignore
new file mode 100644
index 0000000..64b127c
--- /dev/null
+++ b/libs/config/.prettierignore
@@ -0,0 +1,11 @@
+# Ignore files for PNPM, NPM and YARN
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+bun.lock
+
+.turbo/
+node_modules/
+dist/
+coverage/
+CHANGELOG.md
diff --git a/libs/config/CHANGELOG.md b/libs/config/CHANGELOG.md
new file mode 100644
index 0000000..6361e43
--- /dev/null
+++ b/libs/config/CHANGELOG.md
@@ -0,0 +1,3 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
diff --git a/libs/config/LICENSE b/libs/config/LICENSE
new file mode 100644
index 0000000..4672486
--- /dev/null
+++ b/libs/config/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright © 2026 NanoForge
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/libs/config/README.md b/libs/config/README.md
new file mode 100644
index 0000000..fb92022
--- /dev/null
+++ b/libs/config/README.md
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## About
+
+`@nanoforge-dev/config` is a library with the config types and helpers used to define and resolve `nanoforge.config.ts` files across Nanoforge CLI.
+
+## Installation
+
+**Node.js 26 or newer is required.**
+
+```sh
+npm install @nanoforge-dev/config
+yarn add @nanoforge-dev/config
+pnpm add @nanoforge-dev/config
+bun add @nanoforge-dev/config
+```
+
+## Usage
+
+Wrap a `nanoforge.config.ts` file's default export in `defineConfig` to get type-checking and editor autocompletion:
+
+```ts
+import { defineConfig } from "@nanoforge-dev/config";
+
+export default defineConfig({
+ type: "client",
+ entryFile: "src/main.ts",
+ libs: ["../../libs/my-lib"],
+});
+```
+
+The `type` field discriminates between the four config shapes:
+
+- `workspace` — a monorepo root listing its `packages`.
+- `lib` — a shared library consumed by clients/servers.
+- `client` — a browser-side NanoForge app.
+- `server` — a server-side NanoForge app.
+
+Each config type has partial, optional fields. `resolveConfig` (and its per-type variants `resolveWorkspaceConfig`, `resolveLibConfig`, `resolveClientConfig`, `resolveServerConfig`) fill in missing fields with defaults, deep-merging nested objects like `dir` and `out`:
+
+```ts
+import { resolveConfig } from "@nanoforge-dev/config";
+
+const resolved = resolveConfig({ type: "client", dir: { assets: "static" } });
+// resolved.dir.assets === "static"
+// resolved.dir.packages === "nf_modules" (default, not overridden)
+```
+
+Array fields (`packages`, `libs`) are replaced wholesale by an override rather than concatenated with the defaults.
+
+## Links
+
+- [GitHub][source]
+- [npm][npm]
+
+## Contributing
+
+Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
+[documentation][documentation].
+See [the contribution guide][contributing] if you'd like to submit a PR.
+
+## Help
+
+If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle nudge in the right direction, please don't hesitate to ask questions in [discussions][discussions].
+
+[documentation]: https://github.com/NanoForge-dev/CLI
+[discussions]: https://github.com/NanoForge-dev/CLI/discussions
+[source]: https://github.com/NanoForge-dev/CLI/tree/main/libs/config
+[npm]: https://www.npmjs.com/package/@nanoforge-dev/config
+[contributing]: https://github.com/NanoForge-dev/CLI/blob/main/.github/CONTRIBUTING.md
diff --git a/libs/config/cliff.toml b/libs/config/cliff.toml
new file mode 100644
index 0000000..4bb6a65
--- /dev/null
+++ b/libs/config/cliff.toml
@@ -0,0 +1,79 @@
+[changelog]
+header = """
+# Changelog
+
+All notable changes to this project will be documented in this file.\n
+"""
+body = """
+{%- macro remote_url() -%}
+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
+{%- endmacro -%}
+{% if version %}\
+ # [{{ version | trim_start_matches(pat="v") }}]\
+ {% if previous %}\
+ {% if previous.version %}\
+ ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
+ {% else %}\
+ ({{ self::remote_url() }}/tree/{{ version }})\
+ {% endif %}\
+ {% endif %} \
+ - ({{ timestamp | date(format="%Y-%m-%d") }})
+{% else %}\
+ # [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+ ## {{ group | upper_first }}
+ {% for commit in commits %}
+ - {% if commit.scope %}\
+ **{{commit.scope}}:** \
+ {% endif %}\
+ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
+ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
+ {% if commit.breaking %}\
+ {% for footer in commit.footers %}\
+ {% if footer.breaking %}\
+ \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
+ {% endif %}\
+ {% endfor %}\
+ {% endif %}\
+ {% endfor %}
+{% endfor %}\
+{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\
+ \n### New Contributors\n
+ {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\
+ - @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }}
+ {% endfor %}\
+{% endif %}\n
+"""
+trim = true
+footer = ""
+
+[git]
+conventional_commits = true
+filter_unconventional = true
+commit_parsers = [
+ { message = "^feat", group = "Features"},
+ { message = "^fix", group = "Bug Fixes"},
+ { message = "^docs", group = "Documentation"},
+ { message = "^perf", group = "Performance"},
+ { message = "^refactor", group = "Refactor"},
+ { message = "^types", group = "Typings"},
+ { message = ".*deprecated", body = ".*deprecated", group = "Deprecation"},
+ { message = "^revert", skip = true},
+ { message = "^style", group = "Styling"},
+ { message = "^test", group = "Testing"},
+ { message = "^chore", skip = true},
+ { message = "^ci", skip = true},
+ { message = "^build", skip = true},
+ { body = ".*security", group = "Security"},
+]
+filter_commits = true
+protect_breaking_commits = true
+tag_pattern = "^[0-9]+"
+ignore_tags = ""
+topo_order = false
+sort_commits = "newest"
+
+[remote.github]
+owner = "NanoForge-dev"
+repo = "CLI"
diff --git a/libs/config/eslint.config.js b/libs/config/eslint.config.js
new file mode 100644
index 0000000..62ec06d
--- /dev/null
+++ b/libs/config/eslint.config.js
@@ -0,0 +1,3 @@
+import config from "@nanoforge-dev/utils-eslint-config";
+
+export default config;
diff --git a/libs/config/package.json b/libs/config/package.json
new file mode 100644
index 0000000..2965183
--- /dev/null
+++ b/libs/config/package.json
@@ -0,0 +1,87 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "@nanoforge-dev/config",
+ "version": "1.4.2",
+ "description": "NanoForge CLI - Config",
+ "keywords": [
+ "nanoforge",
+ "game",
+ "engine"
+ ],
+ "homepage": "https://github.com/NanoForge-dev/CLI#readme",
+ "bugs": "https://github.com/NanoForge-dev/CLI/issues",
+ "license": "MIT",
+ "contributors": [
+ "Bill ",
+ "Exelo ",
+ "Fexkoser ",
+ "Josephine ",
+ "Tchips "
+ ],
+ "files": [
+ "dist"
+ ],
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.cts",
+ "exports": {
+ ".": {
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/index.cjs"
+ },
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "type": "module",
+ "directories": {
+ "lib": "src"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/NanoForge-dev/CLI.git",
+ "directory": "libs/config"
+ },
+ "funding": "https://github.com/NanoForge-dev/CLI?sponsor",
+ "scripts": {
+ "build": "tsc --noEmit && tsdown --config-loader unrun",
+ "lint": "prettier --check . && eslint --format=pretty src",
+ "format": "prettier --write . && eslint --fix --format=pretty src",
+ "test:unit": "vitest run --config ../../vitest.config.ts",
+ "prepack": "pnpm run build && pnpm run lint",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'libs/config/*'",
+ "release": "cliff-jumper",
+ "docs": "mint-tsdocs generate"
+ },
+ "devDependencies": {
+ "@favware/cliff-jumper": "catalog:ci",
+ "@nanoforge-dev/utils-eslint-config": "catalog:lint",
+ "@nanoforge-dev/utils-prettier-config": "catalog:lint",
+ "@trivago/prettier-plugin-sort-imports": "catalog:lint",
+ "eslint": "catalog:lint",
+ "prettier": "catalog:lint",
+ "tsdown": "catalog:build",
+ "typescript": "catalog:build",
+ "unrun": "catalog:build",
+ "vitest": "catalog:test"
+ },
+ "packageManager": "pnpm@12.0.0",
+ "engines": {
+ "node": "26"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "lint-staged": {
+ "**": [
+ "prettier --ignore-unknown --write"
+ ],
+ "src/**/*.ts": [
+ "eslint --fix"
+ ]
+ }
+}
diff --git a/libs/config/prettier.config.js b/libs/config/prettier.config.js
new file mode 100644
index 0000000..27d0e26
--- /dev/null
+++ b/libs/config/prettier.config.js
@@ -0,0 +1,3 @@
+import config from "@nanoforge-dev/utils-prettier-config";
+
+export default config;
diff --git a/libs/config/src/defaults/default-configs.ts b/libs/config/src/defaults/default-configs.ts
new file mode 100644
index 0000000..26d216c
--- /dev/null
+++ b/libs/config/src/defaults/default-configs.ts
@@ -0,0 +1,54 @@
+import type { ClientConfig, LibConfig, ServerConfig, WorkspaceConfig } from "../types";
+
+export const defaultWorkspaceConfig: Omit = {
+ packages: [],
+};
+
+export const defaultLibConfig: Omit = {
+ dir: {
+ assets: "assets",
+ shared: "shared",
+
+ components: "shared/components",
+ systems: "shared/systems",
+ scenes: "shared/scenes",
+ },
+};
+
+export const defaultClientConfig: Omit = {
+ entryFile: "src/main.ts",
+ out: {
+ dir: "dist",
+ mainFile: "main.js",
+ },
+
+ dir: {
+ assets: "assets",
+ packages: "nf_modules",
+
+ components: "src/components",
+ systems: "src/systems",
+ scenes: "src/scenes",
+ },
+
+ libs: [],
+};
+
+export const defaultServerConfig: Omit = {
+ entryFile: "src/main.ts",
+ out: {
+ dir: "dist",
+ mainFile: "main.js",
+ },
+
+ dir: {
+ assets: "assets",
+ packages: "nf_modules",
+
+ components: "src/components",
+ systems: "src/systems",
+ scenes: "src/scenes",
+ },
+
+ libs: [],
+};
diff --git a/libs/config/src/define-config.ts b/libs/config/src/define-config.ts
new file mode 100644
index 0000000..788e025
--- /dev/null
+++ b/libs/config/src/define-config.ts
@@ -0,0 +1,7 @@
+import type { NanoforgeConfig } from "./types";
+
+/**
+ * Identity function used to get type-checking and editor autocompletion on
+ * a `nanoforge.config.ts` file's default export.
+ */
+export const defineConfig = (config: T): T => config;
diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts
new file mode 100644
index 0000000..83547cd
--- /dev/null
+++ b/libs/config/src/index.ts
@@ -0,0 +1,25 @@
+export {
+ defaultClientConfig,
+ defaultLibConfig,
+ defaultServerConfig,
+ defaultWorkspaceConfig,
+} from "./defaults/default-configs";
+export { defineConfig } from "./define-config";
+export {
+ resolveClientConfig,
+ resolveConfig,
+ resolveLibConfig,
+ resolveServerConfig,
+ resolveWorkspaceConfig,
+} from "./merge/resolve-config";
+export type {
+ BaseConfig,
+ BuildableConfig,
+ ClientConfig,
+ ContainLibConfig,
+ LibConfig,
+ NanoforgeConfig,
+ ServerConfig,
+ SourceableConfig,
+ WorkspaceConfig,
+} from "./types";
diff --git a/libs/config/src/merge/deep-merge.ts b/libs/config/src/merge/deep-merge.ts
new file mode 100644
index 0000000..2d20d88
--- /dev/null
+++ b/libs/config/src/merge/deep-merge.ts
@@ -0,0 +1,35 @@
+const isPlainObject = (value: unknown): value is Record =>
+ typeof value === "object" && value !== null && !Array.isArray(value);
+
+const mergeUnknown = (
+ defaults: Record,
+ override: Record,
+): Record => {
+ const result: Record = { ...defaults };
+
+ for (const key of Object.keys(override)) {
+ const overrideValue = override[key];
+ if (overrideValue === undefined) continue;
+
+ const defaultValue = defaults[key];
+ result[key] =
+ isPlainObject(defaultValue) && isPlainObject(overrideValue)
+ ? mergeUnknown(defaultValue, overrideValue)
+ : overrideValue;
+ }
+
+ return result;
+};
+
+/**
+ * Recursively merges `override` onto `defaults`.
+ *
+ * @remarks
+ * Nested plain objects (e.g. `dir`, `out`) are merged key by key. Array
+ * values (e.g. `packages`, `libs`) are replaced wholesale by `override`,
+ * not concatenated.
+ */
+export const deepMerge = >(
+ defaults: T,
+ override: Partial,
+): T => mergeUnknown(defaults, override as Record) as T;
diff --git a/libs/config/src/merge/resolve-config.ts b/libs/config/src/merge/resolve-config.ts
new file mode 100644
index 0000000..69ebf7b
--- /dev/null
+++ b/libs/config/src/merge/resolve-config.ts
@@ -0,0 +1,51 @@
+import {
+ defaultClientConfig,
+ defaultLibConfig,
+ defaultServerConfig,
+ defaultWorkspaceConfig,
+} from "../defaults/default-configs";
+import type {
+ ClientConfig,
+ LibConfig,
+ NanoforgeConfig,
+ ServerConfig,
+ WorkspaceConfig,
+} from "../types";
+import { deepMerge } from "./deep-merge";
+
+export const resolveWorkspaceConfig = (config: WorkspaceConfig): WorkspaceConfig => ({
+ ...deepMerge(defaultWorkspaceConfig, config),
+ type: "workspace",
+});
+
+export const resolveLibConfig = (config: LibConfig): LibConfig => ({
+ ...deepMerge(defaultLibConfig, config),
+ type: "lib",
+});
+
+export const resolveClientConfig = (config: ClientConfig): ClientConfig => ({
+ ...deepMerge(defaultClientConfig, config),
+ type: "client",
+});
+
+export const resolveServerConfig = (config: ServerConfig): ServerConfig => ({
+ ...deepMerge(defaultServerConfig, config),
+ type: "server",
+});
+
+/**
+ * Merges a `nanoforge.config.ts` config against the defaults matching its
+ * `type`.
+ */
+export const resolveConfig = (config: NanoforgeConfig): NanoforgeConfig => {
+ switch (config.type) {
+ case "workspace":
+ return resolveWorkspaceConfig(config);
+ case "lib":
+ return resolveLibConfig(config);
+ case "client":
+ return resolveClientConfig(config);
+ case "server":
+ return resolveServerConfig(config);
+ }
+};
diff --git a/libs/config/src/types/base-config.type.ts b/libs/config/src/types/base-config.type.ts
new file mode 100644
index 0000000..2cbabf6
--- /dev/null
+++ b/libs/config/src/types/base-config.type.ts
@@ -0,0 +1,6 @@
+/**
+ * Common shape every `nanoforge.config.ts` file's default export satisfies.
+ */
+export interface BaseConfig {
+ type: T;
+}
diff --git a/libs/config/src/types/client-config.type.ts b/libs/config/src/types/client-config.type.ts
new file mode 100644
index 0000000..bcacd00
--- /dev/null
+++ b/libs/config/src/types/client-config.type.ts
@@ -0,0 +1,5 @@
+import type { BaseConfig } from "./base-config.type";
+import type { BuildableConfig, ContainLibConfig, SourceableConfig } from "./mixins.type";
+
+export interface ClientConfig
+ extends BaseConfig<"client">, SourceableConfig, BuildableConfig, ContainLibConfig {}
diff --git a/libs/config/src/types/index.ts b/libs/config/src/types/index.ts
new file mode 100644
index 0000000..fabef4c
--- /dev/null
+++ b/libs/config/src/types/index.ts
@@ -0,0 +1,14 @@
+import type { ClientConfig } from "./client-config.type";
+import type { LibConfig } from "./lib-config.type";
+import type { ServerConfig } from "./server-config.type";
+import type { WorkspaceConfig } from "./workspace-config.type";
+
+export type { BaseConfig } from "./base-config.type";
+export type { ClientConfig } from "./client-config.type";
+export type { LibConfig } from "./lib-config.type";
+export type { BuildableConfig, ContainLibConfig, SourceableConfig } from "./mixins.type";
+export type { ServerConfig } from "./server-config.type";
+export type { WorkspaceConfig } from "./workspace-config.type";
+
+/** Discriminated union of every `nanoforge.config.ts` shape, keyed by `type`. */
+export type NanoforgeConfig = WorkspaceConfig | LibConfig | ClientConfig | ServerConfig;
diff --git a/libs/config/src/types/lib-config.type.ts b/libs/config/src/types/lib-config.type.ts
new file mode 100644
index 0000000..3b2c3cb
--- /dev/null
+++ b/libs/config/src/types/lib-config.type.ts
@@ -0,0 +1,15 @@
+import type { BaseConfig } from "./base-config.type";
+
+export interface LibConfig extends BaseConfig<"lib"> {
+ dir?: {
+ assets?: string;
+ shared?: string;
+
+ /** Used by the editor only. Should be inside `shared`. */
+ components?: string;
+ /** Used by the editor only. Should be inside `shared`. */
+ systems?: string;
+ /** Used by the editor only. Should be inside `shared`. */
+ scenes?: string;
+ };
+}
diff --git a/libs/config/src/types/mixins.type.ts b/libs/config/src/types/mixins.type.ts
new file mode 100644
index 0000000..9c2f32b
--- /dev/null
+++ b/libs/config/src/types/mixins.type.ts
@@ -0,0 +1,25 @@
+export interface ContainLibConfig {
+ libs?: string[];
+}
+
+export interface BuildableConfig {
+ entryFile?: string;
+ out?: {
+ dir?: string;
+ mainFile?: string;
+ };
+}
+
+export interface SourceableConfig {
+ dir?: {
+ assets?: string;
+ packages?: string;
+
+ /** Used by the editor only. */
+ components?: string;
+ /** Used by the editor only. */
+ systems?: string;
+ /** Used by the editor only. */
+ scenes?: string;
+ };
+}
diff --git a/libs/config/src/types/server-config.type.ts b/libs/config/src/types/server-config.type.ts
new file mode 100644
index 0000000..769e898
--- /dev/null
+++ b/libs/config/src/types/server-config.type.ts
@@ -0,0 +1,5 @@
+import type { BaseConfig } from "./base-config.type";
+import type { BuildableConfig, ContainLibConfig, SourceableConfig } from "./mixins.type";
+
+export interface ServerConfig
+ extends BaseConfig<"server">, SourceableConfig, BuildableConfig, ContainLibConfig {}
diff --git a/libs/config/src/types/workspace-config.type.ts b/libs/config/src/types/workspace-config.type.ts
new file mode 100644
index 0000000..8d45c7d
--- /dev/null
+++ b/libs/config/src/types/workspace-config.type.ts
@@ -0,0 +1,5 @@
+import type { BaseConfig } from "./base-config.type";
+
+export interface WorkspaceConfig extends BaseConfig<"workspace"> {
+ packages?: string[];
+}
diff --git a/libs/config/test/define-config.spec.ts b/libs/config/test/define-config.spec.ts
new file mode 100644
index 0000000..0bd4071
--- /dev/null
+++ b/libs/config/test/define-config.spec.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it } from "vitest";
+
+import { defineConfig } from "../src";
+import type { NanoforgeConfig } from "../src";
+
+describe("defineConfig", () => {
+ it("returns the given config unchanged", () => {
+ const config = defineConfig({ type: "client", libs: ["../libs/test-lib"] });
+ expect(config).toEqual({ type: "client", libs: ["../libs/test-lib"] });
+ });
+
+ it("narrows to the matching config shape via the type discriminant", () => {
+ const config: NanoforgeConfig = defineConfig({ type: "server" });
+ if (config.type === "server") {
+ expect(config.libs).toBeUndefined();
+ } else {
+ throw new Error("expected type to be 'server'");
+ }
+ });
+});
diff --git a/libs/config/test/resolve-config.spec.ts b/libs/config/test/resolve-config.spec.ts
new file mode 100644
index 0000000..5f60e27
--- /dev/null
+++ b/libs/config/test/resolve-config.spec.ts
@@ -0,0 +1,76 @@
+import { describe, expect, it } from "vitest";
+
+import {
+ resolveClientConfig,
+ resolveConfig,
+ resolveLibConfig,
+ resolveServerConfig,
+ resolveWorkspaceConfig,
+} from "../src";
+
+describe("resolveWorkspaceConfig", () => {
+ it("fills in defaults for an empty config", () => {
+ expect(resolveWorkspaceConfig({ type: "workspace" })).toEqual({
+ type: "workspace",
+ packages: [],
+ });
+ });
+
+ it("lets an explicit override win", () => {
+ expect(resolveWorkspaceConfig({ type: "workspace", packages: ["apps/*"] })).toEqual({
+ type: "workspace",
+ packages: ["apps/*"],
+ });
+ });
+});
+
+describe("resolveLibConfig", () => {
+ it("fills in nested dir defaults", () => {
+ expect(resolveLibConfig({ type: "lib" }).dir).toEqual({
+ assets: "assets",
+ shared: "shared",
+ components: "shared/components",
+ systems: "shared/systems",
+ scenes: "shared/scenes",
+ });
+ });
+
+ it("merges a partial dir override with defaults", () => {
+ expect(resolveLibConfig({ type: "lib", dir: { assets: "static" } }).dir).toEqual({
+ assets: "static",
+ shared: "shared",
+ components: "shared/components",
+ systems: "shared/systems",
+ scenes: "shared/scenes",
+ });
+ });
+});
+
+describe("resolveClientConfig", () => {
+ it("fills in defaults for an empty config", () => {
+ const resolved = resolveClientConfig({ type: "client" });
+ expect(resolved.entryFile).toBe("src/main.ts");
+ expect(resolved.out).toEqual({ dir: "dist", mainFile: "main.js" });
+ expect(resolved.libs).toEqual([]);
+ });
+
+ it("replaces array fields wholesale instead of concatenating", () => {
+ const resolved = resolveClientConfig({ type: "client", libs: ["../../libs/test-lib"] });
+ expect(resolved.libs).toEqual(["../../libs/test-lib"]);
+ });
+});
+
+describe("resolveServerConfig", () => {
+ it("fills in defaults for an empty config", () => {
+ const resolved = resolveServerConfig({ type: "server" });
+ expect(resolved.entryFile).toBe("src/main.ts");
+ expect(resolved.dir?.assets).toBe("assets");
+ });
+});
+
+describe("resolveConfig", () => {
+ it("dispatches to the resolver matching the config's type", () => {
+ expect(resolveConfig({ type: "workspace" })).toEqual({ type: "workspace", packages: [] });
+ expect(resolveConfig({ type: "server" }).entryFile).toBe("src/main.ts");
+ });
+});
diff --git a/libs/config/tsconfig.json b/libs/config/tsconfig.json
new file mode 100644
index 0000000..1ada333
--- /dev/null
+++ b/libs/config/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ "extends": "../../tsconfig.json",
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist"],
+ "compilerOptions": {
+ "declaration": true,
+ "paths": {
+ "/snippets/*": ["./docs/snippets/*"]
+ }
+ },
+ "mdx": {
+ "checkMdx": true
+ }
+}
diff --git a/libs/config/tsconfig.spec.json b/libs/config/tsconfig.spec.json
new file mode 100644
index 0000000..8270cab
--- /dev/null
+++ b/libs/config/tsconfig.spec.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noEmit": true,
+ "skipLibCheck": true
+ },
+ "include": ["test/**/*.spec.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/libs/config/tsdown.config.ts b/libs/config/tsdown.config.ts
new file mode 100644
index 0000000..609a944
--- /dev/null
+++ b/libs/config/tsdown.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from "tsdown";
+
+export default [
+ defineConfig({
+ entry: ["src/index.ts"],
+ outDir: "dist",
+ format: ["esm", "cjs"],
+ shims: true,
+ dts: true,
+ sourcemap: true,
+ clean: true,
+ minify: true,
+ fixedExtension: false,
+ platform: "node",
+ target: "esnext",
+ treeshake: false,
+ deps: {
+ neverBundle: true,
+ },
+ outputOptions: {
+ assetFileNames: "[name][extname]",
+ },
+ }),
+];
diff --git a/package.json b/package.json
index d653f47..ad4fafa 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,9 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips "
],
"files": [
@@ -31,18 +32,18 @@
"type": "git",
"url": "git+https://github.com/NanoForge-dev/CLI.git"
},
- "funding": "",
+ "funding": "https://github.com/NanoForge-dev/CLI?sponsor",
"scripts": {
- "build": "tsc --noEmit && tsdown",
- "build:dev": "tsc --noEmit && NODE_ENV=development tsdown",
+ "build": "turbo run build --concurrency=5 --filter='!@nanoforge-dev/cli' && tsc --noEmit && tsdown",
+ "build:dev": "turbo run build --concurrency=5 --filter='!@nanoforge-dev/cli' && tsc --noEmit && NODE_ENV=development tsdown",
"start": "node dist/nf.js",
- "lint": "prettier --check . && eslint --format=pretty src",
- "format": "prettier --write . && eslint --fix --format=pretty src",
+ "lint": "prettier --check . && eslint --format=pretty src && turbo run lint --concurrency=5 --filter='!@nanoforge-dev/cli'",
+ "format": "prettier --write . && eslint --fix --format=pretty src && turbo run format --concurrency=5 --filter='!@nanoforge-dev/cli'",
"prepack": "pnpm run build && pnpm run lint",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r . --include-path '.'",
"release": "cliff-jumper",
"test": "pnpm run test:unit && pnpm run test:e2e",
- "test:unit": "vitest run -c vitest.config.ts",
+ "test:unit": "vitest run -c vitest.config.ts && turbo run test:unit --concurrency=5 --filter='!@nanoforge-dev/cli'",
"test:unit:watch": "vitest -c vitest.config.ts",
"test:e2e": "vitest run -c vitest.config.e2e.ts",
"test:e2e:watch": "vitest -c vitest.config.e2e.ts",
@@ -79,21 +80,22 @@
"@trivago/prettier-plugin-sort-imports": "catalog:lint",
"@types/inquirer": "catalog:cli",
"@types/node": "catalog:core",
- "@vitest/coverage-v8": "catalog:tests",
+ "@vitest/coverage-v8": "catalog:test",
"eslint": "catalog:lint",
"husky": "catalog:ci",
"lint-staged": "catalog:ci",
"prettier": "catalog:lint",
"tsdown": "catalog:build",
+ "turbo": "catalog:build",
"typescript": "catalog:build",
- "vitest": "catalog:tests"
+ "vitest": "catalog:test"
},
"optionalDependencies": {
"@nanoforge-dev/editor": "catalog:core"
},
- "packageManager": "pnpm@11.10.0",
+ "packageManager": "pnpm@12.0.0",
"engines": {
- "node": "25"
+ "node": "26"
},
"publishConfig": {
"access": "public"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 949b5b0..d810bb3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,3 +1,104 @@
+---
+lockfileVersion: '9.0'
+
+importers:
+
+ .:
+ configDependencies: {}
+ packageManagerDependencies:
+ pnpm:
+ specifier: 12.0.0
+ version: 12.0.0
+
+packages:
+
+ '@pnpm/exe.darwin-arm64@12.0.0':
+ resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@pnpm/exe.darwin-x64@12.0.0':
+ resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@pnpm/exe.linux-arm64-musl@12.0.0':
+ resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@pnpm/exe.linux-arm64@12.0.0':
+ resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.linux-x64-musl@12.0.0':
+ resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@pnpm/exe.linux-x64@12.0.0':
+ resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.win32-arm64@12.0.0':
+ resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@pnpm/exe.win32-x64@12.0.0':
+ resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==}
+ cpu: [x64]
+ os: [win32]
+
+ pnpm@12.0.0:
+ resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==}
+ engines: {node: '>=18.*'}
+ hasBin: true
+
+snapshots:
+
+ '@pnpm/exe.darwin-arm64@12.0.0':
+ optional: true
+
+ '@pnpm/exe.darwin-x64@12.0.0':
+ optional: true
+
+ '@pnpm/exe.linux-arm64-musl@12.0.0':
+ optional: true
+
+ '@pnpm/exe.linux-arm64@12.0.0':
+ optional: true
+
+ '@pnpm/exe.linux-x64-musl@12.0.0':
+ optional: true
+
+ '@pnpm/exe.linux-x64@12.0.0':
+ optional: true
+
+ '@pnpm/exe.win32-arm64@12.0.0':
+ optional: true
+
+ '@pnpm/exe.win32-x64@12.0.0':
+ optional: true
+
+ pnpm@12.0.0:
+ optionalDependencies:
+ '@pnpm/exe.darwin-arm64': 12.0.0
+ '@pnpm/exe.darwin-x64': 12.0.0
+ '@pnpm/exe.linux-arm64': 12.0.0
+ '@pnpm/exe.linux-arm64-musl': 12.0.0
+ '@pnpm/exe.linux-x64': 12.0.0
+ '@pnpm/exe.linux-x64-musl': 12.0.0
+ '@pnpm/exe.win32-arm64': 12.0.0
+ '@pnpm/exe.win32-x64': 12.0.0
+
+---
lockfileVersion: '9.0'
settings:
@@ -8,17 +109,23 @@ catalogs:
build:
tsdown:
specifier: ^0.22.3
- version: 0.22.3
+ version: 0.22.14
+ turbo:
+ specifier: ^2.10.12
+ version: 2.10.12
typescript:
specifier: ^6.0.3
version: 6.0.3
+ unrun:
+ specifier: ^0.3.0
+ version: 0.3.1
ci:
'@commitlint/cli':
specifier: ^21.2.0
- version: 21.2.0
+ version: 21.2.2
'@commitlint/config-conventional':
specifier: ^21.2.0
- version: 21.2.0
+ version: 21.2.2
'@favware/cliff-jumper':
specifier: ^6.1.0
version: 6.1.0
@@ -30,11 +137,11 @@ catalogs:
version: 9.1.7
lint-staged:
specifier: ^17.0.8
- version: 17.0.8
+ version: 17.4.1
cli:
'@inquirer/prompts':
specifier: ^8.5.2
- version: 8.5.2
+ version: 8.7.0
'@types/inquirer':
specifier: ^9.0.10
version: 9.0.10
@@ -56,10 +163,10 @@ catalogs:
version: 0.1.4
'@types/node':
specifier: ^26.1.0
- version: 26.1.0
+ version: 26.4.0
bun:
specifier: ^1.3.14
- version: 1.3.14
+ version: 1.4.0
libs:
chokidar:
specifier: ^5.0.0
@@ -75,7 +182,7 @@ catalogs:
version: 17.4.2
open:
specifier: ^11.0.0
- version: 11.0.0
+ version: 11.0.2
rc9:
specifier: ^3.0.1
version: 3.0.1
@@ -94,34 +201,34 @@ catalogs:
version: 6.0.2
eslint:
specifier: ^10.6.0
- version: 10.6.0
+ version: 10.9.1
prettier:
specifier: ^3.9.4
- version: 3.9.4
+ version: 3.9.6
loader:
'@nanoforge-dev/loader-client':
specifier: ^1.4.0
- version: 1.4.0
+ version: 1.5.0
'@nanoforge-dev/loader-server':
specifier: ^1.4.0
- version: 1.4.0
+ version: 1.5.0
schematics:
'@angular-devkit/schematics':
specifier: ^22.0.5
- version: 22.0.5
+ version: 22.1.6
'@angular-devkit/schematics-cli':
specifier: ^22.0.5
- version: 22.0.5
+ version: 22.1.6
'@nanoforge-dev/schematics':
specifier: ^2.2.1
version: 2.2.1
- tests:
+ test:
'@vitest/coverage-v8':
specifier: ^4.1.10
- version: 4.1.10
+ version: 4.1.11
vitest:
specifier: ^4.1.10
- version: 4.1.10
+ version: 4.1.11
importers:
@@ -129,19 +236,19 @@ importers:
dependencies:
'@angular-devkit/schematics':
specifier: catalog:schematics
- version: 22.0.5(chokidar@5.0.0)
+ version: 22.1.6(chokidar@5.0.0)
'@angular-devkit/schematics-cli':
specifier: catalog:schematics
- version: 22.0.5(@types/node@26.1.0)(chokidar@5.0.0)
+ version: 22.1.6(@types/node@26.4.0)(chokidar@5.0.0)
'@inquirer/prompts':
specifier: catalog:cli
- version: 8.5.2(@types/node@26.1.0)
+ version: 8.7.0(@types/node@26.4.0)
'@nanoforge-dev/loader-client':
specifier: catalog:loader
- version: 1.4.0
+ version: 1.5.0
'@nanoforge-dev/loader-server':
specifier: catalog:loader
- version: 1.4.0
+ version: 1.5.0
'@nanoforge-dev/schematics':
specifier: catalog:schematics
version: 2.2.1(chokidar@5.0.0)
@@ -150,7 +257,7 @@ importers:
version: 4.3.1
bun:
specifier: catalog:core
- version: 1.3.14
+ version: 1.4.0
chokidar:
specifier: catalog:libs
version: 5.0.0
@@ -171,7 +278,7 @@ importers:
version: 2.2.0
open:
specifier: catalog:libs
- version: 11.0.0
+ version: 11.0.2
ora:
specifier: catalog:cli
version: 9.4.1
@@ -184,10 +291,10 @@ importers:
devDependencies:
'@commitlint/cli':
specifier: catalog:ci
- version: 21.2.0(@types/node@26.1.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3)
+ version: 21.2.2(@types/node@26.4.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3)
'@commitlint/config-conventional':
specifier: catalog:ci
- version: 21.2.0
+ version: 21.2.2
'@favware/cliff-jumper':
specifier: catalog:ci
version: 6.1.0
@@ -196,48 +303,84 @@ importers:
version: 2.1.5
'@nanoforge-dev/utils-eslint-config':
specifier: catalog:lint
- version: 1.0.2(@types/eslint@9.6.1)(eslint@10.6.0(jiti@2.6.1))(prettier@3.9.4)(typescript@6.0.3)
+ version: 1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)
'@nanoforge-dev/utils-prettier-config':
specifier: catalog:lint
version: 1.0.2
'@trivago/prettier-plugin-sort-imports':
specifier: catalog:lint
- version: 6.0.2(prettier@3.9.4)
+ version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2)
'@types/inquirer':
specifier: catalog:cli
version: 9.0.10
'@types/node':
specifier: catalog:core
- version: 26.1.0
+ version: 26.4.0
'@vitest/coverage-v8':
- specifier: catalog:tests
- version: 4.1.10(vitest@4.1.10)
+ specifier: catalog:test
+ version: 4.1.11(vitest@4.1.11)
eslint:
specifier: catalog:lint
- version: 10.6.0(jiti@2.6.1)
+ version: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
husky:
specifier: catalog:ci
version: 9.1.7
lint-staged:
specifier: catalog:ci
- version: 17.0.8
+ version: 17.4.1
prettier:
specifier: catalog:lint
- version: 3.9.4
+ version: 3.9.6
tsdown:
specifier: catalog:build
- version: 0.22.3(typescript@6.0.3)
+ version: 0.22.14(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))
+ turbo:
+ specifier: catalog:build
+ version: 2.10.12
typescript:
specifier: catalog:build
version: 6.0.3
vitest:
- specifier: catalog:tests
- version: 4.1.10(@types/node@26.1.0)(@vitest/coverage-v8@4.1.10)(vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0))
+ specifier: catalog:test
+ version: 4.1.11(@types/node@26.4.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0))
optionalDependencies:
'@nanoforge-dev/editor':
specifier: catalog:core
version: 0.1.4
+ libs/config:
+ devDependencies:
+ '@favware/cliff-jumper':
+ specifier: catalog:ci
+ version: 6.1.0
+ '@nanoforge-dev/utils-eslint-config':
+ specifier: catalog:lint
+ version: 1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)
+ '@nanoforge-dev/utils-prettier-config':
+ specifier: catalog:lint
+ version: 1.0.2
+ '@trivago/prettier-plugin-sort-imports':
+ specifier: catalog:lint
+ version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2)
+ eslint:
+ specifier: catalog:lint
+ version: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
+ prettier:
+ specifier: catalog:lint
+ version: 3.9.6
+ tsdown:
+ specifier: catalog:build
+ version: 0.22.14(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))
+ typescript:
+ specifier: catalog:build
+ version: 6.0.3
+ unrun:
+ specifier: catalog:build
+ version: 0.3.1(synckit@0.11.13)
+ vitest:
+ specifier: catalog:test
+ version: 4.1.11(@types/node@26.4.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0))
+
packages:
'@actions/core@3.0.1':
@@ -258,8 +401,8 @@ packages:
'@actions/io@3.0.2':
resolution: {integrity: sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==}
- '@angular-devkit/core@22.0.5':
- resolution: {integrity: sha512-xxd3b9X0bbdGuYqDj1OgxtlGotrb/gsxQ5+4aqivWBHq4q/1RI8JfVB7gqcoYTvAfTq63Dwsk7Qd33hC4yJ1Wg==}
+ '@angular-devkit/core@22.1.6':
+ resolution: {integrity: sha512-KLBsZoc2RhOy0xSdeYcLoSOqV/fBP3feBmhY9o12ry2IuyW/17sCmWr6XbIfTIYQN6M98Y1ewmwVNFsvV5b0gA==}
engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
peerDependencies:
chokidar: ^5.0.0
@@ -267,27 +410,23 @@ packages:
chokidar:
optional: true
- '@angular-devkit/schematics-cli@22.0.5':
- resolution: {integrity: sha512-X305DwlHLyVE+RoM2C9KjKvShi2FiS4pkjYH6CvfydUmtyVxGj34kgtIPf8Kk+Jn7St7fRja5IUAcxczBgL3SQ==}
+ '@angular-devkit/schematics-cli@22.1.6':
+ resolution: {integrity: sha512-KO+vQiWgWW6i2H3XGu1E6AeZ6cyeehF1WTejATEZ3f02LROBkGyZGDAmdtSPtd3tmaGt3TVY25Mq1laybByKMA==}
engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
hasBin: true
- '@angular-devkit/schematics@22.0.5':
- resolution: {integrity: sha512-yKkImZuFLIbylmk1REyoW3EWQeqnr6qwtrzs4EPeaJgzjj+MZurq5slQ3fanK9lfX2q+W+dnSQFKBXHxlUGJ8Q==}
+ '@angular-devkit/schematics@22.1.6':
+ resolution: {integrity: sha512-IbDO9KbQyQm20uinsfT8d9ZtQw41/WVutI/65mAw39WZfN8Ky+MOgOUJCGLiSccMprvZ9YdodBTnN9bLH149Ag==}
engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
'@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
- '@babel/generator@7.29.7':
- resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
+ '@babel/generator@7.29.8':
+ resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==}
engines: {node: '>=6.9.0'}
- '@babel/generator@8.0.0':
- resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==}
- engines: {node: ^22.18.0 || >=24.11.0}
-
'@babel/helper-globals@7.29.7':
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
engines: {node: '>=6.9.0'}
@@ -296,55 +435,38 @@ packages:
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
engines: {node: '>=6.9.0'}
- '@babel/helper-string-parser@8.0.0':
- resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==}
- engines: {node: ^22.18.0 || >=24.11.0}
-
'@babel/helper-validator-identifier@7.29.7':
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
engines: {node: '>=6.9.0'}
- '@babel/helper-validator-identifier@8.0.2':
- resolution: {integrity: sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==}
- engines: {node: ^22.18.0 || >=24.11.0}
-
- '@babel/parser@7.29.7':
- resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
+ '@babel/parser@7.29.8':
+ resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
engines: {node: '>=6.0.0'}
hasBin: true
- '@babel/parser@8.0.0':
- resolution: {integrity: sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==}
- engines: {node: ^22.18.0 || >=24.11.0}
- hasBin: true
-
'@babel/template@7.29.7':
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
- '@babel/traverse@7.29.7':
- resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
+ '@babel/traverse@7.29.8':
+ resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==}
engines: {node: '>=6.9.0'}
- '@babel/types@7.29.7':
- resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
engines: {node: '>=6.9.0'}
- '@babel/types@8.0.0':
- resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==}
- engines: {node: ^22.18.0 || >=24.11.0}
-
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
- '@commitlint/cli@21.2.0':
- resolution: {integrity: sha512-4GLVIhUaT3c3GBlQ0GB80/5H3xXdn/Tgw4lrsuoOQVDu2wl4Xw0GuzSar8xZKSMv4H3xaKaQXmvH91GmdyYBZA==}
+ '@commitlint/cli@21.2.2':
+ resolution: {integrity: sha512-a+6hQxIxnpdvSvS2apvttPNbEliYsVC3PqFYDiiB2kjbwIsQsj1urvQ4Tkf70pKYozPalKAuRQmm/GHwndduqA==}
engines: {node: '>=22.12.0'}
hasBin: true
- '@commitlint/config-conventional@21.2.0':
- resolution: {integrity: sha512-Qf8WRDVcyVd14if6VTWenebxFbKnVnbzPUJjlzjkyJGeHK2xCGd63Dr1XZzj0plXKQb9P0BfOxoc1HVeCo2BWQ==}
+ '@commitlint/config-conventional@21.2.2':
+ resolution: {integrity: sha512-NxA37SZviusFUEYOQZ5hNnZ1h7O/KiemPkxjOlpzKJNnWxThiwc6/SaZhaPa8fyLvfRBAywhQhJJk8XESHWlpQ==}
engines: {node: '>=22.12.0'}
'@commitlint/config-validator@21.2.0':
@@ -359,40 +481,40 @@ packages:
resolution: {integrity: sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==}
engines: {node: '>=22.12.0'}
- '@commitlint/format@21.2.0':
- resolution: {integrity: sha512-c4q64xaav2U83t7k7RyzJerBZurPer7FxUOY0RL5L/6CZijZ7K+s6HIBGIghj0ey1P2+seRX0J9XQYtDued6tg==}
+ '@commitlint/format@21.2.2':
+ resolution: {integrity: sha512-v6fvxZSc/AvVMROlr3H34+1766bZSYApRUSCAMjWamStPjKMvZ8GdvVA5YW/VQNgbFTmcMz6OYmSTJEvIjPrfA==}
engines: {node: '>=22.12.0'}
- '@commitlint/is-ignored@21.2.0':
- resolution: {integrity: sha512-4/eB0vBN7L88O/oC4ajAEqi7j2ZfNgxl/+11RfAV9YosejZgDXhY2C9VcHnHJhOzPLoSy5P3Mg/46kqeyJfXKw==}
+ '@commitlint/is-ignored@21.2.2':
+ resolution: {integrity: sha512-9UoKNgfFE3LU7FrzierCvk3CdDfMDeVGC86qZiT/n0TIjfq/dmZ9MHuXd45OTNRa26ZanmJRxEtmiXk/lEJihg==}
engines: {node: '>=22.12.0'}
- '@commitlint/lint@21.2.0':
- resolution: {integrity: sha512-ceO5dp9pLjEZ6y6qbq/uXWXDPykqqlTsyzoQ0NzecpisSJhK3kTy9qzQoPeJuWG/IMNdV1lO0RgmzqoAlSi1uw==}
+ '@commitlint/lint@21.2.2':
+ resolution: {integrity: sha512-Fy8JxEBzdmsYWFude/61GxXu5O+wEymwiRK2z9GL9R8mCsXphCoGxAFc5iHn5mjlfcSrhiiONE+ksf4KOjnaPg==}
engines: {node: '>=22.12.0'}
- '@commitlint/load@21.2.0':
- resolution: {integrity: sha512-RjlzWQqruRwIenJEfZtq7kG97co97nKoHpflE5YnF61tDLXxHPrdWImgzw6VL6MlFyaOcVlk74eBV8ZQmc3oIA==}
+ '@commitlint/load@21.2.2':
+ resolution: {integrity: sha512-0Tt6wDPX167cjKC5D4zhm0+20wJJG+TN/TKovMOspfSe78rOnKX+MNzlVNiu6HyQPZChPJ8QBH31MVt6Bb8fCg==}
engines: {node: '>=22.12.0'}
'@commitlint/message@21.2.0':
resolution: {integrity: sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==}
engines: {node: '>=22.12.0'}
- '@commitlint/parse@21.2.0':
- resolution: {integrity: sha512-QHWxG4d0PLTF634/AdyZ0MQS+CLn5YOuJlCFhMMlSGKFxzYGUetkHBj18xgBD+6fVzUrA2lrCdi/vlS2f/oYXg==}
+ '@commitlint/parse@21.2.2':
+ resolution: {integrity: sha512-MEkobPfvRp+z06Wro8HMG1BDGHzZmj82A1LH1nWeG3ipHpg/x4m6v3wEDvMBIKjRFUnfR3nBeFs3MVCr7UdAmg==}
engines: {node: '>=22.12.0'}
- '@commitlint/read@21.2.0':
- resolution: {integrity: sha512-ELx8Ovh/JoAw5lpvDgxc6Y0We9skf2IPI2RFN+gnYgDGjRdMSF8zeodxhZmcclLWzfUIF7hXLOa8gOlllYcvBA==}
+ '@commitlint/read@21.2.1':
+ resolution: {integrity: sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==}
engines: {node: '>=22.12.0'}
- '@commitlint/resolve-extends@21.2.0':
- resolution: {integrity: sha512-4O/1j51+79Wth9s/MGxt/5gs0XYLDgNlYpltQfhAvLE0itusLKs9zruxbiNg1oOkmkb9L9L4USYGjEj7n87NxA==}
+ '@commitlint/resolve-extends@21.2.2':
+ resolution: {integrity: sha512-RPkJ/IFi7sMUUVbZLqwWFtWw/zRDcfFsmrPSiTMrt5wb7AdxOr86EGQFvmGzef5QKV5IPBWWCujqVTw1RWX44A==}
engines: {node: '>=22.12.0'}
- '@commitlint/rules@21.2.0':
- resolution: {integrity: sha512-C2yXMNpiB8ETZKfx5JD8+ExgF8vTU1VQMKPSUUYwqKpw9oJWQBrlXBpdU038mj2WPjof7o9UzFpmTyBeGMZwZg==}
+ '@commitlint/rules@21.2.2':
+ resolution: {integrity: sha512-eplQzyYkBjYB1HyyRj8hkcK11Y9DU9nuBz7uOKEd6NpE9NGDytLFCAnlRE+OoiK/5sHEJsaz2RGhuWBvYzIbNA==}
engines: {node: '>=22.12.0'}
'@commitlint/to-lines@21.0.1':
@@ -419,8 +541,20 @@ packages:
conventional-commits-parser:
optional: true
- '@conventional-changelog/template@1.2.1':
- resolution: {integrity: sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==}
+ '@conventional-changelog/git-client@3.1.2':
+ resolution: {integrity: sha512-jZqwnJwf7nboIlAcw/mkOjVa6DexCcUOgT2oOQgkoi3z9vR8tGFkcMy2BFcYwjhL9sYcDDXkRQDayiDieCoW7A==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ conventional-commits-filter: ^6.0.1
+ conventional-commits-parser: ^7.1.2
+ peerDependenciesMeta:
+ conventional-commits-filter:
+ optional: true
+ conventional-commits-parser:
+ optional: true
+
+ '@conventional-changelog/template@1.4.0':
+ resolution: {integrity: sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==}
engines: {node: '>=22'}
'@dprint/formatter@0.5.1':
@@ -432,17 +566,8 @@ packages:
'@dprint/toml@0.7.0':
resolution: {integrity: sha512-eFaQTcfxKHB+YyTh83x7GEv+gDPuj9q5NFOTaoj5rZmQTbj6OgjjMxUicmS1R8zYcx8YAq5oA9J3YFa5U6x2gA==}
- '@emnapi/core@1.11.1':
- resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
-
- '@emnapi/runtime@1.11.1':
- resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
-
- '@emnapi/wasi-threads@1.2.2':
- resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
-
- '@eslint-community/eslint-utils@4.9.1':
- resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
+ '@eslint-community/eslint-utils@4.10.1':
+ resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
@@ -455,16 +580,16 @@ packages:
resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/config-helpers@0.6.0':
- resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==}
+ '@eslint/config-helpers@0.7.0':
+ resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/core@1.2.1':
resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/js@9.39.4':
- resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==}
+ '@eslint/js@9.39.5':
+ resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@3.0.5':
@@ -508,8 +633,8 @@ packages:
resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
- '@inquirer/checkbox@5.2.1':
- resolution: {integrity: sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==}
+ '@inquirer/checkbox@5.2.3':
+ resolution: {integrity: sha512-XEYX2WA8SBkLPczL6/yXPHLPCvDoptmh9v56Cy05BSV1Smk1vWy19bTC4qJBuIffw7+6l4CcaYYzGqG60RfW1g==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -517,8 +642,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/confirm@6.1.1':
- resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==}
+ '@inquirer/confirm@6.3.0':
+ resolution: {integrity: sha512-pZHXJImFtERmSNMBHcjwuz8Ck5vEFEYNUZnwbb8aJpjHv/TwGuFErNxF2Hp8+V+pNJs2EYPMlyWscvFEqO9jOQ==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -526,8 +651,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/core@11.2.1':
- resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==}
+ '@inquirer/core@12.0.1':
+ resolution: {integrity: sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -535,8 +660,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/editor@5.2.2':
- resolution: {integrity: sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==}
+ '@inquirer/editor@5.3.1':
+ resolution: {integrity: sha512-y43COoyVUjPWIobn2Qep/uI1drPS78aaZZZ9kVi94Tyu/GuW2N8d8Q4rifJXGAXCEAXCPTTMjD8gC1HyvM5ukA==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -544,8 +669,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/expand@5.1.1':
- resolution: {integrity: sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==}
+ '@inquirer/expand@5.1.3':
+ resolution: {integrity: sha512-3NQJiXNJ/aj9wiAsr7pECdp5Qe9J0X9YUJCKsaFXS+ddOxfL6J4AIl3w3T4Gq3kK0WsQY5GMoDokK5X94m6lHw==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -553,8 +678,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/external-editor@3.0.3':
- resolution: {integrity: sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==}
+ '@inquirer/external-editor@3.0.4':
+ resolution: {integrity: sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -562,12 +687,12 @@ packages:
'@types/node':
optional: true
- '@inquirer/figures@2.0.7':
- resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==}
+ '@inquirer/figures@2.0.8':
+ resolution: {integrity: sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
- '@inquirer/input@5.1.2':
- resolution: {integrity: sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==}
+ '@inquirer/input@5.1.4':
+ resolution: {integrity: sha512-3xQkQrOvgOzpSN2ciTVdRDlg1FWMCA8l+0KfB6SNlILoTCGzJTzO/gc0Rwjcb3usuGyKdaGtI6OiyMdeMeLWkg==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -575,8 +700,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/number@4.1.1':
- resolution: {integrity: sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==}
+ '@inquirer/number@4.2.1':
+ resolution: {integrity: sha512-5KaqwZNLRpUuWcoCrYghPP9TMaXL5v2Sk4xqePM7RCVegcJStoXdWibio60YIC1bec+z1fCyb67N6XPJIkZtGA==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -584,8 +709,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/password@5.1.1':
- resolution: {integrity: sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==}
+ '@inquirer/password@5.2.0':
+ resolution: {integrity: sha512-CvVcW09emkBESEOW+4R8CjLNkP3fB3XrjeL8CDvfpjgrJN+V9oerXmJAXXM3l+4xqYPD5Yaujzy/Ph0PLOdDuA==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -593,17 +718,17 @@ packages:
'@types/node':
optional: true
- '@inquirer/prompts@8.4.2':
- resolution: {integrity: sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==}
- engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+ '@inquirer/prompts@8.5.2':
+ resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
- '@inquirer/prompts@8.5.2':
- resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==}
+ '@inquirer/prompts@8.7.0':
+ resolution: {integrity: sha512-yQwBMYvpJ6jqrXtKiOwRD5XezjJoyt3VQvIyjsr5Arqb519nfIohOQymWVJ8/vEgg8xtZerrCsqlSaqt/LPC9A==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -611,8 +736,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/rawlist@5.3.1':
- resolution: {integrity: sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==}
+ '@inquirer/rawlist@5.3.3':
+ resolution: {integrity: sha512-Mu7WrtmDLaXBDEyrRLS70SZgX9ZSm4Up1w0ZxiH8C1OOp9oaVCn2k8q3QGgmlnhsKYUhuaU3zFWhAP6wxkVIMA==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -620,8 +745,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/search@4.2.1':
- resolution: {integrity: sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==}
+ '@inquirer/search@4.3.1':
+ resolution: {integrity: sha512-0VWOvsHWI0rPj6CG70MoP4oXNCB6adcyN8bVFZXnh11eLDdPIK2f2XCmva78acPPDfJisfab7qNakwBh7hdBXw==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -629,8 +754,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/select@5.2.1':
- resolution: {integrity: sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==}
+ '@inquirer/select@5.2.3':
+ resolution: {integrity: sha512-KuRTodDa6xBXX2noIpjuitpX/QT7Sfav7dIZ/OfUY54Hxg95nrGoshSzxx6Ey7qqLbImKdiGkSDt7KjPXjQgmA==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -638,8 +763,8 @@ packages:
'@types/node':
optional: true
- '@inquirer/type@4.0.7':
- resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==}
+ '@inquirer/type@4.1.0':
+ resolution: {integrity: sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
@@ -658,8 +783,8 @@ packages:
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
- '@jridgewell/sourcemap-codec@1.5.5':
- resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
@@ -672,16 +797,16 @@ packages:
resolution: {integrity: sha512-bqbrH8ulFSiZz1iisJHtHeEzj1RV/70uErCYHeW4DIhVPsWbBfWKFvg/ghAbmPk5vsj0YnhDX6DSpik4S1QoiQ==}
engines: {node: '25'}
- '@nanoforge-dev/loader-client@1.4.0':
- resolution: {integrity: sha512-YYE41uqp/7+IXRXLi71UuWFBUXf8ZguMf32A/8qzCmbQN+gUtBdTKLwm+2VVcvih6mueCQCGnulH9t5SmC8+Qg==}
+ '@nanoforge-dev/loader-client@1.5.0':
+ resolution: {integrity: sha512-FUcrfXlcaG4dsjya7fUUFkr5zwKB0nCihYeEOC1K54gl4XV6S1lgNnAfdkl4jZfzyyw4jcZUB+73t4ymBhz08g==}
engines: {node: '25'}
- '@nanoforge-dev/loader-server@1.4.0':
- resolution: {integrity: sha512-jvNoHxx54BAfjUTMZHb/T/8ApbdPV/NNGwiQITUBprH4Uv8jQmbR7zNB5Z6/vIEjSVVxrU0b4fFa+BkdnKX/iQ==}
+ '@nanoforge-dev/loader-server@1.5.0':
+ resolution: {integrity: sha512-1rly9IgunGz8Ux6lGDugUXgmNbnNjWGM8ABy7EsdrKWV74gyj7cuU6X69UolO28XUPCdbeISNcBEZV5hLK8Rlg==}
engines: {node: '25'}
- '@nanoforge-dev/loader-website@1.4.0':
- resolution: {integrity: sha512-+5el/zkSJQL9qCd+4a6fGpIDMQi26QRTWXJ24mJFMaDC74P1lvqnes0u0l0+/s9dunJxfgea0Epse0/Jj/3M3Q==}
+ '@nanoforge-dev/loader-website@1.5.0':
+ resolution: {integrity: sha512-X37wlYyoL/q8ZPk5I8W3z01novfDSopizVQXbk/sw0dRXoONtIb6xnZLlN3z4jkDkPOy6IVes0bLGr8h4aFOcA==}
engines: {node: '25'}
'@nanoforge-dev/schematics@2.2.1':
@@ -696,31 +821,28 @@ packages:
resolution: {integrity: sha512-dTa2ixPJmWVD+L13Iuxx51JIm3gaX+gnRCe8jA0PFw0xjMjLYv+0ZgbWSSVlNmowp6fqpOzfPdWADkkPNQs8LQ==}
engines: {node: '25'}
- '@napi-rs/wasm-runtime@1.1.6':
- resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
- peerDependencies:
- '@emnapi/core': ^1.7.1
- '@emnapi/runtime': ^1.7.1
-
'@octokit/auth-token@6.0.0':
resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==}
engines: {node: '>= 20'}
- '@octokit/core@7.0.6':
- resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==}
+ '@octokit/core@7.0.7':
+ resolution: {integrity: sha512-DcB0M3KFgr9ECI328lhBMVsyFT2DnmNucSBTqEN3exyNKUzkkpUSCHmTRcunF41Eou2TIQKW4seewri8ON9bSA==}
engines: {node: '>= 20'}
- '@octokit/endpoint@11.0.3':
- resolution: {integrity: sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==}
+ '@octokit/endpoint@11.0.4':
+ resolution: {integrity: sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA==}
engines: {node: '>= 20'}
- '@octokit/graphql@9.0.3':
- resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==}
+ '@octokit/graphql@9.0.4':
+ resolution: {integrity: sha512-5s15CCiY8XXQ+FG+b1YQcl6Z2FA++nwAz/tg2VUrTmnMncP+2nnGUEYANImdnxsA2Fnq+Mbl7hDjUTw7cFAwcg==}
engines: {node: '>= 20'}
'@octokit/openapi-types@27.0.0':
resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==}
+ '@octokit/openapi-types@28.0.0':
+ resolution: {integrity: sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==}
+
'@octokit/plugin-paginate-rest@14.0.0':
resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==}
engines: {node: '>= 20'}
@@ -733,105 +855,92 @@ packages:
peerDependencies:
'@octokit/core': '>=6'
- '@octokit/plugin-retry@8.1.0':
- resolution: {integrity: sha512-O1FZgXeiGb2sowEr/hYTr6YunGdSAFWnr2fyW39Ah85H8O33ELASQxcvOFF5LE6Tjekcyu2ms4qAzJVhSaJxTw==}
+ '@octokit/plugin-retry@8.1.1':
+ resolution: {integrity: sha512-VCVvZ/R1+u3WuiBWpNavZ0mY4aaJNAsENrpBP9aLSR2QyOpQgd7DhM5j4AW7z4MQpnJYgwBPf0XqPQoNBRdQwg==}
engines: {node: '>= 20'}
peerDependencies:
'@octokit/core': '>=7'
- '@octokit/request-error@7.1.0':
- resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==}
+ '@octokit/request-error@7.1.1':
+ resolution: {integrity: sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA==}
engines: {node: '>= 20'}
- '@octokit/request@10.0.11':
- resolution: {integrity: sha512-+s7HUxjfFqOMS9VlIwDffq0MikjSAK0gSpG73W+meAvVAvX4MBrHYTK5Bj3Uot55qFT4gzUtfzE4mGWY4Br8/Q==}
+ '@octokit/request@10.0.15':
+ resolution: {integrity: sha512-3CBg9aJ0hO9Pjyij8LbK/xYtEaPws9SW7xKz67daPNxQB1q5Y9OMA7DDOG0A6Hwf9ygGu3tvzusg0LXQ8/wAjA==}
engines: {node: '>= 20'}
'@octokit/types@16.0.0':
resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==}
- '@oven/bun-darwin-aarch64@1.3.14':
- resolution: {integrity: sha512-Omj20SuiHBOUjUBIyqtkNjSUIjOtEOJwmbix/ZyFH4BaQ6OZTaaRWIR4TjHVz0yadHgli6lLTiAh1uarnvD49A==}
- cpu: [arm64]
- os: [darwin]
+ '@octokit/types@17.0.0':
+ resolution: {integrity: sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==}
- '@oven/bun-darwin-x64-baseline@1.3.14':
- resolution: {integrity: sha512-OSfsTZstc898HHElhU4NccaBGOSSDn5VfahiVTnidZ9B/+wb7WTyfZJaBeJcfjwJ9H2W9uTh2TGtl3UfcXgV9g==}
- cpu: [x64]
+ '@oven/bun-darwin-aarch64@1.4.0':
+ resolution: {integrity: sha512-GCpf8QuFLsyioVawP5HrMxA1ZRBlu6Hq9RNnSc3UTUWAzIxBso9trjoZczw1HdgpqSssFkszfIV2zmOzFTjhkw==}
+ cpu: [arm64]
os: [darwin]
- '@oven/bun-darwin-x64@1.3.14':
- resolution: {integrity: sha512-FFj3QdU/OhlDyZOJ8CWfN5eWLpRlT4qjZg7lMQi7jA6GuoY5ajlO1zWLP/MuHYRSbXQUvV52RejNi8DVnAp13w==}
+ '@oven/bun-darwin-x64@1.4.0':
+ resolution: {integrity: sha512-cIrhwOr0SPEraewznhC+c/k6TG8bwFn5uZ4EJuXwjiKJLcAF36q7/bGjWkeXSe48JwMcPRUR054JXF7+cRwSSA==}
cpu: [x64]
os: [darwin]
- '@oven/bun-freebsd-aarch64@1.3.14':
- resolution: {integrity: sha512-LIKrXaFxAHybVO5Pf+9XP2FHUj/5APvXTUKk9dqHm5iFz4oH+W24cmhjkJirNujh9hKeTyrpWSe3no9JZKowIw==}
+ '@oven/bun-freebsd-aarch64@1.4.0':
+ resolution: {integrity: sha512-09x7wnjMR6M5KGBDBhVl2CpfoCIQOkVDbPX2KfIhpXv4N6grbWE7dfLPw/Ydi9gaUMGhU7UKhoz444Nu6RCycA==}
cpu: [arm64]
os: [freebsd]
- '@oven/bun-freebsd-x64@1.3.14':
- resolution: {integrity: sha512-uwD+fGUH1ADpIF3B1U2jWzzb20QwRLZfj5QZ28GUCGrAJ/nTmWrD6YYGsblCY1wuhldRez3lU40AyuvSCyLYmw==}
+ '@oven/bun-freebsd-x64@1.4.0':
+ resolution: {integrity: sha512-dRwzti/qJqV1HWplU27iUWUqp+f2DtFSf2yqQKSb+HH2dDOC//Uqd9u/A5h1DMsLszfP5OGP9UwQIKxVwFODaA==}
cpu: [x64]
os: [freebsd]
- '@oven/bun-linux-aarch64-android@1.3.14':
- resolution: {integrity: sha512-y4kq5b85lsrmFb9Xvi4w9mA5IEFJkLMrSmYn06q24KjL9rUWDWO3VFZEtteZxUN5+ec3Zm5S8OnJw1umaCbVjA==}
+ '@oven/bun-linux-aarch64-android@1.4.0':
+ resolution: {integrity: sha512-HpPIxJfDNPBPhiBNMyZoo/dOLijARfsx5j72vNuLtaTvl0Hh7HUculxjsOQ2WSyGoCgqXMEr1Qqjab1im9u1RA==}
cpu: [arm64]
os: [android]
- '@oven/bun-linux-aarch64-musl@1.3.14':
- resolution: {integrity: sha512-jmqOA92Cd1NL/1XBd4bFkJLxQ86K0RW7ohxS2qzzAvuitO4JiIxjjTeCspoU44zCozH72HpfZfUE2On31OjnWA==}
+ '@oven/bun-linux-aarch64-musl@1.4.0':
+ resolution: {integrity: sha512-RUjAAkJ/CdNV++zVxyANWshPc73CECYsfhk0fWAkoJjtywxJ2BwXzI6nopBBDMfs0HS+fhRGn6zGwU8ccxLeJg==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
- '@oven/bun-linux-aarch64@1.3.14':
- resolution: {integrity: sha512-X5SsPZHs+iYO8R/efIcRtc7gT2Q2DgPfliCxEkx4cXBumwkw0c/EsHMNwH3EgGpCDaZ7IYVPhpCG/xBOQHEwZw==}
+ '@oven/bun-linux-aarch64@1.4.0':
+ resolution: {integrity: sha512-Y5yAtCbHK6JjprXEtkdklDQFPADgs+CkfcliyY5g4JJ8baGHyQSrfpSkX3XVJ2C+aBLsdwNDdW+oczMsAwx6uA==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
- '@oven/bun-linux-x64-android@1.3.14':
- resolution: {integrity: sha512-qe9e1d+3VAEU7nAA2ol9Jvmy/o99PVMSgZhHn7Q/9O3YcDrfEqyQ8zm4zoe5qTEo8HZH0dN03Le0Ys2eQPs7eg==}
+ '@oven/bun-linux-x64-android@1.4.0':
+ resolution: {integrity: sha512-u++KyLlfMn36yWz+AgJs+fZtS46UFDNpSSZhrcitkytONtNwq0X6Q9BDVEFXxYl/+Eec0xme1rb6MgW+U35WeA==}
cpu: [x64]
os: [android]
- '@oven/bun-linux-x64-baseline@1.3.14':
- resolution: {integrity: sha512-q/8EdOC0yUE8FPeoOVq8/Pw5I9/tJaYmUfO/uDUAREx8IUnOJH1RJ5A3BjFqre8pvJoiZA9AovPJq5FnNNjSxA==}
- cpu: [x64]
- os: [linux]
-
- '@oven/bun-linux-x64-musl-baseline@1.3.14':
- resolution: {integrity: sha512-n6iE71G4lQE4XkrZhQQcL5YUlxDbnq6nqV7zeQi33PMsLT/0kYE+RvHOtBWZ3w0wMdXZfINmp63hIb9ijUBGtw==}
- cpu: [x64]
- os: [linux]
-
- '@oven/bun-linux-x64-musl@1.3.14':
- resolution: {integrity: sha512-GBCB/k/sIqcr06eTNgg7g46qiUv35Jasx4XiccJ/n7RGqrE4RWUD/XJBbWFprVPjvqd59+QtSnS99XGqvftHfg==}
+ '@oven/bun-linux-x64-musl@1.4.0':
+ resolution: {integrity: sha512-C1Dv+ISL8YKEKM9jAHzNifOcRUoziy6UMxh+yVXjUCP6QnbRhENDHLaIWWkQZJyBLTn0I3xozflorAlHiGzGqA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
- '@oven/bun-linux-x64@1.3.14':
- resolution: {integrity: sha512-7OVTAKvwfPmSbIV1HpdOoVVx5VRc427GuPPne93N6vk4eQBPId9nXmZDh9/zGaKPdbVjVtQSZafWQoUjx38Utw==}
+ '@oven/bun-linux-x64@1.4.0':
+ resolution: {integrity: sha512-Du44zebtPXJujvMLmtIxEQ6ykOhYt7L/Q+YIGVm+Yy+Pj/fpOnq60ggwIpKp/pGAFbYHNiTrA3JTjuZ9MTbZIg==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
- '@oven/bun-windows-aarch64@1.3.14':
- resolution: {integrity: sha512-T7s3x/BsVKQObGU6QDkZeI6wKynzqGbBH1yI77jrrj5siElclxr3DQrDIk8CV4G5/SJq2HHq4kpLyYY2DKCSmA==}
+ '@oven/bun-windows-aarch64@1.4.0':
+ resolution: {integrity: sha512-FBAYaQpJBP0asgqzL6NFUfjdQqsV+kvTpJ/eWxPKj+RcDgIfPSuE8kvQuPYu5pa8u8JTujYMjmuyvHxVuQsInA==}
cpu: [arm64]
os: [win32]
- '@oven/bun-windows-x64-baseline@1.3.14':
- resolution: {integrity: sha512-uIjLUC1S9DWgICzuoMba7vurBJnBruE4S5CxnvmZkdqWVXRzx1Rgu636HoH+k0qeaQCFh3jeG3JQ1y6fRHv0sw==}
- cpu: [x64]
- os: [win32]
-
- '@oven/bun-windows-x64@1.3.14':
- resolution: {integrity: sha512-mUFWL3BoYkNpjd8e9PqROiFF/1Xeotq20mABJsiQH62jM1g5zqWh4khw1RZ6bX8Q8fWvlPaxG1PjofkmjUi3vg==}
+ '@oven/bun-windows-x64@1.4.0':
+ resolution: {integrity: sha512-jRKv1NPLznMSZY5BEWciMF7zv0Tiyo2pQSxAJ3w+YWJ6y3VWNJQQQdLlV5Jx8lbOFDrJdrc9dD3GV17k3BP41A==}
cpu: [x64]
os: [win32]
- '@oxc-project/types@0.138.0':
- resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==}
+ '@oxc-project/types@0.147.0':
+ resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==}
'@oxfmt/binding-android-arm-eabi@0.35.0':
resolution: {integrity: sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA==}
@@ -962,97 +1071,98 @@ packages:
'@quansync/fs@1.0.0':
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
- '@rolldown/binding-android-arm64@1.1.4':
- resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==}
+ '@rolldown/binding-android-arm-eabi@1.2.6':
+ resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@rolldown/binding-android-arm64@1.2.6':
+ resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
- '@rolldown/binding-darwin-arm64@1.1.4':
- resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==}
+ '@rolldown/binding-darwin-arm64@1.2.6':
+ resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@rolldown/binding-darwin-x64@1.1.4':
- resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==}
+ '@rolldown/binding-darwin-x64@1.2.6':
+ resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@rolldown/binding-freebsd-x64@1.1.4':
- resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==}
+ '@rolldown/binding-freebsd-x64@1.2.6':
+ resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@rolldown/binding-linux-arm-gnueabihf@1.1.4':
- resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==}
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.6':
+ resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@rolldown/binding-linux-arm64-gnu@1.1.4':
- resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==}
+ '@rolldown/binding-linux-arm64-gnu@1.2.6':
+ resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-arm64-musl@1.1.4':
- resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==}
+ '@rolldown/binding-linux-arm64-musl@1.2.6':
+ resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@rolldown/binding-linux-ppc64-gnu@1.1.4':
- resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==}
+ '@rolldown/binding-linux-ppc64-gnu@1.2.6':
+ resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-s390x-gnu@1.1.4':
- resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==}
+ '@rolldown/binding-linux-s390x-gnu@1.2.6':
+ resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-x64-gnu@1.1.4':
- resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==}
+ '@rolldown/binding-linux-x64-gnu@1.2.6':
+ resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-x64-musl@1.1.4':
- resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==}
+ '@rolldown/binding-linux-x64-musl@1.2.6':
+ resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
- '@rolldown/binding-openharmony-arm64@1.1.4':
- resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==}
+ '@rolldown/binding-openharmony-arm64@1.2.6':
+ resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
- '@rolldown/binding-wasm32-wasi@1.1.4':
- resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [wasm32]
-
- '@rolldown/binding-win32-arm64-msvc@1.1.4':
- resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==}
+ '@rolldown/binding-win32-arm64-msvc@1.2.6':
+ resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@rolldown/binding-win32-x64-msvc@1.1.4':
- resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==}
+ '@rolldown/binding-win32-x64-msvc@1.2.6':
+ resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
@@ -1075,6 +1185,10 @@ packages:
resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==}
engines: {node: '>=18'}
+ '@simple-libs/child-process-utils@2.0.0':
+ resolution: {integrity: sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==}
+ engines: {node: '>=22'}
+
'@simple-libs/stream-utils@1.2.0':
resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==}
engines: {node: '>=18'}
@@ -1113,8 +1227,35 @@ packages:
svelte:
optional: true
- '@tybys/wasm-util@0.10.3':
- resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
+ '@turbo/darwin-64@2.10.12':
+ resolution: {integrity: sha512-9nKgKoF6ZOUsM+or0OtNf+TTJSfGvDNP7ZFv/ZGWVwOSCkumyctQiTeHwB4UNljHTnC41AqylgbunLDHoccNrA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@turbo/darwin-arm64@2.10.12':
+ resolution: {integrity: sha512-H4Elb1jqTZVeIC9bbcNwjSzemZ6RegoTOVHeuV5Osirt2Z8UguTyisMEkvZjPVZgMeN9J4ERZBFad40tFnkb7w==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@turbo/linux-64@2.10.12':
+ resolution: {integrity: sha512-lr7KIotukvjZwEXiFSYAeOH3BWzjFVBbSzTbv0fuGFsNukYyH0+g1hB5ecqnJkgkYU+KHEMG1edOhnjiKON1wQ==}
+ cpu: [x64]
+ os: [android, linux]
+
+ '@turbo/linux-arm64@2.10.12':
+ resolution: {integrity: sha512-f0pZDTtvzB5SuNwuXBaKbZHUCMCukgc8nMlHEuvLmj91Fzec+MEbr3cAvGNor5htEDqZnO6Lxt9N/GPI/77oGA==}
+ cpu: [arm64]
+ os: [android, linux]
+
+ '@turbo/windows-64@2.10.12':
+ resolution: {integrity: sha512-SDOueJRjS/QcykWf2KCRtTLmIl5YMKsLbXkXQGhDwcTXvKXZiS5ih5lBl/gkwZIpYFjqA/rAlfMzlAFcVHNe0g==}
+ cpu: [x64]
+ os: [win32]
+
+ '@turbo/windows-arm64@2.10.12':
+ resolution: {integrity: sha512-0i0mVUa4kKk+/B3RwEwPMf9CB+T7ul56hn5FFHNA4VUNTOoLBEd6aNf3FaKfCatDNZ6cicCEf6if9QUTVyzzcA==}
+ cpu: [arm64]
+ os: [win32]
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -1134,14 +1275,11 @@ packages:
'@types/inquirer@9.0.10':
resolution: {integrity: sha512-vFW2WbXwO9eZpRT5GJGFJ/shgyMNnYozmnjakt9jCQSS1lvqX8pZEQMjJ9RdDPct/YxwciQ8+V8OYn9euIrZDA==}
- '@types/jsesc@2.5.1':
- resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==}
-
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
- '@types/node@26.1.0':
- resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==}
+ '@types/node@26.4.0':
+ resolution: {integrity: sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==}
'@types/through@0.0.33':
resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==}
@@ -1149,79 +1287,79 @@ packages:
'@types/validator@13.15.10':
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
- '@typescript-eslint/eslint-plugin@8.62.1':
- resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==}
+ '@typescript-eslint/eslint-plugin@8.68.0':
+ resolution: {integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.62.1
+ '@typescript-eslint/parser': ^8.68.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/parser@8.62.1':
- resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==}
+ '@typescript-eslint/parser@8.68.0':
+ resolution: {integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/project-service@8.62.1':
- resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==}
+ '@typescript-eslint/project-service@8.68.0':
+ resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/scope-manager@8.62.1':
- resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==}
+ '@typescript-eslint/scope-manager@8.68.0':
+ resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.62.1':
- resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==}
+ '@typescript-eslint/tsconfig-utils@8.68.0':
+ resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/type-utils@8.62.1':
- resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==}
+ '@typescript-eslint/type-utils@8.68.0':
+ resolution: {integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/types@8.62.1':
- resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==}
+ '@typescript-eslint/types@8.68.0':
+ resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.62.1':
- resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==}
+ '@typescript-eslint/typescript-estree@8.68.0':
+ resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/utils@8.62.1':
- resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==}
+ '@typescript-eslint/utils@8.68.0':
+ resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/visitor-keys@8.62.1':
- resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==}
+ '@typescript-eslint/visitor-keys@8.68.0':
+ resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@vitest/coverage-v8@4.1.10':
- resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==}
+ '@vitest/coverage-v8@4.1.11':
+ resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==}
peerDependencies:
- '@vitest/browser': 4.1.10
- vitest: 4.1.10
+ '@vitest/browser': 4.1.11
+ vitest: 4.1.11
peerDependenciesMeta:
'@vitest/browser':
optional: true
- '@vitest/expect@4.1.10':
- resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
+ '@vitest/expect@4.1.11':
+ resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
- '@vitest/mocker@4.1.10':
- resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==}
+ '@vitest/mocker@4.1.11':
+ resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -1231,28 +1369,163 @@ packages:
vite:
optional: true
- '@vitest/pretty-format@4.1.10':
- resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==}
+ '@vitest/pretty-format@4.1.11':
+ resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==}
+
+ '@vitest/runner@4.1.11':
+ resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==}
+
+ '@vitest/snapshot@4.1.11':
+ resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==}
+
+ '@vitest/spy@4.1.11':
+ resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==}
+
+ '@vitest/utils@4.1.11':
+ resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
+
+ '@yuku-codegen/binding-android-arm64@0.8.7':
+ resolution: {integrity: sha512-C/0zV5IhgVdYhGJTwrY0v8dknxlhiKwtVJkMUaexu9/QvRmzlV4vfU3hZlUSgqc2BxQHntL1mCVbDq8j0FRFDw==}
+ cpu: [arm64]
+ os: [android]
+
+ '@yuku-codegen/binding-darwin-arm64@0.8.7':
+ resolution: {integrity: sha512-/u+REDMI4a0/lsJXTM4c53/w31OGZLOReZIyg62uhgLs0kc8NHsj/nOcxTdlQjq5gi0zhdkccD9LaLTXcdzPvw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@yuku-codegen/binding-darwin-x64@0.8.7':
+ resolution: {integrity: sha512-sMMzFOwCo4WXR+/6zIBThOocSC50iIIZZdfiIDbaLvj0Ax/rWt/iavyfEAqajyvzydLyCqR/ZItdLWSRlu1umw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@yuku-codegen/binding-freebsd-x64@0.8.7':
+ resolution: {integrity: sha512-MpdpKXix9P+Y1rKgjvcNeNtGjXeL1CmttNhYINrWls8kRpm4xM/oBGTmn6w7to8lAlwj5jm8q03dQPl5mRv4Qw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@yuku-codegen/binding-linux-arm-gnu@0.8.7':
+ resolution: {integrity: sha512-rr1srFLlPAmC1vtxfc9C1YLDe3iH09YjfSeeIidBqKhzx1MATjOAq4mjlRUOnhr/L27MotWIYOFKwVsd5JZFOg==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@yuku-codegen/binding-linux-arm-musl@0.8.7':
+ resolution: {integrity: sha512-eAufXh8qBRpiSO6ueaMDL+yyoXIGLhpUce72YbcACtZU2qhExwBIJyEtQf5kH2Ki0X2aqkSjSfog4OPtKXan3Q==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@yuku-codegen/binding-linux-arm64-gnu@0.8.7':
+ resolution: {integrity: sha512-gw4w6wPoHObBrdIC4duVWLmJOvpdE25j5D7yrM5mACNlK4klRz/lv8hK+ssQk9EJHBgZjSaqZIJVFgqNYbfv7A==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@yuku-codegen/binding-linux-arm64-musl@0.8.7':
+ resolution: {integrity: sha512-L68N6Y4XkqcIaKo3Ra88JEvBEH4AHff44A4INcrxeVWZ8CZtu2tCpfVxe3hR8qQQMcvBSQlDn24KpkCKEhVvfA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
- '@vitest/runner@4.1.10':
- resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==}
+ '@yuku-codegen/binding-linux-x64-gnu@0.8.7':
+ resolution: {integrity: sha512-dzyAbltJmf3Cqlb8HcFuYIf5Yn0fl1vTr3XJ9HiVNNvOlhqPSArOqtw9vI1p6/VTXTjrMLXlU+s+/kNHiIy/Cw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
- '@vitest/snapshot@4.1.10':
- resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==}
+ '@yuku-codegen/binding-linux-x64-musl@0.8.7':
+ resolution: {integrity: sha512-Otw4MH3404q0Bbvl+YTdW9aoUV5vXmUw8260bWvt1XlaoIX/ceSgI4ygheRDMfBPoHt6FDayQaO9OLVUZAgkFA==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
- '@vitest/spy@4.1.10':
- resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==}
+ '@yuku-codegen/binding-win32-arm64@0.8.7':
+ resolution: {integrity: sha512-qo/jyrzryiBuKEsFiuWaBCBe3tRMynQ0qFWFgOEjcCMQeZfBm+wKiVEUEFXXLc7bh8YezguAWp0Mtnhq4ARNyA==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@yuku-codegen/binding-win32-x64@0.8.7':
+ resolution: {integrity: sha512-D5lDsVDx6m00E6bWySlWdH72Ca4TPSaphDqB6QjU6MpuNLIJqoGoatYyq2rOmBE8Zv/kunot/o58KGL03P3eiA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@yuku-parser/binding-android-arm64@0.8.7':
+ resolution: {integrity: sha512-eGKYiUDX7Y0V7tDTmg+JTVnXnjMqfXXsorZ+EDf5kxwchQ3Or1HS14MzI2fw+jFhHR85fCWt+mtX33Yao73hIQ==}
+ cpu: [arm64]
+ os: [android]
+
+ '@yuku-parser/binding-darwin-arm64@0.8.7':
+ resolution: {integrity: sha512-Re0RHelKLnjEURulY2/KxW+Ngb8zuNA4BRZuMwgGQNzVumT6u4U2N2hc01oeYVNVof0i7GrXE4UCNBgbpRRnjQ==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@yuku-parser/binding-darwin-x64@0.8.7':
+ resolution: {integrity: sha512-Hn8DROtQkjlA1ACbPgj4a7eP9IuVOI504oiTwpkWPbpaDWD9KdmnVYCqW+1LfenNK/g7O9NhWGpXEdaCNX7lIA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@yuku-parser/binding-freebsd-x64@0.8.7':
+ resolution: {integrity: sha512-bAP2OV8wRuzplX/jYxv9+vvqQT8JxyNphI8fLfXGL054Xs+4/J5u33cIm3y4rxY8rdoLmmdiJs2Tq7r7lrDRfA==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@yuku-parser/binding-linux-arm-gnu@0.8.7':
+ resolution: {integrity: sha512-kTYwJQQgmZeAWdDIWabiReIZMpmfLueIj1tCmjStUtFGhR1Z0qwxonKVfUC4N7h/VhGGzLZ//7O1kgt1QKqgCg==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@yuku-parser/binding-linux-arm-musl@0.8.7':
+ resolution: {integrity: sha512-uL4jE8HPT2BLlxAXyD10LqgPuXa9eDa0BKpCdSANmzIJghq/2eZo3/gQNtaxPZMupWoxjYzSad9IXrwu7aYPXQ==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@yuku-parser/binding-linux-arm64-gnu@0.8.7':
+ resolution: {integrity: sha512-3gVN4pWSKZmXiNX7cU164dR9MPvesCHnlH6nPfpK+yQsCuYphjKKplcb4SnZBrhiqmXbgb2HR0c2TS0IZUPhgA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@yuku-parser/binding-linux-arm64-musl@0.8.7':
+ resolution: {integrity: sha512-S0mwfEjoLpxzXeZw802Wa4RaELsQiPtWqG6INcy8j4GtvNFtl4LCX3eGO1XLn9pyLAISLzTRyU3zUCBUPin8lg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@yuku-parser/binding-linux-x64-gnu@0.8.7':
+ resolution: {integrity: sha512-lnbWdPmerE5D1uH1G4IEZKnPzCrWCStRGrtgpSIe1RibAo5bZIjDbbbPYXmMHCEh4F+x/JaJpElh26a3r+BPbg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@yuku-parser/binding-linux-x64-musl@0.8.7':
+ resolution: {integrity: sha512-769uwndMvMzUvATWbAcEvyLHKA+DzhHSCl/obBUrRdYfRo26yxui6S8y3z7uJ+Naup7UKrDxrpK7OnQkxkl9KQ==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@yuku-parser/binding-win32-arm64@0.8.7':
+ resolution: {integrity: sha512-mEB/9PlaAkisJ6KWGz0zvywXoU6+80dTlR2LwS7s/jcXXoU6fm2+sitBZXtqu3+Q4DcDgPxM45uWMCzPs0TSRw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@yuku-parser/binding-win32-x64@0.8.7':
+ resolution: {integrity: sha512-8vNB2DP0ou61nGb8tc/qfi41gfyDXz1MHr2zqL3nR+cJ6CEbiuWV/l/a/vv151gCgiZLLAyGkQGENpozdg716w==}
+ cpu: [x64]
+ os: [win32]
- '@vitest/utils@4.1.10':
- resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
+ '@yuku-toolchain/types@0.8.7':
+ resolution: {integrity: sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
- acorn@8.17.0:
- resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
+ acorn@8.18.0:
+ resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -1274,8 +1547,8 @@ packages:
resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==}
engines: {node: '>=18'}
- ansi-regex@6.2.2:
- resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ ansi-regex@6.3.0:
+ resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==}
engines: {node: '>=12'}
ansi-styles@6.2.3:
@@ -1289,16 +1562,16 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ argue-cli@3.1.0:
+ resolution: {integrity: sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==}
+ engines: {node: '>=22'}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
- ast-kit@3.0.0:
- resolution: {integrity: sha512-8OG92q3R35qjC/4i6BLBMg8IB+fClWu/1PEwg2Z9Rn+BuNaiEgJzpzn+pxWOdHJWDCAwu2JP0wCDTozAM4QirQ==}
- engines: {node: ^22.18.0 || >=24.11.0}
-
- ast-v8-to-istanbul@1.0.4:
- resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==}
+ ast-v8-to-istanbul@1.0.5:
+ resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -1310,25 +1583,22 @@ packages:
before-after-hook@4.0.0:
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
- birpc@4.0.0:
- resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==}
-
bottleneck@2.19.5:
resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
- brace-expansion@2.1.1:
- resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
+ brace-expansion@2.1.4:
+ resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
- brace-expansion@5.0.7:
- resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
- engines: {node: 18 || 20 || >=22}
+ brace-expansion@5.0.9:
+ resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
+ engines: {node: 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
- bun@1.3.14:
- resolution: {integrity: sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg==}
+ bun@1.4.0:
+ resolution: {integrity: sha512-iRiFkc2W7UVpCyZXO9tod45TP9QCyN19fWqbpeN/jaM/K7uzeHYx/OSPsahMJazGKBgPsnxRt+4Jc43d8BcHZw==}
cpu: [arm64, x64]
os: [darwin, linux, android, freebsd, win32]
hasBin: true
@@ -1382,10 +1652,6 @@ packages:
resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==}
engines: {node: '>=18.20'}
- cli-truncate@5.2.0:
- resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==}
- engines: {node: '>=20'}
-
cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
@@ -1405,16 +1671,16 @@ packages:
resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==}
engines: {node: '>=22.12.0'}
- content-type@2.0.0:
- resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==}
- engines: {node: '>=18'}
+ content-type@3.0.0:
+ resolution: {integrity: sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw==}
+ engines: {node: '>=22'}
- conventional-changelog-angular@9.2.1:
- resolution: {integrity: sha512-oWSL6ZhnXbYraOFTK3PgRAQJ8fADDAEv5K6AdeyQPLvjFmhG8+ejL0jZZp/R7vTmGJaBvZEE+sE7dB4bCv7sAw==}
+ conventional-changelog-angular@9.4.0:
+ resolution: {integrity: sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==}
engines: {node: '>=22'}
- conventional-changelog-conventionalcommits@10.2.1:
- resolution: {integrity: sha512-n4Kr1HFMTf3iMbES0TMxKIcYtUUv4rKqyQQp2JwfOEfFCOfGT3Tq4mCyJ8S9/YPyWhydjfKrrvnyl+gCjA+mJQ==}
+ conventional-changelog-conventionalcommits@10.4.0:
+ resolution: {integrity: sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA==}
engines: {node: '>=22'}
conventional-changelog-preset-loader@5.0.0:
@@ -1430,8 +1696,8 @@ packages:
engines: {node: '>=18'}
hasBin: true
- conventional-commits-parser@7.0.1:
- resolution: {integrity: sha512-6VtskFpPsNkGVk/TY2RnV/MEdKfvCPBtQZN9x8jh9+k5RWBQ+tiaWn5UFCzTr0Dd88iKx7xghxbjBRp5uIzp3g==}
+ conventional-commits-parser@7.1.2:
+ resolution: {integrity: sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==}
engines: {node: '>=22'}
hasBin: true
@@ -1480,8 +1746,8 @@ packages:
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
engines: {node: '>=18'}
- default-browser@5.5.0:
- resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
+ default-browser@5.5.1:
+ resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==}
engines: {node: '>=18'}
define-lazy-prop@3.0.0:
@@ -1532,11 +1798,11 @@ packages:
error-ex@1.3.4:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
- es-module-lexer@2.3.0:
- resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==}
+ es-module-lexer@2.3.2:
+ resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
- es-toolkit@1.49.0:
- resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==}
+ es-toolkit@1.52.0:
+ resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==}
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
@@ -1598,8 +1864,8 @@ packages:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- eslint@10.6.0:
- resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==}
+ eslint@10.9.1:
+ resolution: {integrity: sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
@@ -1631,9 +1897,6 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
- eventemitter3@5.0.4:
- resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
-
execa@9.6.1:
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
engines: {node: ^18.19.0 || >=20.5.0}
@@ -1660,8 +1923,8 @@ packages:
fast-string-width@3.0.2:
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
- fast-uri@3.1.3:
- resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==}
+ fast-uri@3.1.6:
+ resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==}
fast-wrap-ansi@0.2.2:
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
@@ -1695,8 +1958,8 @@ packages:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
- flatted@3.4.2:
- resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
+ flatted@3.4.4:
+ resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
@@ -1754,12 +2017,6 @@ packages:
engines: {node: ^18.19 || >=20.6}
hasBin: true
- git-raw-commits@5.0.1:
- resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==}
- engines: {node: '>=18'}
- deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
- hasBin: true
-
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
@@ -1803,8 +2060,8 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
- ignore@7.0.5:
- resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ ignore@7.0.6:
+ resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
engines: {node: '>= 4'}
import-fresh@3.3.1:
@@ -1839,10 +2096,6 @@ packages:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
- is-fullwidth-code-point@5.1.0:
- resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==}
- engines: {node: '>=18'}
-
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
@@ -1908,8 +2161,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- js-yaml@4.3.0:
- resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
+ js-yaml@4.3.2:
+ resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
hasBin: true
jsesc@3.1.0:
@@ -1932,8 +2185,8 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
- json-with-bigint@3.5.8:
- resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==}
+ json-with-bigint@3.5.12:
+ resolution: {integrity: sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w==}
jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
@@ -1945,95 +2198,91 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
- libphonenumber-js@1.13.8:
- resolution: {integrity: sha512-80xal1m93rADejw2pMp2MSzFhHCPLEspjHxnH2UtqI+DgAmElsbmLMiqk9niwH9NWAfjsRtaJI+qBrOEmRx9nQ==}
+ libphonenumber-js@1.13.12:
+ resolution: {integrity: sha512-uLVeV1c9OTk6qkdqnj+mpMD+ZdnZ0szVyWu58HwMmpwkHA1gCEkyjd3veZQXDnuw9KEwSRjcc9B1pS9XKIN1fA==}
- lightningcss-android-arm64@1.32.0:
- resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ lightningcss-android-arm64@1.33.0:
+ resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
- lightningcss-darwin-arm64@1.32.0:
- resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ lightningcss-darwin-arm64@1.33.0:
+ resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
- lightningcss-darwin-x64@1.32.0:
- resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ lightningcss-darwin-x64@1.33.0:
+ resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
- lightningcss-freebsd-x64@1.32.0:
- resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ lightningcss-freebsd-x64@1.33.0:
+ resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
- lightningcss-linux-arm-gnueabihf@1.32.0:
- resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
- lightningcss-linux-arm64-gnu@1.32.0:
- resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ lightningcss-linux-arm64-gnu@1.33.0:
+ resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- lightningcss-linux-arm64-musl@1.32.0:
- resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ lightningcss-linux-arm64-musl@1.33.0:
+ resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
- lightningcss-linux-x64-gnu@1.32.0:
- resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ lightningcss-linux-x64-gnu@1.33.0:
+ resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
- lightningcss-linux-x64-musl@1.32.0:
- resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ lightningcss-linux-x64-musl@1.33.0:
+ resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
- lightningcss-win32-arm64-msvc@1.32.0:
- resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ lightningcss-win32-arm64-msvc@1.33.0:
+ resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
- lightningcss-win32-x64-msvc@1.32.0:
- resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ lightningcss-win32-x64-msvc@1.33.0:
+ resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
- lightningcss@1.32.0:
- resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ lightningcss@1.33.0:
+ resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
engines: {node: '>= 12.0.0'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
- lint-staged@17.0.8:
- resolution: {integrity: sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==}
+ lint-staged@17.4.1:
+ resolution: {integrity: sha512-FmJeudcalbSfg1du+JCfvi5vS6Qt08KgbfLWiHinbef+2JJwUZwAWVoaO1AcJVUTWPfk0t30PMQNwPAeCzYQ+Q==}
engines: {node: '>=22.22.1'}
hasBin: true
- listr2@10.2.2:
- resolution: {integrity: sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==}
- engines: {node: '>=22.13.0'}
-
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -2045,15 +2294,14 @@ packages:
resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==}
engines: {node: '>=18'}
- log-update@6.1.0:
- resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
- engines: {node: '>=18'}
-
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
- magicast@0.5.3:
- resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==}
+ magic-string@1.0.0:
+ resolution: {integrity: sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==}
+
+ magicast@0.5.4:
+ resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==}
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
@@ -2063,10 +2311,6 @@ packages:
resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
engines: {node: '>=18'}
- meow@14.1.0:
- resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==}
- engines: {node: '>=20'}
-
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@@ -2075,8 +2319,8 @@ packages:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
- minimatch@10.2.5:
- resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
+ minimatch@10.2.6:
+ resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22}
minimatch@9.0.9:
@@ -2098,8 +2342,8 @@ packages:
resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==}
engines: {node: ^20.17.0 || >=22.9.0}
- nanoid@3.3.15:
- resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
+ nanoid@3.3.18:
+ resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@@ -2114,29 +2358,25 @@ packages:
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
engines: {node: '>=18'}
- obug@2.1.3:
- resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
+ obug@2.1.4:
+ resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
engines: {node: '>=12.20.0'}
- ohash@2.0.11:
- resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+ ohash@2.0.12:
+ resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==}
onetime@7.0.0:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
- open@11.0.0:
- resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
+ open@11.0.2:
+ resolution: {integrity: sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q==}
engines: {node: '>=20'}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
- ora@9.4.0:
- resolution: {integrity: sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==}
- engines: {node: '>=20'}
-
ora@9.4.1:
resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==}
engines: {node: '>=20'}
@@ -2194,26 +2434,30 @@ packages:
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
engines: {node: '>=8.6'}
- picomatch@4.0.4:
- resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
- engines: {node: '>=12'}
-
picomatch@4.0.5:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
+ picomatch@4.0.7:
+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
+ engines: {node: '>=12'}
+
plur@5.1.0:
resolution: {integrity: sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- postcss@8.5.16:
- resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
+ postcss@8.5.26:
+ resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
powershell-utils@0.1.0:
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
engines: {node: '>=20'}
+ powershell-utils@0.2.1:
+ resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==}
+ engines: {node: '>=20'}
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -2222,13 +2466,13 @@ packages:
resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==}
engines: {node: '>=6.0.0'}
- prettier@3.9.4:
- resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==}
+ prettier@3.9.6:
+ resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==}
engines: {node: '>=14'}
hasBin: true
- pretty-ms@9.3.0:
- resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
+ pretty-ms@9.3.1:
+ resolution: {integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==}
engines: {node: '>=18'}
punycode@2.3.1:
@@ -2241,8 +2485,8 @@ packages:
rc9@3.0.1:
resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==}
- readdirp@5.0.0:
- resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
+ readdirp@5.1.1:
+ resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==}
engines: {node: '>= 20.19.0'}
reflect-metadata@0.2.2:
@@ -2267,30 +2511,27 @@ packages:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
- rfdc@1.4.1:
- resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
-
- rolldown-plugin-dts@0.26.0:
- resolution: {integrity: sha512-e+kEPtUiDES0htk5iqkSeF4EzAV7R+vugGB44iPDuw1Kw9E+WyL1VG7PaV0IIjGHLiacztMBcMTyrr8ON9CT1Q==}
+ rolldown-plugin-dts@0.27.14:
+ resolution: {integrity: sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw==}
engines: {node: ^22.18.0 || >=24.11.0}
peerDependencies:
- '@ts-macro/tsc': ^0.3.6
- '@typescript/native-preview': '>=7.0.0-dev.20260325.1'
+ '@typescript/native-preview': '*'
+ '@volar/typescript': ~2.4.0
rolldown: ^1.0.0
- typescript: ^5.0.0 || ^6.0.0
+ typescript: ^5.0.0 || ^6.0.0 || ~7.0.0
vue-tsc: ~3.2.0 || ~3.3.0
peerDependenciesMeta:
- '@ts-macro/tsc':
- optional: true
'@typescript/native-preview':
optional: true
+ '@volar/typescript':
+ optional: true
typescript:
optional: true
vue-tsc:
optional: true
- rolldown@1.1.4:
- resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==}
+ rolldown@1.2.6:
+ resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -2328,16 +2569,8 @@ packages:
resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==}
engines: {node: '>=8'}
- slice-ansi@7.1.2:
- resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==}
- engines: {node: '>=18'}
-
- slice-ansi@8.0.0:
- resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==}
- engines: {node: '>=20'}
-
- smol-toml@1.7.0:
- resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==}
+ smol-toml@1.8.0:
+ resolution: {integrity: sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==}
engines: {node: '>= 18'}
source-map-js@1.2.1:
@@ -2351,8 +2584,8 @@ packages:
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
- std-env@4.1.0:
- resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
+ std-env@4.2.0:
+ resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
stdin-discarder@0.3.2:
resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==}
@@ -2366,8 +2599,8 @@ packages:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
- string-width@8.2.1:
- resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==}
+ string-width@8.2.2:
+ resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==}
engines: {node: '>=20'}
strip-ansi@7.2.0:
@@ -2394,15 +2627,15 @@ packages:
resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==}
engines: {node: ^14.18.0 || >=16.0.0}
- tar@7.5.19:
- resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==}
+ tar@7.5.22:
+ resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==}
engines: {node: '>=18'}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
- tinyexec@1.2.4:
- resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
+ tinyexec@1.3.0:
+ resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==}
engines: {node: '>=18'}
tinyglobby@0.2.17:
@@ -2413,8 +2646,8 @@ packages:
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
engines: {node: ^20.0.0 || >=22.0.0}
- tinyrainbow@3.1.0:
- resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
+ tinyrainbow@3.1.1:
+ resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
engines: {node: '>=14.0.0'}
to-regex-range@5.0.1:
@@ -2431,18 +2664,18 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
- tsdown@0.22.3:
- resolution: {integrity: sha512-louqbfA8Qf//B9jTTL0FPtXTNpjCWv1VPkbcmQMph2pTpzs+LnB1tbe4tDDRVpo2BjF5SgUXaTZe45SxB8pWHg==}
+ tsdown@0.22.14:
+ resolution: {integrity: sha512-ule7Y+fsAN2iZbLDoo7C4KYljFJNJJ+fLshyn+9gozeTspVersWHxwdGB+Dm2hzA38s6muFnUTl0jK3vJm9ifQ==}
engines: {node: ^22.18.0 || >=24.11.0}
hasBin: true
peerDependencies:
'@arethetypeswrong/core': ^0.18.1
- '@tsdown/css': 0.22.3
- '@tsdown/exe': 0.22.3
+ '@tsdown/css': 0.22.14
+ '@tsdown/exe': 0.22.14
'@vitejs/devtools': '*'
publint: ^0.3.8
tsx: '*'
- typescript: ^5.0.0 || ^6.0.0
+ typescript: ^5.0.0 || ^6.0.0 || ^7.0.0
unplugin-unused: ^0.5.0
unrun: '*'
peerDependenciesMeta:
@@ -2472,12 +2705,16 @@ packages:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
+ turbo@2.10.12:
+ resolution: {integrity: sha512-AswgMPnpOoaVZHrrSBejETzEbuIA69OVGwfkHwfrY0A23VjWXBANzgq9+OymWOHAIArB7D1+1z498WY8fGg1Jw==}
+ hasBin: true
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
- typescript-eslint@8.62.1:
- resolution: {integrity: sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==}
+ typescript-eslint@8.68.0:
+ resolution: {integrity: sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -2494,8 +2731,8 @@ packages:
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
- undici@6.27.0:
- resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==}
+ undici@6.28.0:
+ resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==}
engines: {node: '>=18.17'}
unicode-emoji-modifier-base@1.0.0:
@@ -2509,6 +2746,16 @@ packages:
universal-user-agent@7.0.3:
resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==}
+ unrun@0.3.1:
+ resolution: {integrity: sha512-onIck/oNnCaytwths1ZVp1LK2Gq2hPoyFhiHebObuUXqR3S0uHuLLaBK8K6mRRgV7Ptip8AnNvaUsgzwWwBZuA==}
+ engines: {node: ^22.13.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ synckit: ^0.11.11
+ peerDependenciesMeta:
+ synckit:
+ optional: true
+
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -2516,13 +2763,17 @@ packages:
resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==}
engines: {node: '>= 0.10'}
- vite@8.1.2:
- resolution: {integrity: sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==}
+ verkit@0.3.2:
+ resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==}
+ engines: {node: '>=18.12.0'}
+
+ vite@8.2.2:
+ resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
- '@vitejs/devtools': ^0.3.0
+ '@vitejs/devtools': ^0.4.0 || ^0.5.0
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
@@ -2559,20 +2810,20 @@ packages:
yaml:
optional: true
- vitest@4.1.10:
- resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==}
+ vitest@4.1.11:
+ resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
- '@vitest/browser-playwright': 4.1.10
- '@vitest/browser-preview': 4.1.10
- '@vitest/browser-webdriverio': 4.1.10
- '@vitest/coverage-istanbul': 4.1.10
- '@vitest/coverage-v8': 4.1.10
- '@vitest/ui': 4.1.10
+ '@vitest/browser-playwright': 4.1.11
+ '@vitest/browser-preview': 4.1.11
+ '@vitest/browser-webdriverio': 4.1.11
+ '@vitest/coverage-istanbul': 4.1.11
+ '@vitest/coverage-v8': 4.1.11
+ '@vitest/ui': 4.1.11
happy-dom: '*'
jsdom: '*'
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -2614,16 +2865,12 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
- wrap-ansi@10.0.0:
- resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==}
- engines: {node: '>=20'}
-
wrap-ansi@9.0.2:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'}
- wsl-utils@0.3.1:
- resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
+ wsl-utils@1.0.0:
+ resolution: {integrity: sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==}
engines: {node: '>=20'}
y18n@5.0.8:
@@ -2643,18 +2890,27 @@ packages:
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
- yargs@18.0.0:
- resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
+ yargs@18.1.0:
+ resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
- yoctocolors@2.1.2:
- resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
+ yoctocolors@2.2.0:
+ resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==}
engines: {node: '>=18'}
+ yuku-ast@0.8.7:
+ resolution: {integrity: sha512-h6+4bDfyootiMB9vckk5uKo5r5j0GHrkr17FQTDNfEsFT3DWlN9uu1HJwQwc64pgmLCI945fWM3lbTIqxjT3GQ==}
+
+ yuku-codegen@0.8.7:
+ resolution: {integrity: sha512-adwDZSh8oVDzhE6Du9PwVWxcOxeV0e2EVhUuMKWfhSY4wkrDq9eqixlxFF3l/XGUV1E7UFzhpz9393MUumkyNw==}
+
+ yuku-parser@0.8.7:
+ resolution: {integrity: sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==}
+
snapshots:
'@actions/core@3.0.1':
@@ -2669,51 +2925,51 @@ snapshots:
'@actions/github@9.1.1':
dependencies:
'@actions/http-client': 3.0.2
- '@octokit/core': 7.0.6
- '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6)
- '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.6)
- '@octokit/request': 10.0.11
- '@octokit/request-error': 7.1.0
- undici: 6.27.0
+ '@octokit/core': 7.0.7
+ '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.7)
+ '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.7)
+ '@octokit/request': 10.0.15
+ '@octokit/request-error': 7.1.1
+ undici: 6.28.0
'@actions/http-client@3.0.2':
dependencies:
tunnel: 0.0.6
- undici: 6.27.0
+ undici: 6.28.0
'@actions/http-client@4.0.1':
dependencies:
tunnel: 0.0.6
- undici: 6.27.0
+ undici: 6.28.0
'@actions/io@3.0.2': {}
- '@angular-devkit/core@22.0.5(chokidar@5.0.0)':
+ '@angular-devkit/core@22.1.6(chokidar@5.0.0)':
dependencies:
ajv: 8.20.0
ajv-formats: 3.0.1(ajv@8.20.0)
jsonc-parser: 3.3.1
- picomatch: 4.0.4
+ picomatch: 4.0.5
rxjs: 7.8.2
source-map: 0.7.6
optionalDependencies:
chokidar: 5.0.0
- '@angular-devkit/schematics-cli@22.0.5(@types/node@26.1.0)(chokidar@5.0.0)':
+ '@angular-devkit/schematics-cli@22.1.6(@types/node@26.4.0)(chokidar@5.0.0)':
dependencies:
- '@angular-devkit/core': 22.0.5(chokidar@5.0.0)
- '@angular-devkit/schematics': 22.0.5(chokidar@5.0.0)
- '@inquirer/prompts': 8.4.2(@types/node@26.1.0)
+ '@angular-devkit/core': 22.1.6(chokidar@5.0.0)
+ '@angular-devkit/schematics': 22.1.6(chokidar@5.0.0)
+ '@inquirer/prompts': 8.5.2(@types/node@26.4.0)
transitivePeerDependencies:
- '@types/node'
- chokidar
- '@angular-devkit/schematics@22.0.5(chokidar@5.0.0)':
+ '@angular-devkit/schematics@22.1.6(chokidar@5.0.0)':
dependencies:
- '@angular-devkit/core': 22.0.5(chokidar@5.0.0)
+ '@angular-devkit/core': 22.1.6(chokidar@5.0.0)
jsonc-parser: 3.3.1
- magic-string: 0.30.21
- ora: 9.4.0
+ magic-string: 1.0.0
+ ora: 9.4.1
rxjs: 7.8.2
transitivePeerDependencies:
- chokidar
@@ -2724,91 +2980,69 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.1.1
- '@babel/generator@7.29.7':
+ '@babel/generator@7.29.8':
dependencies:
- '@babel/parser': 7.29.7
- '@babel/types': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
- '@babel/generator@8.0.0':
- dependencies:
- '@babel/parser': 8.0.0
- '@babel/types': 8.0.0
- '@jridgewell/gen-mapping': 0.3.13
- '@jridgewell/trace-mapping': 0.3.31
- '@types/jsesc': 2.5.1
- jsesc: 3.1.0
-
'@babel/helper-globals@7.29.7': {}
'@babel/helper-string-parser@7.29.7': {}
- '@babel/helper-string-parser@8.0.0': {}
-
'@babel/helper-validator-identifier@7.29.7': {}
- '@babel/helper-validator-identifier@8.0.2': {}
-
- '@babel/parser@7.29.7':
- dependencies:
- '@babel/types': 7.29.7
-
- '@babel/parser@8.0.0':
+ '@babel/parser@7.29.8':
dependencies:
- '@babel/types': 8.0.0
+ '@babel/types': 7.29.8
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
- '@babel/parser': 7.29.7
- '@babel/types': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
- '@babel/traverse@7.29.7':
+ '@babel/traverse@7.29.8(supports-color@10.2.2)':
dependencies:
'@babel/code-frame': 7.29.7
- '@babel/generator': 7.29.7
+ '@babel/generator': 7.29.8
'@babel/helper-globals': 7.29.7
- '@babel/parser': 7.29.7
+ '@babel/parser': 7.29.8
'@babel/template': 7.29.7
- '@babel/types': 7.29.7
- debug: 4.4.3
+ '@babel/types': 7.29.8
+ debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
- '@babel/types@7.29.7':
+ '@babel/types@7.29.8':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
- '@babel/types@8.0.0':
- dependencies:
- '@babel/helper-string-parser': 8.0.0
- '@babel/helper-validator-identifier': 8.0.2
-
'@bcoe/v8-coverage@1.0.2': {}
- '@commitlint/cli@21.2.0(@types/node@26.1.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3)':
+ '@commitlint/cli@21.2.2(@types/node@26.4.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3)':
dependencies:
- '@commitlint/config-conventional': 21.2.0
- '@commitlint/format': 21.2.0
- '@commitlint/lint': 21.2.0
- '@commitlint/load': 21.2.0(@types/node@26.1.0)(typescript@6.0.3)
- '@commitlint/read': 21.2.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)
+ '@commitlint/config-conventional': 21.2.2
+ '@commitlint/format': 21.2.2
+ '@commitlint/lint': 21.2.2
+ '@commitlint/load': 21.2.2(@types/node@26.4.0)(typescript@6.0.3)
+ '@commitlint/read': 21.2.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)
'@commitlint/types': 21.2.0
- tinyexec: 1.2.4
- yargs: 18.0.0
+ tinyexec: 1.3.0
+ yargs: 18.1.0
transitivePeerDependencies:
- '@types/node'
- conventional-commits-filter
- conventional-commits-parser
- typescript
- '@commitlint/config-conventional@21.2.0':
+ '@commitlint/config-conventional@21.2.2':
dependencies:
'@commitlint/types': 21.2.0
- conventional-changelog-conventionalcommits: 10.2.1
+ conventional-changelog-conventionalcommits: 10.4.0
'@commitlint/config-validator@21.2.0':
dependencies:
@@ -2818,36 +3052,36 @@ snapshots:
'@commitlint/ensure@21.2.0':
dependencies:
'@commitlint/types': 21.2.0
- es-toolkit: 1.49.0
+ es-toolkit: 1.52.0
'@commitlint/execute-rule@21.0.1': {}
- '@commitlint/format@21.2.0':
+ '@commitlint/format@21.2.2':
dependencies:
'@commitlint/types': 21.2.0
picocolors: 1.1.1
- '@commitlint/is-ignored@21.2.0':
+ '@commitlint/is-ignored@21.2.2':
dependencies:
'@commitlint/types': 21.2.0
semver: 7.8.5
- '@commitlint/lint@21.2.0':
+ '@commitlint/lint@21.2.2':
dependencies:
- '@commitlint/is-ignored': 21.2.0
- '@commitlint/parse': 21.2.0
- '@commitlint/rules': 21.2.0
+ '@commitlint/is-ignored': 21.2.2
+ '@commitlint/parse': 21.2.2
+ '@commitlint/rules': 21.2.2
'@commitlint/types': 21.2.0
- '@commitlint/load@21.2.0(@types/node@26.1.0)(typescript@6.0.3)':
+ '@commitlint/load@21.2.2(@types/node@26.4.0)(typescript@6.0.3)':
dependencies:
'@commitlint/config-validator': 21.2.0
'@commitlint/execute-rule': 21.0.1
- '@commitlint/resolve-extends': 21.2.0
+ '@commitlint/resolve-extends': 21.2.2
'@commitlint/types': 21.2.0
cosmiconfig: 9.0.2(typescript@6.0.3)
- cosmiconfig-typescript-loader: 6.3.0(@types/node@26.1.0)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3)
- es-toolkit: 1.49.0
+ cosmiconfig-typescript-loader: 6.3.0(@types/node@26.4.0)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3)
+ es-toolkit: 1.52.0
is-plain-obj: 4.1.0
picocolors: 1.1.1
transitivePeerDependencies:
@@ -2856,31 +3090,31 @@ snapshots:
'@commitlint/message@21.2.0': {}
- '@commitlint/parse@21.2.0':
+ '@commitlint/parse@21.2.2':
dependencies:
'@commitlint/types': 21.2.0
- conventional-changelog-angular: 9.2.1
- conventional-commits-parser: 7.0.1
+ conventional-changelog-angular: 9.4.0
+ conventional-commits-parser: 7.1.2
- '@commitlint/read@21.2.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)':
+ '@commitlint/read@21.2.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)':
dependencies:
'@commitlint/top-level': 21.2.0
'@commitlint/types': 21.2.0
- git-raw-commits: 5.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)
- tinyexec: 1.2.4
+ '@conventional-changelog/git-client': 3.1.2(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)
+ tinyexec: 1.3.0
transitivePeerDependencies:
- conventional-commits-filter
- conventional-commits-parser
- '@commitlint/resolve-extends@21.2.0':
+ '@commitlint/resolve-extends@21.2.2':
dependencies:
'@commitlint/config-validator': 21.2.0
'@commitlint/types': 21.2.0
- es-toolkit: 1.49.0
+ es-toolkit: 1.52.0
global-directory: 5.0.0
resolve-from: 5.0.0
- '@commitlint/rules@21.2.0':
+ '@commitlint/rules@21.2.2':
dependencies:
'@commitlint/ensure': 21.2.0
'@commitlint/message': 21.2.0
@@ -2895,7 +3129,7 @@ snapshots:
'@commitlint/types@21.2.0':
dependencies:
- conventional-commits-parser: 7.0.1
+ conventional-commits-parser: 7.1.2
picocolors: 1.1.1
'@conventional-changelog/git-client@2.7.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)':
@@ -2907,7 +3141,16 @@ snapshots:
conventional-commits-filter: 5.0.0
conventional-commits-parser: 6.4.0
- '@conventional-changelog/template@1.2.1': {}
+ '@conventional-changelog/git-client@3.1.2(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)':
+ dependencies:
+ '@simple-libs/child-process-utils': 2.0.0
+ '@simple-libs/stream-utils': 2.0.0
+ semver: 7.8.5
+ optionalDependencies:
+ conventional-commits-filter: 5.0.0
+ conventional-commits-parser: 6.4.0
+
+ '@conventional-changelog/template@1.4.0': {}
'@dprint/formatter@0.5.1': {}
@@ -2915,38 +3158,22 @@ snapshots:
'@dprint/toml@0.7.0': {}
- '@emnapi/core@1.11.1':
- dependencies:
- '@emnapi/wasi-threads': 1.2.2
- tslib: 2.8.1
- optional: true
-
- '@emnapi/runtime@1.11.1':
+ '@eslint-community/eslint-utils@4.10.1(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))':
dependencies:
- tslib: 2.8.1
- optional: true
-
- '@emnapi/wasi-threads@1.2.2':
- dependencies:
- tslib: 2.8.1
- optional: true
-
- '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@2.6.1))':
- dependencies:
- eslint: 10.6.0(jiti@2.6.1)
+ eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
- '@eslint/config-array@0.23.5':
+ '@eslint/config-array@0.23.5(supports-color@10.2.2)':
dependencies:
'@eslint/object-schema': 3.0.5
- debug: 4.4.3
- minimatch: 10.2.5
+ debug: 4.4.3(supports-color@10.2.2)
+ minimatch: 10.2.6
transitivePeerDependencies:
- supports-color
- '@eslint/config-helpers@0.6.0':
+ '@eslint/config-helpers@0.7.0':
dependencies:
'@eslint/core': 1.2.1
@@ -2954,7 +3181,7 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
- '@eslint/js@9.39.4': {}
+ '@eslint/js@9.39.5': {}
'@eslint/object-schema@3.0.5': {}
@@ -2967,8 +3194,8 @@ snapshots:
dependencies:
'@favware/colorette-spinner': 1.0.1
'@octokit/auth-token': 6.0.0
- '@octokit/core': 7.0.6
- '@octokit/plugin-retry': 8.1.0(@octokit/core@7.0.6)
+ '@octokit/core': 7.0.7
+ '@octokit/plugin-retry': 8.1.1(@octokit/core@7.0.7)
'@sapphire/result': 2.8.0
'@sapphire/utilities': 3.18.2
colorette: 2.0.20
@@ -2976,9 +3203,9 @@ snapshots:
conventional-recommended-bump: 11.2.0
execa: 9.6.1
git-cliff: 2.13.1
- js-yaml: 4.3.0
+ js-yaml: 4.3.2
semver: 7.8.5
- smol-toml: 1.7.0
+ smol-toml: 1.8.0
'@favware/colorette-spinner@1.0.1':
dependencies:
@@ -3002,137 +3229,137 @@ snapshots:
'@inquirer/ansi@2.0.7': {}
- '@inquirer/checkbox@5.2.1(@types/node@26.1.0)':
+ '@inquirer/checkbox@5.2.3(@types/node@26.4.0)':
dependencies:
'@inquirer/ansi': 2.0.7
- '@inquirer/core': 11.2.1(@types/node@26.1.0)
- '@inquirer/figures': 2.0.7
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/core': 12.0.1(@types/node@26.4.0)
+ '@inquirer/figures': 2.0.8
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/confirm@6.1.1(@types/node@26.1.0)':
+ '@inquirer/confirm@6.3.0(@types/node@26.4.0)':
dependencies:
- '@inquirer/core': 11.2.1(@types/node@26.1.0)
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/core': 12.0.1(@types/node@26.4.0)
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/core@11.2.1(@types/node@26.1.0)':
+ '@inquirer/core@12.0.1(@types/node@26.4.0)':
dependencies:
'@inquirer/ansi': 2.0.7
- '@inquirer/figures': 2.0.7
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/figures': 2.0.8
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
cli-width: 4.1.0
fast-wrap-ansi: 0.2.2
mute-stream: 3.0.0
signal-exit: 4.1.0
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/editor@5.2.2(@types/node@26.1.0)':
+ '@inquirer/editor@5.3.1(@types/node@26.4.0)':
dependencies:
- '@inquirer/core': 11.2.1(@types/node@26.1.0)
- '@inquirer/external-editor': 3.0.3(@types/node@26.1.0)
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/core': 12.0.1(@types/node@26.4.0)
+ '@inquirer/external-editor': 3.0.4(@types/node@26.4.0)
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/expand@5.1.1(@types/node@26.1.0)':
+ '@inquirer/expand@5.1.3(@types/node@26.4.0)':
dependencies:
- '@inquirer/core': 11.2.1(@types/node@26.1.0)
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/core': 12.0.1(@types/node@26.4.0)
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/external-editor@3.0.3(@types/node@26.1.0)':
+ '@inquirer/external-editor@3.0.4(@types/node@26.4.0)':
dependencies:
chardet: 2.2.0
iconv-lite: 0.7.3
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/figures@2.0.7': {}
+ '@inquirer/figures@2.0.8': {}
- '@inquirer/input@5.1.2(@types/node@26.1.0)':
+ '@inquirer/input@5.1.4(@types/node@26.4.0)':
dependencies:
- '@inquirer/core': 11.2.1(@types/node@26.1.0)
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/core': 12.0.1(@types/node@26.4.0)
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/number@4.1.1(@types/node@26.1.0)':
+ '@inquirer/number@4.2.1(@types/node@26.4.0)':
dependencies:
- '@inquirer/core': 11.2.1(@types/node@26.1.0)
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/core': 12.0.1(@types/node@26.4.0)
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/password@5.1.1(@types/node@26.1.0)':
+ '@inquirer/password@5.2.0(@types/node@26.4.0)':
dependencies:
'@inquirer/ansi': 2.0.7
- '@inquirer/core': 11.2.1(@types/node@26.1.0)
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/core': 12.0.1(@types/node@26.4.0)
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
-
- '@inquirer/prompts@8.4.2(@types/node@26.1.0)':
- dependencies:
- '@inquirer/checkbox': 5.2.1(@types/node@26.1.0)
- '@inquirer/confirm': 6.1.1(@types/node@26.1.0)
- '@inquirer/editor': 5.2.2(@types/node@26.1.0)
- '@inquirer/expand': 5.1.1(@types/node@26.1.0)
- '@inquirer/input': 5.1.2(@types/node@26.1.0)
- '@inquirer/number': 4.1.1(@types/node@26.1.0)
- '@inquirer/password': 5.1.1(@types/node@26.1.0)
- '@inquirer/rawlist': 5.3.1(@types/node@26.1.0)
- '@inquirer/search': 4.2.1(@types/node@26.1.0)
- '@inquirer/select': 5.2.1(@types/node@26.1.0)
+ '@types/node': 26.4.0
+
+ '@inquirer/prompts@8.5.2(@types/node@26.4.0)':
+ dependencies:
+ '@inquirer/checkbox': 5.2.3(@types/node@26.4.0)
+ '@inquirer/confirm': 6.3.0(@types/node@26.4.0)
+ '@inquirer/editor': 5.3.1(@types/node@26.4.0)
+ '@inquirer/expand': 5.1.3(@types/node@26.4.0)
+ '@inquirer/input': 5.1.4(@types/node@26.4.0)
+ '@inquirer/number': 4.2.1(@types/node@26.4.0)
+ '@inquirer/password': 5.2.0(@types/node@26.4.0)
+ '@inquirer/rawlist': 5.3.3(@types/node@26.4.0)
+ '@inquirer/search': 4.3.1(@types/node@26.4.0)
+ '@inquirer/select': 5.2.3(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
-
- '@inquirer/prompts@8.5.2(@types/node@26.1.0)':
- dependencies:
- '@inquirer/checkbox': 5.2.1(@types/node@26.1.0)
- '@inquirer/confirm': 6.1.1(@types/node@26.1.0)
- '@inquirer/editor': 5.2.2(@types/node@26.1.0)
- '@inquirer/expand': 5.1.1(@types/node@26.1.0)
- '@inquirer/input': 5.1.2(@types/node@26.1.0)
- '@inquirer/number': 4.1.1(@types/node@26.1.0)
- '@inquirer/password': 5.1.1(@types/node@26.1.0)
- '@inquirer/rawlist': 5.3.1(@types/node@26.1.0)
- '@inquirer/search': 4.2.1(@types/node@26.1.0)
- '@inquirer/select': 5.2.1(@types/node@26.1.0)
+ '@types/node': 26.4.0
+
+ '@inquirer/prompts@8.7.0(@types/node@26.4.0)':
+ dependencies:
+ '@inquirer/checkbox': 5.2.3(@types/node@26.4.0)
+ '@inquirer/confirm': 6.3.0(@types/node@26.4.0)
+ '@inquirer/editor': 5.3.1(@types/node@26.4.0)
+ '@inquirer/expand': 5.1.3(@types/node@26.4.0)
+ '@inquirer/input': 5.1.4(@types/node@26.4.0)
+ '@inquirer/number': 4.2.1(@types/node@26.4.0)
+ '@inquirer/password': 5.2.0(@types/node@26.4.0)
+ '@inquirer/rawlist': 5.3.3(@types/node@26.4.0)
+ '@inquirer/search': 4.3.1(@types/node@26.4.0)
+ '@inquirer/select': 5.2.3(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/rawlist@5.3.1(@types/node@26.1.0)':
+ '@inquirer/rawlist@5.3.3(@types/node@26.4.0)':
dependencies:
- '@inquirer/core': 11.2.1(@types/node@26.1.0)
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/core': 12.0.1(@types/node@26.4.0)
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/search@4.2.1(@types/node@26.1.0)':
+ '@inquirer/search@4.3.1(@types/node@26.4.0)':
dependencies:
- '@inquirer/core': 11.2.1(@types/node@26.1.0)
- '@inquirer/figures': 2.0.7
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/core': 12.0.1(@types/node@26.4.0)
+ '@inquirer/figures': 2.0.8
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/select@5.2.1(@types/node@26.1.0)':
+ '@inquirer/select@5.2.3(@types/node@26.4.0)':
dependencies:
'@inquirer/ansi': 2.0.7
- '@inquirer/core': 11.2.1(@types/node@26.1.0)
- '@inquirer/figures': 2.0.7
- '@inquirer/type': 4.0.7(@types/node@26.1.0)
+ '@inquirer/core': 12.0.1(@types/node@26.4.0)
+ '@inquirer/figures': 2.0.8
+ '@inquirer/type': 4.1.0(@types/node@26.4.0)
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
- '@inquirer/type@4.0.7(@types/node@26.1.0)':
+ '@inquirer/type@4.1.0(@types/node@26.4.0)':
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
'@isaacs/fs-minipass@4.0.1':
dependencies:
@@ -3141,17 +3368,17 @@ snapshots:
'@jridgewell/gen-mapping@0.3.13':
dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/sourcemap-codec': 1.6.0
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
- '@jridgewell/sourcemap-codec@1.5.5': {}
+ '@jridgewell/sourcemap-codec@1.6.0': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
- '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/sourcemap-codec': 1.6.0
'@nanoforge-dev/actions@2.1.5':
dependencies:
@@ -3162,45 +3389,45 @@ snapshots:
'@nanoforge-dev/editor@0.1.4':
dependencies:
- bun: 1.3.14
+ bun: 1.4.0
class-transformer: 0.5.1
class-validator: 0.15.1
dotenv: 17.4.2
micromatch: 4.0.8
- tar: 7.5.19
+ tar: 7.5.22
typescript: 6.0.3
optional: true
- '@nanoforge-dev/loader-client@1.4.0':
+ '@nanoforge-dev/loader-client@1.5.0':
dependencies:
- '@nanoforge-dev/loader-website': 1.4.0
- bun: 1.3.14
+ '@nanoforge-dev/loader-website': 1.5.0
+ bun: 1.4.0
commander: 15.0.0
- '@nanoforge-dev/loader-server@1.4.0':
+ '@nanoforge-dev/loader-server@1.5.0':
dependencies:
commander: 15.0.0
- '@nanoforge-dev/loader-website@1.4.0': {}
+ '@nanoforge-dev/loader-website@1.5.0': {}
'@nanoforge-dev/schematics@2.2.1(chokidar@5.0.0)':
dependencies:
- '@angular-devkit/core': 22.0.5(chokidar@5.0.0)
- '@angular-devkit/schematics': 22.0.5(chokidar@5.0.0)
+ '@angular-devkit/core': 22.1.6(chokidar@5.0.0)
+ '@angular-devkit/schematics': 22.1.6(chokidar@5.0.0)
rxjs: 7.8.2
transitivePeerDependencies:
- chokidar
- '@nanoforge-dev/utils-eslint-config@1.0.2(@types/eslint@9.6.1)(eslint@10.6.0(jiti@2.6.1))(prettier@3.9.4)(typescript@6.0.3)':
+ '@nanoforge-dev/utils-eslint-config@1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@eslint/js': 9.39.4
+ '@eslint/js': 9.39.5
'@favware/cliff-jumper': 6.1.0
- eslint-config-prettier: 10.1.8(eslint@10.6.0(jiti@2.6.1))
+ eslint-config-prettier: 10.1.8(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))
eslint-formatter-pretty: 7.1.0
- eslint-plugin-format: 1.5.0(eslint@10.6.0(jiti@2.6.1))
- eslint-plugin-prettier: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.6.1)))(eslint@10.6.0(jiti@2.6.1))(prettier@3.9.4)
+ eslint-plugin-format: 1.5.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))
+ eslint-plugin-prettier: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2)))(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)
globals: 16.5.0
- typescript-eslint: 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)
+ typescript-eslint: 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
transitivePeerDependencies:
- '@types/eslint'
- eslint
@@ -3210,121 +3437,108 @@ snapshots:
'@nanoforge-dev/utils-prettier-config@1.0.2': {}
- '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
- dependencies:
- '@emnapi/core': 1.11.1
- '@emnapi/runtime': 1.11.1
- '@tybys/wasm-util': 0.10.3
- optional: true
-
'@octokit/auth-token@6.0.0': {}
- '@octokit/core@7.0.6':
+ '@octokit/core@7.0.7':
dependencies:
'@octokit/auth-token': 6.0.0
- '@octokit/graphql': 9.0.3
- '@octokit/request': 10.0.11
- '@octokit/request-error': 7.1.0
- '@octokit/types': 16.0.0
+ '@octokit/graphql': 9.0.4
+ '@octokit/request': 10.0.15
+ '@octokit/request-error': 7.1.1
+ '@octokit/types': 17.0.0
before-after-hook: 4.0.0
universal-user-agent: 7.0.3
- '@octokit/endpoint@11.0.3':
+ '@octokit/endpoint@11.0.4':
dependencies:
- '@octokit/types': 16.0.0
+ '@octokit/types': 17.0.0
universal-user-agent: 7.0.3
- '@octokit/graphql@9.0.3':
+ '@octokit/graphql@9.0.4':
dependencies:
- '@octokit/request': 10.0.11
- '@octokit/types': 16.0.0
+ '@octokit/request': 10.0.15
+ '@octokit/types': 17.0.0
universal-user-agent: 7.0.3
'@octokit/openapi-types@27.0.0': {}
- '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)':
+ '@octokit/openapi-types@28.0.0': {}
+
+ '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.7)':
dependencies:
- '@octokit/core': 7.0.6
+ '@octokit/core': 7.0.7
'@octokit/types': 16.0.0
- '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.6)':
+ '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.7)':
dependencies:
- '@octokit/core': 7.0.6
+ '@octokit/core': 7.0.7
'@octokit/types': 16.0.0
- '@octokit/plugin-retry@8.1.0(@octokit/core@7.0.6)':
+ '@octokit/plugin-retry@8.1.1(@octokit/core@7.0.7)':
dependencies:
- '@octokit/core': 7.0.6
- '@octokit/request-error': 7.1.0
- '@octokit/types': 16.0.0
+ '@octokit/core': 7.0.7
+ '@octokit/request-error': 7.1.1
+ '@octokit/types': 17.0.0
bottleneck: 2.19.5
- '@octokit/request-error@7.1.0':
+ '@octokit/request-error@7.1.1':
dependencies:
- '@octokit/types': 16.0.0
+ '@octokit/types': 17.0.0
- '@octokit/request@10.0.11':
+ '@octokit/request@10.0.15':
dependencies:
- '@octokit/endpoint': 11.0.3
- '@octokit/request-error': 7.1.0
- '@octokit/types': 16.0.0
- content-type: 2.0.0
- json-with-bigint: 3.5.8
+ '@octokit/endpoint': 11.0.4
+ '@octokit/request-error': 7.1.1
+ '@octokit/types': 17.0.0
+ content-type: 3.0.0
+ json-with-bigint: 3.5.12
universal-user-agent: 7.0.3
'@octokit/types@16.0.0':
dependencies:
'@octokit/openapi-types': 27.0.0
- '@oven/bun-darwin-aarch64@1.3.14':
- optional: true
-
- '@oven/bun-darwin-x64-baseline@1.3.14':
- optional: true
-
- '@oven/bun-darwin-x64@1.3.14':
- optional: true
-
- '@oven/bun-freebsd-aarch64@1.3.14':
- optional: true
+ '@octokit/types@17.0.0':
+ dependencies:
+ '@octokit/openapi-types': 28.0.0
- '@oven/bun-freebsd-x64@1.3.14':
+ '@oven/bun-darwin-aarch64@1.4.0':
optional: true
- '@oven/bun-linux-aarch64-android@1.3.14':
+ '@oven/bun-darwin-x64@1.4.0':
optional: true
- '@oven/bun-linux-aarch64-musl@1.3.14':
+ '@oven/bun-freebsd-aarch64@1.4.0':
optional: true
- '@oven/bun-linux-aarch64@1.3.14':
+ '@oven/bun-freebsd-x64@1.4.0':
optional: true
- '@oven/bun-linux-x64-android@1.3.14':
+ '@oven/bun-linux-aarch64-android@1.4.0':
optional: true
- '@oven/bun-linux-x64-baseline@1.3.14':
+ '@oven/bun-linux-aarch64-musl@1.4.0':
optional: true
- '@oven/bun-linux-x64-musl-baseline@1.3.14':
+ '@oven/bun-linux-aarch64@1.4.0':
optional: true
- '@oven/bun-linux-x64-musl@1.3.14':
+ '@oven/bun-linux-x64-android@1.4.0':
optional: true
- '@oven/bun-linux-x64@1.3.14':
+ '@oven/bun-linux-x64-musl@1.4.0':
optional: true
- '@oven/bun-windows-aarch64@1.3.14':
+ '@oven/bun-linux-x64@1.4.0':
optional: true
- '@oven/bun-windows-x64-baseline@1.3.14':
+ '@oven/bun-windows-aarch64@1.4.0':
optional: true
- '@oven/bun-windows-x64@1.3.14':
+ '@oven/bun-windows-x64@1.4.0':
optional: true
- '@oxc-project/types@0.138.0': {}
+ '@oxc-project/types@0.147.0': {}
'@oxfmt/binding-android-arm-eabi@0.35.0':
optional: true
@@ -3389,53 +3603,49 @@ snapshots:
dependencies:
quansync: 1.0.0
- '@rolldown/binding-android-arm64@1.1.4':
+ '@rolldown/binding-android-arm-eabi@1.2.6':
optional: true
- '@rolldown/binding-darwin-arm64@1.1.4':
+ '@rolldown/binding-android-arm64@1.2.6':
optional: true
- '@rolldown/binding-darwin-x64@1.1.4':
+ '@rolldown/binding-darwin-arm64@1.2.6':
optional: true
- '@rolldown/binding-freebsd-x64@1.1.4':
+ '@rolldown/binding-darwin-x64@1.2.6':
optional: true
- '@rolldown/binding-linux-arm-gnueabihf@1.1.4':
+ '@rolldown/binding-freebsd-x64@1.2.6':
optional: true
- '@rolldown/binding-linux-arm64-gnu@1.1.4':
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.6':
optional: true
- '@rolldown/binding-linux-arm64-musl@1.1.4':
+ '@rolldown/binding-linux-arm64-gnu@1.2.6':
optional: true
- '@rolldown/binding-linux-ppc64-gnu@1.1.4':
+ '@rolldown/binding-linux-arm64-musl@1.2.6':
optional: true
- '@rolldown/binding-linux-s390x-gnu@1.1.4':
+ '@rolldown/binding-linux-ppc64-gnu@1.2.6':
optional: true
- '@rolldown/binding-linux-x64-gnu@1.1.4':
+ '@rolldown/binding-linux-s390x-gnu@1.2.6':
optional: true
- '@rolldown/binding-linux-x64-musl@1.1.4':
+ '@rolldown/binding-linux-x64-gnu@1.2.6':
optional: true
- '@rolldown/binding-openharmony-arm64@1.1.4':
+ '@rolldown/binding-linux-x64-musl@1.2.6':
optional: true
- '@rolldown/binding-wasm32-wasi@1.1.4':
- dependencies:
- '@emnapi/core': 1.11.1
- '@emnapi/runtime': 1.11.1
- '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ '@rolldown/binding-openharmony-arm64@1.2.6':
optional: true
- '@rolldown/binding-win32-arm64-msvc@1.1.4':
+ '@rolldown/binding-win32-arm64-msvc@1.2.6':
optional: true
- '@rolldown/binding-win32-x64-msvc@1.1.4':
+ '@rolldown/binding-win32-x64-msvc@1.2.6':
optional: true
'@rolldown/pluginutils@1.0.1': {}
@@ -3450,6 +3660,10 @@ snapshots:
dependencies:
'@simple-libs/stream-utils': 1.2.0
+ '@simple-libs/child-process-utils@2.0.0':
+ dependencies:
+ '@simple-libs/stream-utils': 2.0.0
+
'@simple-libs/stream-utils@1.2.0': {}
'@simple-libs/stream-utils@2.0.0': {}
@@ -3460,23 +3674,36 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
- '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.4)':
+ '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.6)(supports-color@10.2.2)':
dependencies:
- '@babel/generator': 7.29.7
- '@babel/parser': 7.29.7
- '@babel/traverse': 7.29.7
- '@babel/types': 7.29.7
+ '@babel/generator': 7.29.8
+ '@babel/parser': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@10.2.2)
+ '@babel/types': 7.29.8
javascript-natural-sort: 0.7.1
lodash-es: 4.18.1
minimatch: 9.0.9
parse-imports-exports: 0.2.4
- prettier: 3.9.4
+ prettier: 3.9.6
transitivePeerDependencies:
- supports-color
- '@tybys/wasm-util@0.10.3':
- dependencies:
- tslib: 2.8.1
+ '@turbo/darwin-64@2.10.12':
+ optional: true
+
+ '@turbo/darwin-arm64@2.10.12':
+ optional: true
+
+ '@turbo/linux-64@2.10.12':
+ optional: true
+
+ '@turbo/linux-arm64@2.10.12':
+ optional: true
+
+ '@turbo/windows-64@2.10.12':
+ optional: true
+
+ '@turbo/windows-arm64@2.10.12':
optional: true
'@types/chai@5.2.3':
@@ -3500,88 +3727,86 @@ snapshots:
'@types/through': 0.0.33
rxjs: 7.8.2
- '@types/jsesc@2.5.1': {}
-
'@types/json-schema@7.0.15': {}
- '@types/node@26.1.0':
+ '@types/node@26.4.0':
dependencies:
undici-types: 8.3.0
'@types/through@0.0.33':
dependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
'@types/validator@13.15.10': {}
- '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)':
+ '@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.62.1
- '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.62.1
- eslint: 10.6.0(jiti@2.6.1)
- ignore: 7.0.5
+ '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.68.0
+ '@typescript-eslint/type-utils': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.68.0
+ eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
+ ignore: 7.0.6
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)':
+ '@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/scope-manager': 8.62.1
- '@typescript-eslint/types': 8.62.1
- '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.62.1
- debug: 4.4.3
- eslint: 10.6.0(jiti@2.6.1)
+ '@typescript-eslint/scope-manager': 8.68.0
+ '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.68.0
+ debug: 4.4.3(supports-color@10.2.2)
+ eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.62.1(typescript@6.0.3)':
+ '@typescript-eslint/project-service@8.68.0(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3)
- '@typescript-eslint/types': 8.62.1
- debug: 4.4.3
+ '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.68.0
+ debug: 4.4.3(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.62.1':
+ '@typescript-eslint/scope-manager@8.68.0':
dependencies:
- '@typescript-eslint/types': 8.62.1
- '@typescript-eslint/visitor-keys': 8.62.1
+ '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/visitor-keys': 8.68.0
- '@typescript-eslint/tsconfig-utils@8.62.1(typescript@6.0.3)':
+ '@typescript-eslint/tsconfig-utils@8.68.0(typescript@6.0.3)':
dependencies:
typescript: 6.0.3
- '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)':
+ '@typescript-eslint/type-utils@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/types': 8.62.1
- '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3)
- '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)
- debug: 4.4.3
- eslint: 10.6.0(jiti@2.6.1)
+ '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ debug: 4.4.3(supports-color@10.2.2)
+ eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/types@8.62.1': {}
+ '@typescript-eslint/types@8.68.0': {}
- '@typescript-eslint/typescript-estree@8.62.1(typescript@6.0.3)':
+ '@typescript-eslint/typescript-estree@8.68.0(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/project-service': 8.62.1(typescript@6.0.3)
- '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3)
- '@typescript-eslint/types': 8.62.1
- '@typescript-eslint/visitor-keys': 8.62.1
- debug: 4.4.3
- minimatch: 10.2.5
+ '@typescript-eslint/project-service': 8.68.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/visitor-keys': 8.68.0
+ debug: 4.4.3(supports-color@10.2.2)
+ minimatch: 10.2.6
semver: 7.8.5
tinyglobby: 0.2.17
ts-api-utils: 2.5.0(typescript@6.0.3)
@@ -3589,82 +3814,156 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)':
+ '@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.6.1))
- '@typescript-eslint/scope-manager': 8.62.1
- '@typescript-eslint/types': 8.62.1
- '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3)
- eslint: 10.6.0(jiti@2.6.1)
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))
+ '@typescript-eslint/scope-manager': 8.68.0
+ '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3)
+ eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.62.1':
+ '@typescript-eslint/visitor-keys@8.68.0':
dependencies:
- '@typescript-eslint/types': 8.62.1
+ '@typescript-eslint/types': 8.68.0
eslint-visitor-keys: 5.0.1
- '@vitest/coverage-v8@4.1.10(vitest@4.1.10)':
+ '@vitest/coverage-v8@4.1.11(vitest@4.1.11)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
- '@vitest/utils': 4.1.10
- ast-v8-to-istanbul: 1.0.4
+ '@vitest/utils': 4.1.11
+ ast-v8-to-istanbul: 1.0.5
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-reports: 3.2.0
- magicast: 0.5.3
- obug: 2.1.3
- std-env: 4.1.0
- tinyrainbow: 3.1.0
- vitest: 4.1.10(@types/node@26.1.0)(@vitest/coverage-v8@4.1.10)(vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0))
+ magicast: 0.5.4
+ obug: 2.1.4
+ std-env: 4.2.0
+ tinyrainbow: 3.1.1
+ vitest: 4.1.11(@types/node@26.4.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0))
- '@vitest/expect@4.1.10':
+ '@vitest/expect@4.1.11':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
- '@vitest/spy': 4.1.10
- '@vitest/utils': 4.1.10
+ '@vitest/spy': 4.1.11
+ '@vitest/utils': 4.1.11
chai: 6.2.2
- tinyrainbow: 3.1.0
+ tinyrainbow: 3.1.1
- '@vitest/mocker@4.1.10(vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0))':
+ '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0))':
dependencies:
- '@vitest/spy': 4.1.10
+ '@vitest/spy': 4.1.11
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0)
+ vite: 8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0)
- '@vitest/pretty-format@4.1.10':
+ '@vitest/pretty-format@4.1.11':
dependencies:
- tinyrainbow: 3.1.0
+ tinyrainbow: 3.1.1
- '@vitest/runner@4.1.10':
+ '@vitest/runner@4.1.11':
dependencies:
- '@vitest/utils': 4.1.10
+ '@vitest/utils': 4.1.11
pathe: 2.0.3
- '@vitest/snapshot@4.1.10':
+ '@vitest/snapshot@4.1.11':
dependencies:
- '@vitest/pretty-format': 4.1.10
- '@vitest/utils': 4.1.10
+ '@vitest/pretty-format': 4.1.11
+ '@vitest/utils': 4.1.11
magic-string: 0.30.21
pathe: 2.0.3
- '@vitest/spy@4.1.10': {}
+ '@vitest/spy@4.1.11': {}
- '@vitest/utils@4.1.10':
+ '@vitest/utils@4.1.11':
dependencies:
- '@vitest/pretty-format': 4.1.10
+ '@vitest/pretty-format': 4.1.11
convert-source-map: 2.0.0
- tinyrainbow: 3.1.0
+ tinyrainbow: 3.1.1
+
+ '@yuku-codegen/binding-android-arm64@0.8.7':
+ optional: true
+
+ '@yuku-codegen/binding-darwin-arm64@0.8.7':
+ optional: true
- acorn-jsx@5.3.2(acorn@8.17.0):
+ '@yuku-codegen/binding-darwin-x64@0.8.7':
+ optional: true
+
+ '@yuku-codegen/binding-freebsd-x64@0.8.7':
+ optional: true
+
+ '@yuku-codegen/binding-linux-arm-gnu@0.8.7':
+ optional: true
+
+ '@yuku-codegen/binding-linux-arm-musl@0.8.7':
+ optional: true
+
+ '@yuku-codegen/binding-linux-arm64-gnu@0.8.7':
+ optional: true
+
+ '@yuku-codegen/binding-linux-arm64-musl@0.8.7':
+ optional: true
+
+ '@yuku-codegen/binding-linux-x64-gnu@0.8.7':
+ optional: true
+
+ '@yuku-codegen/binding-linux-x64-musl@0.8.7':
+ optional: true
+
+ '@yuku-codegen/binding-win32-arm64@0.8.7':
+ optional: true
+
+ '@yuku-codegen/binding-win32-x64@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-android-arm64@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-darwin-arm64@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-darwin-x64@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-freebsd-x64@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-linux-arm-gnu@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-linux-arm-musl@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-linux-arm64-gnu@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-linux-arm64-musl@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-linux-x64-gnu@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-linux-x64-musl@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-win32-arm64@0.8.7':
+ optional: true
+
+ '@yuku-parser/binding-win32-x64@0.8.7':
+ optional: true
+
+ '@yuku-toolchain/types@0.8.7': {}
+
+ acorn-jsx@5.3.2(acorn@8.18.0):
dependencies:
- acorn: 8.17.0
+ acorn: 8.18.0
- acorn@8.17.0: {}
+ acorn@8.18.0: {}
ajv-formats@3.0.1(ajv@8.20.0):
optionalDependencies:
@@ -3680,7 +3979,7 @@ snapshots:
ajv@8.20.0:
dependencies:
fast-deep-equal: 3.1.3
- fast-uri: 3.1.3
+ fast-uri: 3.1.6
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
@@ -3688,7 +3987,7 @@ snapshots:
dependencies:
environment: 1.1.0
- ansi-regex@6.2.2: {}
+ ansi-regex@6.3.0: {}
ansi-styles@6.2.3: {}
@@ -3696,15 +3995,11 @@ snapshots:
argparse@2.0.1: {}
- assertion-error@2.0.1: {}
+ argue-cli@3.1.0: {}
- ast-kit@3.0.0:
- dependencies:
- '@babel/parser': 8.0.0
- estree-walker: 3.0.3
- pathe: 2.0.3
+ assertion-error@2.0.1: {}
- ast-v8-to-istanbul@1.0.4:
+ ast-v8-to-istanbul@1.0.5:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
@@ -3716,15 +4011,13 @@ snapshots:
before-after-hook@4.0.0: {}
- birpc@4.0.0: {}
-
bottleneck@2.19.5: {}
- brace-expansion@2.1.1:
+ brace-expansion@2.1.4:
dependencies:
balanced-match: 1.0.2
- brace-expansion@5.0.7:
+ brace-expansion@5.0.9:
dependencies:
balanced-match: 4.0.4
@@ -3733,24 +4026,20 @@ snapshots:
fill-range: 7.1.1
optional: true
- bun@1.3.14:
+ bun@1.4.0:
optionalDependencies:
- '@oven/bun-darwin-aarch64': 1.3.14
- '@oven/bun-darwin-x64': 1.3.14
- '@oven/bun-darwin-x64-baseline': 1.3.14
- '@oven/bun-freebsd-aarch64': 1.3.14
- '@oven/bun-freebsd-x64': 1.3.14
- '@oven/bun-linux-aarch64': 1.3.14
- '@oven/bun-linux-aarch64-android': 1.3.14
- '@oven/bun-linux-aarch64-musl': 1.3.14
- '@oven/bun-linux-x64': 1.3.14
- '@oven/bun-linux-x64-android': 1.3.14
- '@oven/bun-linux-x64-baseline': 1.3.14
- '@oven/bun-linux-x64-musl': 1.3.14
- '@oven/bun-linux-x64-musl-baseline': 1.3.14
- '@oven/bun-windows-aarch64': 1.3.14
- '@oven/bun-windows-x64': 1.3.14
- '@oven/bun-windows-x64-baseline': 1.3.14
+ '@oven/bun-darwin-aarch64': 1.4.0
+ '@oven/bun-darwin-x64': 1.4.0
+ '@oven/bun-freebsd-aarch64': 1.4.0
+ '@oven/bun-freebsd-x64': 1.4.0
+ '@oven/bun-linux-aarch64': 1.4.0
+ '@oven/bun-linux-aarch64-android': 1.4.0
+ '@oven/bun-linux-aarch64-musl': 1.4.0
+ '@oven/bun-linux-x64': 1.4.0
+ '@oven/bun-linux-x64-android': 1.4.0
+ '@oven/bun-linux-x64-musl': 1.4.0
+ '@oven/bun-windows-aarch64': 1.4.0
+ '@oven/bun-windows-x64': 1.4.0
bundle-name@4.1.0:
dependencies:
@@ -3770,7 +4059,7 @@ snapshots:
chokidar@5.0.0:
dependencies:
- readdirp: 5.0.0
+ readdirp: 5.1.1
chownr@3.0.0:
optional: true
@@ -3780,7 +4069,7 @@ snapshots:
class-validator@0.15.1:
dependencies:
'@types/validator': 13.15.10
- libphonenumber-js: 1.13.8
+ libphonenumber-js: 1.13.12
validator: 13.15.35
cli-cursor@5.0.0:
@@ -3789,11 +4078,6 @@ snapshots:
cli-spinners@3.4.0: {}
- cli-truncate@5.2.0:
- dependencies:
- slice-ansi: 8.0.0
- string-width: 8.2.1
-
cli-width@4.1.0: {}
cliui@9.0.1:
@@ -3808,15 +4092,15 @@ snapshots:
commander@15.0.0: {}
- content-type@2.0.0: {}
+ content-type@3.0.0: {}
- conventional-changelog-angular@9.2.1:
+ conventional-changelog-angular@9.4.0:
dependencies:
- '@conventional-changelog/template': 1.2.1
+ '@conventional-changelog/template': 1.4.0
- conventional-changelog-conventionalcommits@10.2.1:
+ conventional-changelog-conventionalcommits@10.4.0:
dependencies:
- '@conventional-changelog/template': 1.2.1
+ '@conventional-changelog/template': 1.4.0
conventional-changelog-preset-loader@5.0.0: {}
@@ -3827,10 +4111,10 @@ snapshots:
'@simple-libs/stream-utils': 1.2.0
meow: 13.2.0
- conventional-commits-parser@7.0.1:
+ conventional-commits-parser@7.1.2:
dependencies:
'@simple-libs/stream-utils': 2.0.0
- meow: 14.1.0
+ argue-cli: 3.1.0
conventional-recommended-bump@11.2.0:
dependencies:
@@ -3842,9 +4126,9 @@ snapshots:
convert-source-map@2.0.0: {}
- cosmiconfig-typescript-loader@6.3.0(@types/node@26.1.0)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3):
+ cosmiconfig-typescript-loader@6.3.0(@types/node@26.4.0)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3):
dependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
cosmiconfig: 9.0.2(typescript@6.0.3)
jiti: 2.6.1
typescript: 6.0.3
@@ -3853,7 +4137,7 @@ snapshots:
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.1
- js-yaml: 4.3.0
+ js-yaml: 4.3.2
parse-json: 5.2.0
optionalDependencies:
typescript: 6.0.3
@@ -3864,15 +4148,17 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
- debug@4.4.3:
+ debug@4.4.3(supports-color@10.2.2):
dependencies:
ms: 2.1.3
+ optionalDependencies:
+ supports-color: 10.2.2
deep-is@0.1.4: {}
default-browser-id@5.0.1: {}
- default-browser@5.5.0:
+ default-browser@5.5.1:
dependencies:
bundle-name: 4.1.0
default-browser-id: 5.0.1
@@ -3903,17 +4189,17 @@ snapshots:
dependencies:
is-arrayish: 0.2.1
- es-module-lexer@2.3.0: {}
+ es-module-lexer@2.3.2: {}
- es-toolkit@1.49.0: {}
+ es-toolkit@1.52.0: {}
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {}
- eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.6.1)):
+ eslint-config-prettier@10.1.8(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2)):
dependencies:
- eslint: 10.6.0(jiti@2.6.1)
+ eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
eslint-formatter-pretty@7.1.0:
dependencies:
@@ -3923,38 +4209,38 @@ snapshots:
eslint-rule-docs: 1.1.235
log-symbols: 7.0.1
plur: 5.1.0
- string-width: 8.2.1
+ string-width: 8.2.2
supports-hyperlinks: 4.5.0
- eslint-formatting-reporter@0.0.0(eslint@10.6.0(jiti@2.6.1)):
+ eslint-formatting-reporter@0.0.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2)):
dependencies:
- eslint: 10.6.0(jiti@2.6.1)
+ eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
prettier-linter-helpers: 1.0.1
eslint-parser-plain@0.1.1: {}
- eslint-plugin-format@1.5.0(eslint@10.6.0(jiti@2.6.1)):
+ eslint-plugin-format@1.5.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2)):
dependencies:
'@dprint/formatter': 0.5.1
'@dprint/markdown': 0.21.1
'@dprint/toml': 0.7.0
- eslint: 10.6.0(jiti@2.6.1)
- eslint-formatting-reporter: 0.0.0(eslint@10.6.0(jiti@2.6.1))
+ eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
+ eslint-formatting-reporter: 0.0.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))
eslint-parser-plain: 0.1.1
- ohash: 2.0.11
+ ohash: 2.0.12
oxfmt: 0.35.0
- prettier: 3.9.4
+ prettier: 3.9.6
synckit: 0.11.13
- eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.6.1)))(eslint@10.6.0(jiti@2.6.1))(prettier@3.9.4):
+ eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2)))(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6):
dependencies:
- eslint: 10.6.0(jiti@2.6.1)
- prettier: 3.9.4
+ eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
+ prettier: 3.9.6
prettier-linter-helpers: 1.0.1
synckit: 0.11.13
optionalDependencies:
'@types/eslint': 9.6.1
- eslint-config-prettier: 10.1.8(eslint@10.6.0(jiti@2.6.1))
+ eslint-config-prettier: 10.1.8(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))
eslint-rule-docs@1.1.235: {}
@@ -3969,12 +4255,12 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
- eslint@10.6.0(jiti@2.6.1):
+ eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2):
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.6.1))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))
'@eslint-community/regexpp': 4.12.2
- '@eslint/config-array': 0.23.5
- '@eslint/config-helpers': 0.6.0
+ '@eslint/config-array': 0.23.5(supports-color@10.2.2)
+ '@eslint/config-helpers': 0.7.0
'@eslint/core': 1.2.1
'@eslint/plugin-kit': 0.7.2
'@humanfs/node': 0.16.8
@@ -3983,7 +4269,7 @@ snapshots:
'@types/estree': 1.0.9
ajv: 6.15.0
cross-spawn: 7.0.6
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
escape-string-regexp: 4.0.0
eslint-scope: 9.1.2
eslint-visitor-keys: 5.0.1
@@ -3998,7 +4284,7 @@ snapshots:
imurmurhash: 0.1.4
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
- minimatch: 10.2.5
+ minimatch: 10.2.6
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
@@ -4008,8 +4294,8 @@ snapshots:
espree@11.2.0:
dependencies:
- acorn: 8.17.0
- acorn-jsx: 5.3.2(acorn@8.17.0)
+ acorn: 8.18.0
+ acorn-jsx: 5.3.2(acorn@8.18.0)
eslint-visitor-keys: 5.0.1
esquery@1.7.0:
@@ -4028,8 +4314,6 @@ snapshots:
esutils@2.0.3: {}
- eventemitter3@5.0.4: {}
-
execa@9.6.1:
dependencies:
'@sindresorhus/merge-streams': 4.0.0
@@ -4040,10 +4324,10 @@ snapshots:
is-plain-obj: 4.1.0
is-stream: 4.0.1
npm-run-path: 6.0.0
- pretty-ms: 9.3.0
+ pretty-ms: 9.3.1
signal-exit: 4.1.0
strip-final-newline: 4.0.0
- yoctocolors: 2.1.2
+ yoctocolors: 2.2.0
expect-type@1.4.0: {}
@@ -4061,15 +4345,15 @@ snapshots:
dependencies:
fast-string-truncated-width: 3.0.3
- fast-uri@3.1.3: {}
+ fast-uri@3.1.6: {}
fast-wrap-ansi@0.2.2:
dependencies:
fast-string-width: 3.0.2
- fdir@6.5.0(picomatch@4.0.5):
+ fdir@6.5.0(picomatch@4.0.7):
optionalDependencies:
- picomatch: 4.0.5
+ picomatch: 4.0.7
figures@6.1.0:
dependencies:
@@ -4091,10 +4375,10 @@ snapshots:
flat-cache@4.0.1:
dependencies:
- flatted: 3.4.2
+ flatted: 3.4.4
keyv: 4.5.4
- flatted@3.4.2: {}
+ flatted@3.4.4: {}
fsevents@2.3.3:
optional: true
@@ -4141,14 +4425,6 @@ snapshots:
git-cliff-windows-arm64: 2.13.1
git-cliff-windows-x64: 2.13.1
- git-raw-commits@5.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0):
- dependencies:
- '@conventional-changelog/git-client': 2.7.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)
- meow: 13.2.0
- transitivePeerDependencies:
- - conventional-commits-filter
- - conventional-commits-parser
-
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
@@ -4177,7 +4453,7 @@ snapshots:
ignore@5.3.2: {}
- ignore@7.0.5: {}
+ ignore@7.0.6: {}
import-fresh@3.3.1:
dependencies:
@@ -4198,10 +4474,6 @@ snapshots:
is-extglob@2.1.1: {}
- is-fullwidth-code-point@5.1.0:
- dependencies:
- get-east-asian-width: 1.6.0
-
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
@@ -4250,7 +4522,7 @@ snapshots:
js-tokens@4.0.0: {}
- js-yaml@4.3.0:
+ js-yaml@4.3.2:
dependencies:
argparse: 2.0.1
@@ -4266,7 +4538,7 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
- json-with-bigint@3.5.8: {}
+ json-with-bigint@3.5.12: {}
jsonc-parser@3.3.1: {}
@@ -4279,76 +4551,67 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
- libphonenumber-js@1.13.8: {}
+ libphonenumber-js@1.13.12: {}
- lightningcss-android-arm64@1.32.0:
+ lightningcss-android-arm64@1.33.0:
optional: true
- lightningcss-darwin-arm64@1.32.0:
+ lightningcss-darwin-arm64@1.33.0:
optional: true
- lightningcss-darwin-x64@1.32.0:
+ lightningcss-darwin-x64@1.33.0:
optional: true
- lightningcss-freebsd-x64@1.32.0:
+ lightningcss-freebsd-x64@1.33.0:
optional: true
- lightningcss-linux-arm-gnueabihf@1.32.0:
+ lightningcss-linux-arm-gnueabihf@1.33.0:
optional: true
- lightningcss-linux-arm64-gnu@1.32.0:
+ lightningcss-linux-arm64-gnu@1.33.0:
optional: true
- lightningcss-linux-arm64-musl@1.32.0:
+ lightningcss-linux-arm64-musl@1.33.0:
optional: true
- lightningcss-linux-x64-gnu@1.32.0:
+ lightningcss-linux-x64-gnu@1.33.0:
optional: true
- lightningcss-linux-x64-musl@1.32.0:
+ lightningcss-linux-x64-musl@1.33.0:
optional: true
- lightningcss-win32-arm64-msvc@1.32.0:
+ lightningcss-win32-arm64-msvc@1.33.0:
optional: true
- lightningcss-win32-x64-msvc@1.32.0:
+ lightningcss-win32-x64-msvc@1.33.0:
optional: true
- lightningcss@1.32.0:
+ lightningcss@1.33.0:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
- lightningcss-android-arm64: 1.32.0
- lightningcss-darwin-arm64: 1.32.0
- lightningcss-darwin-x64: 1.32.0
- lightningcss-freebsd-x64: 1.32.0
- lightningcss-linux-arm-gnueabihf: 1.32.0
- lightningcss-linux-arm64-gnu: 1.32.0
- lightningcss-linux-arm64-musl: 1.32.0
- lightningcss-linux-x64-gnu: 1.32.0
- lightningcss-linux-x64-musl: 1.32.0
- lightningcss-win32-arm64-msvc: 1.32.0
- lightningcss-win32-x64-msvc: 1.32.0
+ lightningcss-android-arm64: 1.33.0
+ lightningcss-darwin-arm64: 1.33.0
+ lightningcss-darwin-x64: 1.33.0
+ lightningcss-freebsd-x64: 1.33.0
+ lightningcss-linux-arm-gnueabihf: 1.33.0
+ lightningcss-linux-arm64-gnu: 1.33.0
+ lightningcss-linux-arm64-musl: 1.33.0
+ lightningcss-linux-x64-gnu: 1.33.0
+ lightningcss-linux-x64-musl: 1.33.0
+ lightningcss-win32-arm64-msvc: 1.33.0
+ lightningcss-win32-x64-msvc: 1.33.0
lines-and-columns@1.2.4: {}
- lint-staged@17.0.8:
+ lint-staged@17.4.1:
dependencies:
- listr2: 10.2.2
- picomatch: 4.0.5
+ picomatch: 4.0.7
string-argv: 0.3.2
- tinyexec: 1.2.4
+ tinyexec: 1.3.0
optionalDependencies:
yaml: 2.9.0
- listr2@10.2.2:
- dependencies:
- cli-truncate: 5.2.0
- eventemitter3: 5.0.4
- log-update: 6.1.0
- rfdc: 1.4.1
- wrap-ansi: 10.0.0
-
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -4358,24 +4621,20 @@ snapshots:
log-symbols@7.0.1:
dependencies:
is-unicode-supported: 2.1.0
- yoctocolors: 2.1.2
+ yoctocolors: 2.2.0
- log-update@6.1.0:
+ magic-string@0.30.21:
dependencies:
- ansi-escapes: 7.3.0
- cli-cursor: 5.0.0
- slice-ansi: 7.1.2
- strip-ansi: 7.2.0
- wrap-ansi: 9.0.2
+ '@jridgewell/sourcemap-codec': 1.6.0
- magic-string@0.30.21:
+ magic-string@1.0.0:
dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/sourcemap-codec': 1.6.0
- magicast@0.5.3:
+ magicast@0.5.4:
dependencies:
- '@babel/parser': 7.29.7
- '@babel/types': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
source-map-js: 1.2.1
make-dir@4.0.0:
@@ -4384,8 +4643,6 @@ snapshots:
meow@13.2.0: {}
- meow@14.1.0: {}
-
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@@ -4394,13 +4651,13 @@ snapshots:
mimic-function@5.0.1: {}
- minimatch@10.2.5:
+ minimatch@10.2.6:
dependencies:
- brace-expansion: 5.0.7
+ brace-expansion: 5.0.9
minimatch@9.0.9:
dependencies:
- brace-expansion: 2.1.1
+ brace-expansion: 2.1.4
minipass@7.1.3:
optional: true
@@ -4414,7 +4671,7 @@ snapshots:
mute-stream@3.0.0: {}
- nanoid@3.3.15: {}
+ nanoid@3.3.18: {}
natural-compare@1.4.0: {}
@@ -4430,22 +4687,22 @@ snapshots:
path-key: 4.0.0
unicorn-magic: 0.3.0
- obug@2.1.3: {}
+ obug@2.1.4: {}
- ohash@2.0.11: {}
+ ohash@2.0.12: {}
onetime@7.0.0:
dependencies:
mimic-function: 5.0.1
- open@11.0.0:
+ open@11.0.2:
dependencies:
- default-browser: 5.5.0
+ default-browser: 5.5.1
define-lazy-prop: 3.0.0
is-in-ssh: 1.0.0
is-inside-container: 1.0.0
- powershell-utils: 0.1.0
- wsl-utils: 0.3.1
+ powershell-utils: 0.2.1
+ wsl-utils: 1.0.0
optionator@0.9.4:
dependencies:
@@ -4456,17 +4713,6 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
- ora@9.4.0:
- dependencies:
- chalk: 5.6.2
- cli-cursor: 5.0.0
- cli-spinners: 3.4.0
- is-interactive: 2.0.0
- is-unicode-supported: 2.1.0
- log-symbols: 7.0.1
- stdin-discarder: 0.3.2
- string-width: 8.2.1
-
ora@9.4.1:
dependencies:
chalk: 5.6.2
@@ -4476,7 +4722,7 @@ snapshots:
is-unicode-supported: 2.1.0
log-symbols: 7.0.1
stdin-discarder: 0.3.2
- string-width: 8.2.1
+ string-width: 8.2.2
oxfmt@0.35.0:
dependencies:
@@ -4542,31 +4788,33 @@ snapshots:
picomatch@2.3.2:
optional: true
- picomatch@4.0.4: {}
-
picomatch@4.0.5: {}
+ picomatch@4.0.7: {}
+
plur@5.1.0:
dependencies:
irregular-plurals: 3.5.0
- postcss@8.5.16:
+ postcss@8.5.26:
dependencies:
- nanoid: 3.3.15
+ nanoid: 3.3.18
picocolors: 1.1.1
source-map-js: 1.2.1
powershell-utils@0.1.0: {}
+ powershell-utils@0.2.1: {}
+
prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.1:
dependencies:
fast-diff: 1.3.0
- prettier@3.9.4: {}
+ prettier@3.9.6: {}
- pretty-ms@9.3.0:
+ pretty-ms@9.3.1:
dependencies:
parse-ms: 4.0.0
@@ -4579,7 +4827,7 @@ snapshots:
defu: 6.1.7
destr: 2.0.5
- readdirp@5.0.0: {}
+ readdirp@5.1.1: {}
reflect-metadata@0.2.2: {}
@@ -4596,44 +4844,40 @@ snapshots:
onetime: 7.0.0
signal-exit: 4.1.0
- rfdc@1.4.1: {}
-
- rolldown-plugin-dts@0.26.0(rolldown@1.1.4)(typescript@6.0.3):
+ rolldown-plugin-dts@0.27.14(rolldown@1.2.6)(typescript@6.0.3):
dependencies:
- '@babel/generator': 8.0.0
- '@babel/helper-validator-identifier': 8.0.2
- '@babel/parser': 8.0.0
- ast-kit: 3.0.0
- birpc: 4.0.0
dts-resolver: 3.0.0
get-tsconfig: 5.0.0-beta.5
- obug: 2.1.3
- rolldown: 1.1.4
+ obug: 2.1.4
+ rolldown: 1.2.6
+ yuku-ast: 0.8.7
+ yuku-codegen: 0.8.7
+ yuku-parser: 0.8.7
optionalDependencies:
typescript: 6.0.3
transitivePeerDependencies:
- oxc-resolver
- rolldown@1.1.4:
+ rolldown@1.2.6:
dependencies:
- '@oxc-project/types': 0.138.0
+ '@oxc-project/types': 0.147.0
'@rolldown/pluginutils': 1.0.1
optionalDependencies:
- '@rolldown/binding-android-arm64': 1.1.4
- '@rolldown/binding-darwin-arm64': 1.1.4
- '@rolldown/binding-darwin-x64': 1.1.4
- '@rolldown/binding-freebsd-x64': 1.1.4
- '@rolldown/binding-linux-arm-gnueabihf': 1.1.4
- '@rolldown/binding-linux-arm64-gnu': 1.1.4
- '@rolldown/binding-linux-arm64-musl': 1.1.4
- '@rolldown/binding-linux-ppc64-gnu': 1.1.4
- '@rolldown/binding-linux-s390x-gnu': 1.1.4
- '@rolldown/binding-linux-x64-gnu': 1.1.4
- '@rolldown/binding-linux-x64-musl': 1.1.4
- '@rolldown/binding-openharmony-arm64': 1.1.4
- '@rolldown/binding-wasm32-wasi': 1.1.4
- '@rolldown/binding-win32-arm64-msvc': 1.1.4
- '@rolldown/binding-win32-x64-msvc': 1.1.4
+ '@rolldown/binding-android-arm-eabi': 1.2.6
+ '@rolldown/binding-android-arm64': 1.2.6
+ '@rolldown/binding-darwin-arm64': 1.2.6
+ '@rolldown/binding-darwin-x64': 1.2.6
+ '@rolldown/binding-freebsd-x64': 1.2.6
+ '@rolldown/binding-linux-arm-gnueabihf': 1.2.6
+ '@rolldown/binding-linux-arm64-gnu': 1.2.6
+ '@rolldown/binding-linux-arm64-musl': 1.2.6
+ '@rolldown/binding-linux-ppc64-gnu': 1.2.6
+ '@rolldown/binding-linux-s390x-gnu': 1.2.6
+ '@rolldown/binding-linux-x64-gnu': 1.2.6
+ '@rolldown/binding-linux-x64-musl': 1.2.6
+ '@rolldown/binding-openharmony-arm64': 1.2.6
+ '@rolldown/binding-win32-arm64-msvc': 1.2.6
+ '@rolldown/binding-win32-x64-msvc': 1.2.6
run-applescript@7.1.0: {}
@@ -4659,17 +4903,7 @@ snapshots:
dependencies:
unicode-emoji-modifier-base: 1.0.0
- slice-ansi@7.1.2:
- dependencies:
- ansi-styles: 6.2.3
- is-fullwidth-code-point: 5.1.0
-
- slice-ansi@8.0.0:
- dependencies:
- ansi-styles: 6.2.3
- is-fullwidth-code-point: 5.1.0
-
- smol-toml@1.7.0: {}
+ smol-toml@1.8.0: {}
source-map-js@1.2.1: {}
@@ -4677,7 +4911,7 @@ snapshots:
stackback@0.0.2: {}
- std-env@4.1.0: {}
+ std-env@4.2.0: {}
stdin-discarder@0.3.2: {}
@@ -4689,14 +4923,14 @@ snapshots:
get-east-asian-width: 1.6.0
strip-ansi: 7.2.0
- string-width@8.2.1:
+ string-width@8.2.2:
dependencies:
get-east-asian-width: 1.6.0
strip-ansi: 7.2.0
strip-ansi@7.2.0:
dependencies:
- ansi-regex: 6.2.2
+ ansi-regex: 6.3.0
strip-final-newline@4.0.0: {}
@@ -4715,7 +4949,7 @@ snapshots:
dependencies:
'@pkgr/core': 0.3.6
- tar@7.5.19:
+ tar@7.5.22:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
@@ -4726,16 +4960,16 @@ snapshots:
tinybench@2.9.0: {}
- tinyexec@1.2.4: {}
+ tinyexec@1.3.0: {}
tinyglobby@0.2.17:
dependencies:
- fdir: 6.5.0(picomatch@4.0.5)
- picomatch: 4.0.5
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
tinypool@2.1.0: {}
- tinyrainbow@3.1.0: {}
+ tinyrainbow@3.1.1: {}
to-regex-range@5.0.1:
dependencies:
@@ -4748,7 +4982,7 @@ snapshots:
dependencies:
typescript: 6.0.3
- tsdown@0.22.3(typescript@6.0.3):
+ tsdown@0.22.14(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)):
dependencies:
ansis: 4.3.1
cac: 7.0.0
@@ -4756,20 +4990,21 @@ snapshots:
empathic: 2.0.1
hookable: 6.1.1
import-without-cache: 0.4.0
- obug: 2.1.3
- picomatch: 4.0.5
- rolldown: 1.1.4
- rolldown-plugin-dts: 0.26.0(rolldown@1.1.4)(typescript@6.0.3)
- semver: 7.8.5
- tinyexec: 1.2.4
+ obug: 2.1.4
+ picomatch: 4.0.7
+ rolldown: 1.2.6
+ rolldown-plugin-dts: 0.27.14(rolldown@1.2.6)(typescript@6.0.3)
+ tinyexec: 1.3.0
tinyglobby: 0.2.17
tree-kill: 1.2.2
unconfig-core: 7.5.0
+ verkit: 0.3.2
optionalDependencies:
typescript: 6.0.3
+ unrun: 0.3.1(synckit@0.11.13)
transitivePeerDependencies:
- - '@ts-macro/tsc'
- '@typescript/native-preview'
+ - '@volar/typescript'
- oxc-resolver
- vue-tsc
@@ -4777,17 +5012,26 @@ snapshots:
tunnel@0.0.6: {}
+ turbo@2.10.12:
+ optionalDependencies:
+ '@turbo/darwin-64': 2.10.12
+ '@turbo/darwin-arm64': 2.10.12
+ '@turbo/linux-64': 2.10.12
+ '@turbo/linux-arm64': 2.10.12
+ '@turbo/windows-64': 2.10.12
+ '@turbo/windows-arm64': 2.10.12
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
- typescript-eslint@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3):
+ typescript-eslint@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3):
dependencies:
- '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)
- '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3)
- '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)
- eslint: 10.6.0(jiti@2.6.1)
+ '@typescript-eslint/eslint-plugin': 8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -4801,7 +5045,7 @@ snapshots:
undici-types@8.3.0: {}
- undici@6.27.0: {}
+ undici@6.28.0: {}
unicode-emoji-modifier-base@1.0.0: {}
@@ -4809,50 +5053,58 @@ snapshots:
universal-user-agent@7.0.3: {}
+ unrun@0.3.1(synckit@0.11.13):
+ dependencies:
+ rolldown: 1.2.6
+ optionalDependencies:
+ synckit: 0.11.13
+
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
validator@13.15.35: {}
- vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0):
+ verkit@0.3.2: {}
+
+ vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0):
dependencies:
- lightningcss: 1.32.0
- picomatch: 4.0.5
- postcss: 8.5.16
- rolldown: 1.1.4
+ lightningcss: 1.33.0
+ picomatch: 4.0.7
+ postcss: 8.5.26
+ rolldown: 1.2.6
tinyglobby: 0.2.17
optionalDependencies:
- '@types/node': 26.1.0
+ '@types/node': 26.4.0
fsevents: 2.3.3
jiti: 2.6.1
yaml: 2.9.0
- vitest@4.1.10(@types/node@26.1.0)(@vitest/coverage-v8@4.1.10)(vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0)):
+ vitest@4.1.11(@types/node@26.4.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0)):
dependencies:
- '@vitest/expect': 4.1.10
- '@vitest/mocker': 4.1.10(vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0))
- '@vitest/pretty-format': 4.1.10
- '@vitest/runner': 4.1.10
- '@vitest/snapshot': 4.1.10
- '@vitest/spy': 4.1.10
- '@vitest/utils': 4.1.10
- es-module-lexer: 2.3.0
+ '@vitest/expect': 4.1.11
+ '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0))
+ '@vitest/pretty-format': 4.1.11
+ '@vitest/runner': 4.1.11
+ '@vitest/snapshot': 4.1.11
+ '@vitest/spy': 4.1.11
+ '@vitest/utils': 4.1.11
+ es-module-lexer: 2.3.2
expect-type: 1.4.0
magic-string: 0.30.21
- obug: 2.1.3
+ obug: 2.1.4
pathe: 2.0.3
- picomatch: 4.0.5
- std-env: 4.1.0
+ picomatch: 4.0.7
+ std-env: 4.2.0
tinybench: 2.9.0
- tinyexec: 1.2.4
+ tinyexec: 1.3.0
tinyglobby: 0.2.17
- tinyrainbow: 3.1.0
- vite: 8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0)
+ tinyrainbow: 3.1.1
+ vite: 8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:
- '@types/node': 26.1.0
- '@vitest/coverage-v8': 4.1.10(vitest@4.1.10)
+ '@types/node': 26.4.0
+ '@vitest/coverage-v8': 4.1.11(vitest@4.1.11)
transitivePeerDependencies:
- msw
@@ -4867,19 +5119,13 @@ snapshots:
word-wrap@1.2.5: {}
- wrap-ansi@10.0.0:
- dependencies:
- ansi-styles: 6.2.3
- string-width: 8.2.1
- strip-ansi: 7.2.0
-
wrap-ansi@9.0.2:
dependencies:
ansi-styles: 6.2.3
string-width: 7.2.0
strip-ansi: 7.2.0
- wsl-utils@0.3.1:
+ wsl-utils@1.0.0:
dependencies:
is-wsl: 3.1.1
powershell-utils: 0.1.0
@@ -4894,15 +5140,54 @@ snapshots:
yargs-parser@22.0.0: {}
- yargs@18.0.0:
+ yargs@18.1.0:
dependencies:
cliui: 9.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
- string-width: 7.2.0
+ string-width: 8.2.2
y18n: 5.0.8
yargs-parser: 22.0.0
yocto-queue@0.1.0: {}
- yoctocolors@2.1.2: {}
+ yoctocolors@2.2.0: {}
+
+ yuku-ast@0.8.7:
+ dependencies:
+ '@yuku-toolchain/types': 0.8.7
+
+ yuku-codegen@0.8.7:
+ dependencies:
+ '@yuku-toolchain/types': 0.8.7
+ optionalDependencies:
+ '@yuku-codegen/binding-android-arm64': 0.8.7
+ '@yuku-codegen/binding-darwin-arm64': 0.8.7
+ '@yuku-codegen/binding-darwin-x64': 0.8.7
+ '@yuku-codegen/binding-freebsd-x64': 0.8.7
+ '@yuku-codegen/binding-linux-arm-gnu': 0.8.7
+ '@yuku-codegen/binding-linux-arm-musl': 0.8.7
+ '@yuku-codegen/binding-linux-arm64-gnu': 0.8.7
+ '@yuku-codegen/binding-linux-arm64-musl': 0.8.7
+ '@yuku-codegen/binding-linux-x64-gnu': 0.8.7
+ '@yuku-codegen/binding-linux-x64-musl': 0.8.7
+ '@yuku-codegen/binding-win32-arm64': 0.8.7
+ '@yuku-codegen/binding-win32-x64': 0.8.7
+
+ yuku-parser@0.8.7:
+ dependencies:
+ '@yuku-toolchain/types': 0.8.7
+ yuku-ast: 0.8.7
+ optionalDependencies:
+ '@yuku-parser/binding-android-arm64': 0.8.7
+ '@yuku-parser/binding-darwin-arm64': 0.8.7
+ '@yuku-parser/binding-darwin-x64': 0.8.7
+ '@yuku-parser/binding-freebsd-x64': 0.8.7
+ '@yuku-parser/binding-linux-arm-gnu': 0.8.7
+ '@yuku-parser/binding-linux-arm-musl': 0.8.7
+ '@yuku-parser/binding-linux-arm64-gnu': 0.8.7
+ '@yuku-parser/binding-linux-arm64-musl': 0.8.7
+ '@yuku-parser/binding-linux-x64-gnu': 0.8.7
+ '@yuku-parser/binding-linux-x64-musl': 0.8.7
+ '@yuku-parser/binding-win32-arm64': 0.8.7
+ '@yuku-parser/binding-win32-x64': 0.8.7
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 2cd98ab..0c147bb 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,10 +1,17 @@
+packages:
+ - .
+ - libs/*
+# - libs/loader/*
+
allowBuilds:
bun: true
catalogs:
build:
tsdown: ^0.22.3
+ turbo: ^2.10.12
typescript: ^6.0.3
+ unrun: ^0.3.0
ci:
'@commitlint/cli': ^21.2.0
'@commitlint/config-conventional': ^21.2.0
@@ -44,10 +51,6 @@ catalogs:
'@angular-devkit/schematics': ^22.0.5
'@angular-devkit/schematics-cli': ^22.0.5
'@nanoforge-dev/schematics': ^2.2.1
- tests:
+ test:
'@vitest/coverage-v8': ^4.1.10
vitest: ^4.1.10
-
-minimumReleaseAgeExclude:
- - '@nanoforge-dev/editor@0.1.4'
- - '@nanoforge-dev/schematics@2.2.1'
diff --git a/tsdown.config.ts b/tsdown.config.ts
index 4d95f0a..ba29b93 100644
--- a/tsdown.config.ts
+++ b/tsdown.config.ts
@@ -22,7 +22,7 @@ function createTsdownConfig({
target: "esnext",
treeshake: false,
deps: {
- skipNodeModulesBundle: true,
+ neverBundle: true,
},
env: dotenv.config({
path: resolve(
diff --git a/turbo.json b/turbo.json
new file mode 100644
index 0000000..c44a392
--- /dev/null
+++ b/turbo.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "remoteCache": {
+ "enabled": true
+ },
+ "globalDependencies": ["tsconfig.json"],
+ "tasks": {
+ "build": {
+ "dependsOn": ["^build"],
+ "inputs": ["src/**", "test/**", "package.json", "tsconfig.json", "tsdown.config.ts"],
+ "outputs": ["dist/**"],
+ "outputLogs": "errors-only"
+ },
+ "format": {
+ "dependsOn": ["^format"],
+ "inputs": ["**"],
+ "outputs": [],
+ "outputLogs": "errors-only"
+ },
+ "lint": {
+ "dependsOn": ["^lint"],
+ "inputs": ["**"],
+ "outputs": [],
+ "outputLogs": "errors-only"
+ },
+ "test:unit": {
+ "dependsOn": ["^test:unit"],
+ "inputs": ["src/**", "test/**", "package.json", "tsconfig.json", "tsdown.config.ts"],
+ "outputs": [],
+ "outputLogs": "errors-only"
+ },
+ "docs": {
+ "dependsOn": ["^docs"],
+ "inputs": ["src/**", "test/**", "package.json", "tsconfig.json", "../../docs/templates/**"],
+ "outputs": ["../../docs/references/**"],
+ "outputLogs": "errors-only"
+ }
+ }
+}
From 066f34788c6ad56b134c450e0619eaaaf1ec605f Mon Sep 17 00:00:00 2001
From: Exelo
Date: Thu, 3 Sep 2026 19:15:58 +0900
Subject: [PATCH 02/15] feat: add schematics (#200)
* feat: add schematics to cli
* fix: change position of docs
* fix: upgrade packages
* fix: fix tests
---
.husky/pre-push | 6 +-
README.md | 2 +-
docs/docs/schematics/1-overview.mdx | 62 ++
.../schematics/2-schematics/1-application.mdx | 80 +++
.../2-schematics/2-configuration.mdx | 50 ++
.../schematics/2-schematics/3-part-base.mdx | 115 ++++
.../schematics/2-schematics/4-part-main.mdx | 77 +++
.../docs/schematics/2-schematics/5-docker.mdx | 70 +++
.../schematics/2-schematics/6-component.mdx | 120 ++++
.../docs/schematics/2-schematics/7-system.mdx | 105 ++++
e2e/cli-install.test.ts | 2 +-
e2e/cli-new.test.ts | 4 +-
libs/config/README.md | 1 +
libs/config/package.json | 2 +-
libs/config/vitest.config.ts | 18 +
libs/schematics/.cliff-jumperrc.json | 8 +
libs/schematics/.gitignore | 226 +++++++
libs/schematics/.prettierignore | 14 +
libs/schematics/CHANGELOG.md | 245 ++++++++
libs/schematics/LICENSE | 21 +
libs/schematics/README.md | 61 ++
libs/schematics/cliff.toml | 79 +++
libs/schematics/commitlint.config.cjs | 4 +
libs/schematics/e2e/application.test.ts | 170 +++++
libs/schematics/e2e/component.test.ts | 171 +++++
libs/schematics/e2e/configuration.test.ts | 108 ++++
libs/schematics/e2e/docker.test.ts | 45 ++
libs/schematics/e2e/part-base.test.ts | 122 ++++
libs/schematics/e2e/part-main.test.ts | 262 ++++++++
libs/schematics/e2e/system.test.ts | 171 +++++
libs/schematics/e2e/tsconfig.json | 8 +
libs/schematics/eslint.config.js | 3 +
libs/schematics/package.json | 108 ++++
libs/schematics/prettier.config.js | 6 +
libs/schematics/src/collection.json | 40 ++
libs/schematics/src/defaults.ts | 6 +
libs/schematics/src/index.ts | 1 +
.../libs/application/application.factory.ts | 87 +++
.../libs/application/application.options.d.ts | 56 ++
.../libs/application/application.schema.d.ts | 61 ++
.../libs/application/files/js/.prettierignore | 8 +
.../src/libs/application/files/js/README.md | 166 +++++
.../src/libs/application/files/js/__dot__env | 6 +
.../application/files/js/__dot__gitignore | 134 ++++
.../application/files/js/eslint.config.js | 11 +
.../libs/application/files/js/jsconfig.json | 35 ++
.../files/js/package.json.template | 48 ++
.../files/js/pnpm-workspace.yaml.template | 3 +
.../application/files/js/prettier.config.js | 6 +
.../libs/application/files/ts/.prettierignore | 8 +
.../src/libs/application/files/ts/README.md | 166 +++++
.../src/libs/application/files/ts/__dot__env | 6 +
.../application/files/ts/__dot__gitignore | 134 ++++
.../application/files/ts/eslint.config.js | 15 +
.../files/ts/package.json.template | 52 ++
.../files/ts/pnpm-workspace.yaml.template | 3 +
.../application/files/ts/prettier.config.js | 6 +
.../libs/application/files/ts/tsconfig.json | 41 ++
.../src/libs/application/schema.json | 70 +++
.../src/libs/component/component.factory.ts | 41 ++
.../src/libs/component/component.options.d.ts | 16 +
.../src/libs/component/component.schema.d.ts | 21 +
.../files/js/__fileName__.component.js | 72 +++
.../files/ts/__fileName__.component.ts | 55 ++
.../schematics/src/libs/component/schema.json | 30 +
.../configuration/configuration.factory.ts | 94 +++
.../configuration/configuration.options.d.ts | 1 +
.../configuration/configuration.schema.d.ts | 26 +
.../configuration/files/nanoforge.config.json | 1 +
.../src/libs/configuration/schema.json | 34 +
.../src/libs/docker/docker.factory.ts | 36 ++
.../src/libs/docker/docker.options.d.ts | 6 +
.../src/libs/docker/docker.schema.d.ts | 11 +
.../src/libs/docker/files/.dockerignore | 1 +
.../src/libs/docker/files/Dockerfile | 25 +
libs/schematics/src/libs/docker/schema.json | 19 +
.../js/__nanoforgeFolder__/client.save.json | 77 +++
.../js/__nanoforgeFolder__/server.save.json | 49 ++
.../__part__/components/example.component.js | 72 +++
.../files/js/__part__/init/after-init.js | 12 +
.../js/__part__/init/after-registry-init.js | 14 +
.../files/js/__part__/init/after-run.js | 12 +
.../files/js/__part__/init/before-init.js | 13 +
.../js/__part__/init/before-registry-init.js | 14 +
.../files/js/__part__/init/before-run.js | 12 +
.../js/__part__/systems/example.system.js | 44 ++
.../ts/__nanoforgeFolder__/client.save.json | 77 +++
.../ts/__nanoforgeFolder__/server.save.json | 49 ++
.../__part__/components/example.component.ts | 55 ++
.../files/ts/__part__/init/after-init.ts | 5 +
.../ts/__part__/init/after-registry-init.ts | 6 +
.../files/ts/__part__/init/after-run.ts | 5 +
.../files/ts/__part__/init/before-init.ts | 5 +
.../ts/__part__/init/before-registry-init.ts | 6 +
.../files/ts/__part__/init/before-run.ts | 5 +
.../ts/__part__/systems/example.system.ts | 31 +
.../src/libs/part-base/part-base.factory.ts | 51 ++
.../src/libs/part-base/part-base.options.d.ts | 21 +
.../src/libs/part-base/part-base.schema.d.ts | 26 +
.../schematics/src/libs/part-base/schema.json | 35 ++
.../src/libs/part-main/part-main.factory.ts | 84 +++
.../src/libs/part-main/part-main.options.d.ts | 1 +
.../src/libs/part-main/part-main.schema.d.ts | 36 ++
.../schematics/src/libs/part-main/schema.json | 43 ++
.../system/files/js/__fileName__.system.js | 44 ++
.../system/files/ts/__fileName__.system.ts | 31 +
libs/schematics/src/libs/system/schema.json | 30 +
.../src/libs/system/system.factory.ts | 41 ++
.../src/libs/system/system.options.d.ts | 16 +
.../src/libs/system/system.schema.d.ts | 21 +
.../utils/config/config.declarator.spec.ts | 38 ++
.../src/utils/config/config.declarator.ts | 10 +
.../src/utils/config/config.finder.spec.ts | 72 +++
.../src/utils/config/config.finder.ts | 33 +
.../src/utils/config/config.type.ts | 31 +
libs/schematics/src/utils/formatting.spec.ts | 101 +++
libs/schematics/src/utils/formatting.ts | 30 +
libs/schematics/src/utils/main/const.ts | 10 +
libs/schematics/src/utils/main/enums.ts | 8 +
.../src/utils/main/main-functions.spec.ts | 135 ++++
.../src/utils/main/main-functions.ts | 61 ++
libs/schematics/src/utils/main/main-utils.ts | 7 +
.../src/utils/main/main.generator.spec.ts | 222 +++++++
.../src/utils/main/main.generator.ts | 224 +++++++
libs/schematics/src/utils/main/save.type.ts | 38 ++
libs/schematics/src/utils/name.spec.ts | 23 +
libs/schematics/src/utils/name.ts | 12 +
libs/schematics/src/utils/object.spec.ts | 66 ++
libs/schematics/src/utils/object.ts | 21 +
libs/schematics/src/utils/path.ts | 6 +
libs/schematics/src/utils/types.ts | 15 +
libs/schematics/tsconfig.json | 56 ++
libs/schematics/tsdown.config.ts | 44 ++
libs/schematics/vitest.config.e2e.ts | 20 +
libs/schematics/vitest.config.ts | 27 +
package.json | 19 +-
pnpm-lock.yaml | 590 +++++++++++++-----
pnpm-workspace.yaml | 39 +-
tsdown.config.ts | 6 +-
turbo.json | 28 +-
140 files changed, 7061 insertions(+), 178 deletions(-)
create mode 100644 docs/docs/schematics/1-overview.mdx
create mode 100644 docs/docs/schematics/2-schematics/1-application.mdx
create mode 100644 docs/docs/schematics/2-schematics/2-configuration.mdx
create mode 100644 docs/docs/schematics/2-schematics/3-part-base.mdx
create mode 100644 docs/docs/schematics/2-schematics/4-part-main.mdx
create mode 100644 docs/docs/schematics/2-schematics/5-docker.mdx
create mode 100644 docs/docs/schematics/2-schematics/6-component.mdx
create mode 100644 docs/docs/schematics/2-schematics/7-system.mdx
create mode 100644 libs/config/vitest.config.ts
create mode 100644 libs/schematics/.cliff-jumperrc.json
create mode 100644 libs/schematics/.gitignore
create mode 100644 libs/schematics/.prettierignore
create mode 100644 libs/schematics/CHANGELOG.md
create mode 100644 libs/schematics/LICENSE
create mode 100644 libs/schematics/README.md
create mode 100644 libs/schematics/cliff.toml
create mode 100644 libs/schematics/commitlint.config.cjs
create mode 100644 libs/schematics/e2e/application.test.ts
create mode 100644 libs/schematics/e2e/component.test.ts
create mode 100644 libs/schematics/e2e/configuration.test.ts
create mode 100644 libs/schematics/e2e/docker.test.ts
create mode 100644 libs/schematics/e2e/part-base.test.ts
create mode 100644 libs/schematics/e2e/part-main.test.ts
create mode 100644 libs/schematics/e2e/system.test.ts
create mode 100644 libs/schematics/e2e/tsconfig.json
create mode 100644 libs/schematics/eslint.config.js
create mode 100644 libs/schematics/package.json
create mode 100644 libs/schematics/prettier.config.js
create mode 100644 libs/schematics/src/collection.json
create mode 100644 libs/schematics/src/defaults.ts
create mode 100644 libs/schematics/src/index.ts
create mode 100644 libs/schematics/src/libs/application/application.factory.ts
create mode 100644 libs/schematics/src/libs/application/application.options.d.ts
create mode 100644 libs/schematics/src/libs/application/application.schema.d.ts
create mode 100644 libs/schematics/src/libs/application/files/js/.prettierignore
create mode 100644 libs/schematics/src/libs/application/files/js/README.md
create mode 100644 libs/schematics/src/libs/application/files/js/__dot__env
create mode 100644 libs/schematics/src/libs/application/files/js/__dot__gitignore
create mode 100644 libs/schematics/src/libs/application/files/js/eslint.config.js
create mode 100644 libs/schematics/src/libs/application/files/js/jsconfig.json
create mode 100644 libs/schematics/src/libs/application/files/js/package.json.template
create mode 100644 libs/schematics/src/libs/application/files/js/pnpm-workspace.yaml.template
create mode 100644 libs/schematics/src/libs/application/files/js/prettier.config.js
create mode 100644 libs/schematics/src/libs/application/files/ts/.prettierignore
create mode 100644 libs/schematics/src/libs/application/files/ts/README.md
create mode 100644 libs/schematics/src/libs/application/files/ts/__dot__env
create mode 100644 libs/schematics/src/libs/application/files/ts/__dot__gitignore
create mode 100644 libs/schematics/src/libs/application/files/ts/eslint.config.js
create mode 100644 libs/schematics/src/libs/application/files/ts/package.json.template
create mode 100644 libs/schematics/src/libs/application/files/ts/pnpm-workspace.yaml.template
create mode 100644 libs/schematics/src/libs/application/files/ts/prettier.config.js
create mode 100644 libs/schematics/src/libs/application/files/ts/tsconfig.json
create mode 100644 libs/schematics/src/libs/application/schema.json
create mode 100644 libs/schematics/src/libs/component/component.factory.ts
create mode 100644 libs/schematics/src/libs/component/component.options.d.ts
create mode 100644 libs/schematics/src/libs/component/component.schema.d.ts
create mode 100644 libs/schematics/src/libs/component/files/js/__fileName__.component.js
create mode 100644 libs/schematics/src/libs/component/files/ts/__fileName__.component.ts
create mode 100644 libs/schematics/src/libs/component/schema.json
create mode 100644 libs/schematics/src/libs/configuration/configuration.factory.ts
create mode 100644 libs/schematics/src/libs/configuration/configuration.options.d.ts
create mode 100644 libs/schematics/src/libs/configuration/configuration.schema.d.ts
create mode 100644 libs/schematics/src/libs/configuration/files/nanoforge.config.json
create mode 100644 libs/schematics/src/libs/configuration/schema.json
create mode 100644 libs/schematics/src/libs/docker/docker.factory.ts
create mode 100644 libs/schematics/src/libs/docker/docker.options.d.ts
create mode 100644 libs/schematics/src/libs/docker/docker.schema.d.ts
create mode 100644 libs/schematics/src/libs/docker/files/.dockerignore
create mode 100644 libs/schematics/src/libs/docker/files/Dockerfile
create mode 100644 libs/schematics/src/libs/docker/schema.json
create mode 100644 libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/client.save.json
create mode 100644 libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/server.save.json
create mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/components/example.component.js
create mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/after-init.js
create mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/after-registry-init.js
create mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/after-run.js
create mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/before-init.js
create mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/before-registry-init.js
create mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/before-run.js
create mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/systems/example.system.js
create mode 100644 libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/client.save.json
create mode 100644 libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/server.save.json
create mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/components/example.component.ts
create mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/after-init.ts
create mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/after-registry-init.ts
create mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/after-run.ts
create mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/before-init.ts
create mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/before-registry-init.ts
create mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/before-run.ts
create mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/systems/example.system.ts
create mode 100644 libs/schematics/src/libs/part-base/part-base.factory.ts
create mode 100644 libs/schematics/src/libs/part-base/part-base.options.d.ts
create mode 100644 libs/schematics/src/libs/part-base/part-base.schema.d.ts
create mode 100644 libs/schematics/src/libs/part-base/schema.json
create mode 100644 libs/schematics/src/libs/part-main/part-main.factory.ts
create mode 100644 libs/schematics/src/libs/part-main/part-main.options.d.ts
create mode 100644 libs/schematics/src/libs/part-main/part-main.schema.d.ts
create mode 100644 libs/schematics/src/libs/part-main/schema.json
create mode 100644 libs/schematics/src/libs/system/files/js/__fileName__.system.js
create mode 100644 libs/schematics/src/libs/system/files/ts/__fileName__.system.ts
create mode 100644 libs/schematics/src/libs/system/schema.json
create mode 100644 libs/schematics/src/libs/system/system.factory.ts
create mode 100644 libs/schematics/src/libs/system/system.options.d.ts
create mode 100644 libs/schematics/src/libs/system/system.schema.d.ts
create mode 100644 libs/schematics/src/utils/config/config.declarator.spec.ts
create mode 100644 libs/schematics/src/utils/config/config.declarator.ts
create mode 100644 libs/schematics/src/utils/config/config.finder.spec.ts
create mode 100644 libs/schematics/src/utils/config/config.finder.ts
create mode 100644 libs/schematics/src/utils/config/config.type.ts
create mode 100644 libs/schematics/src/utils/formatting.spec.ts
create mode 100644 libs/schematics/src/utils/formatting.ts
create mode 100644 libs/schematics/src/utils/main/const.ts
create mode 100644 libs/schematics/src/utils/main/enums.ts
create mode 100644 libs/schematics/src/utils/main/main-functions.spec.ts
create mode 100644 libs/schematics/src/utils/main/main-functions.ts
create mode 100644 libs/schematics/src/utils/main/main-utils.ts
create mode 100644 libs/schematics/src/utils/main/main.generator.spec.ts
create mode 100644 libs/schematics/src/utils/main/main.generator.ts
create mode 100644 libs/schematics/src/utils/main/save.type.ts
create mode 100644 libs/schematics/src/utils/name.spec.ts
create mode 100644 libs/schematics/src/utils/name.ts
create mode 100644 libs/schematics/src/utils/object.spec.ts
create mode 100644 libs/schematics/src/utils/object.ts
create mode 100644 libs/schematics/src/utils/path.ts
create mode 100644 libs/schematics/src/utils/types.ts
create mode 100644 libs/schematics/tsconfig.json
create mode 100644 libs/schematics/tsdown.config.ts
create mode 100644 libs/schematics/vitest.config.e2e.ts
create mode 100644 libs/schematics/vitest.config.ts
diff --git a/.husky/pre-push b/.husky/pre-push
index 842469b..79c9dc2 100644
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -1,3 +1,3 @@
-pnpm run build
-pnpm run lint
-pnpm run test
+pnpm run repo:build
+pnpm run repo:lint
+pnpm run repo:test
diff --git a/README.md b/README.md
index 8bf0143..231c65d 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
-
+
diff --git a/docs/docs/schematics/1-overview.mdx b/docs/docs/schematics/1-overview.mdx
new file mode 100644
index 0000000..a031fec
--- /dev/null
+++ b/docs/docs/schematics/1-overview.mdx
@@ -0,0 +1,62 @@
+---
+title: Schematics
+description: Overview of the @nanoforge-dev/schematics package
+---
+
+import { Card, CardGroup } from "mintlify/components";
+
+## What are schematics?
+
+NanoForge schematics are code generators built on [Angular DevKit](https://github.com/angular/angular-cli/tree/main/packages/angular_devkit/schematics). They produce the boilerplate files that every NanoForge project needs — from the initial application scaffold down to individual ECS components and systems.
+
+The NanoForge CLI (`nf`) calls these schematics internally whenever you run commands like `nf new` or `nf generate`. You can also invoke them directly via the `schematics` CLI for advanced use cases.
+
+## Installation
+
+```bash
+npm install -g @nanoforge-dev/cli
+```
+
+The schematics package is a dependency of the CLI and does not need to be installed separately. If you want to run schematics without the CLI:
+
+```bash
+npm install -g @angular-devkit/schematics-cli
+npm install @nanoforge-dev/schematics
+```
+
+Then invoke directly:
+
+```bash
+schematics @nanoforge-dev/schematics: [options]
+```
+
+## Available schematics
+
+
+
+ Scaffold a complete NanoForge project with `package.json`, TypeScript or JavaScript config,
+ linting, and optional server setup.
+
+
+ Generate a `nanoforge.config.json` configuration file consumed by the NanoForge CLI.
+
+
+ Create the base folder structure for a client or server part, including example component,
+ system, save file, and optional lifecycle hooks.
+
+
+ Generate the `main.ts` entry point for a client or server part by reading the `.nanoforge` save
+ file.
+
+
+ Generate a production-ready `Dockerfile` and `.dockerignore` tailored to your package manager.
+
+
+ Scaffold an ECS component class with an editor manifest for use in the NanoForge editor.
+
+
+ Scaffold an ECS system function with an editor manifest for use in the NanoForge editor.
+
+
+
+see the [changelog](https://github.com/NanoForge-dev/schematics/blob/main/CHANGELOG.md) for the full release history.
diff --git a/docs/docs/schematics/2-schematics/1-application.mdx b/docs/docs/schematics/2-schematics/1-application.mdx
new file mode 100644
index 0000000..9554bc3
--- /dev/null
+++ b/docs/docs/schematics/2-schematics/1-application.mdx
@@ -0,0 +1,80 @@
+---
+title: Application
+description: Generate a complete NanoForge application scaffold
+---
+
+import { ToolTip } from "mintlify/components";
+
+## Overview
+
+The `application` schematic creates a fully structured NanoForge project with all the base files needed to start development — `package.json`, linting configuration, TypeScript or JavaScript setup, and optionally `.env` for server-enabled apps.
+
+## Usage
+
+```bash
+nf new
+```
+
+Or via Angular DevKit schematics directly:
+
+```bash
+schematics @nanoforge-dev/schematics:application --name=my-app
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| ---------------- | ------------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `name` | `string` | `nanoforge-app` | The name of the application |
+| `version` | `string` | `0.0.0` | Initial version in `package.json` |
+| `author` | `string` | `""` | Author field in `package.json` |
+| `description` | `string` | `""` | Description field in `package.json` |
+| `directory` | `string` | `name` | Destination directory for the generated files |
+| `language` | `"ts" \| "js"` | `ts` | Language to use for generated source files |
+| `strict` | `boolean` | `true` | Enable strict mode |
+| `lint` | `boolean` | `true` | Generate linting configuration files |
+| `packageManager` | `"npm" \| "yarn" \| "pnpm" \| "bun"` | `npm` | Package manager to target in scripts and lockfile references |
+| `server` | `boolean` | `false` | Configure a server for the application |
+| `editor` | `boolean` | `false` | Include editor dependencies |
+| `initFunctions` | `boolean` | `false` | Add init function lifecycle hooks |
+
+## Generated structure
+
+```
+my-app/
+├── package.json
+├── tsconfig.json # TypeScript only
+├── jsconfig.json # JavaScript only
+├── eslint.config.js # if lint: true
+├── prettier.config.js # if lint: true
+├── .prettierignore # if lint: true
+├── .env # if server: true
+├── .gitignore
+└── README.md
+```
+
+## Examples
+
+**Minimal TypeScript app:**
+
+```bash
+schematics @nanoforge-dev/schematics:application --name=my-game
+```
+
+**JavaScript app with pnpm and server:**
+
+```bash
+schematics @nanoforge-dev/schematics:application \
+ --name=my-game \
+ --language=js \
+ --packageManager=pnpm \
+ --server=true
+```
+
+**TypeScript app without linting:**
+
+```bash
+schematics @nanoforge-dev/schematics:application \
+ --name=my-game \
+ --lint=false
+```
diff --git a/docs/docs/schematics/2-schematics/2-configuration.mdx b/docs/docs/schematics/2-schematics/2-configuration.mdx
new file mode 100644
index 0000000..8514f09
--- /dev/null
+++ b/docs/docs/schematics/2-schematics/2-configuration.mdx
@@ -0,0 +1,50 @@
+---
+title: Configuration
+description: Generate a NanoForge configuration file
+---
+
+import { Tooltip } from "mintlify/components";
+
+## Overview
+
+The `configuration` schematic creates a `nanoforge.config.json` file at the root of your project. This file is the central configuration consumed by the NanoForge CLI for builds, starts, and other operations.
+
+## Usage
+
+```bash
+schematics @nanoforge-dev/schematics:configuration
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| --------------- | -------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `name` | `string` | `nanoforge-app` | The name of the application |
+| `directory` | `string` | `.` | Destination directory |
+| `server` | `boolean` | `false` | Include server configuration |
+| `language` | `"ts" \| "js"` | `ts` | Language used in the project |
+| `initFunctions` | `boolean` | `false` | Reference init functions in configuration |
+
+## Generated file
+
+```
+nanoforge.config.json
+```
+
+The generated file is an empty JSON object `{}` that you populate with the fields the NanoForge CLI expects. Refer to the [CLI documentation](https://github.com/NanoForge-dev/CLI) for the full list of supported fields.
+
+## Examples
+
+**Generate configuration at the project root:**
+
+```bash
+schematics @nanoforge-dev/schematics:configuration --name=my-game
+```
+
+**Generate configuration for a server-enabled project:**
+
+```bash
+schematics @nanoforge-dev/schematics:configuration \
+ --name=my-game \
+ --server=true
+```
diff --git a/docs/docs/schematics/2-schematics/3-part-base.mdx b/docs/docs/schematics/2-schematics/3-part-base.mdx
new file mode 100644
index 0000000..eec16aa
--- /dev/null
+++ b/docs/docs/schematics/2-schematics/3-part-base.mdx
@@ -0,0 +1,115 @@
+---
+title: Part Base
+description: Generate the base structure for a NanoForge client or server part
+---
+
+import { Tooltip } from "mintlify/components";
+
+## Overview
+
+The `part-base` schematic generates the foundational folder structure for a NanoForge **client** or **server** part. It creates example components and systems, the `.nanoforge` save file used to track ECS state, and optionally lifecycle init functions.
+
+## Usage
+
+```bash
+schematics @nanoforge-dev/schematics:part-base --part=client
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| --------------- | ---------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `part` | `"client" \| "server"` | _(required)_ | The part of the application to generate |
+| `directory` | `string` | `.` | Destination directory for the generated files |
+| `language` | `"ts" \| "js"` | `ts` | Language to use for generated source files |
+| `server` | `boolean` | `false` | Whether the application has a server |
+| `initFunctions` | `boolean` | `false` | Add lifecycle init function hooks |
+
+## Generated structure
+
+```
+{directory}/
+├── {part}/
+│ ├── components/
+│ │ └── example.component.ts
+│ ├── systems/
+│ │ └── example.system.ts
+│ └── init/ # if initFunctions: true
+│ ├── before-init.ts
+│ ├── after-init.ts
+│ ├── before-registry-init.ts
+│ ├── after-registry-init.ts
+│ ├── before-run.ts
+│ └── after-run.ts
+└── .nanoforge/
+ └── {part}.save.json
+```
+
+## Save file
+
+The `.nanoforge/{part}.save.json` file is the source of truth for ECS state. It lists the libraries, components, systems, and entities registered for this part. The `part-main` schematic reads this file to generate the `main.ts` entry point.
+
+```json
+{
+ "libraries": [ ... ],
+ "components": [
+ {
+ "name": "ExampleComponent",
+ "path": "./components/example.component.ts",
+ "paramsNames": ["paramA", "paramB", "paramC"]
+ }
+ ],
+ "systems": [
+ {
+ "name": "exampleSystem",
+ "path": "./systems/example.system.ts"
+ }
+ ],
+ "entities": [
+ {
+ "id": "exampleEntity",
+ "components": {
+ "ExampleComponent": { "paramA": "example", "paramB": 10 }
+ }
+ }
+ ]
+}
+```
+
+## Init functions
+
+When `initFunctions` is `true`, the following lifecycle hooks are scaffolded:
+
+| File | Hook | Called |
+| ------------------------- | -------------------- | ----------------------------------- |
+| `before-init.ts` | `beforeInit` | Before the app initializes |
+| `after-init.ts` | `afterInit` | After the app initializes |
+| `before-registry-init.ts` | `beforeRegistryInit` | Before the ECS registry initializes |
+| `after-registry-init.ts` | `afterRegistryInit` | After the ECS registry initializes |
+| `before-run.ts` | `beforeRun` | Before the game loop starts |
+| `after-run.ts` | `afterRun` | After the game loop ends |
+
+## Examples
+
+**Generate a TypeScript client part:**
+
+```bash
+schematics @nanoforge-dev/schematics:part-base --part=client
+```
+
+**Generate a server part with init hooks:**
+
+```bash
+schematics @nanoforge-dev/schematics:part-base \
+ --part=server \
+ --initFunctions=true
+```
+
+**Generate a JavaScript client in a specific directory:**
+
+```bash
+schematics @nanoforge-dev/schematics:part-base \
+ --part=client \
+ --language=js \
+ --directory=./my-game
+```
diff --git a/docs/docs/schematics/2-schematics/4-part-main.mdx b/docs/docs/schematics/2-schematics/4-part-main.mdx
new file mode 100644
index 0000000..60a88dc
--- /dev/null
+++ b/docs/docs/schematics/2-schematics/4-part-main.mdx
@@ -0,0 +1,77 @@
+---
+title: Part Main
+description: Generate the main entry point for a NanoForge client or server part
+---
+
+import { Tooltip } from "mintlify/components";
+
+## Overview
+
+The `part-main` schematic reads the `.nanoforge/{part}.save.json` file produced by `part-base` and generates a fully wired `main.ts` (or `main.js`) entry point. It registers all libraries, components, systems, and entities declared in the save file.
+
+
+ This schematic must be run after `part-base`.
+
+
+## Usage
+
+```bash
+schematics @nanoforge-dev/schematics:part-main --part=client
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| --------------- | ---------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `part` | `"client" \| "server"` | _(required)_ | The part of the application to generate |
+| `directory` | `string` | `.` | Project root directory |
+| `language` | `"ts" \| "js"` | `ts` | Language of the generated entry point |
+| `initFunctions` | `boolean` | `false` | Wire init lifecycle functions into the main file |
+| `saveFile` | `string` | — | Custom path to the save JSON file |
+| `outFile` | `string` | — | Custom output path for the generated main file |
+| `editor` | `boolean` | `false` | Generate the editor variant of the main file |
+
+## Output path resolution
+
+The generated file is placed at one of the following paths, in priority order:
+
+1. `outFile` — if provided explicitly
+2. `.nanoforge/editor/{part}/main.{language}` — if `editor: true`
+3. `{part}/main.{language}` — default
+
+## How it works
+
+`part-main` reads `{directory}/.nanoforge/{part}.save.json`, builds the full application bootstrap code, then writes the result as the entry point. Every time you add a component, system, or entity through the NanoForge CLI or editor, regenerating `part-main` keeps the entry point in sync with the save file.
+
+## Examples
+
+**Generate a client main file:**
+
+```bash
+schematics @nanoforge-dev/schematics:part-main --part=client
+```
+
+**Generate a server main file with init hooks:**
+
+```bash
+schematics @nanoforge-dev/schematics:part-main \
+ --part=server \
+ --initFunctions=true
+```
+
+**Generate with a custom save file and output path:**
+
+```bash
+schematics @nanoforge-dev/schematics:part-main \
+ --part=client \
+ --saveFile=./custom.save.json \
+ --outFile=./src/client/main.ts
+```
+
+**Regenerate the editor entry point:**
+
+```bash
+schematics @nanoforge-dev/schematics:part-main \
+ --part=client \
+ --editor=true
+```
diff --git a/docs/docs/schematics/2-schematics/5-docker.mdx b/docs/docs/schematics/2-schematics/5-docker.mdx
new file mode 100644
index 0000000..ce32b7d
--- /dev/null
+++ b/docs/docs/schematics/2-schematics/5-docker.mdx
@@ -0,0 +1,70 @@
+---
+title: Docker
+description: Generate a Dockerfile for a NanoForge application
+---
+
+import { Tooltip } from "mintlify/components";
+
+## Overview
+
+The `docker` schematic generates a production-ready `Dockerfile` and a `.dockerignore` at the specified directory. The Dockerfile is tailored to the package manager you use — selecting the correct base image, lockfile, and install command automatically.
+
+## Usage
+
+```bash
+schematics @nanoforge-dev/schematics:docker
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| ---------------- | ------------------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `directory` | `string` | `.` | Destination directory for the generated files |
+| `packageManager` | `"npm" \| "yarn" \| "pnpm" \| "bun"` | `npm` | Package manager used by the application |
+
+## Generated files
+
+```
+{directory}/
+├── Dockerfile
+└── .dockerignore
+```
+
+## Dockerfile behaviour by package manager
+
+| Package manager | Base image | Install command |
+| --------------- | -------------- | -------------------------------------------------- |
+| `npm` | `node:25-slim` | `npm install --no-lockfile` |
+| `yarn` | `node:25-slim` | `yarn install --frozen-lockfile` |
+| `pnpm` | `node:25-slim` | `pnpm install --frozen-lockfile --allow-build=bun` |
+| `bun` | `oven/bun:1.3` | `bun install --frozen-lockfile` |
+
+The Dockerfile follows a standard multi-step approach:
+
+1. Copy `package.json` and the lockfile.
+2. Install dependencies with frozen lockfile for reproducible builds.
+3. Copy the rest of the source.
+4. Run `build`.
+5. Start with `start`.
+
+## Examples
+
+**Generate a Dockerfile for an npm project:**
+
+```bash
+schematics @nanoforge-dev/schematics:docker
+```
+
+**Generate a Dockerfile for a pnpm project:**
+
+```bash
+schematics @nanoforge-dev/schematics:docker --packageManager=pnpm
+```
+
+**Generate in a specific directory:**
+
+```bash
+schematics @nanoforge-dev/schematics:docker \
+ --packageManager=bun \
+ --directory=./my-game
+```
diff --git a/docs/docs/schematics/2-schematics/6-component.mdx b/docs/docs/schematics/2-schematics/6-component.mdx
new file mode 100644
index 0000000..9b14080
--- /dev/null
+++ b/docs/docs/schematics/2-schematics/6-component.mdx
@@ -0,0 +1,120 @@
+---
+title: Component
+description: Generate a NanoForge ECS component
+---
+
+import Tooltip from "mint/Tooltip";
+
+## Overview
+
+The `component` schematic scaffolds a new ECS component class for a NanoForge client or server part. The generated file includes the component class, a default export required for code generation, and an `EDITOR_COMPONENT_MANIFEST` required for the NanoForge editor to display and configure the component.
+
+## Usage
+
+```bash
+schematics @nanoforge-dev/schematics:component --part=client --name=health
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| ----------- | ---------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `name` | `string` | `example` | Component name |
+| `part` | `"client" \| "server"` | _(required)_ | The part of the application this component belongs to |
+| `directory` | `string` | `.` | Destination directory for the generated file |
+| `language` | `"ts" \| "js"` | `ts` | Language to use for the generated file |
+
+## Generated file
+
+`{directory}/{name}.component.ts`
+
+```ts
+import { type EditorComponentManifest } from "@nanoforge-dev/ecs-client";
+
+export class HealthComponent {
+ name = this.constructor.name;
+
+ constructor(
+ public paramA: string,
+ public paramB: number,
+ public paramC: boolean = false,
+ ) {}
+
+ get foo() {
+ return "bar";
+ }
+
+ get paramAOrDefault() {
+ return this.paramC ? this.paramA : "default";
+ }
+
+ addOne() {
+ this.paramB += 1;
+ }
+}
+
+// Required to generate code
+export default HealthComponent.name;
+
+// Required for the editor to display the component and generate code
+export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = {
+ name: "Health",
+ description: "Health component description",
+ params: [
+ {
+ type: "string",
+ name: "paramA",
+ description: "Param A description",
+ example: "Example value",
+ },
+ {
+ type: "number",
+ name: "paramB",
+ description: "Param B description",
+ example: 3,
+ },
+ {
+ type: "boolean",
+ name: "paramC",
+ description: "Param C description",
+ example: true,
+ default: false,
+ optional: true,
+ },
+ ],
+};
+```
+
+## Editor manifest
+
+The `EDITOR_COMPONENT_MANIFEST` export is required for the NanoForge editor to discover the component, render its configuration panel, and generate code. Each `params` entry describes one constructor parameter:
+
+| Field | Required | Description |
+| ------------- | -------- | ------------------------------------------------------------ |
+| `type` | Yes | Parameter type: `"string"`, `"number"`, or `"boolean"` |
+| `name` | Yes | Constructor parameter name |
+| `description` | Yes | Human-readable description shown in the editor |
+| `example` | Yes | Example value used in code generation |
+| `default` | No | Default value when the parameter is optional |
+| `optional` | No | When `true`, the editor treats the parameter as non-required |
+
+## Examples
+
+**Generate a `PlayerComponent` for the client:**
+
+```bash
+schematics @nanoforge-dev/schematics:component \
+ --part=client \
+ --name=player \
+ --directory=client/components
+```
+
+**Generate a JavaScript server component:**
+
+```bash
+schematics @nanoforge-dev/schematics:component \
+ --part=server \
+ --name=network-player \
+ --language=js \
+ --directory=server/components
+```
diff --git a/docs/docs/schematics/2-schematics/7-system.mdx b/docs/docs/schematics/2-schematics/7-system.mdx
new file mode 100644
index 0000000..51ecf8d
--- /dev/null
+++ b/docs/docs/schematics/2-schematics/7-system.mdx
@@ -0,0 +1,105 @@
+---
+title: System
+description: Generate a NanoForge ECS system
+---
+
+import { Tooltip } from "mintlify/components";
+
+## Overview
+
+The `system` schematic scaffolds a new ECS system function for a NanoForge client or server part. The generated file includes the system function, a default export required for code generation, and an `EDITOR_SYSTEM_MANIFEST` required for the NanoForge editor to display and configure the system.
+
+## Usage
+
+```bash
+schematics @nanoforge-dev/schematics:system --part=client --name=movement
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| ----------- | ---------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `name` | `string` | `example` | System name |
+| `part` | `"client" \| "server"` | _(required)_ | The part of the application this system belongs to |
+| `directory` | `string` | `.` | Destination directory for the generated file |
+| `language` | `"ts" \| "js"` | `ts` | Language to use for the generated file |
+
+## Generated file
+
+`{directory}/{name}.system.ts`
+
+```ts
+import { type Context } from "@nanoforge-dev/common";
+import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs-client";
+
+import { ExampleComponent } from "../components/example.component";
+
+export const movementSystem = (registry: Registry, ctx: Context) => {
+ const entities = registry.getZipper([ExampleComponent]);
+
+ entities.forEach((entity) => {
+ if (entity.ExampleComponent.paramA === "end") {
+ ctx.app.setIsRunning(false);
+ return;
+ }
+
+ if (entity.ExampleComponent.paramB === 0) entity.ExampleComponent.paramA = "end";
+
+ if (entity.ExampleComponent.paramB >= 0)
+ entity.ExampleComponent.paramB = entity.ExampleComponent.paramB - 1;
+ });
+};
+
+// Required to generate code
+export default movementSystem.name;
+
+// Required for the editor to display the system and generate code
+export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = {
+ name: "movement",
+ description:
+ "This system end the game when paramB reaches 0 for any entity with ExampleComponent",
+ dependencies: ["ExampleComponent"],
+};
+```
+
+## Editor manifest
+
+The `EDITOR_SYSTEM_MANIFEST` export is required for the NanoForge editor to discover the system and wire it into the game loop.
+
+| Field | Required | Description |
+| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `name` | Yes | System name as displayed in the editor |
+| `description` | Yes | Human-readable description of what the system does |
+| `dependencies` | Yes | Component names this system depends on |
+
+## System signature
+
+Every NanoForge system follows the same function signature:
+
+```ts
+(registry: Registry, ctx: Context) => void
+```
+
+- **`registry`** — the ECS registry; use `registry.getZipper([...])` to query entities with specific components.
+- **`ctx`** — the application context; provides access to `ctx.app`, `ctx.assets`, and other engine services.
+
+## Examples
+
+**Generate a `PhysicsSystem` for the client:**
+
+```bash
+schematics @nanoforge-dev/schematics:system \
+ --part=client \
+ --name=physics \
+ --directory=client/systems
+```
+
+**Generate a JavaScript server system:**
+
+```bash
+schematics @nanoforge-dev/schematics:system \
+ --part=server \
+ --name=sync-state \
+ --language=js \
+ --directory=server/systems
+```
diff --git a/e2e/cli-install.test.ts b/e2e/cli-install.test.ts
index 3106204..4d4149a 100644
--- a/e2e/cli-install.test.ts
+++ b/e2e/cli-install.test.ts
@@ -125,7 +125,7 @@ describe("nf install (without library name)", () => {
"--strict",
"--no-server",
"--no-init-functions",
- "--no-skip-install",
+ "--skip-install",
"--no-docker",
"--no-git",
"-d",
diff --git a/e2e/cli-new.test.ts b/e2e/cli-new.test.ts
index 931e601..143ac7a 100644
--- a/e2e/cli-new.test.ts
+++ b/e2e/cli-new.test.ts
@@ -104,7 +104,7 @@ describe("nf new (JavaScript, with server)", () => {
"--no-strict",
"--server",
"--init-functions",
- "--no-skip-install",
+ "--skip-install",
"--no-docker",
"--no-git",
"-d",
@@ -210,7 +210,7 @@ describe("nf new (with typescript with docker option)", () => {
"--strict",
"--no-server",
"--no-init-functions",
- "--no-skip-install",
+ "--skip-install",
"--docker",
"--no-git",
"-d",
diff --git a/libs/config/README.md b/libs/config/README.md
index fb92022..c38cd9a 100644
--- a/libs/config/README.md
+++ b/libs/config/README.md
@@ -8,6 +8,7 @@
+
diff --git a/libs/config/package.json b/libs/config/package.json
index 2965183..fcb2007 100644
--- a/libs/config/package.json
+++ b/libs/config/package.json
@@ -51,7 +51,7 @@
"build": "tsc --noEmit && tsdown --config-loader unrun",
"lint": "prettier --check . && eslint --format=pretty src",
"format": "prettier --write . && eslint --fix --format=pretty src",
- "test:unit": "vitest run --config ../../vitest.config.ts",
+ "test:unit": "vitest run --config vitest.config.ts",
"prepack": "pnpm run build && pnpm run lint",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'libs/config/*'",
"release": "cliff-jumper",
diff --git a/libs/config/vitest.config.ts b/libs/config/vitest.config.ts
new file mode 100644
index 0000000..3ff3a4a
--- /dev/null
+++ b/libs/config/vitest.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ include: ["test/**/*.spec.ts"],
+ passWithNoTests: true,
+ coverage: {
+ provider: "v8",
+ include: ["src/**/*.ts"],
+ exclude: [
+ "**/*.{interface,type,d}.ts",
+ "**/{interfaces,types}/*.ts",
+ "**/{interfaces,types}.ts",
+ "**/index.{js,ts}",
+ ],
+ },
+ },
+});
diff --git a/libs/schematics/.cliff-jumperrc.json b/libs/schematics/.cliff-jumperrc.json
new file mode 100644
index 0000000..2caa355
--- /dev/null
+++ b/libs/schematics/.cliff-jumperrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
+ "name": "schematics",
+ "org": "nanoforge-dev",
+ "packagePath": ".",
+ "tagTemplate": "{{new-version}}",
+ "identifierBase": false
+}
diff --git a/libs/schematics/.gitignore b/libs/schematics/.gitignore
new file mode 100644
index 0000000..bca0f13
--- /dev/null
+++ b/libs/schematics/.gitignore
@@ -0,0 +1,226 @@
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+.direnv
diff --git a/libs/schematics/.prettierignore b/libs/schematics/.prettierignore
new file mode 100644
index 0000000..91942fb
--- /dev/null
+++ b/libs/schematics/.prettierignore
@@ -0,0 +1,14 @@
+.idea/
+.vscode/
+
+dist/
+node_modules/
+
+pnpm-lock.yaml
+pnpm-workspace.yaml
+
+CHANGELOG.md
+
+coverage/
+
+/src/libs/*/files/**
diff --git a/libs/schematics/CHANGELOG.md b/libs/schematics/CHANGELOG.md
new file mode 100644
index 0000000..911dfd8
--- /dev/null
+++ b/libs/schematics/CHANGELOG.md
@@ -0,0 +1,245 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+# [2.2.0](https://github.com/NanoForge-dev/schematics/compare/2.1.4...2.2.0) - (2026-06-30)
+
+## Features
+
+- Add pnpm workspace in app schematic (#155) ([5e0924d](https://github.com/NanoForge-dev/schematics/commit/5e0924d217c6ca62e322627f4adee79470c7bf2f)) by @Exeloo
+- Add assets to entity generation (#153) ([a008fc6](https://github.com/NanoForge-dev/schematics/commit/a008fc6784cc4d15f3f09fc41efd9a0ea658c3a5)) by @Exeloo
+- Graphics 2d editor generation (#154) ([89059f7](https://github.com/NanoForge-dev/schematics/commit/89059f7fbb5a505f3669bc691acbf377abd51fa6)) by @Exeloo
+
+# [2.1.4](https://github.com/NanoForge-dev/schematics/compare/1.0.2...2.1.4) - (2026-06-29)
+
+## Bug Fixes
+
+- Issues (#139) ([131e794](https://github.com/NanoForge-dev/schematics/commit/131e794601b9fdcb583ac52eab9ca44a1cf8531f)) by @Exeloo
+- **synchronize-docs:** Use actions key to have the rights (#127) ([f73464d](https://github.com/NanoForge-dev/schematics/commit/f73464d09f08f14f2c82960fe9cef2dfde7c00dc)) by @MartinFillon
+- Give package json path (#124) ([a31e265](https://github.com/NanoForge-dev/schematics/commit/a31e26522078c9b60a63d3d91f1eccda96dadac7)) by @MartinFillon
+- **template:** New format (#113) ([1e89de7](https://github.com/NanoForge-dev/schematics/commit/1e89de7a536f75783c260cf1a0f0b05f99ffcf97)) by @Tchips46
+- Remove name formatting on app (#110) ([9478843](https://github.com/NanoForge-dev/schematics/commit/9478843b45a97714070babc6a7d41b8a9a6f2f65)) by @Exeloo
+- Change save format in part base schematic (#105) ([6eaffe8](https://github.com/NanoForge-dev/schematics/commit/6eaffe8d94ce99727a8c27e1f7d5e3fa39a787ca)) by @Exeloo
+- **docker:** Change base image as alpine doesnt have the right libs (#102) ([3b8c2b2](https://github.com/NanoForge-dev/schematics/commit/3b8c2b2476210b3df6a08415bb5f18b7cbdaf970)) by @MartinFillon
+- Remove ts types in js example system in part base (#93) ([89555b4](https://github.com/NanoForge-dev/schematics/commit/89555b46ffa8f3a4e2f157ecd9fabb2202de1cd7)) by @Exeloo
+- Fields does not replace on application (#86) ([d252953](https://github.com/NanoForge-dev/schematics/commit/d252953d4aef98a3ea505bb7bd45701f139d5599)) by @Exeloo
+- Use of directory variable and js issues (#84) ([9d98d1a](https://github.com/NanoForge-dev/schematics/commit/9d98d1ae977e69cfc07b4e9131bde2ea2b34964f)) by @Exeloo
+
+## Documentation
+
+- Fix docs path (#140) ([fead8fd](https://github.com/NanoForge-dev/schematics/commit/fead8fd1e12ff872a5056f791dcaa7013bd135db)) by @Exeloo
+- Add schematics docs (#61) ([7735324](https://github.com/NanoForge-dev/schematics/commit/7735324bcb5438929a24481e157677cc25f7457c)) by @Exeloo
+- Fix contribution (#47) ([b6ad53e](https://github.com/NanoForge-dev/schematics/commit/b6ad53e9f77b6f6619b5090f08e9d3fb94e90a74)) by @Exeloo
+- Add contributing docs and actions (#41) ([91eea94](https://github.com/NanoForge-dev/schematics/commit/91eea949ea785a03ca66d3a6e07d30d8e24df815)) by @Exeloo
+
+## Features
+
+- Add new release workflow (#137) ([af6c1e0](https://github.com/NanoForge-dev/schematics/commit/af6c1e0a9d49cb0cf46bbe6edde02ccfde04bc89)) by @Exeloo
+- Use new docs actions to synchronize config (#123) ([fa3f4ec](https://github.com/NanoForge-dev/schematics/commit/fa3f4ece55b281bb37771b2f1fe5580ff5f707fc)) by @MartinFillon
+- Add dom to application schema tsconfig (#111) ([368fa7c](https://github.com/NanoForge-dev/schematics/commit/368fa7cdd94027fa9b8cc23516654bd6bb3a0beb)) by @Exeloo
+- Add component and system schematics (#103) ([38c90df](https://github.com/NanoForge-dev/schematics/commit/38c90df9bbe971da83aaa0338f39f875695083cf)) by @Exeloo
+- **editor:** Add component holding entity id (#100) ([433c648](https://github.com/NanoForge-dev/schematics/commit/433c64851faafaf4198ac56801d09b47cce150c4)) by @Tchips46
+- **save:** Allow partial saves component values as record string any (#98) ([9280349](https://github.com/NanoForge-dev/schematics/commit/9280349d9028d99028ab529e3c477cfd11d14606)) by @Tchips46
+- Set save params types to any (#96) ([7a29579](https://github.com/NanoForge-dev/schematics/commit/7a29579e03fc1c4ca2ebc7794b4b3dd60167f741)) by @Exeloo
+- Add core editor to dependencies (#90) ([29cfa08](https://github.com/NanoForge-dev/schematics/commit/29cfa0864ee4a84fdc5aed5e790da454334dd2d5)) by @Exeloo
+- Add better handling of path option (#89) ([1e0c469](https://github.com/NanoForge-dev/schematics/commit/1e0c469f98aac91d81e21d20e36a7d9e7a94df8b)) by @Exeloo
+ - **BREAKING CHANGE:** Old schemas doesn't work anymore and config is changed
+- Add editor option to part main schematic (#88) ([c59ed27](https://github.com/NanoForge-dev/schematics/commit/c59ed2710e2db2bcd3e5c553be9c80f5ad54b732)) by @Exeloo
+- Add lint param handling on application (#79) ([96c2128](https://github.com/NanoForge-dev/schematics/commit/96c212817d763fc2677d28cd57f3d196c6a45f6d)) by @Exeloo
+- Setup docker schema (#75) ([cf7ead5](https://github.com/NanoForge-dev/schematics/commit/cf7ead5c49164a85e3beb65be3c73c842cd14e58)) by @MartinFillon
+- Add network and fix versions in app base (#70) ([6d5963a](https://github.com/NanoForge-dev/schematics/commit/6d5963a954e7a27c442377ecb70716a43e479717)) by @Exeloo
+- Update and add pre-release (#48) ([49422f7](https://github.com/NanoForge-dev/schematics/commit/49422f7ad9e0595378cf6e8b44dc7711d09b8cfa)) by @Exeloo
+
+## Testing
+
+- Add unit tests and e2e tests (#67) ([8eef6b5](https://github.com/NanoForge-dev/schematics/commit/8eef6b51984d1187d3f0b936d670e362ad0224c6)) by @Exeloo
+
+### New Contributors
+
+- @MartinFillon made their first contribution in #151
+- @github-actions[bot] made their first contribution in #150
+- @dependabot[bot] made their first contribution in #143
+- @Tchips46 made their first contribution in #113
+
+# [2.1.4](https://github.com/NanoForge-dev/schematics/compare/1.0.2...2.1.4) - (2026-06-29)
+
+## Bug Fixes
+
+- Issues (#139) ([131e794](https://github.com/NanoForge-dev/schematics/commit/131e794601b9fdcb583ac52eab9ca44a1cf8531f)) by @Exeloo
+- **synchronize-docs:** Use actions key to have the rights (#127) ([f73464d](https://github.com/NanoForge-dev/schematics/commit/f73464d09f08f14f2c82960fe9cef2dfde7c00dc)) by @MartinFillon
+- Give package json path (#124) ([a31e265](https://github.com/NanoForge-dev/schematics/commit/a31e26522078c9b60a63d3d91f1eccda96dadac7)) by @MartinFillon
+- **template:** New format (#113) ([1e89de7](https://github.com/NanoForge-dev/schematics/commit/1e89de7a536f75783c260cf1a0f0b05f99ffcf97)) by @Tchips46
+- Remove name formatting on app (#110) ([9478843](https://github.com/NanoForge-dev/schematics/commit/9478843b45a97714070babc6a7d41b8a9a6f2f65)) by @Exeloo
+- Change save format in part base schematic (#105) ([6eaffe8](https://github.com/NanoForge-dev/schematics/commit/6eaffe8d94ce99727a8c27e1f7d5e3fa39a787ca)) by @Exeloo
+- **docker:** Change base image as alpine doesnt have the right libs (#102) ([3b8c2b2](https://github.com/NanoForge-dev/schematics/commit/3b8c2b2476210b3df6a08415bb5f18b7cbdaf970)) by @MartinFillon
+- Remove ts types in js example system in part base (#93) ([89555b4](https://github.com/NanoForge-dev/schematics/commit/89555b46ffa8f3a4e2f157ecd9fabb2202de1cd7)) by @Exeloo
+- Fields does not replace on application (#86) ([d252953](https://github.com/NanoForge-dev/schematics/commit/d252953d4aef98a3ea505bb7bd45701f139d5599)) by @Exeloo
+- Use of directory variable and js issues (#84) ([9d98d1a](https://github.com/NanoForge-dev/schematics/commit/9d98d1ae977e69cfc07b4e9131bde2ea2b34964f)) by @Exeloo
+
+## Documentation
+
+- Fix docs path (#140) ([fead8fd](https://github.com/NanoForge-dev/schematics/commit/fead8fd1e12ff872a5056f791dcaa7013bd135db)) by @Exeloo
+- Add schematics docs (#61) ([7735324](https://github.com/NanoForge-dev/schematics/commit/7735324bcb5438929a24481e157677cc25f7457c)) by @Exeloo
+- Fix contribution (#47) ([b6ad53e](https://github.com/NanoForge-dev/schematics/commit/b6ad53e9f77b6f6619b5090f08e9d3fb94e90a74)) by @Exeloo
+- Add contributing docs and actions (#41) ([91eea94](https://github.com/NanoForge-dev/schematics/commit/91eea949ea785a03ca66d3a6e07d30d8e24df815)) by @Exeloo
+
+## Features
+
+- Add new release workflow (#137) ([af6c1e0](https://github.com/NanoForge-dev/schematics/commit/af6c1e0a9d49cb0cf46bbe6edde02ccfde04bc89)) by @Exeloo
+- Use new docs actions to synchronize config (#123) ([fa3f4ec](https://github.com/NanoForge-dev/schematics/commit/fa3f4ece55b281bb37771b2f1fe5580ff5f707fc)) by @MartinFillon
+- Add dom to application schema tsconfig (#111) ([368fa7c](https://github.com/NanoForge-dev/schematics/commit/368fa7cdd94027fa9b8cc23516654bd6bb3a0beb)) by @Exeloo
+- Add component and system schematics (#103) ([38c90df](https://github.com/NanoForge-dev/schematics/commit/38c90df9bbe971da83aaa0338f39f875695083cf)) by @Exeloo
+- **editor:** Add component holding entity id (#100) ([433c648](https://github.com/NanoForge-dev/schematics/commit/433c64851faafaf4198ac56801d09b47cce150c4)) by @Tchips46
+- **save:** Allow partial saves component values as record string any (#98) ([9280349](https://github.com/NanoForge-dev/schematics/commit/9280349d9028d99028ab529e3c477cfd11d14606)) by @Tchips46
+- Set save params types to any (#96) ([7a29579](https://github.com/NanoForge-dev/schematics/commit/7a29579e03fc1c4ca2ebc7794b4b3dd60167f741)) by @Exeloo
+- Add core editor to dependencies (#90) ([29cfa08](https://github.com/NanoForge-dev/schematics/commit/29cfa0864ee4a84fdc5aed5e790da454334dd2d5)) by @Exeloo
+- Add better handling of path option (#89) ([1e0c469](https://github.com/NanoForge-dev/schematics/commit/1e0c469f98aac91d81e21d20e36a7d9e7a94df8b)) by @Exeloo
+ - **BREAKING CHANGE:** Old schemas doesn't work anymore and config is changed
+- Add editor option to part main schematic (#88) ([c59ed27](https://github.com/NanoForge-dev/schematics/commit/c59ed2710e2db2bcd3e5c553be9c80f5ad54b732)) by @Exeloo
+- Add lint param handling on application (#79) ([96c2128](https://github.com/NanoForge-dev/schematics/commit/96c212817d763fc2677d28cd57f3d196c6a45f6d)) by @Exeloo
+- Setup docker schema (#75) ([cf7ead5](https://github.com/NanoForge-dev/schematics/commit/cf7ead5c49164a85e3beb65be3c73c842cd14e58)) by @MartinFillon
+- Add network and fix versions in app base (#70) ([6d5963a](https://github.com/NanoForge-dev/schematics/commit/6d5963a954e7a27c442377ecb70716a43e479717)) by @Exeloo
+- Update and add pre-release (#48) ([49422f7](https://github.com/NanoForge-dev/schematics/commit/49422f7ad9e0595378cf6e8b44dc7711d09b8cfa)) by @Exeloo
+
+## Testing
+
+- Add unit tests and e2e tests (#67) ([8eef6b5](https://github.com/NanoForge-dev/schematics/commit/8eef6b51984d1187d3f0b936d670e362ad0224c6)) by @Exeloo
+
+### New Contributors
+
+- @MartinFillon made their first contribution in #149
+- @dependabot[bot] made their first contribution in #143
+- @github-actions[bot] made their first contribution in #114
+- @Tchips46 made their first contribution in #113
+
+# [@nanoforge-dev/schematics@2.1.3](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@2.1.2...@nanoforge-dev/schematics@2.1.3) - (2026-04-19)
+
+## Bug Fixes
+
+- **template:** New format (#113) ([1e89de7](https://github.com/NanoForge-dev/schematics/commit/1e89de7a536f75783c260cf1a0f0b05f99ffcf97)) by @Tchips46
+
+# [@nanoforge-dev/schematics@2.1.2](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@2.1.1...@nanoforge-dev/schematics@2.1.2) - (2026-04-17)
+
+## Bug Fixes
+
+- Remove name formatting on app (#110) ([9478843](https://github.com/NanoForge-dev/schematics/commit/9478843b45a97714070babc6a7d41b8a9a6f2f65)) by @Exeloo
+
+## Features
+
+- Add dom to application schema tsconfig (#111) ([368fa7c](https://github.com/NanoForge-dev/schematics/commit/368fa7cdd94027fa9b8cc23516654bd6bb3a0beb)) by @Exeloo
+
+# [@nanoforge-dev/schematics@2.1.1](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@2.1.0...@nanoforge-dev/schematics@2.1.1) - (2026-04-02)
+
+## Bug Fixes
+
+- Change save format in part base schematic (#105) ([6eaffe8](https://github.com/NanoForge-dev/schematics/commit/6eaffe8d94ce99727a8c27e1f7d5e3fa39a787ca)) by @Exeloo
+
+# [@nanoforge-dev/schematics@2.1.0](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@2.0.0...@nanoforge-dev/schematics@2.1.0) - (2026-04-02)
+
+## Bug Fixes
+
+- **docker:** Change base image as alpine doesnt have the right libs (#102) ([3b8c2b2](https://github.com/NanoForge-dev/schematics/commit/3b8c2b2476210b3df6a08415bb5f18b7cbdaf970)) by @MartinFillon
+- Remove ts types in js example system in part base (#93) ([89555b4](https://github.com/NanoForge-dev/schematics/commit/89555b46ffa8f3a4e2f157ecd9fabb2202de1cd7)) by @Exeloo
+
+## Features
+
+- Add component and system schematics (#103) ([38c90df](https://github.com/NanoForge-dev/schematics/commit/38c90df9bbe971da83aaa0338f39f875695083cf)) by @Exeloo
+- **editor:** Add component holding entity id (#100) ([433c648](https://github.com/NanoForge-dev/schematics/commit/433c64851faafaf4198ac56801d09b47cce150c4)) by @Tchips46
+- **save:** Allow partial saves component values as record string any (#98) ([9280349](https://github.com/NanoForge-dev/schematics/commit/9280349d9028d99028ab529e3c477cfd11d14606)) by @Tchips46
+- Set save params types to any (#96) ([7a29579](https://github.com/NanoForge-dev/schematics/commit/7a29579e03fc1c4ca2ebc7794b4b3dd60167f741)) by @Exeloo
+
+### New Contributors
+
+- @Tchips46 made their first contribution in #100
+
+# [@nanoforge-dev/schematics@2.0.0](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@1.2.2...@nanoforge-dev/schematics@2.0.0) - (2026-03-17)
+
+## Features
+
+- Add core editor to dependencies (#90) ([29cfa08](https://github.com/NanoForge-dev/schematics/commit/29cfa0864ee4a84fdc5aed5e790da454334dd2d5)) by @Exeloo
+- Add better handling of path option (#89) ([1e0c469](https://github.com/NanoForge-dev/schematics/commit/1e0c469f98aac91d81e21d20e36a7d9e7a94df8b)) by @Exeloo
+ - **BREAKING CHANGE:** Old schemas doesn't work anymore and config is changed
+- Add editor option to part main schematic (#88) ([c59ed27](https://github.com/NanoForge-dev/schematics/commit/c59ed2710e2db2bcd3e5c553be9c80f5ad54b732)) by @Exeloo
+
+# [@nanoforge-dev/schematics@1.2.2](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@1.2.1...@nanoforge-dev/schematics@1.2.2) - (2026-03-15)
+
+## Bug Fixes
+
+- Fields does not replace on application (#86) ([d252953](https://github.com/NanoForge-dev/schematics/commit/d252953d4aef98a3ea505bb7bd45701f139d5599)) by @Exeloo
+
+# [@nanoforge-dev/schematics@1.2.1](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@1.2.0...@nanoforge-dev/schematics@1.2.1) - (2026-03-15)
+
+## Bug Fixes
+
+- Use of directory variable and js issues (#84) ([9d98d1a](https://github.com/NanoForge-dev/schematics/commit/9d98d1ae977e69cfc07b4e9131bde2ea2b34964f)) by @Exeloo
+
+## Features
+
+- Add lint param handling on application (#79) ([96c2128](https://github.com/NanoForge-dev/schematics/commit/96c212817d763fc2677d28cd57f3d196c6a45f6d)) by @Exeloo
+
+# [@nanoforge-dev/schematics@1.2.0](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@1.1.0...@nanoforge-dev/schematics@1.2.0) - (2026-02-27)
+
+## Features
+
+- Setup docker schema (#75) ([cf7ead5](https://github.com/NanoForge-dev/schematics/commit/cf7ead5c49164a85e3beb65be3c73c842cd14e58)) by @MartinFillon
+
+### New Contributors
+
+- @MartinFillon made their first contribution in #75
+- @dependabot[bot] made their first contribution in #76
+
+# [@nanoforge-dev/schematics@1.1.0](https://github.com/NanoForge-dev/schematics/tree/@nanoforge-dev/schematics@1.1.0) - (2026-02-19)
+
+## Documentation
+
+- Add schematics docs (#61) ([7735324](https://github.com/NanoForge-dev/schematics/commit/7735324bcb5438929a24481e157677cc25f7457c)) by @Exeloo
+- Fix contribution (#47) ([b6ad53e](https://github.com/NanoForge-dev/schematics/commit/b6ad53e9f77b6f6619b5090f08e9d3fb94e90a74)) by @Exeloo
+- Add contributing docs and actions (#41) ([91eea94](https://github.com/NanoForge-dev/schematics/commit/91eea949ea785a03ca66d3a6e07d30d8e24df815)) by @Exeloo
+
+## Features
+
+- Add network and fix versions in app base (#70) ([6d5963a](https://github.com/NanoForge-dev/schematics/commit/6d5963a954e7a27c442377ecb70716a43e479717)) by @Exeloo
+- Update and add pre-release (#48) ([49422f7](https://github.com/NanoForge-dev/schematics/commit/49422f7ad9e0595378cf6e8b44dc7711d09b8cfa)) by @Exeloo
+
+## Testing
+
+- Add unit tests and e2e tests (#67) ([8eef6b5](https://github.com/NanoForge-dev/schematics/commit/8eef6b51984d1187d3f0b936d670e362ad0224c6)) by @Exeloo
+
+# [1.0.2](https://github.com/NanoForge-dev/schematics/compare/1.0.1...1.0.2) - (2025-12-06)
+
+## Bug Fixes
+
+- Split ecs between client and server (#38) ([086d42f](https://github.com/NanoForge-dev/schematics/commit/086d42f1639cc764fb6f21c6ce8d9426ec2581e7)) by @Exeloo
+
+# [1.0.1](https://github.com/NanoForge-dev/schematics/compare/1.0.0...1.0.1) - (2025-12-04)
+
+## Bug Fixes
+
+- **configuration:** Simplify schema and change defaults (#34) ([af29b00](https://github.com/NanoForge-dev/schematics/commit/af29b0011ae078dc75c9625e8f470f8506a7c7cb)) by @Exeloo
+
+# [1.0.0](https://github.com/NanoForge-dev/schematics/compare/0.0.1...1.0.0) - (2025-11-30)
+
+## Bug Fixes
+
+- Change exports in package json and update schema paths (#17) ([b78b72a](https://github.com/NanoForge-dev/schematics/commit/b78b72a40b2287d841a3cac90adb0d37b4493d4e)) by @Exeloo
+
+## Features
+
+- **schematics:** Add part-main schematics for generating client/server main files (#29) ([6f34c1c](https://github.com/NanoForge-dev/schematics/commit/6f34c1c0f70d67eb78645108a665f7c5ebab8086)) by @Exeloo
+- **schematics:** Replace client by base-part schematics to handle server (#20) ([c5ad539](https://github.com/NanoForge-dev/schematics/commit/c5ad539d7ac524de724c49dfa6b0e7b310700a44)) by @Exeloo
+- **schematics:** Add client base schematics (#19) ([f02b26d](https://github.com/NanoForge-dev/schematics/commit/f02b26db6be9226857fbf3fe0ffe1abf6de31024)) by @Exeloo
+
+### New Contributors
+
+- @renovate[bot] made their first contribution in #27
+
+# [0.0.1](https://github.com/NanoForge-dev/schematics/tree/0.0.1) - (2025-11-28)
+
+## Features
+
+- Add base application and configuration schematics (#12) ([d881653](https://github.com/NanoForge-dev/schematics/commit/d881653dd57a886f12e18b9acf0d48d31d3686e6))
+- Init schematics project (#11) ([3fb0e61](https://github.com/NanoForge-dev/schematics/commit/3fb0e61075cf8094309765a0f685d2411c7f8aa8)) by @Exeloo
+
+### New Contributors
+
+- @Exeloo made their first contribution in #11
diff --git a/libs/schematics/LICENSE b/libs/schematics/LICENSE
new file mode 100644
index 0000000..4672486
--- /dev/null
+++ b/libs/schematics/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright © 2026 NanoForge
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/libs/schematics/README.md b/libs/schematics/README.md
new file mode 100644
index 0000000..43f1d5f
--- /dev/null
+++ b/libs/schematics/README.md
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## About
+
+This repository contains the Schematics of NanoForge. Check [releases][github-releases] to see versions of the Schematics. Nanoforge is a powerful game engine for web browser.
+
+## Usage
+
+To use Nanoforge Schematics, please refer to the [CLI documentation][cli-source] !
+
+First, install the CLI :
+
+```bash
+npm install -g @nanoforge-dev/cli
+```
+
+And then create a new project :
+
+```bash
+nf new
+```
+
+## Schematics
+
+This repository provide multiples schematics, usable with Angular Devkit schematics.
+
+- `application` : Base of a Nanoforge app
+- `configuration` : Template of `nanoforge.config.json` with premade fields
+- `part-base` : Base of Nanoforge client or server
+- `part-main` : Client or server `main.ts` from a config
+- `component` : ECS component class for a client or server part
+- `system` : ECS system function for a client or server part
+- `docker` : Dockerfile and `.dockerignore` tailored to your package manager
+
+## Contributing
+
+Please read through our [contribution guidelines][contributing] before starting a pull request. We welcome contributions of all kinds, not just code! If you're stuck for ideas, look for the [good first issue][good-first-issue] label on issues in the repository. If you have any questions about the project, feel free to ask them on [Discussions][discussions]. Before creating your own issue or pull request, always check to see if one already exists! Don't rush contributions, take your time and ensure you're doing it correctly.
+
+## Help
+
+If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle nudge in the right direction, please ask on [Discussions][discussions].
+
+[contributing]: https://github.com/NanoForge-dev/CLI/blob/main/.github/CONTRIBUTING.md
+[discussions]: https://github.com/NanoForge-dev/CLI/discussions
+[cli-source]: https://github.com/NanoForge-dev/CLI
+[github-releases]: https://github.com/NanoForge-dev/CLI/releases
+[good-first-issue]: https://github.com/NanoForge-dev/CLI/contribute
diff --git a/libs/schematics/cliff.toml b/libs/schematics/cliff.toml
new file mode 100644
index 0000000..9c2cd71
--- /dev/null
+++ b/libs/schematics/cliff.toml
@@ -0,0 +1,79 @@
+[changelog]
+header = """
+# Changelog
+
+All notable changes to this project will be documented in this file.\n
+"""
+body = """
+{%- macro remote_url() -%}
+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
+{%- endmacro -%}
+{% if version %}\
+ # [{{ version | trim_start_matches(pat="v") }}]\
+ {% if previous %}\
+ {% if previous.version %}\
+ ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
+ {% else %}\
+ ({{ self::remote_url() }}/tree/{{ version }})\
+ {% endif %}\
+ {% endif %} \
+ - ({{ timestamp | date(format="%Y-%m-%d") }})
+{% else %}\
+ # [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+ ## {{ group | upper_first }}
+ {% for commit in commits %}
+ - {% if commit.scope %}\
+ **{{commit.scope}}:** \
+ {% endif %}\
+ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
+ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
+ {% if commit.breaking %}\
+ {% for footer in commit.footers %}\
+ {% if footer.breaking %}\
+ \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
+ {% endif %}\
+ {% endfor %}\
+ {% endif %}\
+ {% endfor %}
+{% endfor %}\
+{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\
+ \n### New Contributors\n
+ {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\
+ - @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }}
+ {% endfor %}\
+{% endif %}\n
+"""
+trim = true
+footer = ""
+
+[git]
+conventional_commits = true
+filter_unconventional = true
+commit_parsers = [
+ { message = "^feat", group = "Features"},
+ { message = "^fix", group = "Bug Fixes"},
+ { message = "^docs", group = "Documentation"},
+ { message = "^perf", group = "Performance"},
+ { message = "^refactor", group = "Refactor"},
+ { message = "^types", group = "Typings"},
+ { message = ".*deprecated", body = ".*deprecated", group = "Deprecation"},
+ { message = "^revert", skip = true},
+ { message = "^style", group = "Styling"},
+ { message = "^test", group = "Testing"},
+ { message = "^chore", skip = true},
+ { message = "^ci", skip = true},
+ { message = "^build", skip = true},
+ { body = ".*security", group = "Security"},
+]
+filter_commits = true
+protect_breaking_commits = true
+tag_pattern = "^[0-9]+"
+ignore_tags = ""
+topo_order = false
+sort_commits = "newest"
+
+[remote.github]
+owner = "NanoForge-dev"
+repo = "schematics"
diff --git a/libs/schematics/commitlint.config.cjs b/libs/schematics/commitlint.config.cjs
new file mode 100644
index 0000000..a6580b4
--- /dev/null
+++ b/libs/schematics/commitlint.config.cjs
@@ -0,0 +1,4 @@
+module.exports = {
+ extends: ["@commitlint/config-conventional"],
+ rules: {},
+};
diff --git a/libs/schematics/e2e/application.test.ts b/libs/schematics/e2e/application.test.ts
new file mode 100644
index 0000000..aa0e92f
--- /dev/null
+++ b/libs/schematics/e2e/application.test.ts
@@ -0,0 +1,170 @@
+import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
+import { resolve } from "node:path";
+import { beforeAll, describe, expect, it } from "vitest";
+
+const collectionPath = resolve(__dirname, "../dist/collection.json");
+
+describe("application schematic", () => {
+ const runner = new SchematicTestRunner("schematics", collectionPath);
+
+ describe("with TypeScript (default)", () => {
+ let tree: UnitTestTree;
+ let packageJson: any;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("application", { name: "my-app" });
+ packageJson = JSON.parse(tree.readContent("/my-app/package.json"));
+ });
+
+ it("should generate project files", () => {
+ expect(tree.files).toContain("/my-app/package.json");
+ expect(tree.files).toContain("/my-app/tsconfig.json");
+ expect(tree.files).toContain("/my-app/eslint.config.js");
+ expect(tree.files).toContain("/my-app/prettier.config.js");
+ expect(tree.files).toContain("/my-app/README.md");
+ expect(tree.files).toContain("/my-app/.gitignore");
+ expect(tree.files).not.toContain("/my-app/.env");
+ expect(tree.files).not.toContain("/my-app/pnpm-workspace.yaml");
+ });
+
+ it("should set the project name in package.json", () => {
+ expect(packageJson.name).toBe("my-app");
+ });
+
+ it("should not include ecs-server by default", () => {
+ expect(packageJson.devDependencies["@nanoforge-dev/ecs-server"]).toBeUndefined();
+ });
+
+ it("should not include pnpm config by default", () => {
+ expect(packageJson).not.toHaveProperty("pnpm");
+ });
+
+ it("should not include init functions docs by default", () => {
+ expect(tree.readContent("/my-app/README.md")).not.toContain(
+ "│ └── init/ # Lifecycle hooks (before-init, after-run, …)",
+ );
+ });
+ });
+
+ describe("with JavaScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("application", {
+ name: "js-app",
+ language: "js",
+ });
+ });
+
+ it("should generate JS project files", () => {
+ expect(tree.files).toContain("/js-app/package.json");
+ expect(tree.files).toContain("/js-app/jsconfig.json");
+ expect(tree.files).not.toContain("/js-app/tsconfig.json");
+ expect(tree.files).not.toContain("/js-app/.env");
+ });
+ });
+
+ describe("with JavaScript and no lint", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("application", {
+ name: "js-no-lint-app",
+ language: "js",
+ lint: false,
+ });
+ });
+
+ it("should generate JS project files without lint files", () => {
+ expect(tree.files).toContain("/js-no-lint-app/package.json");
+ expect(tree.files).not.toContain("/js-no-lint-app/eslint.config.json");
+ expect(tree.files).not.toContain("/js-no-lint-app/prettier.config.json");
+ expect(tree.files).not.toContain("/js-no-lint-app/.prettierignore");
+ expect(tree.files).not.toContain("/js-no-lint-app/jsconfig.json");
+ expect(tree.files).not.toContain("/js-no-lint-app/tsconfig.json");
+ });
+ });
+
+ describe("with server enabled", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("application", {
+ name: "server-app",
+ server: true,
+ });
+ });
+
+ it("should include env file", () => {
+ expect(tree.files).toContain("/server-app/.env");
+ });
+
+ it("should include ecs-server dependency", () => {
+ const packageJson = JSON.parse(tree.readContent("/server-app/package.json"));
+ expect(packageJson.devDependencies["@nanoforge-dev/ecs-server"]).toBeDefined();
+ });
+ });
+
+ describe("with custom options", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("application", {
+ name: "custom-app",
+ version: "1.2.3",
+ author: "Test Author",
+ description: "A test project",
+ editor: true,
+ });
+ });
+
+ it("should apply custom options to package.json", () => {
+ const packageJson = JSON.parse(tree.readContent("/custom-app/package.json"));
+ expect(packageJson.version).toBe("1.2.3");
+ expect(packageJson.author).toBe("Test Author");
+ expect(packageJson.description).toBe("A test project");
+ expect(packageJson.devDependencies["@nanoforge-dev/core-editor"]).toBeDefined();
+ expect(packageJson.devDependencies["@nanoforge-dev/graphics-2d-editor"]).toBeDefined();
+ });
+ });
+
+ describe("with custom directory", () => {
+ it("should generate files in the specified directory", async () => {
+ const tree = await runner.runSchematic("application", {
+ name: "my-app",
+ directory: "custom-dir",
+ });
+ expect(tree.files).toContain("/custom-dir/package.json");
+ });
+ });
+
+ describe("with pnpm", () => {
+ it("should generate specifications of pnpm", async () => {
+ const tree = await runner.runSchematic("application", {
+ name: "pnpm-app",
+ packageManager: "pnpm",
+ server: true,
+ });
+ expect(tree.files).toContain("/pnpm-app/package.json");
+ expect(tree.files).toContain("/pnpm-app/pnpm-workspace.yaml");
+ const packageJson = JSON.parse(tree.readContent("/pnpm-app/package.json"));
+ expect(packageJson).toHaveProperty("pnpm", {
+ neverBuiltDependencies: [],
+ });
+ });
+ });
+
+ describe("with init functions", () => {
+ it("should generate specifications of init functions", async () => {
+ const tree = await runner.runSchematic("application", {
+ name: "init-functions-app",
+ initFunctions: true,
+ });
+ expect(tree.files).toContain("/init-functions-app/README.md");
+ const content = tree.readContent("/init-functions-app/README.md");
+ expect(content).toContain(
+ "│ └── init/ # Lifecycle hooks (before-init, after-run, …)",
+ );
+ });
+ });
+});
diff --git a/libs/schematics/e2e/component.test.ts b/libs/schematics/e2e/component.test.ts
new file mode 100644
index 0000000..aaf3cf8
--- /dev/null
+++ b/libs/schematics/e2e/component.test.ts
@@ -0,0 +1,171 @@
+import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
+import { resolve } from "node:path";
+import { beforeAll, describe, expect, it } from "vitest";
+
+const collectionPath = resolve(__dirname, "../dist/collection.json");
+
+describe("component schematic", () => {
+ const runner = new SchematicTestRunner("schematics", collectionPath);
+
+ describe("TypeScript component for client part", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("component", {
+ name: "myComponent",
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ });
+ });
+
+ it("should generate a .ts component file", () => {
+ expect(tree.files).toContain("/my-app/my-component.component.ts");
+ });
+
+ it("should use PascalCase class name", () => {
+ const content = tree.readContent("/my-app/my-component.component.ts");
+ expect(content).toContain("class MyComponentComponent");
+ });
+
+ it("should use the correct part in the import", () => {
+ const content = tree.readContent("/my-app/my-component.component.ts");
+ expect(content).toContain("@nanoforge-dev/ecs-client");
+ });
+
+ it("should export the component name as default", () => {
+ const content = tree.readContent("/my-app/my-component.component.ts");
+ expect(content).toContain("export default MyComponentComponent.name");
+ });
+
+ it("should export the EDITOR_COMPONENT_MANIFEST", () => {
+ const content = tree.readContent("/my-app/my-component.component.ts");
+ expect(content).toContain("export const EDITOR_COMPONENT_MANIFEST");
+ expect(content).toContain('name: "MyComponent"');
+ });
+ });
+
+ describe("TypeScript component for server part", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("component", {
+ name: "myComponent",
+ directory: "my-app",
+ part: "server",
+ language: "ts",
+ });
+ });
+
+ it("should generate a .ts component file", () => {
+ expect(tree.files).toContain("/my-app/my-component.component.ts");
+ });
+
+ it("should use the correct part in the import", () => {
+ const content = tree.readContent("/my-app/my-component.component.ts");
+ expect(content).toContain("@nanoforge-dev/ecs-server");
+ });
+ });
+
+ describe("JavaScript component", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("component", {
+ name: "myComponent",
+ directory: "my-app",
+ part: "client",
+ language: "js",
+ });
+ });
+
+ it("should generate a .js component file", () => {
+ expect(tree.files).toContain("/my-app/my-component.component.js");
+ });
+
+ it("should use PascalCase class name", () => {
+ const content = tree.readContent("/my-app/my-component.component.js");
+ expect(content).toContain("class MyComponentComponent");
+ });
+
+ it("should use the correct part in the JSDoc typedef", () => {
+ const content = tree.readContent("/my-app/my-component.component.js");
+ expect(content).toContain("@nanoforge-dev/ecs-client");
+ });
+
+ it("should export the EDITOR_COMPONENT_MANIFEST", () => {
+ const content = tree.readContent("/my-app/my-component.component.js");
+ expect(content).toContain("export const EDITOR_COMPONENT_MANIFEST");
+ expect(content).toContain('name: "MyComponent"');
+ });
+ });
+
+ describe("name formatting", () => {
+ it("should handle kebab-case name", async () => {
+ const tree = await runner.runSchematic("component", {
+ name: "my-component",
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ });
+ expect(tree.files).toContain("/my-app/my-component.component.ts");
+ const content = tree.readContent("/my-app/my-component.component.ts");
+ expect(content).toContain("class MyComponentComponent");
+ });
+
+ it("should handle PascalCase name", async () => {
+ const tree = await runner.runSchematic("component", {
+ name: "MyComponent",
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ });
+ expect(tree.files).toContain("/my-app/my-component.component.ts");
+ const content = tree.readContent("/my-app/my-component.component.ts");
+ expect(content).toContain("class MyComponentComponent");
+ });
+
+ it("should handle snake_case name", async () => {
+ const tree = await runner.runSchematic("component", {
+ name: "my_component",
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ });
+ expect(tree.files).toContain("/my-app/my-component.component.ts");
+ const content = tree.readContent("/my-app/my-component.component.ts");
+ expect(content).toContain("class MyComponentComponent");
+ });
+ });
+
+ describe("custom directory", () => {
+ it("should generate file in the specified directory", async () => {
+ const tree = await runner.runSchematic("component", {
+ name: "myComponent",
+ directory: "src/client/components",
+ part: "client",
+ language: "ts",
+ });
+ expect(tree.files).toContain("/src/client/components/my-component.component.ts");
+ });
+ });
+
+ describe("default values", () => {
+ it("should default to TypeScript when language is not specified", async () => {
+ const tree = await runner.runSchematic("component", {
+ name: "myComponent",
+ directory: "my-app",
+ part: "client",
+ });
+ expect(tree.files).toContain("/my-app/my-component.component.ts");
+ });
+
+ it("should default to current directory when directory is not specified", async () => {
+ const tree = await runner.runSchematic("component", {
+ name: "myComponent",
+ part: "client",
+ });
+ expect(tree.files).toContain("/my-component.component.ts");
+ });
+ });
+});
diff --git a/libs/schematics/e2e/configuration.test.ts b/libs/schematics/e2e/configuration.test.ts
new file mode 100644
index 0000000..474e9cc
--- /dev/null
+++ b/libs/schematics/e2e/configuration.test.ts
@@ -0,0 +1,108 @@
+import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
+import { resolve } from "node:path";
+import { beforeAll, describe, expect, it } from "vitest";
+
+const collectionPath = resolve(__dirname, "../dist/collection.json");
+
+describe("configuration schematic", () => {
+ const runner = new SchematicTestRunner("schematics", collectionPath);
+
+ describe("on an empty tree", () => {
+ let tree: UnitTestTree;
+ let config: Record;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("configuration", {
+ name: "my-name",
+ });
+ config = JSON.parse(tree.readContent("/nanoforge.config.json"));
+ });
+
+ it("should generate nanoforge.config.json", () => {
+ expect(tree.files).toContain("/nanoforge.config.json");
+ });
+
+ it("should create a valid JSON config file", () => {
+ expect(() => JSON.parse(tree.readContent("/nanoforge.config.json"))).not.toThrow();
+ });
+
+ it("should include default client build config", () => {
+ expect(config.name).toBe("my-name");
+ expect(config.language).toBe("ts");
+ expect(config.initFunctions).not.toBeDefined();
+ });
+
+ it("should include default client build config", () => {
+ expect(config.client).toBeDefined();
+ expect(config.client.enable).toBe(true);
+ });
+
+ it("should have server disabled", () => {
+ expect(config.server.enable).toBe(false);
+ });
+ });
+
+ describe("with js", () => {
+ let tree: UnitTestTree;
+ let config: Record;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("configuration", {
+ name: "my-name",
+ language: "js",
+ initFunctions: true,
+ });
+ config = JSON.parse(tree.readContent("/nanoforge.config.json"));
+ });
+
+ it("should generate nanoforge.config.json", () => {
+ expect(tree.files).toContain("/nanoforge.config.json");
+ });
+
+ it("should include default client build config", () => {
+ expect(config.name).toBe("my-name");
+ expect(config.language).toBe("js");
+ expect(config.initFunctions).toBe(true);
+ });
+
+ it("should include default client build config", () => {
+ expect(config.client).toBeDefined();
+ expect(config.client.build.entry).toBe("client/main.js");
+ });
+
+ it("should include default editor config", () => {
+ expect(config.client.editor.entry).toBe(".nanoforge/editor/client/main.js");
+ });
+ });
+
+ describe("with server enabled", () => {
+ let tree: UnitTestTree;
+ let config: Record;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("configuration", {
+ server: true,
+ });
+ config = JSON.parse(tree.readContent("/nanoforge.config.json"));
+ });
+
+ it("should generate config file", () => {
+ expect(tree.files).toContain("/nanoforge.config.json");
+ });
+
+ it("should include server config with enable flag", () => {
+ expect(config.server).toBeDefined();
+ expect(config.server.enable).toBe(true);
+ });
+ });
+
+ describe("with custom directory", () => {
+ it("should generate config in the specified directory", async () => {
+ const tree = await runner.runSchematic("configuration", {
+ name: "my-app",
+ directory: "custom-dir",
+ });
+ expect(tree.files).toContain("/custom-dir/nanoforge.config.json");
+ });
+ });
+});
diff --git a/libs/schematics/e2e/docker.test.ts b/libs/schematics/e2e/docker.test.ts
new file mode 100644
index 0000000..f77fc19
--- /dev/null
+++ b/libs/schematics/e2e/docker.test.ts
@@ -0,0 +1,45 @@
+import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
+import { resolve } from "node:path";
+import { beforeAll, describe, expect, it } from "vitest";
+
+const collectionPath = resolve(__dirname, "../dist/collection.json");
+
+describe("docker schematic", () => {
+ const runner = new SchematicTestRunner("schematics", collectionPath);
+
+ describe("with npm package manager", () => {
+ let tree: UnitTestTree;
+
+ describe.each(["npm", "yarn", "pnpm", "bun"] as const)(
+ "with ${packageManager} package manager",
+ (packageManager) => {
+ beforeAll(async () => {
+ tree = await runner.runSchematic("docker", {
+ directory: `${packageManager}-test-app`,
+ packageManager,
+ });
+ });
+
+ it("should create Dockerfile", () => {
+ expect(tree.files).toContain(`/${packageManager}-test-app/Dockerfile`);
+ expect(tree.files).toContain(`/${packageManager}-test-app/.dockerignore`);
+ });
+
+ it("should create docker file with install command", () => {
+ const content = tree.readContent(`/${packageManager}-test-app/Dockerfile`);
+ expect(content).toContain(`RUN ${packageManager} install`);
+ });
+
+ it("should create docker file with build command", () => {
+ const content = tree.readContent(`/${packageManager}-test-app/Dockerfile`);
+ expect(content).toContain(`RUN ${packageManager} run build`);
+ });
+
+ it("should create docker file with start command", () => {
+ const content = tree.readContent(`/${packageManager}-test-app/Dockerfile`);
+ expect(content).toContain(`CMD ["${packageManager}", "run", "start"]`);
+ });
+ },
+ );
+ });
+});
diff --git a/libs/schematics/e2e/part-base.test.ts b/libs/schematics/e2e/part-base.test.ts
new file mode 100644
index 0000000..0e917f3
--- /dev/null
+++ b/libs/schematics/e2e/part-base.test.ts
@@ -0,0 +1,122 @@
+import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
+import { resolve } from "node:path";
+import { beforeAll, describe, expect, it } from "vitest";
+
+const collectionPath = resolve(__dirname, "../dist/collection.json");
+
+describe("part-base schematic", () => {
+ const runner = new SchematicTestRunner("schematics", collectionPath);
+
+ describe("client part with TypeScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("part-base", {
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ });
+ });
+
+ it("should generate client directory structure", () => {
+ expect(tree.files).toContain("/my-app/client/components/example.component.ts");
+ expect(tree.files).toContain("/my-app/client/systems/example.system.ts");
+ });
+
+ it("should generate the client save file", () => {
+ expect(tree.files).toContain("/my-app/.nanoforge/client.save.json");
+ });
+
+ it("should not generate server save file", () => {
+ expect(tree.files).not.toContain("/my-app/.nanoforge/server.save.json");
+ });
+
+ it("should not generate init functions by default", () => {
+ const initFiles = tree.files.filter((f) => f.includes("/init/"));
+ expect(initFiles).toHaveLength(0);
+ });
+
+ it("should generate a valid save file", () => {
+ const save = JSON.parse(tree.readContent("/my-app/.nanoforge/client.save.json"));
+ expect(save.libraries).toBeDefined();
+ expect(save.components).toBeDefined();
+ expect(save.systems).toBeDefined();
+ expect(save.entities).toBeDefined();
+ });
+ });
+
+ describe("server part with TypeScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("part-base", {
+ directory: "my-app",
+ part: "server",
+ language: "ts",
+ });
+ });
+
+ it("should generate server directory structure", () => {
+ expect(tree.files).toContain("/my-app/server/components/example.component.ts");
+ expect(tree.files).toContain("/my-app/server/systems/example.system.ts");
+ });
+
+ it("should generate the server save file", () => {
+ expect(tree.files).toContain("/my-app/.nanoforge/server.save.json");
+ });
+
+ it("should not generate client save file", () => {
+ expect(tree.files).not.toContain("/my-app/.nanoforge/client.save.json");
+ });
+ });
+
+ describe("with init functions enabled", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("part-base", {
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ initFunctions: true,
+ });
+ });
+
+ it("should generate init function files", () => {
+ expect(tree.files).toContain("/my-app/client/init/before-init.ts");
+ expect(tree.files).toContain("/my-app/client/init/after-init.ts");
+ expect(tree.files).toContain("/my-app/client/init/before-registry-init.ts");
+ expect(tree.files).toContain("/my-app/client/init/after-registry-init.ts");
+ expect(tree.files).toContain("/my-app/client/init/before-run.ts");
+ expect(tree.files).toContain("/my-app/client/init/after-run.ts");
+ });
+ });
+
+ describe("with JavaScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("part-base", {
+ directory: "my-app",
+ part: "client",
+ language: "js",
+ });
+ });
+
+ it("should generate JS files", () => {
+ expect(tree.files).toContain("/my-app/client/components/example.component.js");
+ expect(tree.files).toContain("/my-app/client/systems/example.system.js");
+ });
+ });
+
+ describe("with custom directory", () => {
+ it("should generate files in the specified directory", async () => {
+ const tree = await runner.runSchematic("part-base", {
+ name: "my-app",
+ part: "client",
+ directory: "custom-dir",
+ });
+ expect(tree.files).toContain("/custom-dir/client/components/example.component.ts");
+ });
+ });
+});
diff --git a/libs/schematics/e2e/part-main.test.ts b/libs/schematics/e2e/part-main.test.ts
new file mode 100644
index 0000000..736f8b6
--- /dev/null
+++ b/libs/schematics/e2e/part-main.test.ts
@@ -0,0 +1,262 @@
+import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
+import { mkdirSync, rmSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { afterAll, beforeAll, describe, expect, it } from "vitest";
+
+import { type Save } from "@utils/main/save.type";
+
+const collectionPath = resolve(__dirname, "../dist/collection.json");
+const tmpDir = resolve(__dirname, "../.tmp-e2e");
+
+const clientSave: Save = {
+ libraries: [
+ {
+ id: "ecsLibrary",
+ type: "component-system",
+ name: "ECSClientLibrary",
+ path: "@nanoforge-dev/ecs-client",
+ },
+ {
+ id: "graphicsLibrary",
+ type: "graphics",
+ name: "Graphics2DLibrary",
+ path: "@nanoforge-dev/graphics-2d",
+ },
+ ],
+ components: [
+ {
+ name: "ExampleComponent",
+ path: "./components/example.component.ts",
+ paramsNames: ["exampleValueName", "exampleNum", "__RESERVED_ASSET_exampleAsset"],
+ },
+ ],
+ systems: [{ name: "exampleSystem", path: "./systems/example.system.js" }],
+ entities: [
+ {
+ id: "player",
+ components: {
+ ExampleComponent: {
+ exampleValueName: "test",
+ exampleNum: 5,
+ exampleAsset: "/path/to/asset",
+ },
+ },
+ },
+ ],
+};
+
+const serverSave: Save = {
+ libraries: [
+ {
+ id: "ecsLibrary",
+ type: "component-system",
+ name: "ECSServerLibrary",
+ path: "@nanoforge-dev/ecs-server",
+ },
+ ],
+ components: [],
+ systems: [],
+ entities: [],
+};
+
+describe("part-main schematic", () => {
+ const runner = new SchematicTestRunner("schematics", collectionPath);
+
+ beforeAll(() => {
+ mkdirSync(tmpDir, { recursive: true });
+ writeFileSync(resolve(tmpDir, "client.save.json"), JSON.stringify(clientSave));
+ writeFileSync(resolve(tmpDir, "server.save.json"), JSON.stringify(serverSave));
+ });
+
+ afterAll(() => {
+ rmSync(tmpDir, { recursive: true, force: true });
+ });
+
+ describe("client main with TypeScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("part-main", {
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ saveFile: resolve(tmpDir, "client.save.json"),
+ });
+ });
+
+ it("should generate client/main.ts", () => {
+ expect(tree.files).toContain("/my-app/client/main.ts");
+ });
+
+ it("should contain NanoforgeFactory.createClient()", () => {
+ const content = tree.readContent("/my-app/client/main.ts");
+ expect(content).toContain("NanoforgeFactory.createClient()");
+ });
+
+ it("should contain typed main function", () => {
+ const content = tree.readContent("/my-app/client/main.ts");
+ expect(content).toContain("export async function main(options: IRunOptions)");
+ });
+
+ it("should import and instantiate libraries", () => {
+ const content = tree.readContent("/my-app/client/main.ts");
+ expect(content).toContain('import { ECSClientLibrary } from "@nanoforge-dev/ecs-client"');
+ expect(content).toContain('import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d"');
+ expect(content).toContain("const ecsLibrary = new ECSClientLibrary();");
+ expect(content).toContain("const graphicsLibrary = new Graphics2DLibrary();");
+ });
+
+ it("should register entities and systems", () => {
+ const content = tree.readContent("/my-app/client/main.ts");
+ expect(content).toContain("const player = registry.spawnEntity();");
+ expect(content).toContain(
+ 'registry.addComponent(player, new ExampleComponent("test", 5, assetManagerLibrary.getAsset("/path/to/asset")));',
+ );
+ expect(content).toContain("registry.addSystem(exampleSystem);");
+ });
+
+ it("should not contain editor libs", () => {
+ const content = tree.readContent("/my-app/client/main.ts");
+ expect(content).not.toContain("@nanoforge-dev/graphics-2d-editor");
+ });
+ });
+
+ describe("server main with TypeScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("part-main", {
+ directory: "my-app",
+ part: "server",
+ language: "ts",
+ saveFile: resolve(tmpDir, "server.save.json"),
+ });
+ });
+
+ it("should generate server/main.ts", () => {
+ expect(tree.files).toContain("/my-app/server/main.ts");
+ });
+
+ it("should contain NanoforgeFactory.createServer()", () => {
+ const content = tree.readContent("/my-app/server/main.ts");
+ expect(content).toContain("NanoforgeFactory.createServer()");
+ });
+ });
+
+ describe("client main with JavaScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("part-main", {
+ directory: "my-app",
+ part: "client",
+ language: "js",
+ saveFile: resolve(tmpDir, "client.save.json"),
+ });
+ });
+
+ it("should generate client/main.js", () => {
+ expect(tree.files).toContain("/my-app/client/main.js");
+ });
+
+ it("should not contain TypeScript type annotations", () => {
+ const content = tree.readContent("/my-app/client/main.js");
+ expect(content).not.toContain("IRunOptions");
+ expect(content).toContain("export async function main(options)");
+ });
+ });
+
+ describe("with init functions enabled", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("part-main", {
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ initFunctions: true,
+ saveFile: resolve(tmpDir, "client.save.json"),
+ });
+ });
+
+ it("should import init functions", () => {
+ const content = tree.readContent("/my-app/client/main.ts");
+ expect(content).toContain('import { beforeInit } from "./init/before-init"');
+ expect(content).toContain('import { afterRun } from "./init/after-run"');
+ });
+
+ it("should call init functions in the main body", () => {
+ const content = tree.readContent("/my-app/client/main.ts");
+ expect(content).toContain("await beforeInit(app);");
+ expect(content).toContain("await afterInit(app);");
+ expect(content).toContain("await beforeRegistryInit(app, registry);");
+ expect(content).toContain("await afterRegistryInit(app, registry);");
+ expect(content).toContain("await beforeRun(app);");
+ expect(content).toContain("await afterRun(app);");
+ });
+ });
+
+ describe("editor mode", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("part-main", {
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ editor: true,
+ saveFile: resolve(tmpDir, "client.save.json"),
+ });
+ });
+
+ it("should generate the editor main file under .nanoforge/editor", () => {
+ expect(tree.files).toContain("/my-app/.nanoforge/editor/client/main.ts");
+ });
+
+ it("should import from @nanoforge-dev/core-editor", () => {
+ const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
+ expect(content).toContain('from "@nanoforge-dev/core-editor"');
+ });
+
+ it("should use IEditorRunOptions type", () => {
+ const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
+ expect(content).toContain("IEditorRunOptions");
+ expect(content).toContain("export async function main(options: IEditorRunOptions)");
+ });
+
+ it("should use entity paramsValues from editor save", () => {
+ const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
+ expect(content).toContain(
+ 'options.editor.save.entities[0].components["ExampleComponent"]["exampleValueName"]',
+ );
+ expect(content).toContain(
+ 'assetManagerLibrary.getAsset(options.editor.save.entities[0].components["ExampleComponent"]["exampleAsset"])',
+ );
+ });
+
+ it("should import components and systems with relative path to root", () => {
+ const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
+ expect(content).toContain(
+ 'import { ExampleComponent } from "../../../client/components/example.component"',
+ );
+ expect(content).toContain(
+ 'import { exampleSystem } from "../../../client/systems/example.system"',
+ );
+ });
+
+ it("should create a entityId reserved component", () => {
+ const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
+ expect(content).toContain(
+ 'registry.addComponent(player, { name: "__RESERVED_entityId", entityId: "player" });',
+ );
+ });
+
+ it("should init graphics editor", () => {
+ const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
+ expect(content).toContain(
+ 'import { Graphics2DEditorLibrary } from "@nanoforge-dev/graphics-2d-editor"',
+ );
+ expect(content).toContain("const graphicsEditorLibrary = new Graphics2DEditorLibrary();");
+ });
+ });
+});
diff --git a/libs/schematics/e2e/system.test.ts b/libs/schematics/e2e/system.test.ts
new file mode 100644
index 0000000..b392fc3
--- /dev/null
+++ b/libs/schematics/e2e/system.test.ts
@@ -0,0 +1,171 @@
+import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
+import { resolve } from "node:path";
+import { beforeAll, describe, expect, it } from "vitest";
+
+const collectionPath = resolve(__dirname, "../dist/collection.json");
+
+describe("system schematic", () => {
+ const runner = new SchematicTestRunner("schematics", collectionPath);
+
+ describe("TypeScript system for client part", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("system", {
+ name: "mySystem",
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ });
+ });
+
+ it("should generate a .ts system file", () => {
+ expect(tree.files).toContain("/my-app/my-system.system.ts");
+ });
+
+ it("should use camelCase function name", () => {
+ const content = tree.readContent("/my-app/my-system.system.ts");
+ expect(content).toContain("export const mySystemSystem");
+ });
+
+ it("should use the correct part in the import", () => {
+ const content = tree.readContent("/my-app/my-system.system.ts");
+ expect(content).toContain("@nanoforge-dev/ecs-client");
+ });
+
+ it("should export the system name as default", () => {
+ const content = tree.readContent("/my-app/my-system.system.ts");
+ expect(content).toContain("export default mySystemSystem.name");
+ });
+
+ it("should export the EDITOR_SYSTEM_MANIFEST", () => {
+ const content = tree.readContent("/my-app/my-system.system.ts");
+ expect(content).toContain("export const EDITOR_SYSTEM_MANIFEST");
+ expect(content).toContain('name: "mySystem"');
+ });
+ });
+
+ describe("TypeScript system for server part", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("system", {
+ name: "mySystem",
+ directory: "my-app",
+ part: "server",
+ language: "ts",
+ });
+ });
+
+ it("should generate a .ts system file", () => {
+ expect(tree.files).toContain("/my-app/my-system.system.ts");
+ });
+
+ it("should use the correct part in the import", () => {
+ const content = tree.readContent("/my-app/my-system.system.ts");
+ expect(content).toContain("@nanoforge-dev/ecs-server");
+ });
+ });
+
+ describe("JavaScript system", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("system", {
+ name: "mySystem",
+ directory: "my-app",
+ part: "client",
+ language: "js",
+ });
+ });
+
+ it("should generate a .js system file", () => {
+ expect(tree.files).toContain("/my-app/my-system.system.js");
+ });
+
+ it("should use camelCase function name", () => {
+ const content = tree.readContent("/my-app/my-system.system.js");
+ expect(content).toContain("export const mySystemSystem");
+ });
+
+ it("should use the correct part in the JSDoc typedef", () => {
+ const content = tree.readContent("/my-app/my-system.system.js");
+ expect(content).toContain("@nanoforge-dev/ecs-client");
+ });
+
+ it("should export the EDITOR_SYSTEM_MANIFEST", () => {
+ const content = tree.readContent("/my-app/my-system.system.js");
+ expect(content).toContain("export const EDITOR_SYSTEM_MANIFEST");
+ expect(content).toContain('name: "mySystem"');
+ });
+ });
+
+ describe("name formatting", () => {
+ it("should handle kebab-case name", async () => {
+ const tree = await runner.runSchematic("system", {
+ name: "my-system",
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ });
+ expect(tree.files).toContain("/my-app/my-system.system.ts");
+ const content = tree.readContent("/my-app/my-system.system.ts");
+ expect(content).toContain("export const mySystemSystem");
+ });
+
+ it("should handle PascalCase name", async () => {
+ const tree = await runner.runSchematic("system", {
+ name: "MySystem",
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ });
+ expect(tree.files).toContain("/my-app/my-system.system.ts");
+ const content = tree.readContent("/my-app/my-system.system.ts");
+ expect(content).toContain("export const mySystemSystem");
+ });
+
+ it("should handle snake_case name", async () => {
+ const tree = await runner.runSchematic("system", {
+ name: "my_system",
+ directory: "my-app",
+ part: "client",
+ language: "ts",
+ });
+ expect(tree.files).toContain("/my-app/my-system.system.ts");
+ const content = tree.readContent("/my-app/my-system.system.ts");
+ expect(content).toContain("export const mySystemSystem");
+ });
+ });
+
+ describe("custom directory", () => {
+ it("should generate file in the specified directory", async () => {
+ const tree = await runner.runSchematic("system", {
+ name: "mySystem",
+ directory: "src/client/systems",
+ part: "client",
+ language: "ts",
+ });
+ expect(tree.files).toContain("/src/client/systems/my-system.system.ts");
+ });
+ });
+
+ describe("default values", () => {
+ it("should default to TypeScript when language is not specified", async () => {
+ const tree = await runner.runSchematic("system", {
+ name: "mySystem",
+ directory: "my-app",
+ part: "client",
+ });
+ expect(tree.files).toContain("/my-app/my-system.system.ts");
+ });
+
+ it("should default to current directory when directory is not specified", async () => {
+ const tree = await runner.runSchematic("system", {
+ name: "mySystem",
+ part: "client",
+ });
+ expect(tree.files).toContain("/my-system.system.ts");
+ });
+ });
+});
diff --git a/libs/schematics/e2e/tsconfig.json b/libs/schematics/e2e/tsconfig.json
new file mode 100644
index 0000000..a0601d8
--- /dev/null
+++ b/libs/schematics/e2e/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["./**/*.test.ts"],
+ "exclude": []
+}
diff --git a/libs/schematics/eslint.config.js b/libs/schematics/eslint.config.js
new file mode 100644
index 0000000..5e9532d
--- /dev/null
+++ b/libs/schematics/eslint.config.js
@@ -0,0 +1,3 @@
+import eslintConfig from "@nanoforge-dev/utils-eslint-config";
+
+export default [...eslintConfig, { ignores: ["src/libs/**/files/**/*"] }];
diff --git a/libs/schematics/package.json b/libs/schematics/package.json
new file mode 100644
index 0000000..80641fd
--- /dev/null
+++ b/libs/schematics/package.json
@@ -0,0 +1,108 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "@nanoforge-dev/schematics",
+ "version": "2.2.0",
+ "description": "NanoForge Schematics",
+ "keywords": [
+ "nanoforge",
+ "game",
+ "schematics"
+ ],
+ "homepage": "https://github.com/NanoForge-dev/schematics#readme",
+ "bugs": "https://github.com/NanoForge-dev/schematics/issues",
+ "license": "MIT",
+ "contributors": [
+ "Bill ",
+ "Exelo ",
+ "Fexkoser ",
+ "Josephine ",
+ "Tchips "
+ ],
+ "files": [
+ "dist"
+ ],
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.cts",
+ "exports": {
+ ".": {
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/index.cjs"
+ },
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "./package.json": "./package.json",
+ "./collection.json": "./dist/collection.json"
+ },
+ "type": "module",
+ "directories": {
+ "lib": "src"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/NanoForge-dev/schematics.git"
+ },
+ "funding": "https://github.com/NanoForge-dev/schematics?sponsor",
+ "scripts": {
+ "build": "tsc --noEmit && tsdown",
+ "postbuild": "pnpm run copy:collection && pnpm run copy:lib",
+ "copy:collection": "cpx src/collection.json dist && cpx 'src/libs/**/schema.json' dist/libs",
+ "copy:lib": "cpx 'src/libs/**/files/**/{.,}*' dist/libs",
+ "lint": "prettier --check . && eslint --format=pretty src",
+ "format": "prettier --write . && eslint --fix --format=pretty src",
+ "prepack": "pnpm run build && pnpm run lint",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r . --include-path 'libs/schematics/*'",
+ "release": "cliff-jumper",
+ "test": "pnpm run test:unit && pnpm run test:e2e",
+ "test:unit": "vitest run -c vitest.config.ts",
+ "test:unit:watch": "vitest -c vitest.config.ts",
+ "test:e2e": "vitest run -c vitest.config.e2e.ts",
+ "test:e2e:watch": "vitest -c vitest.config.e2e.ts",
+ "test:coverage": "pnpm run test:unit --coverage && pnpm run test:e2e --coverage"
+ },
+ "dependencies": {
+ "@angular-devkit/core": "catalog:schematics",
+ "@angular-devkit/schematics": "catalog:schematics",
+ "rxjs": "catalog:schematics"
+ },
+ "devDependencies": {
+ "@angular-devkit/schematics-cli": "catalog:schematics",
+ "@commitlint/cli": "catalog:ci",
+ "@commitlint/config-conventional": "catalog:ci",
+ "@favware/cliff-jumper": "catalog:ci",
+ "@nanoforge-dev/actions": "catalog:ci",
+ "@nanoforge-dev/utils-eslint-config": "catalog:lint",
+ "@nanoforge-dev/utils-prettier-config": "catalog:lint",
+ "@trivago/prettier-plugin-sort-imports": "catalog:lint",
+ "@types/node": "catalog:core",
+ "@vitest/coverage-v8": "catalog:test",
+ "cpx2": "catalog:build",
+ "eslint": "catalog:lint",
+ "husky": "catalog:ci",
+ "lint-staged": "catalog:ci",
+ "prettier": "catalog:lint",
+ "tsdown": "catalog:build",
+ "typescript": "catalog:build",
+ "vitest": "catalog:test"
+ },
+ "packageManager": "pnpm@11.9.0",
+ "engines": {
+ "node": "26"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "schematics": "./dist/collection.json",
+ "lint-staged": {
+ "**": [
+ "prettier --ignore-unknown --write"
+ ],
+ "src/**/*.ts": [
+ "eslint --fix"
+ ]
+ }
+}
diff --git a/libs/schematics/prettier.config.js b/libs/schematics/prettier.config.js
new file mode 100644
index 0000000..121c003
--- /dev/null
+++ b/libs/schematics/prettier.config.js
@@ -0,0 +1,6 @@
+import prettierConfig from "@nanoforge-dev/utils-prettier-config";
+
+export default {
+ ...prettierConfig,
+ importOrder: ["^@utils/(.*)$", "^~/(.*)$", "^[./]"],
+};
diff --git a/libs/schematics/src/collection.json b/libs/schematics/src/collection.json
new file mode 100644
index 0000000..19462ce
--- /dev/null
+++ b/libs/schematics/src/collection.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
+ "schematics": {
+ "application": {
+ "factory": "./libs/application/application.factory#main",
+ "description": "Create a NanoForge Base application.",
+ "schema": "./libs/application/schema.json"
+ },
+ "configuration": {
+ "factory": "./libs/configuration/configuration.factory#main",
+ "description": "Create a NanoForge Configuration.",
+ "schema": "./libs/configuration/schema.json"
+ },
+ "part-base": {
+ "factory": "./libs/part-base/part-base.factory#main",
+ "description": "Create a NanoForge Base for client or server.",
+ "schema": "./libs/part-base/schema.json"
+ },
+ "part-main": {
+ "factory": "./libs/part-main/part-main.factory#main",
+ "description": "Create a Main file for client or server.",
+ "schema": "./libs/part-main/schema.json"
+ },
+ "docker": {
+ "factory": "./libs/docker/docker.factory#main",
+ "description": "Create a Dockerfile for the application.",
+ "schema": "./libs/docker/schema.json"
+ },
+ "component": {
+ "factory": "./libs/component/component.factory#main",
+ "description": "Create a NanoForge component.",
+ "schema": "./libs/component/schema.json"
+ },
+ "system": {
+ "factory": "./libs/system/system.factory#main",
+ "description": "Create a NanoForge system.",
+ "schema": "./libs/system/schema.json"
+ }
+ }
+}
diff --git a/libs/schematics/src/defaults.ts b/libs/schematics/src/defaults.ts
new file mode 100644
index 0000000..5c44c97
--- /dev/null
+++ b/libs/schematics/src/defaults.ts
@@ -0,0 +1,6 @@
+export const DEFAULT_APP_NAME = "nanoforge-app";
+export const DEFAULT_VERSION = "0.0.0";
+export const DEFAULT_AUTHOR = "";
+export const DEFAULT_DESCRIPTION = "";
+export const DEFAULT_LANGUAGE = "ts";
+export const DEFAULT_PACKAGE_MANAGER = "npm";
diff --git a/libs/schematics/src/index.ts b/libs/schematics/src/index.ts
new file mode 100644
index 0000000..8156f2d
--- /dev/null
+++ b/libs/schematics/src/index.ts
@@ -0,0 +1 @@
+export * from "./defaults";
diff --git a/libs/schematics/src/libs/application/application.factory.ts b/libs/schematics/src/libs/application/application.factory.ts
new file mode 100644
index 0000000..5e0bb21
--- /dev/null
+++ b/libs/schematics/src/libs/application/application.factory.ts
@@ -0,0 +1,87 @@
+import { type Path, join, normalize, strings } from "@angular-devkit/core";
+import {
+ type Rule,
+ type SchematicContext,
+ type Source,
+ type Tree,
+ apply,
+ mergeWith,
+ move,
+ renameTemplateFiles,
+ template,
+ url,
+} from "@angular-devkit/schematics";
+import { firstValueFrom } from "rxjs";
+
+import {
+ DEFAULT_APP_NAME,
+ DEFAULT_AUTHOR,
+ DEFAULT_DESCRIPTION,
+ DEFAULT_LANGUAGE,
+ DEFAULT_PACKAGE_MANAGER,
+ DEFAULT_VERSION,
+} from "~/defaults";
+
+import { type ApplicationOptions } from "./application.options";
+import { type ApplicationSchema } from "./application.schema";
+
+const transform = (schema: ApplicationSchema): ApplicationOptions => {
+ return {
+ name: schema.name ?? DEFAULT_APP_NAME,
+ version: schema.version ?? DEFAULT_VERSION,
+ author: schema.author ?? DEFAULT_AUTHOR,
+ description: schema.description ?? DEFAULT_DESCRIPTION,
+ language: schema.language ?? DEFAULT_LANGUAGE,
+ strict: schema.strict ?? true,
+ lint: schema.lint ?? true,
+ packageManager: schema.packageManager ?? DEFAULT_PACKAGE_MANAGER,
+ server: schema.server ?? false,
+ editor: schema.editor ?? false,
+ initFunctions: schema.initFunctions ?? false,
+ };
+};
+
+const generate = (options: ApplicationOptions, path: string): Source => {
+ return apply(url(join("./files" as Path, options.language)), [
+ template({
+ dot: ".",
+ ...strings,
+ ...options,
+ }),
+ renameTemplateFiles(),
+ move(normalize(path)),
+ ]);
+};
+
+export const main = (schema: ApplicationSchema): Rule => {
+ const options = transform(schema);
+ const path = schema.directory ?? options.name;
+
+ return async (baseTree: Tree, context: SchematicContext) => {
+ let tree = await mergeWith(generate(options, path))(baseTree, context);
+ if (!tree) return tree;
+ if (!("delete" in tree)) {
+ if (typeof tree === "function") return tree;
+ tree = await firstValueFrom(tree);
+ }
+
+ const basePath = join("/" as Path, path);
+
+ if (!options.lint) {
+ tree.delete(join(basePath, "eslint.config.js"));
+ tree.delete(join(basePath, "prettier.config.js"));
+ tree.delete(join(basePath, ".prettierignore"));
+ if (options.language === "js") tree.delete(join(basePath, "jsconfig.json"));
+ }
+
+ if (!options.server) {
+ tree.delete(join(basePath, ".env"));
+ }
+
+ if (options.packageManager !== "pnpm") {
+ tree.delete(join(basePath, "pnpm-workspace.yaml"));
+ }
+
+ return tree;
+ };
+};
diff --git a/libs/schematics/src/libs/application/application.options.d.ts b/libs/schematics/src/libs/application/application.options.d.ts
new file mode 100644
index 0000000..71201d8
--- /dev/null
+++ b/libs/schematics/src/libs/application/application.options.d.ts
@@ -0,0 +1,56 @@
+export interface ApplicationOptions {
+ /**
+ * NanoForge application name
+ */
+ name: string;
+
+ /**
+ * NanoForge application version
+ */
+ version: string;
+
+ /**
+ * NanoForge application author
+ */
+ author: string;
+
+ /**
+ * NanoForge application description
+ */
+ description: string;
+
+ /**
+ * NanoForge Application language
+ */
+ language: "js" | "ts";
+
+ /**
+ * With strict mode (TypeScript or JavaScript but only on compatible editor)
+ */
+ strict: boolean;
+
+ /**
+ * Generate lint files
+ */
+ lint: boolean;
+
+ /**
+ * The used package manager
+ */
+ packageManager: "npm" | "yarn" | "pnpm" | "bun";
+
+ /**
+ * Configure a server for the application
+ */
+ server: boolean;
+
+ /**
+ * Add editor dependencies
+ */
+ editor: boolean;
+
+ /**
+ * Add init functions to the application
+ */
+ initFunctions: boolean;
+}
diff --git a/libs/schematics/src/libs/application/application.schema.d.ts b/libs/schematics/src/libs/application/application.schema.d.ts
new file mode 100644
index 0000000..60579c3
--- /dev/null
+++ b/libs/schematics/src/libs/application/application.schema.d.ts
@@ -0,0 +1,61 @@
+export interface ApplicationSchema {
+ /**
+ * NanoForge application name
+ */
+ name: string;
+
+ /**
+ * NanoForge application version
+ */
+ version?: string;
+
+ /**
+ * NanoForge application author
+ */
+ author?: string;
+
+ /**
+ * NanoForge application description
+ */
+ description?: string;
+
+ /**
+ * NanoForge application destination directory
+ */
+ directory?: string;
+
+ /**
+ * NanoForge Application language
+ */
+ language?: "js" | "ts";
+
+ /**
+ * With strict mode (TypeScript or JavaScript but only on compatible editor)
+ */
+ strict?: boolean;
+
+ /**
+ * Generate lint files
+ */
+ lint?: boolean;
+
+ /**
+ * The used package manager
+ */
+ packageManager?: "npm" | "yarn" | "pnpm" | "bun";
+
+ /**
+ * Configure a server for the application
+ */
+ server?: boolean;
+
+ /**
+ * Add editor dependencies
+ */
+ editor: boolean;
+
+ /**
+ * Add init functions to the application
+ */
+ initFunctions: boolean;
+}
diff --git a/libs/schematics/src/libs/application/files/js/.prettierignore b/libs/schematics/src/libs/application/files/js/.prettierignore
new file mode 100644
index 0000000..2f0ede7
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/js/.prettierignore
@@ -0,0 +1,8 @@
+# Ignore files for PNPM, NPM and YARN
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+bun.lock
+
+/node_modules
+/.nanoforge
diff --git a/libs/schematics/src/libs/application/files/js/README.md b/libs/schematics/src/libs/application/files/js/README.md
new file mode 100644
index 0000000..94b63de
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/js/README.md
@@ -0,0 +1,166 @@
+# <%= name %>
+
+---
+
+## Prerequisites
+
+- [Node.js](https://nodejs.org/) 18+
+- Package manager: **<%= packageManager %>**
+- [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `<%= packageManager %> install -g @nanoforge-dev/cli`
+
+---
+
+## Installation
+
+```bash
+<%= packageManager %> install
+```
+
+---
+
+## Development
+
+Start the game in watch mode with hot reload:
+
+```bash
+nf dev
+```
+
+<% if (editor) { %>Open the visual editor (optional — lets you edit entities, components and systems without touching code):
+```bash
+nf editor
+```
+
+<% } %>---
+
+## Project structure
+
+```
+<%= name %>/
+├── .nanoforge/ # NanoForge internal save files (do not edit manually)
+├── client/ # Client-side code (runs in the browser)
+│ ├── static/ # Static assets
+│ ├── components/ # ECS components
+│ ├── systems/ # ECS systems<% if (initFunctions) { %>
+│ └── init/ # Lifecycle hooks (before-init, after-run, …)<% } %><% if (server) { %>
+├── server/ # Server-side code
+│ ├── static/
+│ ├── components/
+│ ├── systems/<% if (initFunctions) { %>
+│ └── init/<% } %><% } %>
+├── package.json
+├── jsconfig.json
+└── nanoforge.config.json # Game configuration
+```
+
+---
+
+## Modifying the game
+
+### Add a component
+
+```bash
+nf generate component --part client
+```
+
+A component is a plain class that holds data for an entity. Edit the generated file in `client/components/`:
+
+```js
+export class MyComponent {
+ constructor(speed) {
+ this.speed = speed;
+ }
+}
+
+export default MyComponent.name;
+```
+
+### Add a system
+
+```bash
+nf generate system --part client
+```
+
+A system is a function that runs every tick and operates on entities that have specific components. Edit the generated file in `client/systems/`:
+
+```js
+import { MyComponent } from "../components/my.component.js";
+
+export const mySystem = (registry, ctx) => {
+ const entities = registry.getZipper([MyComponent]);
+
+ entities.forEach((entity) => {
+ entity.MyComponent.speed += 1;
+ });
+};
+
+export default mySystem.name;
+```
+
+<% if (server) { %>
+### Server-side code
+
+Server components and systems live in `server/` and are imported from `@nanoforge-dev/ecs-server`. They follow the same pattern as the client side.
+
+Configure the server connection in `.env`:
+
+| Variable | Default | Description |
+|---------------------------------------|-------------|---------------------------------|
+| `NANOFORGE_CLIENT_SERVER_ADDRESS` | `127.0.0.1` | Server address (client-side) |
+| `NANOFORGE_CLIENT_SERVER_TCP_PORT` | `4444` | TCP port the client connects to |
+| `NANOFORGE_CLIENT_SERVER_UDP_PORT` | `4445` | UDP port the client connects to |
+| `NANOFORGE_SERVER_LISTENING_TCP_PORT` | `4444` | TCP port the server listens on |
+| `NANOFORGE_SERVER_LISTENING_UDP_PORT` | `4445` | UDP port the server listens on |
+
+<% } %>---
+
+## Build
+
+Compile the game for production:
+
+```bash
+<%= packageManager %> run build
+# or
+nf build
+```
+
+Output is written to the `.nanoforge/` directory.
+
+---
+
+## Run
+
+Start the game server and open it in a browser:
+
+```bash
+<%= packageManager %> run start
+# or
+nf start
+```
+
+<% if (lint) { %>---
+
+## Code quality
+
+```bash
+# Check formatting and linting
+<%= packageManager %> run lint
+
+# Auto-fix formatting and linting
+<%= packageManager %> run format
+```
+
+<% } %>---
+
+## CLI reference
+
+| Command | Description |
+|--------------------------------|-------------------------------------------|
+| `nf dev` | Start in development mode with hot reload |
+| `nf build` | Build for production |
+| `nf start` | Run the built game |
+| `nf generate component ` | Scaffold a new component |
+| `nf generate system ` | Scaffold a new system |<% if (editor) { %>
+| `nf editor` | Open the visual editor |<% } %>
+
+Full CLI documentation:
diff --git a/libs/schematics/src/libs/application/files/js/__dot__env b/libs/schematics/src/libs/application/files/js/__dot__env
new file mode 100644
index 0000000..75b4595
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/js/__dot__env
@@ -0,0 +1,6 @@
+NANOFORGE_CLIENT_SERVER_TCP_PORT=4444
+NANOFORGE_CLIENT_SERVER_UDP_PORT=4445
+NANOFORGE_CLIENT_SERVER_ADDRESS=127.0.0.1
+
+NANOFORGE_SERVER_LISTENING_TCP_PORT=4444
+NANOFORGE_SERVER_LISTENING_UDP_PORT=4445
diff --git a/libs/schematics/src/libs/application/files/js/__dot__gitignore b/libs/schematics/src/libs/application/files/js/__dot__gitignore
new file mode 100644
index 0000000..ba6da87
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/js/__dot__gitignore
@@ -0,0 +1,134 @@
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### JetBrains template
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# File-based project format
+*.iws
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+### NanoForge template
+# Compilation directories
+.nanoforge/client
+.nanoforge/server
diff --git a/libs/schematics/src/libs/application/files/js/eslint.config.js b/libs/schematics/src/libs/application/files/js/eslint.config.js
new file mode 100644
index 0000000..a90e742
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/js/eslint.config.js
@@ -0,0 +1,11 @@
+import pluginJs from "@eslint/js";
+import eslintConfigPrettier from "eslint-config-prettier";
+import globals from "globals";
+
+export default [
+ { files: ["{client,server}/**/*.{js}"] },
+ { languageOptions: { globals: globals.node } },
+
+ pluginJs.configs.recommended,
+ eslintConfigPrettier,
+];
diff --git a/libs/schematics/src/libs/application/files/js/jsconfig.json b/libs/schematics/src/libs/application/files/js/jsconfig.json
new file mode 100644
index 0000000..5b67de8
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/js/jsconfig.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://json.schemastore.org/jsconfig.json",
+ "compilerOptions": {
+ "allowUnreachableCode": <%= !strict %>,
+ "allowUnusedLabels": <%= !strict %>,
+ "exactOptionalPropertyTypes": <%= strict %>,
+ "noFallthroughCasesInSwitch": <%= strict %>,
+ "noImplicitOverride": <%= strict %>,
+ "noImplicitReturns": <%= strict %>,
+ "noPropertyAccessFromIndexSignature": <%= !strict %>,
+ "noUncheckedIndexedAccess": <%= strict %>,
+ "noUnusedLocals": <%= strict %>,
+ "noUnusedParameters": <%= strict %>,
+
+ "allowArbitraryExtensions": false,
+ "allowImportingTsExtensions": false,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "resolvePackageJsonExports": true,
+ "resolvePackageJsonImports": true,
+
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": false,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+
+ "experimentalDecorators": true,
+ "lib": ["ESNext", "esnext.disposable", "DOM"],
+ "target": "ESNext",
+ "useDefineForClassFields": true
+ },
+ "include": ["client/**/*.js", "server/**/*.js"],
+ "exclude": ["node_modules", ".nanoforge"]
+}
diff --git a/libs/schematics/src/libs/application/files/js/package.json.template b/libs/schematics/src/libs/application/files/js/package.json.template
new file mode 100644
index 0000000..4d2bd37
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/js/package.json.template
@@ -0,0 +1,48 @@
+{
+ "name": "<%= name %>",
+ "version": "<%= version %>",
+ "description": "<%= description %>",
+ "author": "<%= author %>",
+ "scripts": {
+ "build": "nf build",
+ "start": "nf start"<% if (lint) { %>,
+ "lint": "prettier --check . && eslint --format=pretty \"{client,server}/**/*.js\"",
+ "format": "prettier --write . && eslint --fix --format=pretty \"{client,server}/**/*.js\""<% } %>
+ },
+ "devDependencies": {<% if (lint) { %>
+ "@eslint/js": "^9.39.0",<% } %>
+ "@nanoforge-dev/asset-manager": "^1",
+ "@nanoforge-dev/cli": "latest",
+ "@nanoforge-dev/common": "^1",
+ "@nanoforge-dev/config": "^1",
+ "@nanoforge-dev/core": "^1",<% if (editor) { %>
+ "@nanoforge-dev/core-editor": "^1",<% } %>
+ "@nanoforge-dev/ecs-client": "^1",<% if (server) { %>
+ "@nanoforge-dev/ecs-server": "^1",<% } %>
+ "@nanoforge-dev/graphics-2d": "^1",<% if (editor) { %>
+ "@nanoforge-dev/graphics-2d-editor": "^1",<% } %>
+ "@nanoforge-dev/input": "^1",
+ "@nanoforge-dev/music": "^1",<% if (server) { %>
+ "@nanoforge-dev/network-client": "^1",
+ "@nanoforge-dev/network-server": "^1",<% } %>
+ "@nanoforge-dev/sound": "^1"<% if (lint) { %>,
+ "eslint": "^9.39.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-prettier": "^5.5.4",
+ "globals": "^16.5.0",
+ "lint-staged": "^16.2.6",
+ "prettier": "^3.6.2"<% } %>
+ },<% if (lint) { %>
+ "lint-staged": {
+ "**/*.js": [
+ "prettier --write"
+ ],
+ "{client,server}/**/*.js": [
+ "eslint --fix"
+ ]
+ },<% } %><% if (packageManager === "pnpm") { %>
+ "pnpm": {
+ "neverBuiltDependencies": []
+ },<% } %>
+ "private": true
+}
diff --git a/libs/schematics/src/libs/application/files/js/pnpm-workspace.yaml.template b/libs/schematics/src/libs/application/files/js/pnpm-workspace.yaml.template
new file mode 100644
index 0000000..236b123
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/js/pnpm-workspace.yaml.template
@@ -0,0 +1,3 @@
+allowBuilds:
+ bun: true<% if (server) { %>
+ wrtc: true<% } %>
diff --git a/libs/schematics/src/libs/application/files/js/prettier.config.js b/libs/schematics/src/libs/application/files/js/prettier.config.js
new file mode 100644
index 0000000..8e5015d
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/js/prettier.config.js
@@ -0,0 +1,6 @@
+export default {
+ useTabs: false,
+ singleQuote: false,
+ trailingComma: "all",
+ printWidth: 100,
+};
diff --git a/libs/schematics/src/libs/application/files/ts/.prettierignore b/libs/schematics/src/libs/application/files/ts/.prettierignore
new file mode 100644
index 0000000..2f0ede7
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/ts/.prettierignore
@@ -0,0 +1,8 @@
+# Ignore files for PNPM, NPM and YARN
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+bun.lock
+
+/node_modules
+/.nanoforge
diff --git a/libs/schematics/src/libs/application/files/ts/README.md b/libs/schematics/src/libs/application/files/ts/README.md
new file mode 100644
index 0000000..44878e2
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/ts/README.md
@@ -0,0 +1,166 @@
+# <%= name %>
+
+---
+
+## Prerequisites
+
+- [Node.js](https://nodejs.org/) 18+
+- Package manager: **<%= packageManager %>**
+- [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `<%= packageManager %> install -g @nanoforge-dev/cli`
+
+---
+
+## Installation
+
+```bash
+<%= packageManager %> install
+```
+
+---
+
+## Development
+
+Start the game in watch mode with hot reload:
+
+```bash
+nf dev
+```
+
+<% if (editor) { %>Open the visual editor (optional — lets you edit entities, components and systems without touching code):
+```bash
+nf editor
+```
+
+<% } %>---
+
+## Project structure
+
+```
+<%= name %>/
+├── .nanoforge/ # NanoForge internal save files (do not edit manually)
+├── client/ # Client-side code (runs in the browser)
+│ ├── static/ # Static assets
+│ ├── components/ # ECS components
+│ ├── systems/ # ECS systems<% if (initFunctions) { %>
+│ └── init/ # Lifecycle hooks (before-init, after-run, …)<% } %><% if (server) { %>
+├── server/ # Server-side code
+│ ├── static/
+│ ├── components/
+│ ├── systems/<% if (initFunctions) { %>
+│ └── init/<% } %><% } %>
+├── package.json
+├── tsconfig.json
+└── nanoforge.config.json # Game configuration
+```
+
+---
+
+## Modifying the game
+
+### Add a component
+
+```bash
+nf generate component --part client
+```
+
+A component is a plain class that holds data for an entity. Edit the generated file in `client/components/`:
+
+```ts
+export class MyComponent {
+ constructor(public speed: number) {}
+}
+
+export default MyComponent.name;
+```
+
+### Add a system
+
+```bash
+nf generate system --part client
+```
+
+A system is a function that runs every tick and operates on entities that have specific components. Edit the generated file in `client/systems/`:
+
+```ts
+import { type Context } from "@nanoforge-dev/common";
+import { type Registry } from "@nanoforge-dev/ecs-client";
+import { MyComponent } from "../components/my.component";
+
+export const mySystem = (registry: Registry, ctx: Context) => {
+ const entities = registry.getZipper([MyComponent]);
+
+ entities.forEach((entity) => {
+ entity.MyComponent.speed += 1;
+ });
+};
+
+export default mySystem.name;
+```
+
+<% if (server) { %>
+### Server-side code
+
+Server components and systems live in `server/` and are imported from `@nanoforge-dev/ecs-server`. They follow the same pattern as the client side.
+
+Configure the server connection in `.env`:
+
+| Variable | Default | Description |
+|---------------------------------------|-------------|---------------------------------|
+| `NANOFORGE_CLIENT_SERVER_ADDRESS` | `127.0.0.1` | Server address (client-side) |
+| `NANOFORGE_CLIENT_SERVER_TCP_PORT` | `4444` | TCP port the client connects to |
+| `NANOFORGE_CLIENT_SERVER_UDP_PORT` | `4445` | UDP port the client connects to |
+| `NANOFORGE_SERVER_LISTENING_TCP_PORT` | `4444` | TCP port the server listens on |
+| `NANOFORGE_SERVER_LISTENING_UDP_PORT` | `4445` | UDP port the server listens on |
+
+<% } %>---
+
+## Build
+
+Compile the game for production:
+
+```bash
+<%= packageManager %> run build
+# or
+nf build
+```
+
+Output is written to the `.nanoforge/` directory.
+
+---
+
+## Run
+
+Start the game server and open it in a browser:
+
+```bash
+<%= packageManager %> run start
+# or
+nf start
+```
+
+<% if (lint) { %>---
+
+## Code quality
+
+```bash
+# Check formatting and linting
+<%= packageManager %> run lint
+
+# Auto-fix formatting and linting
+<%= packageManager %> run format
+```
+
+<% } %>---
+
+## CLI reference
+
+| Command | Description |
+|--------------------------------|-------------------------------------------|
+| `nf dev` | Start in development mode with hot reload |
+| `nf build` | Build for production |
+| `nf start` | Run the built game |
+| `nf generate component ` | Scaffold a new component |
+| `nf generate system ` | Scaffold a new system |<% if (editor) { %>
+| `nf editor` | Open the visual editor |<% } %>
+
+Full CLI documentation:
diff --git a/libs/schematics/src/libs/application/files/ts/__dot__env b/libs/schematics/src/libs/application/files/ts/__dot__env
new file mode 100644
index 0000000..75b4595
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/ts/__dot__env
@@ -0,0 +1,6 @@
+NANOFORGE_CLIENT_SERVER_TCP_PORT=4444
+NANOFORGE_CLIENT_SERVER_UDP_PORT=4445
+NANOFORGE_CLIENT_SERVER_ADDRESS=127.0.0.1
+
+NANOFORGE_SERVER_LISTENING_TCP_PORT=4444
+NANOFORGE_SERVER_LISTENING_UDP_PORT=4445
diff --git a/libs/schematics/src/libs/application/files/ts/__dot__gitignore b/libs/schematics/src/libs/application/files/ts/__dot__gitignore
new file mode 100644
index 0000000..ba6da87
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/ts/__dot__gitignore
@@ -0,0 +1,134 @@
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### JetBrains template
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# File-based project format
+*.iws
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+### NanoForge template
+# Compilation directories
+.nanoforge/client
+.nanoforge/server
diff --git a/libs/schematics/src/libs/application/files/ts/eslint.config.js b/libs/schematics/src/libs/application/files/ts/eslint.config.js
new file mode 100644
index 0000000..2f3639d
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/ts/eslint.config.js
@@ -0,0 +1,15 @@
+import pluginJs from "@eslint/js";
+import eslintConfigPrettier from "eslint-config-prettier";
+import globals from "globals";
+import tseslint from "typescript-eslint";
+
+export default [
+ { files: ["{client,server}/**/*.{ts}"] },
+ { languageOptions: { globals: globals.node } },
+
+ pluginJs.configs.recommended,
+ ...tseslint.configs.recommended,
+ ...tseslint.configs.strict,
+ eslintConfigPrettier,
+ { ignores: ["**/*.js", "**/*.d.ts"] },
+];
diff --git a/libs/schematics/src/libs/application/files/ts/package.json.template b/libs/schematics/src/libs/application/files/ts/package.json.template
new file mode 100644
index 0000000..3f4a160
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/ts/package.json.template
@@ -0,0 +1,52 @@
+{
+ "name": "<%= name %>",
+ "version": "<%= version %>",
+ "description": "<%= description %>",
+ "author": "<%= author %>",
+ "scripts": {
+ "build": "nf build",
+ "start": "nf start"<% if (lint) { %>,
+ "lint": "prettier --check . && eslint --format=pretty \"{client,server}/**/*.ts\"",
+ "format": "prettier --write . && eslint --fix --format=pretty \"{client,server}/**/*.ts\""<% } %>
+ },
+ "devDependencies": {<% if (lint) { %>
+ "@eslint/js": "^9.39.0",<% } %>
+ "@nanoforge-dev/asset-manager": "^1",
+ "@nanoforge-dev/cli": "latest",
+ "@nanoforge-dev/common": "^1",
+ "@nanoforge-dev/config": "^1",
+ "@nanoforge-dev/core": "^1",<% if (editor) { %>
+ "@nanoforge-dev/core-editor": "^1",<% } %>
+ "@nanoforge-dev/ecs-client": "^1",<% if (server) { %>
+ "@nanoforge-dev/ecs-server": "^1",<% } %>
+ "@nanoforge-dev/graphics-2d": "^1",<% if (editor) { %>
+ "@nanoforge-dev/graphics-2d-editor": "^1",<% } %>
+ "@nanoforge-dev/input": "^1",
+ "@nanoforge-dev/music": "^1",<% if (server) { %>
+ "@nanoforge-dev/network-client": "^1",
+ "@nanoforge-dev/network-server": "^1",<% } %>
+ "@nanoforge-dev/sound": "^1",<% if (lint) { %>
+ "@typescript-eslint/eslint-plugin": "^8.46.2",
+ "@typescript-eslint/parser": "^8.46.2",
+ "eslint": "^9.39.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-prettier": "^5.5.4",
+ "globals": "^16.5.0",
+ "lint-staged": "^16.2.6",
+ "prettier": "^3.6.2",<% } %>
+ "typescript": "^5.9.3"<% if (lint) { %>,
+ "typescript-eslint": "^8.46.2"<% } %>
+ },<% if (lint) { %>
+ "lint-staged": {
+ "**/*.ts": [
+ "prettier --write"
+ ],
+ "{client,server}/**/*.ts": [
+ "eslint --fix"
+ ]
+ },<% } %><% if (packageManager === "pnpm") { %>
+ "pnpm": {
+ "neverBuiltDependencies": []
+ },<% } %>
+ "private": true
+}
diff --git a/libs/schematics/src/libs/application/files/ts/pnpm-workspace.yaml.template b/libs/schematics/src/libs/application/files/ts/pnpm-workspace.yaml.template
new file mode 100644
index 0000000..236b123
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/ts/pnpm-workspace.yaml.template
@@ -0,0 +1,3 @@
+allowBuilds:
+ bun: true<% if (server) { %>
+ wrtc: true<% } %>
diff --git a/libs/schematics/src/libs/application/files/ts/prettier.config.js b/libs/schematics/src/libs/application/files/ts/prettier.config.js
new file mode 100644
index 0000000..8e5015d
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/ts/prettier.config.js
@@ -0,0 +1,6 @@
+export default {
+ useTabs: false,
+ singleQuote: false,
+ trailingComma: "all",
+ printWidth: 100,
+};
diff --git a/libs/schematics/src/libs/application/files/ts/tsconfig.json b/libs/schematics/src/libs/application/files/ts/tsconfig.json
new file mode 100644
index 0000000..cdbe237
--- /dev/null
+++ b/libs/schematics/src/libs/application/files/ts/tsconfig.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ // Mapped from https://www.typescriptlang.org/tsconfig
+ "compilerOptions": {
+ // Type Checking
+ "allowUnreachableCode": <%= !strict %>,
+ "allowUnusedLabels": <%= !strict %>,
+ "exactOptionalPropertyTypes": <%= strict %>,
+ "noFallthroughCasesInSwitch": <%= strict %>,
+ "noImplicitOverride": <%= strict %>,
+ "noImplicitReturns": <%= strict %>,
+ "noPropertyAccessFromIndexSignature": <%= !strict %>,
+ "noUncheckedIndexedAccess": <%= strict %>,
+ "noUnusedLocals": <%= strict %>,
+ "noUnusedParameters": <%= strict %>,
+ "strict": <%= strict %>,
+
+ // Modules
+ "allowArbitraryExtensions": false,
+ "allowImportingTsExtensions": false,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "resolvePackageJsonExports": true,
+ "resolvePackageJsonImports": true,
+
+ // Interop Constraints
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": false,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+
+ // Language and Environment
+ "experimentalDecorators": true,
+ "lib": ["ESNext", "esnext.disposable", "DOM"],
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ },
+ "include": ["client/**/*.ts", "server/**/*.ts"],
+ "exclude": ["node_modules", ".nanoforge"]
+}
diff --git a/libs/schematics/src/libs/application/schema.json b/libs/schematics/src/libs/application/schema.json
new file mode 100644
index 0000000..d570149
--- /dev/null
+++ b/libs/schematics/src/libs/application/schema.json
@@ -0,0 +1,70 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "SchematicsNanoForgeBaseApplication",
+ "title": "NanoForge Base Application Options Schema",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the application",
+ "default": "nanoforge-app"
+ },
+ "version": {
+ "type": "string",
+ "description": "NanoForge application version",
+ "default": "0.0.0"
+ },
+ "author": {
+ "type": "string",
+ "description": "NanoForge application author",
+ "default": ""
+ },
+ "description": {
+ "type": "string",
+ "description": "NanoForge application description",
+ "default": ""
+ },
+ "directory": {
+ "type": "string",
+ "description": "NanoForge application destination directory"
+ },
+ "language": {
+ "type": "string",
+ "enum": ["ts", "js"],
+ "description": "NanoForge application language",
+ "default": "ts"
+ },
+ "strict": {
+ "type": "boolean",
+ "description": "With strict mode (TypeScript or JavaScript but only on compatible editor)",
+ "default": true
+ },
+ "lint": {
+ "type": "boolean",
+ "description": "Generate lint files",
+ "default": true
+ },
+ "packageManager": {
+ "type": "string",
+ "enum": ["npm", "yarn", "pnpm", "bun"],
+ "description": "NanoForge application package manager",
+ "default": "npm"
+ },
+ "server": {
+ "type": "boolean",
+ "description": "Configure a server for the application",
+ "default": false
+ },
+ "editor": {
+ "type": "boolean",
+ "description": "Add editor dependencies",
+ "default": false
+ },
+ "initFunctions": {
+ "type": "boolean",
+ "description": "Add init functions to the application",
+ "default": false
+ }
+ },
+ "required": ["name"]
+}
diff --git a/libs/schematics/src/libs/component/component.factory.ts b/libs/schematics/src/libs/component/component.factory.ts
new file mode 100644
index 0000000..da8ba33
--- /dev/null
+++ b/libs/schematics/src/libs/component/component.factory.ts
@@ -0,0 +1,41 @@
+import { type Path, join, normalize, strings } from "@angular-devkit/core";
+import {
+ type Rule,
+ type Source,
+ apply,
+ mergeWith,
+ move,
+ template,
+ url,
+} from "@angular-devkit/schematics";
+
+import { toKebabCase, toPascalCase } from "@utils/formatting";
+
+import { type ComponentOptions } from "./component.options";
+import { type ComponentSchema } from "./component.schema";
+
+const transform = (schema: ComponentSchema): ComponentOptions => {
+ return {
+ part: schema.part,
+ className: toPascalCase(schema.name),
+ fileName: toKebabCase(schema.name),
+ };
+};
+
+const generate = (options: ComponentOptions, path: string, language: "ts" | "js"): Source => {
+ const rules = [
+ template({
+ ...strings,
+ ...options,
+ }),
+ move(normalize(path)),
+ ];
+
+ return apply(url(join("./files" as Path, language)), rules);
+};
+
+export const main = (schema: ComponentSchema): Rule => {
+ const options = transform(schema);
+
+ return mergeWith(generate(options, schema.directory, schema.language));
+};
diff --git a/libs/schematics/src/libs/component/component.options.d.ts b/libs/schematics/src/libs/component/component.options.d.ts
new file mode 100644
index 0000000..270628d
--- /dev/null
+++ b/libs/schematics/src/libs/component/component.options.d.ts
@@ -0,0 +1,16 @@
+export interface ComponentOptions {
+ /**
+ * The part of the application to generate
+ */
+ part: "client" | "server";
+
+ /**
+ * Component class name
+ */
+ className: string;
+
+ /**
+ * Component filename
+ */
+ fileName: string;
+}
diff --git a/libs/schematics/src/libs/component/component.schema.d.ts b/libs/schematics/src/libs/component/component.schema.d.ts
new file mode 100644
index 0000000..4285128
--- /dev/null
+++ b/libs/schematics/src/libs/component/component.schema.d.ts
@@ -0,0 +1,21 @@
+export interface ComponentSchema {
+ /**
+ * Component destination directory
+ */
+ name: string;
+
+ /**
+ * Component destination directory
+ */
+ directory: string;
+
+ /**
+ * The part of the application to generate
+ */
+ part: "client" | "server";
+
+ /**
+ * NanoForge Application language
+ */
+ language: "js" | "ts";
+}
diff --git a/libs/schematics/src/libs/component/files/js/__fileName__.component.js b/libs/schematics/src/libs/component/files/js/__fileName__.component.js
new file mode 100644
index 0000000..10ad6b3
--- /dev/null
+++ b/libs/schematics/src/libs/component/files/js/__fileName__.component.js
@@ -0,0 +1,72 @@
+/**
+ * @typedef {import("@nanoforge-dev/ecs-<%= part %>").EditorComponentManifest} EditorComponentManifest
+ */
+
+export class <%= className %>Component {
+ static name = "<%= className %>Component";
+ name = this.constructor.name;
+
+ paramA;
+ paramB;
+ paramC;
+
+ /**
+ * <%= className %> component constructor
+ * @param {string} paramA
+ * @param {number} paramB
+ * @param {boolean} [paramC = false]
+ */
+ constructor(paramA, paramB, paramC = false) {
+ this.paramA = paramA;
+ this.paramB = paramB;
+ this.paramC = paramC;
+ }
+
+ get foo() {
+ return "bar";
+ }
+
+ get paramAOrDefault() {
+ return this.paramC ? this.paramA : "default";
+ }
+
+ addOne() {
+ this.paramB += 1;
+ }
+}
+
+// * Required to generate code
+export default <%= className %>Component.name;
+
+/**
+ * Editor component manifest
+ * * Required for the editor to display the component and generate code
+ * @type {EditorComponentManifest}
+ */
+export const EDITOR_COMPONENT_MANIFEST = {
+ name: "<%= className %>",
+ description: "<%= className %> component description",
+ params: [
+ {
+ type: "string",
+ name: "paramA",
+ description: "Param A description",
+ example: "Example value",
+ },
+ {
+ type: "number",
+ name: "paramB",
+ description: "Param B description",
+ example: 3,
+ },
+ {
+ type: "boolean",
+ name: "paramC",
+ description: "Param C description",
+ example: true,
+ default: false,
+ // Not required because it has a default value
+ optional: true,
+ },
+ ],
+};
diff --git a/libs/schematics/src/libs/component/files/ts/__fileName__.component.ts b/libs/schematics/src/libs/component/files/ts/__fileName__.component.ts
new file mode 100644
index 0000000..2be836f
--- /dev/null
+++ b/libs/schematics/src/libs/component/files/ts/__fileName__.component.ts
@@ -0,0 +1,55 @@
+import { type EditorComponentManifest } from "@nanoforge-dev/ecs-<%= part %>";
+
+export class <%= className %>Component {
+ name = this.constructor.name;
+
+ constructor(
+ public paramA: string,
+ public paramB: number,
+ public paramC: boolean = false,
+ ) {}
+
+ get foo() {
+ return "bar";
+ }
+
+ get paramAOrDefault() {
+ return this.paramC ? this.paramA : "default";
+ }
+
+ addOne() {
+ this.paramB += 1;
+ }
+}
+
+// * Required to generate code
+export default <%= className %>Component.name;
+
+// * Required for the editor to display the component and generate code
+export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = {
+ name: "<%= className %>",
+ description: "<%= className %> component description",
+ params: [
+ {
+ type: "string",
+ name: "paramA",
+ description: "Param A description",
+ example: "Example value",
+ },
+ {
+ type: "number",
+ name: "paramB",
+ description: "Param B description",
+ example: 3,
+ },
+ {
+ type: "boolean",
+ name: "paramC",
+ description: "Param C description",
+ example: true,
+ default: false,
+ // Not required because it has a default value
+ optional: true,
+ },
+ ],
+};
diff --git a/libs/schematics/src/libs/component/schema.json b/libs/schematics/src/libs/component/schema.json
new file mode 100644
index 0000000..a5a6d7f
--- /dev/null
+++ b/libs/schematics/src/libs/component/schema.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "SchematicsNanoForgeComponent",
+ "title": "NanoForge Component Options Schema",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Component name",
+ "default": "example"
+ },
+ "directory": {
+ "type": "string",
+ "description": "Component destination directory",
+ "default": "."
+ },
+ "part": {
+ "type": "string",
+ "enum": ["client", "server"],
+ "description": "The part of the application to generate"
+ },
+ "language": {
+ "type": "string",
+ "enum": ["ts", "js"],
+ "description": "NanoForge application language",
+ "default": "ts"
+ }
+ },
+ "required": ["part"]
+}
diff --git a/libs/schematics/src/libs/configuration/configuration.factory.ts b/libs/schematics/src/libs/configuration/configuration.factory.ts
new file mode 100644
index 0000000..a4a0a17
--- /dev/null
+++ b/libs/schematics/src/libs/configuration/configuration.factory.ts
@@ -0,0 +1,94 @@
+import { type Path, join, normalize, strings } from "@angular-devkit/core";
+import {
+ type Rule,
+ type Source,
+ type Tree,
+ apply,
+ branchAndMerge,
+ chain,
+ mergeWith,
+ move,
+ template,
+ url,
+} from "@angular-devkit/schematics";
+
+import { ConfigDeclarator } from "@utils/config/config.declarator";
+import { ConfigFinder } from "@utils/config/config.finder";
+import { type Config } from "@utils/config/config.type";
+import { type DeepPartial } from "@utils/types";
+
+import { type ConfigurationOptions } from "./configuration.options";
+import { type ConfigurationSchema } from "./configuration.schema";
+
+const transform = (schema: ConfigurationSchema): ConfigurationOptions => {
+ void schema;
+
+ return {};
+};
+
+const generate = (options: ConfigurationOptions, path: string): Source => {
+ return apply(url(join("./files" as Path)), [
+ template({
+ ...strings,
+ ...options,
+ }),
+ move(normalize(path)),
+ ]);
+};
+
+const addConfiguration = (options: DeepPartial, path: Path) => {
+ return (tree: Tree) => {
+ const config = new ConfigFinder(tree).find(path);
+ if (!config) return tree;
+
+ const content = tree.read(config)?.toString();
+ const declarator = new ConfigDeclarator();
+
+ if (!content) return tree;
+
+ tree.overwrite(config, declarator.declare(content, options));
+ return tree;
+ };
+};
+
+const getConfig = (schema: ConfigurationSchema): DeepPartial => {
+ const res: DeepPartial = {
+ name: schema.name,
+ language: schema.language,
+ client: {
+ enable: true,
+ },
+ server: {
+ enable: schema.server ?? false,
+ },
+ };
+
+ if (schema.initFunctions) {
+ res["initFunctions"] = true;
+ }
+
+ if (schema.language === "js") {
+ if ("client" in res && res.client) {
+ res.client["build"] = { entry: "client/main.js" };
+ res.client["editor"] = { entry: ".nanoforge/editor/client/main.js" };
+ }
+ if (schema.server && "server" in res && res.server) {
+ res.server["build"] = { entry: "server/main.js" };
+ res.server["editor"] = { entry: ".nanoforge/editor/client/main.js" };
+ }
+ }
+
+ return res;
+};
+
+export const main = (schema: ConfigurationSchema): Rule => {
+ const options = transform(schema);
+ const directory = schema.directory;
+
+ return branchAndMerge(
+ chain([
+ mergeWith(generate(options, directory)),
+ addConfiguration(getConfig(schema), directory as Path),
+ ]),
+ );
+};
diff --git a/libs/schematics/src/libs/configuration/configuration.options.d.ts b/libs/schematics/src/libs/configuration/configuration.options.d.ts
new file mode 100644
index 0000000..3061ac5
--- /dev/null
+++ b/libs/schematics/src/libs/configuration/configuration.options.d.ts
@@ -0,0 +1 @@
+export interface ConfigurationOptions {}
diff --git a/libs/schematics/src/libs/configuration/configuration.schema.d.ts b/libs/schematics/src/libs/configuration/configuration.schema.d.ts
new file mode 100644
index 0000000..fb569f6
--- /dev/null
+++ b/libs/schematics/src/libs/configuration/configuration.schema.d.ts
@@ -0,0 +1,26 @@
+export interface ConfigurationSchema {
+ /**
+ * The name of the application
+ */
+ name: string;
+
+ /**
+ * NanoForge application destination directory
+ */
+ directory: string;
+
+ /**
+ * Configure a server for the application
+ */
+ server: boolean;
+
+ /**
+ * NanoForge Application language
+ */
+ language: "js" | "ts";
+
+ /**
+ * Add init functions to the application
+ */
+ initFunctions: boolean;
+}
diff --git a/libs/schematics/src/libs/configuration/files/nanoforge.config.json b/libs/schematics/src/libs/configuration/files/nanoforge.config.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/libs/schematics/src/libs/configuration/files/nanoforge.config.json
@@ -0,0 +1 @@
+{}
diff --git a/libs/schematics/src/libs/configuration/schema.json b/libs/schematics/src/libs/configuration/schema.json
new file mode 100644
index 0000000..427d3da
--- /dev/null
+++ b/libs/schematics/src/libs/configuration/schema.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "SchematicsNanoForgeConfiguration",
+ "title": "NanoForge Base Application Options Schema",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the application",
+ "default": "nanoforge-app"
+ },
+ "directory": {
+ "type": "string",
+ "description": "NanoForge application destination directory",
+ "default": "."
+ },
+ "server": {
+ "type": "boolean",
+ "description": "Server configuration",
+ "default": false
+ },
+ "language": {
+ "type": "string",
+ "enum": ["ts", "js"],
+ "description": "NanoForge application language",
+ "default": "ts"
+ },
+ "initFunctions": {
+ "type": "boolean",
+ "description": "Add init functions to the main application",
+ "default": false
+ }
+ }
+}
diff --git a/libs/schematics/src/libs/docker/docker.factory.ts b/libs/schematics/src/libs/docker/docker.factory.ts
new file mode 100644
index 0000000..34909a4
--- /dev/null
+++ b/libs/schematics/src/libs/docker/docker.factory.ts
@@ -0,0 +1,36 @@
+import { type Path, normalize, strings } from "@angular-devkit/core";
+import {
+ type Rule,
+ type Source,
+ apply,
+ mergeWith,
+ move,
+ template,
+ url,
+} from "@angular-devkit/schematics";
+
+import { DEFAULT_PACKAGE_MANAGER } from "~/defaults";
+
+import { type DockerOptions } from "./docker.options";
+import { type DockerSchema } from "./docker.schema";
+
+const transform = (schema: DockerSchema): DockerOptions => {
+ return {
+ packageManager: schema.packageManager ?? DEFAULT_PACKAGE_MANAGER,
+ };
+};
+
+const generate = (options: DockerOptions, path: string): Source => {
+ return apply(url("./files" as Path), [
+ template({
+ ...strings,
+ ...options,
+ }),
+ move(normalize(path)),
+ ]);
+};
+
+export const main = (schema: DockerSchema): Rule => {
+ const options = transform(schema);
+ return mergeWith(generate(options, schema.directory));
+};
diff --git a/libs/schematics/src/libs/docker/docker.options.d.ts b/libs/schematics/src/libs/docker/docker.options.d.ts
new file mode 100644
index 0000000..489d255
--- /dev/null
+++ b/libs/schematics/src/libs/docker/docker.options.d.ts
@@ -0,0 +1,6 @@
+export interface DockerOptions {
+ /**
+ * NanoForge application package manager
+ */
+ packageManager: "npm" | "yarn" | "pnpm" | "bun";
+}
diff --git a/libs/schematics/src/libs/docker/docker.schema.d.ts b/libs/schematics/src/libs/docker/docker.schema.d.ts
new file mode 100644
index 0000000..f3c29da
--- /dev/null
+++ b/libs/schematics/src/libs/docker/docker.schema.d.ts
@@ -0,0 +1,11 @@
+export interface DockerSchema {
+ /**
+ * Target directory for generated Docker files
+ */
+ directory: string;
+
+ /**
+ * NanoForge application package manager
+ */
+ packageManager: "npm" | "yarn" | "pnpm" | "bun";
+}
diff --git a/libs/schematics/src/libs/docker/files/.dockerignore b/libs/schematics/src/libs/docker/files/.dockerignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/libs/schematics/src/libs/docker/files/.dockerignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/libs/schematics/src/libs/docker/files/Dockerfile b/libs/schematics/src/libs/docker/files/Dockerfile
new file mode 100644
index 0000000..62571d5
--- /dev/null
+++ b/libs/schematics/src/libs/docker/files/Dockerfile
@@ -0,0 +1,25 @@
+<% if (packageManager === 'bun') { %>FROM oven/bun:1.3<% } else { %>FROM node:25-slim<% } %>
+<% if (packageManager === 'pnpm') { %>
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+RUN npm install -g pnpm
+<% } %><% if (packageManager === 'yarn') { %>
+RUN npm install -g yarn
+<% } %>
+WORKDIR /app
+
+COPY package.json ./
+<% if (packageManager === 'npm') { %>COPY package-lock.json ./
+<% } else if (packageManager === 'yarn') { %>COPY yarn.lock ./
+<% } else if (packageManager === 'pnpm') { %>COPY pnpm-lock.yaml ./
+<% } else if (packageManager === 'bun') { %>COPY bun.lock ./
+<% } %>
+<% if (packageManager === 'npm') { %>RUN npm install --no-lockfile
+<% } else if (packageManager === 'pnpm') { %>RUN pnpm install --frozen-lockfile --allow-build=bun
+<% } else { %>RUN <%= packageManager %> install --frozen-lockfile
+<% } %>
+COPY . .
+
+RUN <%= packageManager %> run build
+
+CMD ["<%= packageManager %>", "run", "start"]
diff --git a/libs/schematics/src/libs/docker/schema.json b/libs/schematics/src/libs/docker/schema.json
new file mode 100644
index 0000000..5055696
--- /dev/null
+++ b/libs/schematics/src/libs/docker/schema.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "SchematicsNanoForgeDocker",
+ "title": "NanoForge Base Docker Options Schema",
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string",
+ "description": "NanoForge application destination directory",
+ "default": "."
+ },
+ "packageManager": {
+ "type": "string",
+ "enum": ["npm", "yarn", "pnpm", "bun"],
+ "description": "NanoForge application package manager",
+ "default": "npm"
+ }
+ }
+}
diff --git a/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/client.save.json b/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/client.save.json
new file mode 100644
index 0000000..843e417
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/client.save.json
@@ -0,0 +1,77 @@
+{
+ "libraries": [
+ {
+ "id": "assetManagerLibrary",
+ "type": "asset-manager",
+ "name": "AssetManagerLibrary",
+ "path": "@nanoforge-dev/asset-manager",
+ "params": []
+ },
+ {
+ "id": "ecsLibrary",
+ "type": "component-system",
+ "name": "ECSClientLibrary",
+ "path": "@nanoforge-dev/ecs-client",
+ "params": []
+ },
+ {
+ "id": "graphicsLibrary",
+ "type": "graphics",
+ "name": "Graphics2DLibrary",
+ "path": "@nanoforge-dev/graphics-2d",
+ "params": []
+ },
+ {
+ "id": "inputLibrary",
+ "type": "input",
+ "name": "InputLibrary",
+ "path": "@nanoforge-dev/input",
+ "params": []
+ },
+ {
+ "id": "musicLibrary",
+ "type": "music",
+ "name": "MusicLibrary",
+ "path": "@nanoforge-dev/music",
+ "params": []
+ },<% if (server) { %>
+ {
+ "id": "networkLibrary",
+ "type": "network",
+ "name": "NetworkClientLibrary",
+ "path": "@nanoforge-dev/network-client",
+ "params": []
+ },<% } %>
+ {
+ "id": "soundLibrary",
+ "type": "sound",
+ "name": "SoundLibrary",
+ "path": "@nanoforge-dev/sound",
+ "params": []
+ }
+ ],
+ "components": [
+ {
+ "name": "ExampleComponent",
+ "path": "./components/example.component.js",
+ "paramsNames": ["paramA", "paramB", "paramC"]
+ }
+ ],
+ "systems": [
+ {
+ "name": "exampleSystem",
+ "path": "./systems/example.system.js"
+ }
+ ],
+ "entities": [
+ {
+ "id": "exampleEntity",
+ "components": {
+ "ExampleComponent": {
+ "paramA": "example",
+ "paramB": 10
+ }
+ }
+ }
+ ]
+}
diff --git a/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/server.save.json b/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/server.save.json
new file mode 100644
index 0000000..ba77067
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/server.save.json
@@ -0,0 +1,49 @@
+{
+ "libraries": [
+ {
+ "id": "assetManagerLibrary",
+ "type": "asset-manager",
+ "name": "AssetManagerLibrary",
+ "path": "@nanoforge-dev/asset-manager",
+ "params": []
+ },
+ {
+ "id": "ecsLibrary",
+ "type": "component-system",
+ "name": "ECSServerLibrary",
+ "path": "@nanoforge-dev/ecs-server",
+ "params": []
+ }<% if (server) { %>,
+ {
+ "id": "networkLibrary",
+ "type": "network",
+ "name": "NetworkServerLibrary",
+ "path": "@nanoforge-dev/network-server",
+ "params": []
+ }<% } %>
+ ],
+ "components": [
+ {
+ "name": "ExampleComponent",
+ "path": "./components/example.component.js",
+ "paramsNames": ["paramA", "paramB", "paramC"]
+ }
+ ],
+ "systems": [
+ {
+ "name": "exampleSystem",
+ "path": "./systems/example.system.js"
+ }
+ ],
+ "entities": [
+ {
+ "id": "exampleEntity",
+ "components": {
+ "ExampleComponent": {
+ "paramA": "example",
+ "paramB": 10
+ }
+ }
+ }
+ ]
+}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/components/example.component.js b/libs/schematics/src/libs/part-base/files/js/__part__/components/example.component.js
new file mode 100644
index 0000000..bf9728b
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/js/__part__/components/example.component.js
@@ -0,0 +1,72 @@
+/**
+ * @typedef {import("@nanoforge-dev/ecs-<%= part %>").EditorComponentManifest} EditorComponentManifest
+ */
+
+export class ExampleComponent {
+ static name = "ExampleComponent";
+ name = this.constructor.name;
+
+ paramA;
+ paramB;
+ paramC;
+
+ /**
+ * Example component constructor
+ * @param {string} paramA
+ * @param {number} paramB
+ * @param {boolean} [paramC = false]
+ */
+ constructor(paramA, paramB, paramC = false) {
+ this.paramA = paramA;
+ this.paramB = paramB;
+ this.paramC = paramC;
+ }
+
+ get foo() {
+ return "bar";
+ }
+
+ get paramAOrDefault() {
+ return this.paramC ? this.paramA : "default";
+ }
+
+ addOne() {
+ this.paramB += 1;
+ }
+}
+
+// * Required to generate code
+export default ExampleComponent.name;
+
+/**
+ * Editor component manifest
+ * * Required for the editor to display the component and generate code
+ * @type {EditorComponentManifest}
+ */
+export const EDITOR_COMPONENT_MANIFEST = {
+ name: "Example",
+ description: "Example component description",
+ params: [
+ {
+ type: "string",
+ name: "paramA",
+ description: "Param A description",
+ example: "Example value",
+ },
+ {
+ type: "number",
+ name: "paramB",
+ description: "Param B description",
+ example: 3,
+ },
+ {
+ type: "boolean",
+ name: "paramC",
+ description: "Param C description",
+ example: true,
+ default: false,
+ // Not required because it has a default value
+ optional: true,
+ },
+ ],
+};
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/after-init.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/after-init.js
new file mode 100644
index 0000000..74d2124
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/js/__part__/init/after-init.js
@@ -0,0 +1,12 @@
+/**
+ * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
+ */
+
+/**
+ * Actions that are executed after the application initialization
+ * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
+ * @returns {Promise}
+ */
+export async function afterInit(app) {
+ // Actions after app initialization
+}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/after-registry-init.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/after-registry-init.js
new file mode 100644
index 0000000..8de3bf6
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/js/__part__/init/after-registry-init.js
@@ -0,0 +1,14 @@
+/**
+ * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
+ * @typedef {import("@nanoforge-dev/ecs").Registry} Registry
+ */
+
+/**
+ * Actions that are executed after the registry components, entities and systems initialization
+ * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
+ * @param {Registry} registry - ECS registry instance
+ * @returns {Promise}
+ */
+export async function afterRegistryInit(app, registry) {
+ // Actions after registry components, entities and systems initialization
+}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/after-run.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/after-run.js
new file mode 100644
index 0000000..e21bbca
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/js/__part__/init/after-run.js
@@ -0,0 +1,12 @@
+/**
+ * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
+ */
+
+/**
+ * Actions that are executed when the application ended
+ * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
+ * @returns {Promise}
+ */
+export async function afterRun(app) {
+ // Actions after app ended
+}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/before-init.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/before-init.js
new file mode 100644
index 0000000..602b388
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/js/__part__/init/before-init.js
@@ -0,0 +1,13 @@
+/**
+ * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
+ */
+
+/**
+ * Actions that are executed before the application initialization
+ * But are executed after base libraries addition
+ * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
+ * @returns {Promise}
+ */
+export async function beforeInit(app) {
+ // Actions before app initialization
+}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/before-registry-init.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/before-registry-init.js
new file mode 100644
index 0000000..ae6c374
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/js/__part__/init/before-registry-init.js
@@ -0,0 +1,14 @@
+/**
+ * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
+ * @typedef {import("@nanoforge-dev/ecs").Registry} Registry
+ */
+
+/**
+ * Actions that are executed before the registry components, entities and systems initialization
+ * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
+ * @param {Registry} registry - ECS registry instance
+ * @returns {Promise}
+ */
+export async function beforeRegistryInit(app, registry) {
+ // Actions before registry components, entities and systems initialization
+}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/before-run.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/before-run.js
new file mode 100644
index 0000000..e77e47a
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/js/__part__/init/before-run.js
@@ -0,0 +1,12 @@
+/**
+ * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
+ */
+
+/**
+ * Actions that are executed before the application started
+ * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
+ * @returns {Promise}
+ */
+export async function beforeRun(app) {
+ // Actions before app started
+}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/systems/example.system.js b/libs/schematics/src/libs/part-base/files/js/__part__/systems/example.system.js
new file mode 100644
index 0000000..c178bfa
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/js/__part__/systems/example.system.js
@@ -0,0 +1,44 @@
+/**
+ * @typedef {import("@nanoforge-dev/common").Context} Context
+ * @typedef {import("@nanoforge-dev/ecs-<%= part %>").EditorSystemManifest} EditorSystemManifest
+ * @typedef {import("@nanoforge-dev/ecs-<%= part %>").Registry} Registry
+ */
+
+import { ExampleComponent } from "../components/example.component";
+
+/**
+ * Example system
+ * This system end the game when paramB reaches 0 for any entity with ExampleComponent
+ * @param {Registry} registry - ECS registry instance
+ * @param {Context} ctx - Nanoforge <%= part %> instance
+ */
+export const exampleSystem = (registry, ctx) => {
+ const entities = registry.getZipper([ExampleComponent]);
+
+ entities.forEach((entity) => {
+ if (entity.ExampleComponent.paramA === "end") {
+ ctx.app.setIsRunning(false);
+ return;
+ }
+
+ if (entity.ExampleComponent.paramB === 0) entity.ExampleComponent.paramA = "end";
+
+ if (entity.ExampleComponent.paramB >= 0)
+ entity.ExampleComponent.paramB = entity.ExampleComponent.paramB - 1;
+ });
+};
+
+// * Required to generate code
+export default exampleSystem.name;
+
+/**
+ * Editor component manifest
+ * * Required for the editor to display the system and generate code
+ * @type {EditorSystemManifest}
+ */
+export const EDITOR_SYSTEM_MANIFEST = {
+ name: "Example",
+ description:
+ "This system end the game when paramB reaches 0 for any entity with ExampleComponent",
+ dependencies: ["ExampleComponent"],
+};
diff --git a/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/client.save.json b/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/client.save.json
new file mode 100644
index 0000000..c3be787
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/client.save.json
@@ -0,0 +1,77 @@
+{
+ "libraries": [
+ {
+ "id": "assetManagerLibrary",
+ "type": "asset-manager",
+ "name": "AssetManagerLibrary",
+ "path": "@nanoforge-dev/asset-manager",
+ "params": []
+ },
+ {
+ "id": "ecsLibrary",
+ "type": "component-system",
+ "name": "ECSClientLibrary",
+ "path": "@nanoforge-dev/ecs-client",
+ "params": []
+ },
+ {
+ "id": "graphicsLibrary",
+ "type": "graphics",
+ "name": "Graphics2DLibrary",
+ "path": "@nanoforge-dev/graphics-2d",
+ "params": []
+ },
+ {
+ "id": "inputLibrary",
+ "type": "input",
+ "name": "InputLibrary",
+ "path": "@nanoforge-dev/input",
+ "params": []
+ },
+ {
+ "id": "musicLibrary",
+ "type": "music",
+ "name": "MusicLibrary",
+ "path": "@nanoforge-dev/music",
+ "params": []
+ },<% if (server) { %>
+ {
+ "id": "networkLibrary",
+ "type": "network",
+ "name": "NetworkClientLibrary",
+ "path": "@nanoforge-dev/network-client",
+ "params": []
+ },<% } %>
+ {
+ "id": "soundLibrary",
+ "type": "sound",
+ "name": "SoundLibrary",
+ "path": "@nanoforge-dev/sound",
+ "params": []
+ }
+ ],
+ "components": [
+ {
+ "name": "ExampleComponent",
+ "path": "./components/example.component.ts",
+ "paramsNames": ["paramA", "paramB", "paramC"]
+ }
+ ],
+ "systems": [
+ {
+ "name": "exampleSystem",
+ "path": "./systems/example.system.ts"
+ }
+ ],
+ "entities": [
+ {
+ "id": "exampleEntity",
+ "components": {
+ "ExampleComponent": {
+ "paramA": "example",
+ "paramB": 10
+ }
+ }
+ }
+ ]
+}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/server.save.json b/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/server.save.json
new file mode 100644
index 0000000..47f7bba
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/server.save.json
@@ -0,0 +1,49 @@
+{
+ "libraries": [
+ {
+ "id": "assetManagerLibrary",
+ "type": "asset-manager",
+ "name": "AssetManagerLibrary",
+ "path": "@nanoforge-dev/asset-manager",
+ "params": []
+ },
+ {
+ "id": "ecsLibrary",
+ "type": "component-system",
+ "name": "ECSServerLibrary",
+ "path": "@nanoforge-dev/ecs-server",
+ "params": []
+ }<% if (server) { %>,
+ {
+ "id": "networkLibrary",
+ "type": "network",
+ "name": "NetworkServerLibrary",
+ "path": "@nanoforge-dev/network-server",
+ "params": []
+ }<% } %>
+ ],
+ "components": [
+ {
+ "name": "ExampleComponent",
+ "path": "./components/example.component.ts",
+ "paramsNames": ["paramA", "paramB", "paramC"]
+ }
+ ],
+ "systems": [
+ {
+ "name": "exampleSystem",
+ "path": "./systems/example.system.ts"
+ }
+ ],
+ "entities": [
+ {
+ "id": "exampleEntity",
+ "components": {
+ "ExampleComponent": {
+ "paramA": "example",
+ "paramB": 10
+ }
+ }
+ }
+ ]
+}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/components/example.component.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/components/example.component.ts
new file mode 100644
index 0000000..4f98934
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/ts/__part__/components/example.component.ts
@@ -0,0 +1,55 @@
+import { type EditorComponentManifest } from "@nanoforge-dev/ecs-<%= part %>";
+
+export class ExampleComponent {
+ name = this.constructor.name;
+
+ constructor(
+ public paramA: string,
+ public paramB: number,
+ public paramC: boolean = false,
+ ) {}
+
+ get foo() {
+ return "bar";
+ }
+
+ get paramAOrDefault() {
+ return this.paramC ? this.paramA : "default";
+ }
+
+ addOne() {
+ this.paramB += 1;
+ }
+}
+
+// * Required to generate code
+export default ExampleComponent.name;
+
+// * Required for the editor to display the component and generate code
+export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = {
+ name: "Example",
+ description: "Example component description",
+ params: [
+ {
+ type: "string",
+ name: "paramA",
+ description: "Param A description",
+ example: "Example value",
+ },
+ {
+ type: "number",
+ name: "paramB",
+ description: "Param B description",
+ example: 3,
+ },
+ {
+ type: "boolean",
+ name: "paramC",
+ description: "Param C description",
+ example: true,
+ default: false,
+ // Not required because it has a default value
+ optional: true,
+ },
+ ],
+};
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-init.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-init.ts
new file mode 100644
index 0000000..c5c3cee
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-init.ts
@@ -0,0 +1,5 @@
+import { type <%= appClass %> } from "@nanoforge-dev/core";
+
+export async function afterInit(app: <%= appClass %>) {
+ // Actions after app initialization
+}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-registry-init.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-registry-init.ts
new file mode 100644
index 0000000..8233635
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-registry-init.ts
@@ -0,0 +1,6 @@
+import { type <%= appClass %> } from "@nanoforge-dev/core";
+import { type Registry } from "@nanoforge-dev/ecs";
+
+export async function afterRegistryInit(app: <%= appClass %>, registry: Registry) {
+ // Actions after registry components, entities and systems initialization
+}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-run.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-run.ts
new file mode 100644
index 0000000..1d72689
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-run.ts
@@ -0,0 +1,5 @@
+import { type <%= appClass %> } from "@nanoforge-dev/core";
+
+export async function afterRun(app: <%= appClass %>) {
+ // Actions after app ended
+}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-init.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-init.ts
new file mode 100644
index 0000000..b45b2b8
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-init.ts
@@ -0,0 +1,5 @@
+import { type <%= appClass %> } from "@nanoforge-dev/core";
+
+export async function beforeInit(app: <%= appClass %>) {
+ // Actions before app initialization
+}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-registry-init.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-registry-init.ts
new file mode 100644
index 0000000..91eda4a
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-registry-init.ts
@@ -0,0 +1,6 @@
+import { type <%= appClass %> } from "@nanoforge-dev/core";
+import { type Registry } from "@nanoforge-dev/ecs";
+
+export async function beforeRegistryInit(app: <%= appClass %>, registry: Registry) {
+ // Actions before registry components, entities and systems initialization
+}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-run.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-run.ts
new file mode 100644
index 0000000..2e0c679
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-run.ts
@@ -0,0 +1,5 @@
+import { type <%= appClass %> } from "@nanoforge-dev/core";
+
+export async function beforeRun(app: <%= appClass %>) {
+ // Actions before app started
+}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/systems/example.system.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/systems/example.system.ts
new file mode 100644
index 0000000..10162b1
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/files/ts/__part__/systems/example.system.ts
@@ -0,0 +1,31 @@
+import { type Context } from "@nanoforge-dev/common";
+import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs-<%= part %>";
+
+import { ExampleComponent } from "../components/example.component";
+
+export const exampleSystem = (registry: Registry, ctx: Context) => {
+ const entities = registry.getZipper([ExampleComponent]);
+
+ entities.forEach((entity) => {
+ if (entity.ExampleComponent.paramA === "end") {
+ ctx.app.setIsRunning(false);
+ return;
+ }
+
+ if (entity.ExampleComponent.paramB === 0) entity.ExampleComponent.paramA = "end";
+
+ if (entity.ExampleComponent.paramB >= 0)
+ entity.ExampleComponent.paramB = entity.ExampleComponent.paramB - 1;
+ });
+};
+
+// * Required to generate code
+export default exampleSystem.name;
+
+// * Required for the editor to display the system and generate code
+export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = {
+ name: "Example",
+ description:
+ "This system end the game when paramB reaches 0 for any entity with ExampleComponent",
+ dependencies: ["ExampleComponent"],
+};
diff --git a/libs/schematics/src/libs/part-base/part-base.factory.ts b/libs/schematics/src/libs/part-base/part-base.factory.ts
new file mode 100644
index 0000000..2144656
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/part-base.factory.ts
@@ -0,0 +1,51 @@
+import { type Path, join, normalize, strings } from "@angular-devkit/core";
+import {
+ type Rule,
+ type Source,
+ apply,
+ filter,
+ mergeWith,
+ move,
+ template,
+ url,
+} from "@angular-devkit/schematics";
+
+import { type PartBaseOptions } from "./part-base.options";
+import { type PartBaseSchema } from "./part-base.schema";
+
+const transform = (schema: PartBaseSchema): PartBaseOptions => {
+ return {
+ part: schema.part,
+ server: schema.server,
+ appClass: schema.part === "client" ? "NanoforgeClient" : "NanoforgeServer",
+ nanoforgeFolder: ".nanoforge",
+ };
+};
+
+const generate = (
+ options: PartBaseOptions,
+ path: string,
+ initFunctions: boolean,
+ language: "ts" | "js",
+): Source => {
+ const rules = [
+ template({
+ ...strings,
+ ...options,
+ }),
+ move(normalize(path)),
+ filter((path) => {
+ const splited = path.split("/");
+ return splited.at(-2) !== ".nanoforge" || splited.at(-1) === `${options.part}.save.json`;
+ }),
+ ];
+ if (!initFunctions) rules.push(filter((path) => path.split("/").at(-2) !== "init"));
+
+ return apply(url(join("./files" as Path, language)), rules);
+};
+
+export const main = (schema: PartBaseSchema): Rule => {
+ const options = transform(schema);
+
+ return mergeWith(generate(options, schema.directory, schema.initFunctions, schema.language));
+};
diff --git a/libs/schematics/src/libs/part-base/part-base.options.d.ts b/libs/schematics/src/libs/part-base/part-base.options.d.ts
new file mode 100644
index 0000000..9e5934b
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/part-base.options.d.ts
@@ -0,0 +1,21 @@
+export interface PartBaseOptions {
+ /**
+ * The part of the application to generate
+ */
+ part: "client" | "server";
+
+ /**
+ * App class name
+ */
+ appClass: string;
+
+ /**
+ * Configure a server for the application
+ */
+ server: boolean;
+
+ /**
+ * Name of Nanoforge folder
+ */
+ nanoforgeFolder: string;
+}
diff --git a/libs/schematics/src/libs/part-base/part-base.schema.d.ts b/libs/schematics/src/libs/part-base/part-base.schema.d.ts
new file mode 100644
index 0000000..d708ed0
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/part-base.schema.d.ts
@@ -0,0 +1,26 @@
+export interface PartBaseSchema {
+ /**
+ * NanoForge application destination directory
+ */
+ directory: string;
+
+ /**
+ * The part of the application to generate
+ */
+ part: "client" | "server";
+
+ /**
+ * NanoForge Application language
+ */
+ language: "js" | "ts";
+
+ /**
+ * Add init functions to the application
+ */
+ initFunctions: boolean;
+
+ /**
+ * Configure a server for the application
+ */
+ server: boolean;
+}
diff --git a/libs/schematics/src/libs/part-base/schema.json b/libs/schematics/src/libs/part-base/schema.json
new file mode 100644
index 0000000..95d3bc1
--- /dev/null
+++ b/libs/schematics/src/libs/part-base/schema.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "SchematicsNanoForgePartBase",
+ "title": "NanoForge Part Base Options Schema",
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string",
+ "description": "NanoForge application destination directory",
+ "default": "."
+ },
+ "part": {
+ "type": "string",
+ "enum": ["client", "server"],
+ "description": "The part of the application to generate"
+ },
+ "language": {
+ "type": "string",
+ "enum": ["ts", "js"],
+ "description": "NanoForge application language",
+ "default": "ts"
+ },
+ "initFunctions": {
+ "type": "boolean",
+ "description": "Add init functions to the application",
+ "default": false
+ },
+ "server": {
+ "type": "boolean",
+ "description": "There is a Server in app",
+ "default": false
+ }
+ },
+ "required": ["part"]
+}
diff --git a/libs/schematics/src/libs/part-main/part-main.factory.ts b/libs/schematics/src/libs/part-main/part-main.factory.ts
new file mode 100644
index 0000000..1bd9770
--- /dev/null
+++ b/libs/schematics/src/libs/part-main/part-main.factory.ts
@@ -0,0 +1,84 @@
+import { type Path, join, normalize, strings } from "@angular-devkit/core";
+import {
+ type Rule,
+ type Source,
+ type Tree,
+ apply,
+ asSource,
+ mergeWith,
+ move,
+ template,
+} from "@angular-devkit/schematics";
+import * as fs from "fs";
+
+import { generateMain } from "@utils/main/main-functions";
+import { type Save, SaveLibraryTypeEnum } from "@utils/main/save.type";
+
+import { type PartMainOptions } from "./part-main.options";
+import { type PartMainSchema } from "./part-main.schema";
+
+const transform = (schema: PartMainSchema): PartMainOptions => {
+ void schema;
+
+ return {};
+};
+
+const generate = (options: PartMainOptions, path: string, schema: PartMainSchema): Source => {
+ const rules = [
+ template({
+ ...strings,
+ ...options,
+ }),
+ move(normalize(path)),
+ ];
+
+ return apply(asSource(writeMain(schema, path)), rules);
+};
+
+const getMainPath = (schema: PartMainSchema): string => {
+ if (schema.outFile) return schema.outFile;
+ if (schema.editor)
+ return join(".nanoforge/editor" as Path, schema.part, `main.${schema.language}`);
+ return join(schema.part as Path, `main.${schema.language}`);
+};
+
+const writeMain = (schema: PartMainSchema, path: string) => {
+ return (tree: Tree) => {
+ let save = getSave(path, schema.part, schema.saveFile);
+
+ const resPath = getMainPath(schema);
+
+ if (schema.editor) {
+ save = getEditorSave(save);
+ }
+
+ const content = generateMain(schema, save);
+ fs.rmSync(join(path as Path, resPath), { force: true });
+ tree.create(resPath, content);
+ return tree;
+ };
+};
+
+const getSave = (path: string, part: "client" | "server", saveFile?: string): Save => {
+ return JSON.parse(
+ fs.readFileSync(saveFile ?? join(path as Path, `.nanoforge/${part}.save.json`), "utf-8"),
+ ) as Save;
+};
+
+const getEditorSave = (save: Save): Save => {
+ if (save.libraries.some((lib) => lib.type === SaveLibraryTypeEnum.GRAPHICS))
+ save.libraries.push({
+ id: "graphicsEditorLibrary",
+ type: "graphics-editor",
+ name: "Graphics2DEditorLibrary",
+ path: "@nanoforge-dev/graphics-2d-editor",
+ });
+
+ return save;
+};
+
+export const main = (schema: PartMainSchema): Rule => {
+ const options = transform(schema);
+
+ return mergeWith(generate(options, schema.directory, schema));
+};
diff --git a/libs/schematics/src/libs/part-main/part-main.options.d.ts b/libs/schematics/src/libs/part-main/part-main.options.d.ts
new file mode 100644
index 0000000..d16f512
--- /dev/null
+++ b/libs/schematics/src/libs/part-main/part-main.options.d.ts
@@ -0,0 +1 @@
+export interface PartMainOptions {}
diff --git a/libs/schematics/src/libs/part-main/part-main.schema.d.ts b/libs/schematics/src/libs/part-main/part-main.schema.d.ts
new file mode 100644
index 0000000..f2a474a
--- /dev/null
+++ b/libs/schematics/src/libs/part-main/part-main.schema.d.ts
@@ -0,0 +1,36 @@
+export interface PartMainSchema {
+ /**
+ * NanoForge application destination directory
+ */
+ directory: string;
+
+ /**
+ * The part of the application to generate
+ */
+ part: "client" | "server";
+
+ /**
+ * NanoForge Application language
+ */
+ language: "js" | "ts";
+
+ /**
+ * Add init functions to the application
+ */
+ initFunctions: boolean;
+
+ /**
+ * Save file path with components and systems in JSON format
+ */
+ saveFile?: string;
+
+ /**
+ * Path of the out file
+ */
+ outFile?: string;
+
+ /**
+ * Build main editor
+ */
+ editor: boolean;
+}
diff --git a/libs/schematics/src/libs/part-main/schema.json b/libs/schematics/src/libs/part-main/schema.json
new file mode 100644
index 0000000..d2f84a8
--- /dev/null
+++ b/libs/schematics/src/libs/part-main/schema.json
@@ -0,0 +1,43 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "SchematicsNanoForgePartMain",
+ "title": "NanoForge Part Main Options Schema",
+ "type": "object",
+ "properties": {
+ "directory": {
+ "type": "string",
+ "description": "NanoForge application destination directory",
+ "default": "."
+ },
+ "part": {
+ "type": "string",
+ "enum": ["client", "server"],
+ "description": "The part of the application to generate"
+ },
+ "language": {
+ "type": "string",
+ "enum": ["ts", "js"],
+ "description": "NanoForge application language",
+ "default": "ts"
+ },
+ "initFunctions": {
+ "type": "boolean",
+ "description": "Add init functions to the main application",
+ "default": false
+ },
+ "saveFile": {
+ "type": "string",
+ "description": "Save file path with components and systems in JSON format"
+ },
+ "outFile": {
+ "type": "string",
+ "description": "Path of the out file"
+ },
+ "editor": {
+ "type": "boolean",
+ "description": "Build main editor",
+ "default": false
+ }
+ },
+ "required": ["part"]
+}
diff --git a/libs/schematics/src/libs/system/files/js/__fileName__.system.js b/libs/schematics/src/libs/system/files/js/__fileName__.system.js
new file mode 100644
index 0000000..64240a7
--- /dev/null
+++ b/libs/schematics/src/libs/system/files/js/__fileName__.system.js
@@ -0,0 +1,44 @@
+/**
+ * @typedef {import("@nanoforge-dev/common").Context} Context
+ * @typedef {import("@nanoforge-dev/ecs-<%= part %>").EditorSystemManifest} EditorSystemManifest
+ * @typedef {import("@nanoforge-dev/ecs-<%= part %>").Registry} Registry
+ */
+
+import { ExampleComponent } from "../components/example.component";
+
+/**
+ * <%= functionName %> system
+ * This system end the game when paramB reaches 0 for any entity with ExampleComponent
+ * @param {Registry} registry - ECS registry instance
+ * @param {Context} ctx - Nanoforge <%= part %> instance
+ */
+export const <%= functionName %>System = (registry, ctx) => {
+ const entities = registry.getZipper([ExampleComponent]);
+
+ entities.forEach((entity) => {
+ if (entity.ExampleComponent.paramA === "end") {
+ ctx.app.setIsRunning(false);
+ return;
+ }
+
+ if (entity.ExampleComponent.paramB === 0) entity.ExampleComponent.paramA = "end";
+
+ if (entity.ExampleComponent.paramB >= 0)
+ entity.ExampleComponent.paramB = entity.ExampleComponent.paramB - 1;
+ });
+};
+
+// * Required to generate code
+export default <%= functionName %>System.name;
+
+/**
+ * Editor component manifest
+ * * Required for the editor to display the system and generate code
+ * @type {EditorSystemManifest}
+ */
+export const EDITOR_SYSTEM_MANIFEST = {
+ name: "<%= functionName %>",
+ description:
+ "This system end the game when paramB reaches 0 for any entity with ExampleComponent",
+ dependencies: ["ExampleComponent"],
+};
diff --git a/libs/schematics/src/libs/system/files/ts/__fileName__.system.ts b/libs/schematics/src/libs/system/files/ts/__fileName__.system.ts
new file mode 100644
index 0000000..d1f9ce6
--- /dev/null
+++ b/libs/schematics/src/libs/system/files/ts/__fileName__.system.ts
@@ -0,0 +1,31 @@
+import { type Context } from "@nanoforge-dev/common";
+import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs-<%= part %>";
+
+import { ExampleComponent } from "../components/example.component";
+
+export const <%= functionName %>System = (registry: Registry, ctx: Context) => {
+ const entities = registry.getZipper([ExampleComponent]);
+
+ entities.forEach((entity) => {
+ if (entity.ExampleComponent.paramA === "end") {
+ ctx.app.setIsRunning(false);
+ return;
+ }
+
+ if (entity.ExampleComponent.paramB === 0) entity.ExampleComponent.paramA = "end";
+
+ if (entity.ExampleComponent.paramB >= 0)
+ entity.ExampleComponent.paramB = entity.ExampleComponent.paramB - 1;
+ });
+};
+
+// * Required to generate code
+export default <%= functionName %>System.name;
+
+// * Required for the editor to display the system and generate code
+export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = {
+ name: "<%= functionName %>",
+ description:
+ "This system end the game when paramB reaches 0 for any entity with ExampleComponent",
+ dependencies: ["ExampleComponent"],
+};
diff --git a/libs/schematics/src/libs/system/schema.json b/libs/schematics/src/libs/system/schema.json
new file mode 100644
index 0000000..0578ac2
--- /dev/null
+++ b/libs/schematics/src/libs/system/schema.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "SchematicsNanoForgeSystem",
+ "title": "NanoForge System Options Schema",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "System name",
+ "default": "example"
+ },
+ "directory": {
+ "type": "string",
+ "description": "System destination directory",
+ "default": "."
+ },
+ "part": {
+ "type": "string",
+ "enum": ["client", "server"],
+ "description": "The part of the application to generate"
+ },
+ "language": {
+ "type": "string",
+ "enum": ["ts", "js"],
+ "description": "NanoForge application language",
+ "default": "ts"
+ }
+ },
+ "required": ["part"]
+}
diff --git a/libs/schematics/src/libs/system/system.factory.ts b/libs/schematics/src/libs/system/system.factory.ts
new file mode 100644
index 0000000..93fabde
--- /dev/null
+++ b/libs/schematics/src/libs/system/system.factory.ts
@@ -0,0 +1,41 @@
+import { type Path, join, normalize, strings } from "@angular-devkit/core";
+import {
+ type Rule,
+ type Source,
+ apply,
+ mergeWith,
+ move,
+ template,
+ url,
+} from "@angular-devkit/schematics";
+
+import { toCamelCase, toKebabCase } from "@utils/formatting";
+
+import { type SystemOptions } from "./system.options";
+import { type SystemSchema } from "./system.schema";
+
+const transform = (schema: SystemSchema): SystemOptions => {
+ return {
+ part: schema.part,
+ functionName: toCamelCase(schema.name),
+ fileName: toKebabCase(schema.name),
+ };
+};
+
+const generate = (options: SystemOptions, path: string, language: "ts" | "js"): Source => {
+ const rules = [
+ template({
+ ...strings,
+ ...options,
+ }),
+ move(normalize(path)),
+ ];
+
+ return apply(url(join("./files" as Path, language)), rules);
+};
+
+export const main = (schema: SystemSchema): Rule => {
+ const options = transform(schema);
+
+ return mergeWith(generate(options, schema.directory, schema.language));
+};
diff --git a/libs/schematics/src/libs/system/system.options.d.ts b/libs/schematics/src/libs/system/system.options.d.ts
new file mode 100644
index 0000000..c16d2e6
--- /dev/null
+++ b/libs/schematics/src/libs/system/system.options.d.ts
@@ -0,0 +1,16 @@
+export interface SystemOptions {
+ /**
+ * The part of the application to generate
+ */
+ part: "client" | "server";
+
+ /**
+ * System function name
+ */
+ functionName: string;
+
+ /**
+ * System filename
+ */
+ fileName: string;
+}
diff --git a/libs/schematics/src/libs/system/system.schema.d.ts b/libs/schematics/src/libs/system/system.schema.d.ts
new file mode 100644
index 0000000..19c7bfe
--- /dev/null
+++ b/libs/schematics/src/libs/system/system.schema.d.ts
@@ -0,0 +1,21 @@
+export interface SystemSchema {
+ /**
+ * System destination directory
+ */
+ name: string;
+
+ /**
+ * System destination directory
+ */
+ directory: string;
+
+ /**
+ * The part of the application to generate
+ */
+ part: "client" | "server";
+
+ /**
+ * NanoForge Application language
+ */
+ language: "js" | "ts";
+}
diff --git a/libs/schematics/src/utils/config/config.declarator.spec.ts b/libs/schematics/src/utils/config/config.declarator.spec.ts
new file mode 100644
index 0000000..6d4c34b
--- /dev/null
+++ b/libs/schematics/src/utils/config/config.declarator.spec.ts
@@ -0,0 +1,38 @@
+import { describe, expect, it } from "vitest";
+
+import { ConfigDeclarator } from "./config.declarator";
+
+describe("ConfigDeclarator", () => {
+ it("should return a valid JSON string", () => {
+ const result = new ConfigDeclarator().declare("{}", {});
+ expect(() => JSON.parse(result)).not.toThrow();
+ });
+
+ it("should include default client config", () => {
+ const result = JSON.parse(new ConfigDeclarator().declare("{}", {}));
+ expect(result.client).toBeUndefined();
+ });
+
+ it("should not include server config when server is not enabled", () => {
+ const result = JSON.parse(new ConfigDeclarator().declare("{}", {}));
+ expect(result.server).toBeUndefined();
+ });
+
+ it("should format output with 2-space indentation", () => {
+ const result = new ConfigDeclarator().declare('{ "server": { "enable": true } }', {});
+ expect(result).toContain("\n ");
+ });
+
+ it("should include server config when baseContent has server.enable true", () => {
+ const baseContent = JSON.stringify({ server: { enable: true } });
+ const result = JSON.parse(new ConfigDeclarator().declare(baseContent, {}));
+ expect(result.server).toBeDefined();
+ expect(result.server.enable).toBe(true);
+ });
+
+ it("should include server config when options has server.enable true", () => {
+ const result = JSON.parse(new ConfigDeclarator().declare("{}", { server: { enable: true } }));
+ expect(result.server).toBeDefined();
+ expect(result.server.enable).toBe(true);
+ });
+});
diff --git a/libs/schematics/src/utils/config/config.declarator.ts b/libs/schematics/src/utils/config/config.declarator.ts
new file mode 100644
index 0000000..1ad2405
--- /dev/null
+++ b/libs/schematics/src/utils/config/config.declarator.ts
@@ -0,0 +1,10 @@
+import { type Config } from "@utils/config/config.type";
+import { deepMerge } from "@utils/object";
+import { type DeepPartial } from "@utils/types";
+
+export class ConfigDeclarator {
+ public declare(baseContent: string, options: DeepPartial): string {
+ const content = JSON.parse(baseContent);
+ return JSON.stringify(deepMerge(content, options), null, 2);
+ }
+}
diff --git a/libs/schematics/src/utils/config/config.finder.spec.ts b/libs/schematics/src/utils/config/config.finder.spec.ts
new file mode 100644
index 0000000..e37a8e3
--- /dev/null
+++ b/libs/schematics/src/utils/config/config.finder.spec.ts
@@ -0,0 +1,72 @@
+import { type Path } from "@angular-devkit/core";
+import { type DirEntry, type Tree } from "@angular-devkit/schematics";
+import { describe, expect, it, vi } from "vitest";
+
+import { ConfigFinder } from "./config.finder";
+
+function createMockDirEntry(
+ path: string,
+ subfiles: string[],
+ parent: DirEntry | null = null,
+): DirEntry {
+ return {
+ path: path as Path,
+ subfiles: subfiles as any,
+ subdirs: [] as any,
+ parent,
+ dir: vi.fn() as any,
+ file: vi.fn() as any,
+ visit: vi.fn() as any,
+ };
+}
+
+function createMockTree(dirEntry: DirEntry): Tree {
+ return {
+ getDir: vi.fn().mockReturnValue(dirEntry),
+ } as unknown as Tree;
+}
+
+describe("ConfigFinder", () => {
+ it("should find nanoforge.config.json in the given directory", () => {
+ const dir = createMockDirEntry("/project", ["nanoforge.config.json"]);
+ const tree = createMockTree(dir);
+ const finder = new ConfigFinder(tree);
+
+ const result = finder.find("/project" as Path);
+
+ expect(result).toBe("/project/nanoforge.config.json");
+ });
+
+ it("should search parent directories recursively", () => {
+ const parent = createMockDirEntry("/", ["nanoforge.config.json"]);
+ const child = createMockDirEntry("/project/src", [], parent);
+ const tree = createMockTree(child);
+ const finder = new ConfigFinder(tree);
+
+ const result = finder.find("/project/src" as Path);
+
+ expect(result).toBe("/nanoforge.config.json");
+ });
+
+ it("should return null when config is not found", () => {
+ const root = createMockDirEntry("/", [], null);
+ const child = createMockDirEntry("/project", [], root);
+ const tree = createMockTree(child);
+ const finder = new ConfigFinder(tree);
+
+ const result = finder.find("/project" as Path);
+
+ expect(result).toBeNull();
+ });
+
+ it("should ignore files that do not match nanoforge.config.json", () => {
+ const root = createMockDirEntry("/", [], null);
+ const dir = createMockDirEntry("/project", ["package.json", "tsconfig.json"], root);
+ const tree = createMockTree(dir);
+ const finder = new ConfigFinder(tree);
+
+ const result = finder.find("/project" as Path);
+
+ expect(result).toBeNull();
+ });
+});
diff --git a/libs/schematics/src/utils/config/config.finder.ts b/libs/schematics/src/utils/config/config.finder.ts
new file mode 100644
index 0000000..04ff4f4
--- /dev/null
+++ b/libs/schematics/src/utils/config/config.finder.ts
@@ -0,0 +1,33 @@
+import { type Path, join } from "@angular-devkit/core";
+import { type DirEntry, type Tree } from "@angular-devkit/schematics";
+
+export interface FindOptions {
+ path: Path;
+}
+
+export class ConfigFinder {
+ constructor(private tree: Tree) {}
+
+ public find(path: Path): Path | null {
+ const dir = this.tree.getDir(path);
+ return this.findIn(dir);
+ }
+
+ /**
+ * Recursively searches for the module file in the given directory.
+ *
+ * @param directory - The directory to search in.
+ * @returns The path to the module file, or null if not found.
+ */
+ private findIn(directory: DirEntry | null): Path | null {
+ if (!directory) {
+ return null;
+ }
+ const moduleFilename = directory.subfiles.find((filename) =>
+ /^nanoforge.config.json$/.test(filename),
+ );
+ return moduleFilename !== undefined
+ ? join(directory.path, moduleFilename.valueOf())
+ : this.findIn(directory.parent);
+ }
+}
diff --git a/libs/schematics/src/utils/config/config.type.ts b/libs/schematics/src/utils/config/config.type.ts
new file mode 100644
index 0000000..a0df082
--- /dev/null
+++ b/libs/schematics/src/utils/config/config.type.ts
@@ -0,0 +1,31 @@
+export interface BuildConfig {
+ entry: string;
+}
+
+export interface EditorConfig {
+ entry: string;
+ save: string;
+}
+
+export interface ClientConfig {
+ enable: boolean;
+ port: string;
+ outDir: string;
+ build: BuildConfig;
+ editor: EditorConfig;
+}
+
+export interface ServerConfig {
+ enable: boolean;
+ outDir: string;
+ build: BuildConfig;
+ editor: EditorConfig;
+}
+
+export interface Config {
+ name: string;
+ language: "ts" | "js";
+ initFunctions: boolean;
+ client: ClientConfig;
+ server: ServerConfig;
+}
diff --git a/libs/schematics/src/utils/formatting.spec.ts b/libs/schematics/src/utils/formatting.spec.ts
new file mode 100644
index 0000000..dfaf4b4
--- /dev/null
+++ b/libs/schematics/src/utils/formatting.spec.ts
@@ -0,0 +1,101 @@
+import { describe, expect, it } from "vitest";
+
+import { toCamelCase, toKebabCase, toPascalCase } from "./formatting";
+
+describe("toKebabCase", () => {
+ it("should convert camelCase to kebab-case", () => {
+ expect(toKebabCase("myVariableName")).toBe("my-variable-name");
+ });
+
+ it("should convert PascalCase to kebab-case", () => {
+ expect(toKebabCase("MyVariableName")).toBe("my-variable-name");
+ });
+
+ it("should convert spaces to hyphens", () => {
+ expect(toKebabCase("my variable name")).toBe("my-variable-name");
+ });
+
+ it("should convert underscores to hyphens", () => {
+ expect(toKebabCase("my_variable_name")).toBe("my-variable-name");
+ });
+
+ it("should return an already kebab-case string unchanged", () => {
+ expect(toKebabCase("my-variable-name")).toBe("my-variable-name");
+ });
+
+ it("should handle a single word", () => {
+ expect(toKebabCase("hello")).toBe("hello");
+ });
+
+ it("should handle an empty string", () => {
+ expect(toKebabCase("")).toBe("");
+ });
+});
+
+describe("toPascalCase", () => {
+ it("should convert camelCase to PascalCase", () => {
+ expect(toPascalCase("myVariableName")).toBe("MyVariableName");
+ });
+
+ it("should convert kebab-case to PascalCase", () => {
+ expect(toPascalCase("my-variable-name")).toBe("MyVariableName");
+ });
+
+ it("should convert snake_case to PascalCase", () => {
+ expect(toPascalCase("my_variable_name")).toBe("MyVariableName");
+ });
+
+ it("should convert spaces to PascalCase", () => {
+ expect(toPascalCase("my variable name")).toBe("MyVariableName");
+ });
+
+ it("should handle already PascalCase string", () => {
+ expect(toPascalCase("MyVariableName")).toBe("MyVariableName");
+ });
+
+ it("should handle a single word", () => {
+ expect(toPascalCase("hello")).toBe("Hello");
+ });
+
+ it("should handle an empty string", () => {
+ expect(toPascalCase("")).toBe("");
+ });
+
+ it("should handle SCREAMING_SNAKE_CASE", () => {
+ expect(toPascalCase("MY_VARIABLE_NAME")).toBe("MyVariableName");
+ });
+});
+
+describe("toCamelCase", () => {
+ it("should convert kebab-case to camelCase", () => {
+ expect(toCamelCase("my-variable-name")).toBe("myVariableName");
+ });
+
+ it("should convert snake_case to camelCase", () => {
+ expect(toCamelCase("my_variable_name")).toBe("myVariableName");
+ });
+
+ it("should convert spaces to camelCase", () => {
+ expect(toCamelCase("my variable name")).toBe("myVariableName");
+ });
+
+ it("should handle already camelCase string", () => {
+ expect(toCamelCase("myVariableName")).toBe("myVariableName");
+ });
+
+ it("should convert PascalCase to camelCase", () => {
+ expect(toCamelCase("MyVariableName")).toBe("myVariableName");
+ });
+
+ it("should handle a single word", () => {
+ expect(toCamelCase("hello")).toBe("hello");
+ });
+
+ it("should handle an empty string", () => {
+ expect(toCamelCase("")).toBe("");
+ });
+
+ it("should handle SCREAMING_SNAKE_CASE", () => {
+ expect(toCamelCase("MY_VARIABLE_NAME")).toBe("myVariableName");
+ });
+});
diff --git a/libs/schematics/src/utils/formatting.ts b/libs/schematics/src/utils/formatting.ts
new file mode 100644
index 0000000..d86154e
--- /dev/null
+++ b/libs/schematics/src/utils/formatting.ts
@@ -0,0 +1,30 @@
+const toWords = (str: string): string[] => {
+ 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);
+};
+
+export const toKebabCase = (str: string): string => {
+ return toWords(str)
+ .map((word) => word.toLowerCase())
+ .join("-");
+};
+
+export const toPascalCase = (str: string): string => {
+ return toWords(str)
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join("");
+};
+
+export const toCamelCase = (str: string): string => {
+ const words = toWords(str);
+ return words
+ .map((word, i) =>
+ i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
+ )
+ .join("");
+};
diff --git a/libs/schematics/src/utils/main/const.ts b/libs/schematics/src/utils/main/const.ts
new file mode 100644
index 0000000..e685784
--- /dev/null
+++ b/libs/schematics/src/utils/main/const.ts
@@ -0,0 +1,10 @@
+import { SaveLibraryTypeEnum } from "@utils/main/save.type";
+
+export const LIBS_FUNCTIONS_NAME: Record = {
+ [SaveLibraryTypeEnum.ASSET_MANAGER]: "useAssetManager",
+ [SaveLibraryTypeEnum.COMPONENT_SYSTEM]: "useComponentSystem",
+ [SaveLibraryTypeEnum.GRAPHICS]: "useGraphics",
+ [SaveLibraryTypeEnum.INPUT]: "useInput",
+ [SaveLibraryTypeEnum.NETWORK]: "useNetwork",
+ [SaveLibraryTypeEnum.SOUND]: "useSound",
+};
diff --git a/libs/schematics/src/utils/main/enums.ts b/libs/schematics/src/utils/main/enums.ts
new file mode 100644
index 0000000..9bbeb67
--- /dev/null
+++ b/libs/schematics/src/utils/main/enums.ts
@@ -0,0 +1,8 @@
+export enum InitFunctionEnum {
+ BEFORE_INIT = "beforeInit",
+ AFTER_INIT = "afterInit",
+ BEFORE_REGISTRY_INIT = "beforeRegistryInit",
+ AFTER_REGISTRY_INIT = "afterRegistryInit",
+ BEFORE_RUN = "beforeRun",
+ AFTER_RUN = "afterRun",
+}
diff --git a/libs/schematics/src/utils/main/main-functions.spec.ts b/libs/schematics/src/utils/main/main-functions.spec.ts
new file mode 100644
index 0000000..0f5874f
--- /dev/null
+++ b/libs/schematics/src/utils/main/main-functions.spec.ts
@@ -0,0 +1,135 @@
+import { describe, expect, it } from "vitest";
+
+import { generateMain } from "./main-functions";
+import { type Save, SaveLibraryTypeEnum } from "./save.type";
+
+const emptySave: Save = {
+ libraries: [],
+ components: [],
+ systems: [],
+ entities: [],
+};
+
+describe("generateMain", () => {
+ it("should generate a client main with TypeScript types", () => {
+ const result = generateMain(
+ { part: "client", language: "ts", initFunctions: false, editor: false },
+ emptySave,
+ );
+ expect(result).toContain("NanoforgeFactory.createClient()");
+ expect(result).toContain("IRunOptions");
+ expect(result).toContain("export async function main(options: IRunOptions)");
+ });
+
+ it("should generate a server main without types", () => {
+ const result = generateMain(
+ { part: "server", language: "js", initFunctions: false, editor: false },
+ emptySave,
+ );
+ expect(result).toContain("NanoforgeFactory.createServer()");
+ expect(result).not.toContain("IRunOptions");
+ expect(result).toContain("export async function main(options)");
+ });
+
+ it("should include init function imports and calls when enabled", () => {
+ const result = generateMain(
+ { part: "client", language: "ts", initFunctions: true, editor: false },
+ emptySave,
+ );
+ expect(result).toContain('import { beforeInit } from "./init/before-init";');
+ expect(result).toContain("await beforeInit(app);");
+ expect(result).toContain("await afterInit(app);");
+ expect(result).toContain("await beforeRegistryInit(app, registry);");
+ expect(result).toContain("await afterRegistryInit(app, registry);");
+ expect(result).toContain("await beforeRun(app);");
+ expect(result).toContain("await afterRun(app);");
+ });
+
+ it("should not include init functions when disabled", () => {
+ const result = generateMain(
+ { part: "client", language: "ts", initFunctions: false, editor: false },
+ emptySave,
+ );
+ expect(result).not.toContain("beforeInit");
+ expect(result).not.toContain("afterRun");
+ });
+
+ it("should generate library imports, instances, and init calls", () => {
+ const save: Save = {
+ ...emptySave,
+ libraries: [
+ {
+ id: "gfx",
+ type: SaveLibraryTypeEnum.GRAPHICS,
+ name: "Graphics",
+ path: "@nanoforge/graphics",
+ },
+ ],
+ };
+ const result = generateMain(
+ { part: "client", language: "ts", initFunctions: false, editor: false },
+ save,
+ );
+ expect(result).toContain('import { Graphics } from "@nanoforge/graphics";');
+ expect(result).toContain("const gfx = new Graphics();");
+ expect(result).toContain("app.useGraphics(gfx);");
+ });
+
+ it("should generate entity and system registration", () => {
+ const save: Save = {
+ libraries: [
+ {
+ id: "ecs",
+ type: SaveLibraryTypeEnum.COMPONENT_SYSTEM,
+ name: "ECS",
+ path: "@nanoforge/ecs",
+ },
+ ],
+ components: [{ name: "Position", path: "./components/position", paramsNames: ["x", "y"] }],
+ systems: [{ name: "PhysicsSystem", path: "./systems/physics" }],
+ entities: [
+ {
+ id: "player",
+ components: { Position: { x: 1, y: 2 } },
+ },
+ ],
+ };
+ const result = generateMain(
+ { part: "client", language: "ts", initFunctions: false, editor: false },
+ save,
+ );
+ expect(result).toContain("const registry = ecs.registry;");
+ expect(result).toContain("const player = registry.spawnEntity();");
+ expect(result).toContain("registry.addComponent(player, new Position(1, 2));");
+ expect(result).toContain("registry.addSystem(PhysicsSystem);");
+ });
+
+ it("should generate entity even in reverse order", () => {
+ const save: Save = {
+ libraries: [
+ {
+ id: "ecs",
+ type: SaveLibraryTypeEnum.COMPONENT_SYSTEM,
+ name: "ECS",
+ path: "@nanoforge/ecs",
+ },
+ ],
+ components: [{ name: "Position", path: "./components/position", paramsNames: ["y", "x"] }],
+ systems: [{ name: "PhysicsSystem", path: "./systems/physics" }],
+ entities: [
+ {
+ id: "player",
+ components: { Position: { x: 1, y: 2 } },
+ },
+ ],
+ };
+ const result = generateMain(
+ { part: "client", language: "ts", initFunctions: false, editor: false },
+ save,
+ );
+ expect(result).toContain("const registry = ecs.registry;");
+ expect(result).toContain("const player = registry.spawnEntity();");
+ expect(result).toContain("registry.addComponent(player, new Position(2, 1));");
+ expect(result).toContain("registry.addSystem(PhysicsSystem);");
+ });
+});
diff --git a/libs/schematics/src/utils/main/main-functions.ts b/libs/schematics/src/utils/main/main-functions.ts
new file mode 100644
index 0000000..23c7e46
--- /dev/null
+++ b/libs/schematics/src/utils/main/main-functions.ts
@@ -0,0 +1,61 @@
+import { join } from "path";
+
+import { InitFunctionEnum } from "@utils/main/enums";
+
+import { MainGenerator } from "./main.generator";
+import { type Save } from "./save.type";
+
+export interface MainOptions {
+ part: "client" | "server";
+ language: "js" | "ts";
+ initFunctions: boolean;
+ editor: boolean;
+ outFile?: string;
+}
+
+export const generateMain = (options: MainOptions, save: Save) => {
+ const isServer = options.part === "server";
+ const hasTypes = options.language === "ts";
+ const initFunctions = options.initFunctions;
+
+ return (
+ !options.editor
+ ? new MainGenerator()
+ : new MainGenerator(
+ true,
+ join(
+ "../".repeat(options.outFile ? options.outFile.split("/").length - 1 : 3),
+ options.part,
+ ),
+ )
+ )
+ .generateBaseImports(hasTypes)
+ .generateLibsImports(save.libraries)
+ .generateInitFunctionsImportsIfNeeded(initFunctions)
+ .generateComponentsImports(save.components)
+ .generateSystemsImports(save.systems)
+
+ .generateMainFunction(hasTypes, (generator) => {
+ generator
+ .generateApp(isServer)
+
+ .generateLibsInstances(save.libraries)
+ .generateLibsInit(save.libraries)
+
+ .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.BEFORE_INIT)
+ .generateAppInit()
+ .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.AFTER_INIT)
+
+ .generateRegistry(save.libraries)
+
+ .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.BEFORE_REGISTRY_INIT)
+ .generateEntities(save.libraries, save.components, save.entities)
+ .generateSystems(save.systems)
+ .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.AFTER_REGISTRY_INIT)
+
+ .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.BEFORE_RUN)
+ .generateAppRun(initFunctions)
+ .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.AFTER_RUN);
+ })
+ .toString();
+};
diff --git a/libs/schematics/src/utils/main/main-utils.ts b/libs/schematics/src/utils/main/main-utils.ts
new file mode 100644
index 0000000..05c79e0
--- /dev/null
+++ b/libs/schematics/src/utils/main/main-utils.ts
@@ -0,0 +1,7 @@
+export const getStringParam = (value: any): string => {
+ if (typeof value === "string") return `"${value}"`;
+ if (typeof value === "number") return value.toString();
+ if (typeof value === "boolean") return value ? "true" : "false";
+ if (typeof value === "object") return JSON.stringify(value);
+ return "undefined";
+};
diff --git a/libs/schematics/src/utils/main/main.generator.spec.ts b/libs/schematics/src/utils/main/main.generator.spec.ts
new file mode 100644
index 0000000..2cd0223
--- /dev/null
+++ b/libs/schematics/src/utils/main/main.generator.spec.ts
@@ -0,0 +1,222 @@
+import { describe, expect, it } from "vitest";
+
+import { MainGenerator } from "./main.generator";
+import { SaveLibraryTypeEnum } from "./save.type";
+
+describe("MainGenerator", () => {
+ it("should start with an empty buffer", () => {
+ expect(new MainGenerator().toString()).toBe("");
+ });
+
+ describe("generateBaseImports", () => {
+ it("should include type import when hasTypes is true", () => {
+ const result = new MainGenerator().generateBaseImports(true).toString();
+ expect(result).toContain('import { type IRunOptions } from "@nanoforge-dev/common";');
+ expect(result).toContain('import { NanoforgeFactory } from "@nanoforge-dev/core";');
+ });
+
+ it("should omit type import when hasTypes is false", () => {
+ const result = new MainGenerator().generateBaseImports(false).toString();
+ expect(result).not.toContain("IRunOptions");
+ expect(result).toContain('import { NanoforgeFactory } from "@nanoforge-dev/core";');
+ });
+ });
+
+ describe("generateApp", () => {
+ it("should generate createClient call for client", () => {
+ const result = new MainGenerator().generateApp(false).toString();
+ expect(result).toContain("NanoforgeFactory.createClient()");
+ });
+
+ it("should generate createServer call for server", () => {
+ const result = new MainGenerator().generateApp(true).toString();
+ expect(result).toContain("NanoforgeFactory.createServer()");
+ });
+ });
+
+ describe("generateAppInit", () => {
+ it("should generate await app.init(options)", () => {
+ const result = new MainGenerator().generateAppInit().toString();
+ expect(result).toContain("await app.init(options);");
+ });
+ });
+
+ describe("generateAppRun", () => {
+ it("should generate await app.run()", () => {
+ const result = new MainGenerator().generateAppRun(false).toString();
+ expect(result).toContain("await app.run();");
+ });
+ });
+
+ describe("generateMainFunction", () => {
+ it("should generate typed function signature when hasTypes is true", () => {
+ const result = new MainGenerator().generateMainFunction(true, () => {}).toString();
+ expect(result).toContain("export async function main(options: IRunOptions) {");
+ expect(result).toContain("}");
+ });
+
+ it("should generate untyped function signature when hasTypes is false", () => {
+ const result = new MainGenerator().generateMainFunction(false, () => {}).toString();
+ expect(result).toContain("export async function main(options) {");
+ });
+
+ it("should indent content inside the function body", () => {
+ const result = new MainGenerator()
+ .generateMainFunction(false, (gen) => {
+ gen.generateAppInit();
+ })
+ .toString();
+ expect(result).toContain(" await app.init(options);");
+ });
+ });
+
+ describe("generateLibsImports", () => {
+ it("should generate sorted import statements", () => {
+ const libs = [
+ {
+ id: "gfx",
+ type: SaveLibraryTypeEnum.GRAPHICS,
+ name: "Graphics",
+ path: "@nanoforge/graphics",
+ },
+ {
+ id: "ecs",
+ type: SaveLibraryTypeEnum.COMPONENT_SYSTEM,
+ name: "ECS",
+ path: "@nanoforge/ecs",
+ },
+ ];
+ const result = new MainGenerator().generateLibsImports(libs).toString();
+ const lines = result.trim().split("\n");
+ expect(lines[0]).toContain("@nanoforge/ecs");
+ expect(lines[1]).toContain("@nanoforge/graphics");
+ });
+ });
+
+ describe("generateLibsInstances", () => {
+ it("should generate const declarations for each library", () => {
+ const libs = [{ id: "gfx", type: SaveLibraryTypeEnum.GRAPHICS, name: "Graphics", path: "" }];
+ const result = new MainGenerator().generateLibsInstances(libs).toString();
+ expect(result).toContain("const gfx = new Graphics();");
+ });
+ });
+
+ describe("generateLibsInit", () => {
+ it("should use known init function for known library types", () => {
+ const libs = [{ id: "gfx", type: SaveLibraryTypeEnum.GRAPHICS, name: "Graphics", path: "" }];
+ const result = new MainGenerator().generateLibsInit(libs).toString();
+ expect(result).toContain("app.useGraphics(gfx);");
+ });
+
+ it("should use app.use with Symbol for unknown library types", () => {
+ const libs = [{ id: "custom", type: "custom-lib", name: "Custom", path: "" }];
+ const result = new MainGenerator().generateLibsInit(libs).toString();
+ expect(result).toContain('app.use(Symbol("custom-lib"), custom);');
+ });
+ });
+
+ describe("generateRegistry", () => {
+ it("should use component-system library id for registry", () => {
+ const libs = [
+ { id: "myEcs", type: SaveLibraryTypeEnum.COMPONENT_SYSTEM, name: "ECS", path: "" },
+ ];
+ const result = new MainGenerator().generateRegistry(libs).toString();
+ expect(result).toContain("const registry = myEcs.registry;");
+ });
+
+ it("should fallback to ecsLibrary when no component-system lib is found", () => {
+ const result = new MainGenerator().generateRegistry([]).toString();
+ expect(result).toContain("const registry = ecsLibrary.registry;");
+ });
+ });
+
+ describe("generateEntities", () => {
+ it("should generate entity spawn and component additions", () => {
+ const components = [
+ {
+ name: "Position",
+ path: "non",
+ paramsNames: ["x", "y"],
+ },
+ {
+ name: "Velocity",
+ path: "non",
+ paramsNames: ["fast", "__RESERVED_ASSET_asset"],
+ },
+ ];
+ const entities = [
+ {
+ id: "player",
+ components: {
+ Position: {
+ x: 1,
+ y: 2,
+ },
+ Velocity: {
+ fast: 3,
+ asset: "test",
+ },
+ },
+ },
+ ];
+ const result = new MainGenerator().generateEntities([], components, entities).toString();
+ expect(result).toContain("const player = registry.spawnEntity();");
+ expect(result).toContain("registry.addComponent(player, new Position(1, 2));");
+ expect(result).toContain(
+ 'registry.addComponent(player, new Velocity(3, assetManagerLibrary.getAsset("test")));',
+ );
+ });
+ });
+
+ describe("generateSystems", () => {
+ it("should generate addSystem calls", () => {
+ const systems = [
+ { name: "PhysicsSystem", path: "" },
+ { name: "RenderSystem", path: "" },
+ ];
+ const result = new MainGenerator().generateSystems(systems).toString();
+ expect(result).toContain("registry.addSystem(PhysicsSystem);");
+ expect(result).toContain("registry.addSystem(RenderSystem);");
+ });
+ });
+
+ describe("generateInitFunctionIfNeeded", () => {
+ it("should generate init function call when needed is true", () => {
+ const result = new MainGenerator()
+ .generateInitFunctionIfNeeded(true, "beforeInit" as any)
+ .toString();
+ expect(result).toContain("await beforeInit(app);");
+ });
+
+ it("should not generate anything when needed is false", () => {
+ const result = new MainGenerator()
+ .generateInitFunctionIfNeeded(false, "beforeInit" as any)
+ .toString();
+ expect(result).toBe("");
+ });
+
+ it("should pass registry param for registry init functions", () => {
+ const result = new MainGenerator()
+ .generateInitFunctionIfNeeded(true, "beforeRegistryInit" as any)
+ .toString();
+ expect(result).toContain("await beforeRegistryInit(app, registry);");
+ });
+ });
+
+ describe("generateInitFunctionsImportsIfNeeded", () => {
+ it("should generate all init function imports when needed", () => {
+ const result = new MainGenerator().generateInitFunctionsImportsIfNeeded(true).toString();
+ expect(result).toContain("afterInit");
+ expect(result).toContain("afterRegistryInit");
+ expect(result).toContain("afterRun");
+ expect(result).toContain("beforeInit");
+ expect(result).toContain("beforeRegistryInit");
+ expect(result).toContain("beforeRun");
+ });
+
+ it("should not generate imports when not needed", () => {
+ const result = new MainGenerator().generateInitFunctionsImportsIfNeeded(false).toString();
+ expect(result).toBe("");
+ });
+ });
+});
diff --git a/libs/schematics/src/utils/main/main.generator.ts b/libs/schematics/src/utils/main/main.generator.ts
new file mode 100644
index 0000000..bc20904
--- /dev/null
+++ b/libs/schematics/src/utils/main/main.generator.ts
@@ -0,0 +1,224 @@
+import { joinRelative } from "@utils/path";
+
+import { LIBS_FUNCTIONS_NAME } from "./const";
+import { InitFunctionEnum } from "./enums";
+import { getStringParam } from "./main-utils";
+import {
+ type SaveComponent,
+ type SaveEntity,
+ type SaveLibrary,
+ SaveLibraryTypeEnum,
+ type SaveSystem,
+} from "./save.type";
+
+export class MainGenerator {
+ private buffer = "";
+ private indentation = 0;
+ private readonly editor: boolean;
+ private readonly pathToRoot: string;
+
+ constructor(editor: boolean = false, pathToRoot: string = "") {
+ this.editor = editor;
+ this.pathToRoot = pathToRoot;
+ }
+
+ toString(): string {
+ return this.buffer;
+ }
+
+ generateBaseImports(hasTypes: boolean): MainGenerator {
+ if (this.editor) {
+ if (hasTypes)
+ this.writeLine(
+ `import { type IEditorRunOptions, NanoforgeFactory } from "@nanoforge-dev/core-editor";`,
+ );
+ else this.writeLine(`import { NanoforgeFactory } from "@nanoforge-dev/core-editor";`);
+ } else {
+ if (hasTypes) this.writeLine(`import { type IRunOptions } from "@nanoforge-dev/common";`);
+ this.writeLine(`import { NanoforgeFactory } from "@nanoforge-dev/core";`);
+ }
+
+ this.endSection();
+ return this;
+ }
+
+ generateLibsImports(libs: SaveLibrary[]): MainGenerator {
+ return this.generateImports(libs, false);
+ }
+
+ generateComponentsImports(libs: SaveComponent[]): MainGenerator {
+ return this.generateImports(libs, true);
+ }
+
+ generateSystemsImports(libs: SaveSystem[]): MainGenerator {
+ return this.generateImports(libs, true);
+ }
+
+ generateMainFunction(hasTypes: boolean, cb: (generator: MainGenerator) => void): MainGenerator {
+ if (this.editor)
+ this.writeLine(
+ `export async function main(options${hasTypes ? ": IEditorRunOptions" : ""}) {`,
+ );
+ else this.writeLine(`export async function main(options${hasTypes ? ": IRunOptions" : ""}) {`);
+ this.indentation += 1;
+ cb(this);
+ this.indentation -= 1;
+ this.writeLine("}");
+ return this;
+ }
+
+ generateApp(isServer: boolean): MainGenerator {
+ this.writeLine(`const app = NanoforgeFactory.create${isServer ? "Server" : "Client"}();`);
+ this.endSection();
+ return this;
+ }
+
+ generateAppInit(): MainGenerator {
+ this.writeLine(`await app.init(options);`);
+ this.endSection();
+ return this;
+ }
+
+ generateAppRun(hasInitFunctions: boolean): MainGenerator {
+ this.writeLine(`await app.run();`);
+ if (hasInitFunctions) this.endSection();
+ return this;
+ }
+
+ generateLibsInstances(libs: SaveLibrary[]): MainGenerator {
+ libs.forEach(({ id, name }) => this.writeLine(`const ${id} = new ${name}();`));
+ this.endSection();
+ return this;
+ }
+
+ generateLibsInit(libs: SaveLibrary[]): MainGenerator {
+ libs.forEach(({ id, type }) => this.writeLine(this.getInitFunction(id, type)));
+ this.endSection();
+ return this;
+ }
+
+ generateRegistry(libs: SaveLibrary[]): MainGenerator {
+ const libName =
+ libs.find((lib) => lib.type === SaveLibraryTypeEnum.COMPONENT_SYSTEM)?.id ?? "ecsLibrary";
+ this.writeLine(`const registry = ${libName}.registry;`);
+ this.endSection();
+ return this;
+ }
+
+ generateEntities(
+ libs: SaveLibrary[],
+ components: SaveComponent[],
+ entities: SaveEntity[],
+ ): MainGenerator {
+ entities.forEach((entity, index) => this.generateEntity(libs, components, entity, index));
+ return this;
+ }
+
+ generateSystems(systems: SaveSystem[]): MainGenerator {
+ systems.forEach(({ name }) => this.writeLine(`registry.addSystem(${name});`));
+ this.endSection();
+ return this;
+ }
+
+ generateInitFunctionIfNeeded(needed: boolean, func: InitFunctionEnum): MainGenerator {
+ if (needed) return this.generateInitFunction(func);
+ return this;
+ }
+
+ generateInitFunctionsImportsIfNeeded(needed: boolean): MainGenerator {
+ if (!needed) return this;
+ return this.generateImports(
+ [
+ { name: "afterInit", path: "./init/after-init" },
+ { name: "afterRegistryInit", path: "./init/after-registry-init" },
+ { name: "afterRun", path: "./init/after-run" },
+ { name: "beforeInit", path: "./init/before-init" },
+ { name: "beforeRegistryInit", path: "./init/before-registry-init" },
+ { name: "beforeRun", path: "./init/before-run" },
+ ],
+ true,
+ );
+ }
+
+ private generateInitFunction(func: InitFunctionEnum): MainGenerator {
+ const params = ["app"];
+ if (
+ [InitFunctionEnum.BEFORE_REGISTRY_INIT, InitFunctionEnum.AFTER_REGISTRY_INIT].includes(func)
+ )
+ params.push("registry");
+ this.writeLine(`await ${func}(${params.join(", ")});`);
+ if (func !== InitFunctionEnum.AFTER_RUN) this.endSection();
+ return this;
+ }
+
+ private generateImports(els: { name: string; path: string }[], relative: boolean): MainGenerator {
+ [...els]
+ .sort((a, b) => a.path.localeCompare(b.path))
+ .forEach(({ name, path }) => {
+ path = path.replace(/\.(?:ts|js)$/, "");
+ this.writeLine(
+ `import { ${name} } from "${relative ? joinRelative(this.pathToRoot, path) : path}";`,
+ );
+ });
+ this.endSection();
+ return this;
+ }
+
+ private generateEntity(
+ libs: SaveLibrary[],
+ components: SaveComponent[],
+ entity: SaveEntity,
+ entityIndex: number,
+ ): void {
+ this.writeLine(`const ${entity.id} = registry.spawnEntity();`);
+ Object.entries(entity.components).forEach(([componentName, rawParams]) => {
+ const originalsParams = components.find(({ name }) => name === componentName)?.paramsNames;
+ if (!originalsParams)
+ throw new Error("Missing component in entity or inexistent saved component");
+ const params: string[] = originalsParams.map((ogParam) => {
+ let isAsset = false;
+ if (ogParam.startsWith("__RESERVED_ASSET_")) {
+ isAsset = true;
+ ogParam = ogParam.replace("__RESERVED_ASSET_", "");
+ }
+
+ const param = this.editor
+ ? `options.editor.save.entities[${entityIndex}].components["${componentName}"]["${ogParam}"]`
+ : getStringParam(rawParams[ogParam]);
+ if (isAsset) return this.getAssetParam(param, libs);
+ return param;
+ });
+ this.writeLine(
+ `registry.addComponent(${entity.id}, new ${componentName}(${params.join(", ")}));`,
+ );
+ });
+ if (this.editor) {
+ this.writeLine(
+ `registry.addComponent(${entity.id}, { name: "__RESERVED_entityId", entityId: "${entity.id}" });`,
+ );
+ }
+ this.endSection();
+ }
+
+ private getAssetParam(param: string, libs: SaveLibrary[]): string {
+ const libName =
+ libs.find((lib) => lib.type === SaveLibraryTypeEnum.ASSET_MANAGER)?.id ??
+ "assetManagerLibrary";
+ return `${libName}.getAsset(${param})`;
+ }
+
+ private getInitFunction(name: string, type: SaveLibraryTypeEnum | string): string {
+ const baseName = LIBS_FUNCTIONS_NAME[type];
+ if (baseName) return `app.${baseName}(${name});`;
+ return `app.use(Symbol("${type}"), ${name});`;
+ }
+
+ private writeLine(text: string, indentation = true): void {
+ const line = `${" ".repeat(indentation ? this.indentation * 2 : 0)}${text}\n`;
+ this.buffer += line;
+ }
+
+ private endSection(): void {
+ this.writeLine("");
+ }
+}
diff --git a/libs/schematics/src/utils/main/save.type.ts b/libs/schematics/src/utils/main/save.type.ts
new file mode 100644
index 0000000..36372a3
--- /dev/null
+++ b/libs/schematics/src/utils/main/save.type.ts
@@ -0,0 +1,38 @@
+export enum SaveLibraryTypeEnum {
+ COMPONENT_SYSTEM = "component-system",
+ GRAPHICS = "graphics",
+ ASSET_MANAGER = "asset-manager",
+ NETWORK = "network",
+ INPUT = "input",
+ SOUND = "sound",
+}
+
+export interface SaveLibrary {
+ id: string;
+ type: SaveLibraryTypeEnum | string;
+ name: string;
+ path: string;
+}
+
+export interface SaveComponent {
+ name: string;
+ path: string;
+ paramsNames: string[];
+}
+
+export interface SaveSystem {
+ name: string;
+ path: string;
+}
+
+export interface SaveEntity {
+ id: string;
+ components: Record>;
+}
+
+export interface Save {
+ libraries: SaveLibrary[];
+ components: SaveComponent[];
+ systems: SaveSystem[];
+ entities: SaveEntity[];
+}
diff --git a/libs/schematics/src/utils/name.spec.ts b/libs/schematics/src/utils/name.spec.ts
new file mode 100644
index 0000000..350acbf
--- /dev/null
+++ b/libs/schematics/src/utils/name.spec.ts
@@ -0,0 +1,23 @@
+import { basename } from "node:path";
+import { describe, expect, it } from "vitest";
+
+import { resolvePackageName } from "./name";
+
+describe("resolvePackageName", () => {
+ it("should return the base filename for a simple path", () => {
+ expect(resolvePackageName("my-package")).toBe("my-package");
+ });
+
+ it("should return the base filename from a nested path", () => {
+ expect(resolvePackageName("some/path/my-package")).toBe("my-package");
+ });
+
+ it("should return scoped package name when path starts with @", () => {
+ expect(resolvePackageName("@scope/my-package")).toBe("@scope/my-package");
+ });
+
+ it("should return cwd basename when path is '.'", () => {
+ const expected = basename(process.cwd());
+ expect(resolvePackageName(".")).toBe(expected);
+ });
+});
diff --git a/libs/schematics/src/utils/name.ts b/libs/schematics/src/utils/name.ts
new file mode 100644
index 0000000..649feea
--- /dev/null
+++ b/libs/schematics/src/utils/name.ts
@@ -0,0 +1,12 @@
+import { basename, parse } from "path";
+
+export const resolvePackageName = (path: string): string => {
+ const { base: baseFilename, dir: dirname } = parse(path);
+ if (baseFilename === ".") {
+ return basename(process.cwd());
+ }
+ if (dirname.match(/^@[^\s]/)) {
+ return `${dirname}/${baseFilename}`;
+ }
+ return baseFilename;
+};
diff --git a/libs/schematics/src/utils/object.spec.ts b/libs/schematics/src/utils/object.spec.ts
new file mode 100644
index 0000000..aa4bd25
--- /dev/null
+++ b/libs/schematics/src/utils/object.spec.ts
@@ -0,0 +1,66 @@
+import { describe, expect, it } from "vitest";
+
+import { deepMerge, isObject } from "./object";
+
+describe("isObject", () => {
+ it("should return true for a plain object", () => {
+ expect(isObject({})).toBe(true);
+ });
+
+ it("should return true for an object with properties", () => {
+ expect(isObject({ a: 1 })).toBe(true);
+ });
+
+ it("should return false for an array", () => {
+ expect(isObject([1, 2, 3])).toBe(false);
+ });
+
+ it("should return falsy for null", () => {
+ expect(isObject(null)).toBeFalsy();
+ });
+
+ it("should return falsy for undefined", () => {
+ expect(isObject(undefined)).toBeFalsy();
+ });
+
+ it("should return false for a string", () => {
+ expect(isObject("hello")).toBe(false);
+ });
+
+ it("should return false for a number", () => {
+ expect(isObject(42)).toBe(false);
+ });
+});
+
+describe("deepMerge", () => {
+ it("should merge flat objects", () => {
+ expect(deepMerge({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
+ });
+
+ it("should override values from source", () => {
+ expect(deepMerge({ a: 1 }, { a: 2 })).toEqual({ a: 2 });
+ });
+
+ it("should deep merge nested objects", () => {
+ const target = { a: { b: 1, c: 2 } };
+ const source = { a: { c: 3, d: 4 } };
+ expect(deepMerge(target, source)).toEqual({ a: { b: 1, c: 3, d: 4 } });
+ });
+
+ it("should handle multiple sources", () => {
+ expect(deepMerge({ a: 1 }, { b: 2 }, { c: 3 })).toEqual({ a: 1, b: 2, c: 3 });
+ });
+
+ it("should return target when source is empty", () => {
+ const target = { a: 1 };
+ expect(deepMerge(target, {})).toEqual({ a: 1 });
+ });
+
+ it("should create nested keys in target if missing", () => {
+ expect(deepMerge({}, { a: { b: 1 } })).toEqual({ a: { b: 1 } });
+ });
+
+ it("should not override a primitive with an object when target is not an object", () => {
+ expect(deepMerge({ a: 1 }, { a: { b: 2 } })).toEqual({ a: 1 });
+ });
+});
diff --git a/libs/schematics/src/utils/object.ts b/libs/schematics/src/utils/object.ts
new file mode 100644
index 0000000..b42a4e2
--- /dev/null
+++ b/libs/schematics/src/utils/object.ts
@@ -0,0 +1,21 @@
+export const isObject = (item: any) => {
+ return item && typeof item === "object" && !Array.isArray(item);
+};
+
+export const deepMerge = (target: any, ...sources: [any, ...any[]]): any => {
+ if (!sources.length) return target;
+ const source = sources.shift();
+
+ if (isObject(target) && isObject(source)) {
+ for (const key in source) {
+ if (isObject(source[key])) {
+ if (!target[key]) Object.assign(target, { [key]: {} });
+ deepMerge(target[key], source[key]);
+ } else {
+ Object.assign(target, { [key]: source[key] });
+ }
+ }
+ }
+
+ return deepMerge(target, ...sources);
+};
diff --git a/libs/schematics/src/utils/path.ts b/libs/schematics/src/utils/path.ts
new file mode 100644
index 0000000..ecd11a8
--- /dev/null
+++ b/libs/schematics/src/utils/path.ts
@@ -0,0 +1,6 @@
+import { join } from "path";
+
+export const joinRelative = (...paths: [string, ...string[]]): string => {
+ const path = join(...paths);
+ return path.startsWith("./") || path.startsWith("../") ? path : `./${path}`;
+};
diff --git a/libs/schematics/src/utils/types.ts b/libs/schematics/src/utils/types.ts
new file mode 100644
index 0000000..2f49720
--- /dev/null
+++ b/libs/schematics/src/utils/types.ts
@@ -0,0 +1,15 @@
+export type DeepPartial =
+ | T
+ | (T extends Date
+ ? T
+ : T extends Array
+ ? DeepPartial[]
+ : T extends Map
+ ? Map, DeepPartial>
+ : T extends Set
+ ? Set>
+ : T extends object
+ ? {
+ [K in keyof T]?: DeepPartial;
+ }
+ : T);
diff --git a/libs/schematics/tsconfig.json b/libs/schematics/tsconfig.json
new file mode 100644
index 0000000..af69dd6
--- /dev/null
+++ b/libs/schematics/tsconfig.json
@@ -0,0 +1,56 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ // Mapped from https://www.typescriptlang.org/tsconfig
+ "compilerOptions": {
+ // Type Checking
+ "allowUnreachableCode": false,
+ "allowUnusedLabels": false,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noPropertyAccessFromIndexSignature": false,
+ "noUncheckedIndexedAccess": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "skipLibCheck": true,
+ "strict": true,
+
+ // Modules
+ "allowArbitraryExtensions": false,
+ "allowImportingTsExtensions": false,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "resolvePackageJsonExports": true,
+ "resolvePackageJsonImports": true,
+
+ // Emit
+ "declaration": true,
+ "declarationMap": true,
+ "importHelpers": false,
+ "newLine": "lf",
+ "noEmitHelpers": true,
+ "outDir": "dist",
+ "removeComments": false,
+ "sourceMap": true,
+
+ // Interop Constraints
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+
+ // Language and Environment
+ "rootDir": ".",
+ "lib": ["ESNext", "esnext.disposable"],
+ "types": ["node"],
+ "target": "ESNext",
+ "experimentalDecorators": true,
+ "useDefineForClassFields": true,
+ "paths": {
+ "~/*": ["./src/*"],
+ "@utils/*": ["./src/utils/*"]
+ }
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist", "e2e", "src/libs/**/files/**/*", "src/**/*.spec.ts"]
+}
diff --git a/libs/schematics/tsdown.config.ts b/libs/schematics/tsdown.config.ts
new file mode 100644
index 0000000..df494a8
--- /dev/null
+++ b/libs/schematics/tsdown.config.ts
@@ -0,0 +1,44 @@
+import { type UserConfig, defineConfig } from "tsdown";
+
+function createTsdownConfig({
+ entry = ["src/index.ts"],
+ outDir = "dist",
+ format = ["esm", "cjs"],
+ shims = true,
+}: UserConfig = {}) {
+ return defineConfig({
+ entry,
+ outDir,
+ format,
+ shims,
+ dts: true,
+ fixedExtension: false,
+ platform: "node",
+ sourcemap: true,
+ target: "esnext",
+ treeshake: false,
+ deps: {
+ skipNodeModulesBundle: true,
+ },
+ });
+}
+
+function createLibTsdownConfig(name: string) {
+ return createTsdownConfig({
+ entry: [`src/libs/${name}/${name}.factory.ts`],
+ outDir: `dist/libs/${name}`,
+ format: ["esm"],
+ shims: false,
+ });
+}
+
+export default [
+ createTsdownConfig(),
+ createLibTsdownConfig("application"),
+ createLibTsdownConfig("configuration"),
+ createLibTsdownConfig("part-base"),
+ createLibTsdownConfig("part-main"),
+ createLibTsdownConfig("docker"),
+ createLibTsdownConfig("component"),
+ createLibTsdownConfig("system"),
+];
diff --git a/libs/schematics/vitest.config.e2e.ts b/libs/schematics/vitest.config.e2e.ts
new file mode 100644
index 0000000..928e373
--- /dev/null
+++ b/libs/schematics/vitest.config.e2e.ts
@@ -0,0 +1,20 @@
+import { resolve } from "node:path";
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ "~": resolve(__dirname, "src"),
+ "@utils": resolve(__dirname, "src/utils"),
+ },
+ },
+ test: {
+ include: ["e2e/**/*.test.ts"],
+ passWithNoTests: true,
+ coverage: {
+ provider: "v8",
+ include: ["dist/**/*.js"],
+ exclude: ["dist/libs/**/files/**/*", "**/index.js"],
+ },
+ },
+});
diff --git a/libs/schematics/vitest.config.ts b/libs/schematics/vitest.config.ts
new file mode 100644
index 0000000..46a68f8
--- /dev/null
+++ b/libs/schematics/vitest.config.ts
@@ -0,0 +1,27 @@
+import { resolve } from "node:path";
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ "~": resolve(__dirname, "src"),
+ "@utils": resolve(__dirname, "src/utils"),
+ },
+ },
+ test: {
+ include: ["src/**/*.spec.ts"],
+ passWithNoTests: true,
+ coverage: {
+ provider: "v8",
+ include: ["src/**/*.ts"],
+ exclude: [
+ "src/**/*.spec.ts",
+ "src/libs/**/*",
+ "**/*.{interface,type,d}.ts",
+ "**/{interfaces,types}/*.ts",
+ "**/{interfaces,types}.ts",
+ "**/index.{js,ts}",
+ ],
+ },
+ },
+});
diff --git a/package.json b/package.json
index ad4fafa..834fb62 100644
--- a/package.json
+++ b/package.json
@@ -34,16 +34,23 @@
},
"funding": "https://github.com/NanoForge-dev/CLI?sponsor",
"scripts": {
- "build": "turbo run build --concurrency=5 --filter='!@nanoforge-dev/cli' && tsc --noEmit && tsdown",
- "build:dev": "turbo run build --concurrency=5 --filter='!@nanoforge-dev/cli' && tsc --noEmit && NODE_ENV=development tsdown",
+ "repo:build": "turbo run build --concurrency=5",
+ "repo:build:dev": "turbo run build:dev --concurrency=5",
+ "repo:lint": "turbo run lint --concurrency=5",
+ "repo:format": "turbo run format --concurrency=5",
+ "repo:test": "pnpm run repo:test:unit && pnpm run repo:test:e2e",
+ "repo:test:unit": "turbo run test:unit --concurrency=5",
+ "repo:test:e2e": "turbo run test:e2e --concurrency=5",
+ "build": "tsc --noEmit && tsdown",
+ "build:dev": "tsc --noEmit && NODE_ENV=development tsdown",
"start": "node dist/nf.js",
- "lint": "prettier --check . && eslint --format=pretty src && turbo run lint --concurrency=5 --filter='!@nanoforge-dev/cli'",
- "format": "prettier --write . && eslint --fix --format=pretty src && turbo run format --concurrency=5 --filter='!@nanoforge-dev/cli'",
+ "lint": "prettier --check . && eslint --format=pretty src",
+ "format": "prettier --write . && eslint --fix --format=pretty src",
"prepack": "pnpm run build && pnpm run lint",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r . --include-path '.'",
"release": "cliff-jumper",
"test": "pnpm run test:unit && pnpm run test:e2e",
- "test:unit": "vitest run -c vitest.config.ts && turbo run test:unit --concurrency=5 --filter='!@nanoforge-dev/cli'",
+ "test:unit": "vitest run -c vitest.config.ts",
"test:unit:watch": "vitest -c vitest.config.ts",
"test:e2e": "vitest run -c vitest.config.e2e.ts",
"test:e2e:watch": "vitest -c vitest.config.e2e.ts",
@@ -56,7 +63,7 @@
"@inquirer/prompts": "catalog:cli",
"@nanoforge-dev/loader-client": "catalog:loader",
"@nanoforge-dev/loader-server": "catalog:loader",
- "@nanoforge-dev/schematics": "catalog:schematics",
+ "@nanoforge-dev/schematics": "workspace:*",
"ansis": "catalog:cli",
"bun": "catalog:core",
"chokidar": "catalog:libs",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d810bb3..420850e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -107,8 +107,11 @@ settings:
catalogs:
build:
+ cpx2:
+ specifier: ^9.0.0
+ version: 9.0.0
tsdown:
- specifier: ^0.22.3
+ specifier: ^0.22.14
version: 0.22.14
turbo:
specifier: ^2.10.12
@@ -121,10 +124,10 @@ catalogs:
version: 0.3.1
ci:
'@commitlint/cli':
- specifier: ^21.2.0
+ specifier: ^21.2.2
version: 21.2.2
'@commitlint/config-conventional':
- specifier: ^21.2.0
+ specifier: ^21.2.2
version: 21.2.2
'@favware/cliff-jumper':
specifier: ^6.1.0
@@ -136,11 +139,11 @@ catalogs:
specifier: ^9.1.7
version: 9.1.7
lint-staged:
- specifier: ^17.0.8
+ specifier: ^17.4.1
version: 17.4.1
cli:
'@inquirer/prompts':
- specifier: ^8.5.2
+ specifier: ^8.7.0
version: 8.7.0
'@types/inquirer':
specifier: ^9.0.10
@@ -161,11 +164,14 @@ catalogs:
'@nanoforge-dev/editor':
specifier: ^0.1.4
version: 0.1.4
+ '@types/bun':
+ specifier: ^1.3.14
+ version: 1.4.0
'@types/node':
- specifier: ^26.1.0
+ specifier: ^26.4.0
version: 26.4.0
bun:
- specifier: ^1.3.14
+ specifier: ^1.4.0
version: 1.4.0
libs:
chokidar:
@@ -181,7 +187,7 @@ catalogs:
specifier: ^17.4.2
version: 17.4.2
open:
- specifier: ^11.0.0
+ specifier: ^11.0.2
version: 11.0.2
rc9:
specifier: ^3.0.1
@@ -200,34 +206,37 @@ catalogs:
specifier: ^6.0.2
version: 6.0.2
eslint:
- specifier: ^10.6.0
+ specifier: ^10.9.1
version: 10.9.1
prettier:
- specifier: ^3.9.4
+ specifier: ^3.9.6
version: 3.9.6
loader:
'@nanoforge-dev/loader-client':
- specifier: ^1.4.0
+ specifier: ^1.5.0
version: 1.5.0
'@nanoforge-dev/loader-server':
- specifier: ^1.4.0
+ specifier: ^1.5.0
version: 1.5.0
schematics:
+ '@angular-devkit/core':
+ specifier: ^22.0.4
+ version: 22.1.6
'@angular-devkit/schematics':
- specifier: ^22.0.5
+ specifier: ^22.1.6
version: 22.1.6
'@angular-devkit/schematics-cli':
- specifier: ^22.0.5
+ specifier: ^22.1.6
version: 22.1.6
- '@nanoforge-dev/schematics':
- specifier: ^2.2.1
- version: 2.2.1
+ rxjs:
+ specifier: ^7.8.2
+ version: 7.8.2
test:
'@vitest/coverage-v8':
- specifier: ^4.1.10
+ specifier: ^4.1.11
version: 4.1.11
vitest:
- specifier: ^4.1.10
+ specifier: ^4.1.11
version: 4.1.11
importers:
@@ -250,8 +259,8 @@ importers:
specifier: catalog:loader
version: 1.5.0
'@nanoforge-dev/schematics':
- specifier: catalog:schematics
- version: 2.2.1(chokidar@5.0.0)
+ specifier: workspace:*
+ version: link:libs/schematics
ansis:
specifier: catalog:cli
version: 4.3.1
@@ -381,6 +390,171 @@ importers:
specifier: catalog:test
version: 4.1.11(@types/node@26.4.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0))
+ libs/loader/client:
+ dependencies:
+ '@nanoforge-dev/loader-website':
+ specifier: workspace:*
+ version: link:../website
+ bun:
+ specifier: catalog:core
+ version: 1.4.0
+ commander:
+ specifier: catalog:cli
+ version: 15.0.0
+ devDependencies:
+ '@favware/cliff-jumper':
+ specifier: catalog:ci
+ version: 6.1.0
+ '@nanoforge-dev/utils-eslint-config':
+ specifier: catalog:lint
+ version: 1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)
+ '@nanoforge-dev/utils-prettier-config':
+ specifier: catalog:lint
+ version: 1.0.2
+ '@trivago/prettier-plugin-sort-imports':
+ specifier: catalog:lint
+ version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2)
+ '@types/bun':
+ specifier: catalog:core
+ version: 1.4.0
+ eslint:
+ specifier: catalog:lint
+ version: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
+ prettier:
+ specifier: catalog:lint
+ version: 3.9.6
+ typescript:
+ specifier: catalog:build
+ version: 6.0.3
+
+ libs/loader/server:
+ dependencies:
+ commander:
+ specifier: catalog:cli
+ version: 15.0.0
+ devDependencies:
+ '@favware/cliff-jumper':
+ specifier: catalog:ci
+ version: 6.1.0
+ '@nanoforge-dev/utils-eslint-config':
+ specifier: catalog:lint
+ version: 1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)
+ '@nanoforge-dev/utils-prettier-config':
+ specifier: catalog:lint
+ version: 1.0.2
+ '@trivago/prettier-plugin-sort-imports':
+ specifier: catalog:lint
+ version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2)
+ bun:
+ specifier: catalog:core
+ version: 1.4.0
+ eslint:
+ specifier: catalog:lint
+ version: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
+ prettier:
+ specifier: catalog:lint
+ version: 3.9.6
+ typescript:
+ specifier: catalog:build
+ version: 6.0.3
+
+ libs/loader/website:
+ devDependencies:
+ '@favware/cliff-jumper':
+ specifier: catalog:ci
+ version: 6.1.0
+ '@nanoforge-dev/utils-eslint-config':
+ specifier: catalog:lint
+ version: 1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)
+ '@nanoforge-dev/utils-prettier-config':
+ specifier: catalog:lint
+ version: 1.0.2
+ '@trivago/prettier-plugin-sort-imports':
+ specifier: catalog:lint
+ version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2)
+ '@types/bun':
+ specifier: catalog:core
+ version: 1.4.0
+ bun:
+ specifier: catalog:core
+ version: 1.4.0
+ eslint:
+ specifier: catalog:lint
+ version: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
+ prettier:
+ specifier: catalog:lint
+ version: 3.9.6
+ typescript:
+ specifier: catalog:build
+ version: 6.0.3
+
+ libs/schematics:
+ dependencies:
+ '@angular-devkit/core':
+ specifier: catalog:schematics
+ version: 22.1.6(chokidar@5.0.0)
+ '@angular-devkit/schematics':
+ specifier: catalog:schematics
+ version: 22.1.6(chokidar@5.0.0)
+ rxjs:
+ specifier: catalog:schematics
+ version: 7.8.2
+ devDependencies:
+ '@angular-devkit/schematics-cli':
+ specifier: catalog:schematics
+ version: 22.1.6(@types/node@26.4.0)(chokidar@5.0.0)
+ '@commitlint/cli':
+ specifier: catalog:ci
+ version: 21.2.2(@types/node@26.4.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2)(typescript@6.0.3)
+ '@commitlint/config-conventional':
+ specifier: catalog:ci
+ version: 21.2.2
+ '@favware/cliff-jumper':
+ specifier: catalog:ci
+ version: 6.1.0
+ '@nanoforge-dev/actions':
+ specifier: catalog:ci
+ version: 2.1.5
+ '@nanoforge-dev/utils-eslint-config':
+ specifier: catalog:lint
+ version: 1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)
+ '@nanoforge-dev/utils-prettier-config':
+ specifier: catalog:lint
+ version: 1.0.2
+ '@trivago/prettier-plugin-sort-imports':
+ specifier: catalog:lint
+ version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2)
+ '@types/node':
+ specifier: catalog:core
+ version: 26.4.0
+ '@vitest/coverage-v8':
+ specifier: catalog:test
+ version: 4.1.11(vitest@4.1.11)
+ cpx2:
+ specifier: catalog:build
+ version: 9.0.0
+ eslint:
+ specifier: catalog:lint
+ version: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
+ husky:
+ specifier: catalog:ci
+ version: 9.1.7
+ lint-staged:
+ specifier: catalog:ci
+ version: 17.4.1
+ prettier:
+ specifier: catalog:lint
+ version: 3.9.6
+ tsdown:
+ specifier: catalog:build
+ version: 0.22.14(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))
+ typescript:
+ specifier: catalog:build
+ version: 6.0.3
+ vitest:
+ specifier: catalog:test
+ version: 4.1.11(@types/node@26.4.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0))
+
packages:
'@actions/core@3.0.1':
@@ -809,10 +983,6 @@ packages:
resolution: {integrity: sha512-X37wlYyoL/q8ZPk5I8W3z01novfDSopizVQXbk/sw0dRXoONtIb6xnZLlN3z4jkDkPOy6IVes0bLGr8h4aFOcA==}
engines: {node: '25'}
- '@nanoforge-dev/schematics@2.2.1':
- resolution: {integrity: sha512-MdmhFncakgejGRIf1HTPKTskhdv8bIst21uWRSZCXGgGv0UNYEfDiziOsWPVCzGc9dXei59RIDznBwgpAMeXLg==}
- engines: {node: '25'}
-
'@nanoforge-dev/utils-eslint-config@1.0.2':
resolution: {integrity: sha512-hIsVGGpVIW3RvPZocWEf15Y5eqmIxTtLfWI66wqx+AO6PHcatUBhEi4sk1i54Qb2yGFueRBS/dSXj0kxG4Rbcw==}
engines: {node: '25'}
@@ -825,16 +995,16 @@ packages:
resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==}
engines: {node: '>= 20'}
- '@octokit/core@7.0.7':
- resolution: {integrity: sha512-DcB0M3KFgr9ECI328lhBMVsyFT2DnmNucSBTqEN3exyNKUzkkpUSCHmTRcunF41Eou2TIQKW4seewri8ON9bSA==}
+ '@octokit/core@7.0.8':
+ resolution: {integrity: sha512-L7y8eYc+AwxGr2PWI4WFt1VG4TiJ66c26BD16mXpYIlXxG0SMigM1+m4aTSlYyBr5BlQsGAlz8uDCoZN4SEMcg==}
engines: {node: '>= 20'}
- '@octokit/endpoint@11.0.4':
- resolution: {integrity: sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA==}
+ '@octokit/endpoint@11.0.5':
+ resolution: {integrity: sha512-iXa654H3yFafF/ieHkukfbgWo2rmXD2ceD0ZOtrPhw1bc3FDch1d9N/TNs0FQ1/cIbwb7kspUX8jzIs8nzb9DQ==}
engines: {node: '>= 20'}
- '@octokit/graphql@9.0.4':
- resolution: {integrity: sha512-5s15CCiY8XXQ+FG+b1YQcl6Z2FA++nwAz/tg2VUrTmnMncP+2nnGUEYANImdnxsA2Fnq+Mbl7hDjUTw7cFAwcg==}
+ '@octokit/graphql@9.0.5':
+ resolution: {integrity: sha512-bt/hm03LeU6Vy7FwTrkkC9p3XGT/lBwClglMqxBSe5/q0E5CdJTXeAqEI0vlw89/LF/G6tryTIH8HirZ3prMVg==}
engines: {node: '>= 20'}
'@octokit/openapi-types@27.0.0':
@@ -843,6 +1013,9 @@ packages:
'@octokit/openapi-types@28.0.0':
resolution: {integrity: sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==}
+ '@octokit/openapi-types@29.0.1':
+ resolution: {integrity: sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==}
+
'@octokit/plugin-paginate-rest@14.0.0':
resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==}
engines: {node: '>= 20'}
@@ -861,12 +1034,12 @@ packages:
peerDependencies:
'@octokit/core': '>=7'
- '@octokit/request-error@7.1.1':
- resolution: {integrity: sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA==}
+ '@octokit/request-error@7.1.2':
+ resolution: {integrity: sha512-XZRuT3xZ84D3gYErI1DZvhJ33dCWVV6uzBtWkaBB4TvA/L6eOeTZodxLFVB44bBEEo3vEx7y00UfX1tBLrtLRg==}
engines: {node: '>= 20'}
- '@octokit/request@10.0.15':
- resolution: {integrity: sha512-3CBg9aJ0hO9Pjyij8LbK/xYtEaPws9SW7xKz67daPNxQB1q5Y9OMA7DDOG0A6Hwf9ygGu3tvzusg0LXQ8/wAjA==}
+ '@octokit/request@10.0.16':
+ resolution: {integrity: sha512-A0zWGjHzISIb+9ccG8s0dq7LKO5zVpJLRICjgUb+sJxEWqn8RUHB1rD3AE51+PECvXHIxqZ1VVvs4fHTSD9nUQ==}
engines: {node: '>= 20'}
'@octokit/types@16.0.0':
@@ -875,6 +1048,9 @@ packages:
'@octokit/types@17.0.0':
resolution: {integrity: sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==}
+ '@octokit/types@18.0.0':
+ resolution: {integrity: sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==}
+
'@oven/bun-darwin-aarch64@1.4.0':
resolution: {integrity: sha512-GCpf8QuFLsyioVawP5HrMxA1ZRBlu6Hq9RNnSc3UTUWAzIxBso9trjoZczw1HdgpqSssFkszfIV2zmOzFTjhkw==}
cpu: [arm64]
@@ -1257,6 +1433,9 @@ packages:
cpu: [arm64]
os: [win32]
+ '@types/bun@1.4.0':
+ resolution: {integrity: sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ==}
+
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -1287,63 +1466,63 @@ packages:
'@types/validator@13.15.10':
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
- '@typescript-eslint/eslint-plugin@8.68.0':
- resolution: {integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==}
+ '@typescript-eslint/eslint-plugin@8.69.0':
+ resolution: {integrity: sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.68.0
+ '@typescript-eslint/parser': ^8.69.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/parser@8.68.0':
- resolution: {integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==}
+ '@typescript-eslint/parser@8.69.0':
+ resolution: {integrity: sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/project-service@8.68.0':
- resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==}
+ '@typescript-eslint/project-service@8.69.0':
+ resolution: {integrity: sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/scope-manager@8.68.0':
- resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==}
+ '@typescript-eslint/scope-manager@8.69.0':
+ resolution: {integrity: sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.68.0':
- resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==}
+ '@typescript-eslint/tsconfig-utils@8.69.0':
+ resolution: {integrity: sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/type-utils@8.68.0':
- resolution: {integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==}
+ '@typescript-eslint/type-utils@8.69.0':
+ resolution: {integrity: sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/types@8.68.0':
- resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==}
+ '@typescript-eslint/types@8.69.0':
+ resolution: {integrity: sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.68.0':
- resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==}
+ '@typescript-eslint/typescript-estree@8.69.0':
+ resolution: {integrity: sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/utils@8.68.0':
- resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==}
+ '@typescript-eslint/utils@8.69.0':
+ resolution: {integrity: sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/visitor-keys@8.68.0':
- resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==}
+ '@typescript-eslint/visitor-keys@8.69.0':
+ resolution: {integrity: sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/coverage-v8@4.1.11':
@@ -1597,6 +1776,9 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
+ bun-types@1.4.0:
+ resolution: {integrity: sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q==}
+
bun@1.4.0:
resolution: {integrity: sha512-iRiFkc2W7UVpCyZXO9tod45TP9QCyN19fWqbpeN/jaM/K7uzeHYx/OSPsahMJazGKBgPsnxRt+4Jc43d8BcHZw==}
cpu: [arm64, x64]
@@ -1726,10 +1908,19 @@ packages:
typescript:
optional: true
+ cpx2@9.0.0:
+ resolution: {integrity: sha512-KRZGQCmHrM10xfLjrCBTQHpEIJgKtZyOOO5fygu9KE00IjQOL76FHjeON62R5PWS/W2J5ofgRJKF8rpUlEuLPQ==}
+ engines: {node: ^22.0.0 || >=24.0.0, npm: '>=10'}
+ hasBin: true
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
+ debounce@3.0.0:
+ resolution: {integrity: sha512-64byRbF0/AirwbuHqB3/ZpMG9/nckDa6ZA0yd6UnaQNwbbemCOwvz2sL5sjXLHhZHADyiwLm0M5qMhltUUx+TA==}
+ engines: {node: '>=20'}
+
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -1950,6 +2141,9 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ find-index@0.1.1:
+ resolution: {integrity: sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg==}
+
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -2021,6 +2215,14 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
+ glob2base@0.0.12:
+ resolution: {integrity: sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA==}
+ engines: {node: '>= 0.10'}
+
+ glob@13.0.6:
+ resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
+ engines: {node: 18 || 20 || >=22}
+
global-directory@5.0.0:
resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==}
engines: {node: '>=20'}
@@ -2060,8 +2262,8 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
- ignore@7.0.6:
- resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
+ ignore@7.0.8:
+ resolution: {integrity: sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==}
engines: {node: '>= 4'}
import-fresh@3.3.1:
@@ -2294,6 +2496,10 @@ packages:
resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==}
engines: {node: '>=18'}
+ lru-cache@11.5.2:
+ resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
+ engines: {node: 20 || >=22}
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -2327,6 +2533,9 @@ packages:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'}
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -2394,6 +2603,10 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
+ p-map@7.0.7:
+ resolution: {integrity: sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ==}
+ engines: {node: '>=18'}
+
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -2424,6 +2637,10 @@ packages:
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
engines: {node: '>=12'}
+ path-scurry@2.0.2:
+ resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
+ engines: {node: 18 || 20 || >=22}
+
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -2558,6 +2775,10 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ shell-quote@1.10.0:
+ resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==}
+ engines: {node: '>= 0.4'}
+
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -2611,6 +2832,9 @@ packages:
resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
engines: {node: '>=18'}
+ subarg@1.0.0:
+ resolution: {integrity: sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==}
+
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
@@ -2713,8 +2937,8 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
- typescript-eslint@8.68.0:
- resolution: {integrity: sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==}
+ typescript-eslint@8.69.0:
+ resolution: {integrity: sha512-B3MltX0VqjUBNEe3b3sSuiRbfa6XrfHFtBiPamjT5AsW/dfq+y+bc0wyuS9DxAS1LyzCxRp2+rxzpLUvqM2BvA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -2925,11 +3149,11 @@ snapshots:
'@actions/github@9.1.1':
dependencies:
'@actions/http-client': 3.0.2
- '@octokit/core': 7.0.7
- '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.7)
- '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.7)
- '@octokit/request': 10.0.15
- '@octokit/request-error': 7.1.1
+ '@octokit/core': 7.0.8
+ '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.8)
+ '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.8)
+ '@octokit/request': 10.0.16
+ '@octokit/request-error': 7.1.2
undici: 6.28.0
'@actions/http-client@3.0.2':
@@ -3039,6 +3263,22 @@ snapshots:
- conventional-commits-parser
- typescript
+ '@commitlint/cli@21.2.2(@types/node@26.4.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2)(typescript@6.0.3)':
+ dependencies:
+ '@commitlint/config-conventional': 21.2.2
+ '@commitlint/format': 21.2.2
+ '@commitlint/lint': 21.2.2
+ '@commitlint/load': 21.2.2(@types/node@26.4.0)(typescript@6.0.3)
+ '@commitlint/read': 21.2.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2)
+ '@commitlint/types': 21.2.0
+ tinyexec: 1.3.0
+ yargs: 18.1.0
+ transitivePeerDependencies:
+ - '@types/node'
+ - conventional-commits-filter
+ - conventional-commits-parser
+ - typescript
+
'@commitlint/config-conventional@21.2.2':
dependencies:
'@commitlint/types': 21.2.0
@@ -3106,6 +3346,16 @@ snapshots:
- conventional-commits-filter
- conventional-commits-parser
+ '@commitlint/read@21.2.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2)':
+ dependencies:
+ '@commitlint/top-level': 21.2.0
+ '@commitlint/types': 21.2.0
+ '@conventional-changelog/git-client': 3.1.2(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2)
+ tinyexec: 1.3.0
+ transitivePeerDependencies:
+ - conventional-commits-filter
+ - conventional-commits-parser
+
'@commitlint/resolve-extends@21.2.2':
dependencies:
'@commitlint/config-validator': 21.2.0
@@ -3150,6 +3400,15 @@ snapshots:
conventional-commits-filter: 5.0.0
conventional-commits-parser: 6.4.0
+ '@conventional-changelog/git-client@3.1.2(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2)':
+ dependencies:
+ '@simple-libs/child-process-utils': 2.0.0
+ '@simple-libs/stream-utils': 2.0.0
+ semver: 7.8.5
+ optionalDependencies:
+ conventional-commits-filter: 5.0.0
+ conventional-commits-parser: 7.1.2
+
'@conventional-changelog/template@1.4.0': {}
'@dprint/formatter@0.5.1': {}
@@ -3194,8 +3453,8 @@ snapshots:
dependencies:
'@favware/colorette-spinner': 1.0.1
'@octokit/auth-token': 6.0.0
- '@octokit/core': 7.0.7
- '@octokit/plugin-retry': 8.1.1(@octokit/core@7.0.7)
+ '@octokit/core': 7.0.8
+ '@octokit/plugin-retry': 8.1.1(@octokit/core@7.0.8)
'@sapphire/result': 2.8.0
'@sapphire/utilities': 3.18.2
colorette: 2.0.20
@@ -3410,14 +3669,6 @@ snapshots:
'@nanoforge-dev/loader-website@1.5.0': {}
- '@nanoforge-dev/schematics@2.2.1(chokidar@5.0.0)':
- dependencies:
- '@angular-devkit/core': 22.1.6(chokidar@5.0.0)
- '@angular-devkit/schematics': 22.1.6(chokidar@5.0.0)
- rxjs: 7.8.2
- transitivePeerDependencies:
- - chokidar
-
'@nanoforge-dev/utils-eslint-config@1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@eslint/js': 9.39.5
@@ -3427,7 +3678,7 @@ snapshots:
eslint-plugin-format: 1.5.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))
eslint-plugin-prettier: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2)))(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)
globals: 16.5.0
- typescript-eslint: 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ typescript-eslint: 8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
transitivePeerDependencies:
- '@types/eslint'
- eslint
@@ -3439,57 +3690,59 @@ snapshots:
'@octokit/auth-token@6.0.0': {}
- '@octokit/core@7.0.7':
+ '@octokit/core@7.0.8':
dependencies:
'@octokit/auth-token': 6.0.0
- '@octokit/graphql': 9.0.4
- '@octokit/request': 10.0.15
- '@octokit/request-error': 7.1.1
- '@octokit/types': 17.0.0
+ '@octokit/graphql': 9.0.5
+ '@octokit/request': 10.0.16
+ '@octokit/request-error': 7.1.2
+ '@octokit/types': 18.0.0
before-after-hook: 4.0.0
universal-user-agent: 7.0.3
- '@octokit/endpoint@11.0.4':
+ '@octokit/endpoint@11.0.5':
dependencies:
- '@octokit/types': 17.0.0
+ '@octokit/types': 18.0.0
universal-user-agent: 7.0.3
- '@octokit/graphql@9.0.4':
+ '@octokit/graphql@9.0.5':
dependencies:
- '@octokit/request': 10.0.15
- '@octokit/types': 17.0.0
+ '@octokit/request': 10.0.16
+ '@octokit/types': 18.0.0
universal-user-agent: 7.0.3
'@octokit/openapi-types@27.0.0': {}
'@octokit/openapi-types@28.0.0': {}
- '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.7)':
+ '@octokit/openapi-types@29.0.1': {}
+
+ '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.8)':
dependencies:
- '@octokit/core': 7.0.7
+ '@octokit/core': 7.0.8
'@octokit/types': 16.0.0
- '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.7)':
+ '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.8)':
dependencies:
- '@octokit/core': 7.0.7
+ '@octokit/core': 7.0.8
'@octokit/types': 16.0.0
- '@octokit/plugin-retry@8.1.1(@octokit/core@7.0.7)':
+ '@octokit/plugin-retry@8.1.1(@octokit/core@7.0.8)':
dependencies:
- '@octokit/core': 7.0.7
- '@octokit/request-error': 7.1.1
+ '@octokit/core': 7.0.8
+ '@octokit/request-error': 7.1.2
'@octokit/types': 17.0.0
bottleneck: 2.19.5
- '@octokit/request-error@7.1.1':
+ '@octokit/request-error@7.1.2':
dependencies:
- '@octokit/types': 17.0.0
+ '@octokit/types': 18.0.0
- '@octokit/request@10.0.15':
+ '@octokit/request@10.0.16':
dependencies:
- '@octokit/endpoint': 11.0.4
- '@octokit/request-error': 7.1.1
- '@octokit/types': 17.0.0
+ '@octokit/endpoint': 11.0.5
+ '@octokit/request-error': 7.1.2
+ '@octokit/types': 18.0.0
content-type: 3.0.0
json-with-bigint: 3.5.12
universal-user-agent: 7.0.3
@@ -3502,6 +3755,10 @@ snapshots:
dependencies:
'@octokit/openapi-types': 28.0.0
+ '@octokit/types@18.0.0':
+ dependencies:
+ '@octokit/openapi-types': 29.0.1
+
'@oven/bun-darwin-aarch64@1.4.0':
optional: true
@@ -3706,6 +3963,10 @@ snapshots:
'@turbo/windows-arm64@2.10.12':
optional: true
+ '@types/bun@1.4.0':
+ dependencies:
+ bun-types: 1.4.0
+
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -3739,57 +4000,57 @@ snapshots:
'@types/validator@13.15.10': {}
- '@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.68.0
- '@typescript-eslint/type-utils': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.68.0
+ '@typescript-eslint/parser': 8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.69.0
+ '@typescript-eslint/type-utils': 8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.69.0
eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
- ignore: 7.0.6
+ ignore: 7.0.8
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/parser@8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/scope-manager': 8.68.0
- '@typescript-eslint/types': 8.68.0
- '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.68.0
+ '@typescript-eslint/scope-manager': 8.69.0
+ '@typescript-eslint/types': 8.69.0
+ '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.69.0
debug: 4.4.3(supports-color@10.2.2)
eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.68.0(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/project-service@8.69.0(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3)
- '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.69.0
debug: 4.4.3(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.68.0':
+ '@typescript-eslint/scope-manager@8.69.0':
dependencies:
- '@typescript-eslint/types': 8.68.0
- '@typescript-eslint/visitor-keys': 8.68.0
+ '@typescript-eslint/types': 8.69.0
+ '@typescript-eslint/visitor-keys': 8.69.0
- '@typescript-eslint/tsconfig-utils@8.68.0(typescript@6.0.3)':
+ '@typescript-eslint/tsconfig-utils@8.69.0(typescript@6.0.3)':
dependencies:
typescript: 6.0.3
- '@typescript-eslint/type-utils@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/type-utils@8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/types': 8.68.0
- '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/types': 8.69.0
+ '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
debug: 4.4.3(supports-color@10.2.2)
eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
ts-api-utils: 2.5.0(typescript@6.0.3)
@@ -3797,14 +4058,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/types@8.68.0': {}
+ '@typescript-eslint/types@8.69.0': {}
- '@typescript-eslint/typescript-estree@8.68.0(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/typescript-estree@8.69.0(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/project-service': 8.68.0(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3)
- '@typescript-eslint/types': 8.68.0
- '@typescript-eslint/visitor-keys': 8.68.0
+ '@typescript-eslint/project-service': 8.69.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.69.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.69.0
+ '@typescript-eslint/visitor-keys': 8.69.0
debug: 4.4.3(supports-color@10.2.2)
minimatch: 10.2.6
semver: 7.8.5
@@ -3814,20 +4075,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
+ '@typescript-eslint/utils@8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))
- '@typescript-eslint/scope-manager': 8.68.0
- '@typescript-eslint/types': 8.68.0
- '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.69.0
+ '@typescript-eslint/types': 8.69.0
+ '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3)
eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.68.0':
+ '@typescript-eslint/visitor-keys@8.69.0':
dependencies:
- '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/types': 8.69.0
eslint-visitor-keys: 5.0.1
'@vitest/coverage-v8@4.1.11(vitest@4.1.11)':
@@ -4026,6 +4287,10 @@ snapshots:
fill-range: 7.1.1
optional: true
+ bun-types@1.4.0:
+ dependencies:
+ '@types/node': 26.4.0
+
bun@1.4.0:
optionalDependencies:
'@oven/bun-darwin-aarch64': 1.4.0
@@ -4142,12 +4407,25 @@ snapshots:
optionalDependencies:
typescript: 6.0.3
+ cpx2@9.0.0:
+ dependencies:
+ debounce: 3.0.0
+ glob: 13.0.6
+ glob2base: 0.0.12
+ ignore: 7.0.8
+ minimatch: 10.2.6
+ p-map: 7.0.7
+ shell-quote: 1.10.0
+ subarg: 1.0.0
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
+ debounce@3.0.0: {}
+
debug@4.4.3(supports-color@10.2.2):
dependencies:
ms: 2.1.3
@@ -4368,6 +4646,8 @@ snapshots:
to-regex-range: 5.0.1
optional: true
+ find-index@0.1.1: {}
+
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -4429,6 +4709,16 @@ snapshots:
dependencies:
is-glob: 4.0.3
+ glob2base@0.0.12:
+ dependencies:
+ find-index: 0.1.1
+
+ glob@13.0.6:
+ dependencies:
+ minimatch: 10.2.6
+ minipass: 7.1.3
+ path-scurry: 2.0.2
+
global-directory@5.0.0:
dependencies:
ini: 6.0.0
@@ -4453,7 +4743,7 @@ snapshots:
ignore@5.3.2: {}
- ignore@7.0.6: {}
+ ignore@7.0.8: {}
import-fresh@3.3.1:
dependencies:
@@ -4623,6 +4913,8 @@ snapshots:
is-unicode-supported: 2.1.0
yoctocolors: 2.2.0
+ lru-cache@11.5.2: {}
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.6.0
@@ -4659,8 +4951,9 @@ snapshots:
dependencies:
brace-expansion: 2.1.4
- minipass@7.1.3:
- optional: true
+ minimist@1.2.8: {}
+
+ minipass@7.1.3: {}
minizlib@3.1.0:
dependencies:
@@ -4756,6 +5049,8 @@ snapshots:
dependencies:
p-limit: 3.1.0
+ p-map@7.0.7: {}
+
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -4781,6 +5076,11 @@ snapshots:
path-key@4.0.0: {}
+ path-scurry@2.0.2:
+ dependencies:
+ lru-cache: 11.5.2
+ minipass: 7.1.3
+
pathe@2.0.3: {}
picocolors@1.1.1: {}
@@ -4895,6 +5195,8 @@ snapshots:
shebang-regex@3.0.0: {}
+ shell-quote@1.10.0: {}
+
siginfo@2.0.0: {}
signal-exit@4.1.0: {}
@@ -4934,6 +5236,10 @@ snapshots:
strip-final-newline@4.0.0: {}
+ subarg@1.0.0:
+ dependencies:
+ minimist: 1.2.8
+
supports-color@10.2.2: {}
supports-color@7.2.0:
@@ -5025,12 +5331,12 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
- typescript-eslint@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3):
+ typescript-eslint@8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3):
dependencies:
- '@typescript-eslint/eslint-plugin': 8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3)
- '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/eslint-plugin': 8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.69.0(supports-color@10.2.2)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.69.0(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
eslint: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
typescript: 6.0.3
transitivePeerDependencies:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 0c147bb..110a13b 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,26 +1,27 @@
packages:
- .
- libs/*
-# - libs/loader/*
+ - libs/loader/*
allowBuilds:
bun: true
catalogs:
build:
- tsdown: ^0.22.3
+ cpx2: ^9.0.0
+ tsdown: ^0.22.14
turbo: ^2.10.12
typescript: ^6.0.3
unrun: ^0.3.0
ci:
- '@commitlint/cli': ^21.2.0
- '@commitlint/config-conventional': ^21.2.0
+ '@commitlint/cli': ^21.2.2
+ '@commitlint/config-conventional': ^21.2.2
'@favware/cliff-jumper': ^6.1.0
'@nanoforge-dev/actions': ^2.1.5
husky: ^9.1.7
- lint-staged: ^17.0.8
+ lint-staged: ^17.4.1
cli:
- '@inquirer/prompts': ^8.5.2
+ '@inquirer/prompts': ^8.7.0
'@types/inquirer': ^9.0.10
ansis: ^4.3.1
commander: ^15.0.0
@@ -28,29 +29,31 @@ catalogs:
ora: ^9.4.1
core:
'@nanoforge-dev/editor': ^0.1.4
- '@types/node': ^26.1.0
- bun: ^1.3.14
+ '@types/bun': ^1.3.14
+ '@types/node': ^26.4.0
+ bun: ^1.4.0
libs:
chokidar: ^5.0.0
class-transformer: ^0.5.1
class-validator: ^0.15.1
dotenv: ^17.4.2
- open: ^11.0.0
+ open: ^11.0.2
rc9: ^3.0.1
reflect-metadata: ^0.2.2
lint:
'@nanoforge-dev/utils-eslint-config': ^1.0.2
'@nanoforge-dev/utils-prettier-config': ^1.0.2
'@trivago/prettier-plugin-sort-imports': ^6.0.2
- eslint: ^10.6.0
- prettier: ^3.9.4
+ eslint: ^10.9.1
+ prettier: ^3.9.6
loader:
- '@nanoforge-dev/loader-client': ^1.4.0
- '@nanoforge-dev/loader-server': ^1.4.0
+ '@nanoforge-dev/loader-client': ^1.5.0
+ '@nanoforge-dev/loader-server': ^1.5.0
schematics:
- '@angular-devkit/schematics': ^22.0.5
- '@angular-devkit/schematics-cli': ^22.0.5
- '@nanoforge-dev/schematics': ^2.2.1
+ '@angular-devkit/core': ^22.0.4
+ '@angular-devkit/schematics': ^22.1.6
+ '@angular-devkit/schematics-cli': ^22.1.6
+ rxjs: ^7.8.2
test:
- '@vitest/coverage-v8': ^4.1.10
- vitest: ^4.1.10
+ '@vitest/coverage-v8': ^4.1.11
+ vitest: ^4.1.11
diff --git a/tsdown.config.ts b/tsdown.config.ts
index ba29b93..073c2e3 100644
--- a/tsdown.config.ts
+++ b/tsdown.config.ts
@@ -21,9 +21,9 @@ function createTsdownConfig({
platform: "node",
target: "esnext",
treeshake: false,
- deps: {
- neverBundle: true,
- },
+ // No `deps.neverBundle: true` here: it externalizes anything matching tsdown's
+ // npm-package-specifier heuristic, which our `@lib/*` and `@utils/*` path aliases
+ // match too, leaving them as unresolvable imports in the built output.
env: dotenv.config({
path: resolve(
process.cwd(),
diff --git a/turbo.json b/turbo.json
index c44a392..42e96c0 100644
--- a/turbo.json
+++ b/turbo.json
@@ -11,6 +11,12 @@
"outputs": ["dist/**"],
"outputLogs": "errors-only"
},
+ "build:dev": {
+ "dependsOn": ["^build:dev"],
+ "inputs": ["src/**", "test/**", "package.json", "tsconfig.json", "tsdown.config.ts"],
+ "outputs": ["dist/**"],
+ "outputLogs": "errors-only"
+ },
"format": {
"dependsOn": ["^format"],
"inputs": ["**"],
@@ -25,7 +31,27 @@
},
"test:unit": {
"dependsOn": ["^test:unit"],
- "inputs": ["src/**", "test/**", "package.json", "tsconfig.json", "tsdown.config.ts"],
+ "inputs": [
+ "src/**",
+ "test/**",
+ "package.json",
+ "tsconfig.json",
+ "tsdown.config.ts",
+ "vitest.config.ts"
+ ],
+ "outputs": [],
+ "outputLogs": "errors-only"
+ },
+ "test:e2e": {
+ "dependsOn": ["build", "^test:e2e"],
+ "inputs": [
+ "src/**",
+ "e2e/**",
+ "package.json",
+ "tsconfig.json",
+ "tsdown.config.ts",
+ "vitest.config.e2e.ts"
+ ],
"outputs": [],
"outputLogs": "errors-only"
},
From 406842e5345a9677954c49c64dadda6e81b45f1b Mon Sep 17 00:00:00 2001
From: Exelo
Date: Sat, 12 Sep 2026 23:31:08 +0900
Subject: [PATCH 03/15] feat: add config parser (#214)
* feat: add config parser
* chore: change package.json fields order
---
libs/config/README.md | 24 ++++++
libs/config/package.json | 5 +-
libs/config/src/index.ts | 3 +
libs/config/src/parse/config-error.ts | 14 +++
libs/config/src/parse/load-config-module.ts | 38 +++++++++
libs/config/src/parse/parse-config.ts | 10 +++
libs/config/src/parse/validate-config.ts | 85 +++++++++++++++++++
libs/config/test/parse-config.spec.ts | 49 +++++++++++
.../parse/fixtures/invalid-type-config.ts | 3 +
.../test/parse/fixtures/no-default-export.ts | 3 +
.../test/parse/fixtures/throwing-config.ts | 1 +
.../test/parse/fixtures/valid-config.js | 4 +
.../test/parse/fixtures/valid-config.ts | 4 +
pnpm-workspace.yaml | 4 +-
14 files changed, 244 insertions(+), 3 deletions(-)
create mode 100644 libs/config/src/parse/config-error.ts
create mode 100644 libs/config/src/parse/load-config-module.ts
create mode 100644 libs/config/src/parse/parse-config.ts
create mode 100644 libs/config/src/parse/validate-config.ts
create mode 100644 libs/config/test/parse-config.spec.ts
create mode 100644 libs/config/test/parse/fixtures/invalid-type-config.ts
create mode 100644 libs/config/test/parse/fixtures/no-default-export.ts
create mode 100644 libs/config/test/parse/fixtures/throwing-config.ts
create mode 100644 libs/config/test/parse/fixtures/valid-config.js
create mode 100644 libs/config/test/parse/fixtures/valid-config.ts
diff --git a/libs/config/README.md b/libs/config/README.md
index c38cd9a..05cc5c3 100644
--- a/libs/config/README.md
+++ b/libs/config/README.md
@@ -62,6 +62,30 @@ const resolved = resolveConfig({ type: "client", dir: { assets: "static" } });
Array fields (`packages`, `libs`) are replaced wholesale by an override rather than concatenated with the defaults.
+### Loading a config file
+
+`parseConfig` loads a `nanoforge.config.ts`/`.js` file from a given path, validates its default export, and resolves it against the defaults in one step:
+
+```ts
+import { parseConfig } from "@nanoforge-dev/config";
+
+const config = await parseConfig("./nanoforge.config.ts");
+```
+
+If the file is missing, fails to load, has no default export, or its default export isn't a recognizable `NanoforgeConfig`, `parseConfig` rejects with a `ConfigParseError` whose `code` (`"not-found" | "load-failed" | "no-default-export" | "invalid-type"`) identifies the failure:
+
+```ts
+import { ConfigParseError, parseConfig } from "@nanoforge-dev/config";
+
+try {
+ await parseConfig("./nanoforge.config.ts");
+} catch (error) {
+ if (error instanceof ConfigParseError && error.code === "not-found") {
+ // ...
+ }
+}
+```
+
## Links
- [GitHub][source]
diff --git a/libs/config/package.json b/libs/config/package.json
index fcb2007..25a6b79 100644
--- a/libs/config/package.json
+++ b/libs/config/package.json
@@ -57,6 +57,10 @@
"release": "cliff-jumper",
"docs": "mint-tsdocs generate"
},
+ "dependencies": {
+ "unrun": "catalog:libs",
+ "zod": "catalog:libs"
+ },
"devDependencies": {
"@favware/cliff-jumper": "catalog:ci",
"@nanoforge-dev/utils-eslint-config": "catalog:lint",
@@ -66,7 +70,6 @@
"prettier": "catalog:lint",
"tsdown": "catalog:build",
"typescript": "catalog:build",
- "unrun": "catalog:build",
"vitest": "catalog:test"
},
"packageManager": "pnpm@12.0.0",
diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts
index 83547cd..c4d0991 100644
--- a/libs/config/src/index.ts
+++ b/libs/config/src/index.ts
@@ -5,6 +5,9 @@ export {
defaultWorkspaceConfig,
} from "./defaults/default-configs";
export { defineConfig } from "./define-config";
+export { ConfigParseError } from "./parse/config-error";
+export type { ConfigParseErrorCode } from "./parse/config-error";
+export { parseConfig } from "./parse/parse-config";
export {
resolveClientConfig,
resolveConfig,
diff --git a/libs/config/src/parse/config-error.ts b/libs/config/src/parse/config-error.ts
new file mode 100644
index 0000000..d97e854
--- /dev/null
+++ b/libs/config/src/parse/config-error.ts
@@ -0,0 +1,14 @@
+export type ConfigParseErrorCode =
+ "not-found" | "load-failed" | "no-default-export" | "invalid-type";
+
+export class ConfigParseError extends Error {
+ constructor(
+ public readonly code: ConfigParseErrorCode,
+ message: string,
+ public readonly path: string,
+ options?: { cause?: unknown },
+ ) {
+ super(message, options);
+ this.name = "ConfigParseError";
+ }
+}
diff --git a/libs/config/src/parse/load-config-module.ts b/libs/config/src/parse/load-config-module.ts
new file mode 100644
index 0000000..ae1264d
--- /dev/null
+++ b/libs/config/src/parse/load-config-module.ts
@@ -0,0 +1,38 @@
+import { access } from "node:fs/promises";
+
+import { ConfigParseError } from "./config-error";
+
+export const loadConfigModule = async (path: string | URL): Promise => {
+ const pathLabel = String(path);
+
+ try {
+ await access(path);
+ } catch (cause) {
+ throw new ConfigParseError("not-found", `Config file not found: ${pathLabel}`, pathLabel, {
+ cause,
+ });
+ }
+
+ let module: { default?: unknown };
+ try {
+ const { unrun } = await import("unrun");
+ ({ module } = await unrun<{ default?: unknown }>({ path, preset: "bundle-require" }));
+ } catch (cause) {
+ throw new ConfigParseError(
+ "load-failed",
+ `Failed to load config file: ${pathLabel}`,
+ pathLabel,
+ { cause },
+ );
+ }
+
+ if (module.default === undefined) {
+ throw new ConfigParseError(
+ "no-default-export",
+ `Config file has no default export: ${pathLabel}`,
+ pathLabel,
+ );
+ }
+
+ return module.default;
+};
diff --git a/libs/config/src/parse/parse-config.ts b/libs/config/src/parse/parse-config.ts
new file mode 100644
index 0000000..d3ca6b2
--- /dev/null
+++ b/libs/config/src/parse/parse-config.ts
@@ -0,0 +1,10 @@
+import { resolveConfig } from "../merge/resolve-config";
+import type { NanoforgeConfig } from "../types";
+import { loadConfigModule } from "./load-config-module";
+import { assertNanoforgeConfig } from "./validate-config";
+
+export const parseConfig = async (path: string | URL): Promise => {
+ const raw = await loadConfigModule(path);
+ assertNanoforgeConfig(raw, String(path));
+ return resolveConfig(raw);
+};
diff --git a/libs/config/src/parse/validate-config.ts b/libs/config/src/parse/validate-config.ts
new file mode 100644
index 0000000..c504586
--- /dev/null
+++ b/libs/config/src/parse/validate-config.ts
@@ -0,0 +1,85 @@
+import { z } from "zod";
+
+import type { NanoforgeConfig } from "../types";
+import { ConfigParseError } from "./config-error";
+
+const buildableConfigShape = {
+ entryFile: z.string().optional(),
+ out: z
+ .object({
+ dir: z.string().optional(),
+ mainFile: z.string().optional(),
+ })
+ .optional(),
+};
+
+const containLibConfigShape = {
+ libs: z.array(z.string()).optional(),
+};
+
+const sourceableConfigShape = {
+ dir: z
+ .object({
+ assets: z.string().optional(),
+ packages: z.string().optional(),
+ components: z.string().optional(),
+ systems: z.string().optional(),
+ scenes: z.string().optional(),
+ })
+ .optional(),
+};
+
+const workspaceConfigSchema = z.object({
+ type: z.literal("workspace"),
+ packages: z.array(z.string()).optional(),
+});
+
+const libConfigSchema = z.object({
+ type: z.literal("lib"),
+ dir: z
+ .object({
+ assets: z.string().optional(),
+ shared: z.string().optional(),
+ components: z.string().optional(),
+ systems: z.string().optional(),
+ scenes: z.string().optional(),
+ })
+ .optional(),
+});
+
+const clientConfigSchema = z.object({
+ type: z.literal("client"),
+ ...sourceableConfigShape,
+ ...buildableConfigShape,
+ ...containLibConfigShape,
+});
+
+const serverConfigSchema = z.object({
+ type: z.literal("server"),
+ ...sourceableConfigShape,
+ ...buildableConfigShape,
+ ...containLibConfigShape,
+});
+
+const nanoforgeConfigSchema = z.discriminatedUnion("type", [
+ workspaceConfigSchema,
+ libConfigSchema,
+ clientConfigSchema,
+ serverConfigSchema,
+]);
+
+export function assertNanoforgeConfig(
+ value: unknown,
+ path: string,
+): asserts value is NanoforgeConfig {
+ const result = nanoforgeConfigSchema.safeParse(value);
+
+ if (!result.success) {
+ throw new ConfigParseError(
+ "invalid-type",
+ `Config file's default export is not a valid NanoforgeConfig: ${path}\n${z.prettifyError(result.error)}`,
+ path,
+ { cause: result.error },
+ );
+ }
+}
diff --git a/libs/config/test/parse-config.spec.ts b/libs/config/test/parse-config.spec.ts
new file mode 100644
index 0000000..fd5fb6e
--- /dev/null
+++ b/libs/config/test/parse-config.spec.ts
@@ -0,0 +1,49 @@
+import { describe, expect, it } from "vitest";
+
+import { ConfigParseError, parseConfig } from "../src";
+
+const fixture = (name: string) => new URL(`./parse/fixtures/${name}`, import.meta.url);
+
+describe("parseConfig", () => {
+ it("resolves a .ts config file into a fully-defaulted config", async () => {
+ const resolved = await parseConfig(fixture("valid-config.ts"));
+ expect(resolved.type).toBe("client");
+ expect(resolved).toMatchObject({
+ entryFile: "custom.ts",
+ out: { dir: "dist", mainFile: "main.js" },
+ libs: [],
+ });
+ });
+
+ it("resolves a .js config file the same way as .ts", async () => {
+ const resolved = await parseConfig(fixture("valid-config.js"));
+ expect(resolved).toEqual({ type: "workspace", packages: ["apps/*"] });
+ });
+
+ it("rejects with code 'not-found' when the file doesn't exist", async () => {
+ await expect(parseConfig(fixture("missing-config.ts"))).rejects.toMatchObject({
+ constructor: ConfigParseError,
+ code: "not-found",
+ });
+ });
+
+ it("rejects with code 'load-failed' when the file throws while loading", async () => {
+ const error = await parseConfig(fixture("throwing-config.ts")).catch((e) => e);
+ expect(error).toMatchObject({ constructor: ConfigParseError, code: "load-failed" });
+ expect(error.cause).toBeInstanceOf(Error);
+ });
+
+ it("rejects with code 'no-default-export' when there's no default export", async () => {
+ await expect(parseConfig(fixture("no-default-export.ts"))).rejects.toMatchObject({
+ constructor: ConfigParseError,
+ code: "no-default-export",
+ });
+ });
+
+ it("rejects with code 'invalid-type' when the type is unrecognized", async () => {
+ await expect(parseConfig(fixture("invalid-type-config.ts"))).rejects.toMatchObject({
+ constructor: ConfigParseError,
+ code: "invalid-type",
+ });
+ });
+});
diff --git a/libs/config/test/parse/fixtures/invalid-type-config.ts b/libs/config/test/parse/fixtures/invalid-type-config.ts
new file mode 100644
index 0000000..850f8cd
--- /dev/null
+++ b/libs/config/test/parse/fixtures/invalid-type-config.ts
@@ -0,0 +1,3 @@
+export default {
+ type: "nope",
+};
diff --git a/libs/config/test/parse/fixtures/no-default-export.ts b/libs/config/test/parse/fixtures/no-default-export.ts
new file mode 100644
index 0000000..f85a7a9
--- /dev/null
+++ b/libs/config/test/parse/fixtures/no-default-export.ts
@@ -0,0 +1,3 @@
+export const notDefault = {
+ type: "client",
+};
diff --git a/libs/config/test/parse/fixtures/throwing-config.ts b/libs/config/test/parse/fixtures/throwing-config.ts
new file mode 100644
index 0000000..c3fc99d
--- /dev/null
+++ b/libs/config/test/parse/fixtures/throwing-config.ts
@@ -0,0 +1 @@
+throw new Error("boom");
diff --git a/libs/config/test/parse/fixtures/valid-config.js b/libs/config/test/parse/fixtures/valid-config.js
new file mode 100644
index 0000000..e9727de
--- /dev/null
+++ b/libs/config/test/parse/fixtures/valid-config.js
@@ -0,0 +1,4 @@
+export default {
+ type: "workspace",
+ packages: ["apps/*"],
+};
diff --git a/libs/config/test/parse/fixtures/valid-config.ts b/libs/config/test/parse/fixtures/valid-config.ts
new file mode 100644
index 0000000..ae4e5c1
--- /dev/null
+++ b/libs/config/test/parse/fixtures/valid-config.ts
@@ -0,0 +1,4 @@
+export default {
+ type: "client",
+ entryFile: "custom.ts",
+};
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 110a13b..10feea7 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,7 +1,6 @@
packages:
- .
- libs/*
- - libs/loader/*
allowBuilds:
bun: true
@@ -12,7 +11,6 @@ catalogs:
tsdown: ^0.22.14
turbo: ^2.10.12
typescript: ^6.0.3
- unrun: ^0.3.0
ci:
'@commitlint/cli': ^21.2.2
'@commitlint/config-conventional': ^21.2.2
@@ -40,6 +38,8 @@ catalogs:
open: ^11.0.2
rc9: ^3.0.1
reflect-metadata: ^0.2.2
+ unrun: ^0.3.0
+ zod: ^4.5.4
lint:
'@nanoforge-dev/utils-eslint-config': ^1.0.2
'@nanoforge-dev/utils-prettier-config': ^1.0.2
From 9ffb07c62304518d12913b16a4938c012024565f Mon Sep 17 00:00:00 2001
From: Exelo
Date: Sun, 13 Sep 2026 22:31:28 +0900
Subject: [PATCH 04/15] feat: add new project and workspace schematics and
remove old ones (#216)
* feat: add new project and workspace schematics and remove old ones
* fix: remove comments in generated code and add name to project
---
docs/docs/schematics/1-overview.mdx | 26 +-
.../schematics/2-schematics/1-application.mdx | 80 ---
.../schematics/2-schematics/1-workspace.mdx | 69 +++
.../2-schematics/2-configuration.mdx | 50 --
.../schematics/2-schematics/2-project.mdx | 98 ++++
.../{6-component.mdx => 3-component.mdx} | 0
.../schematics/2-schematics/3-part-base.mdx | 115 -----
.../schematics/2-schematics/4-part-main.mdx | 77 ---
.../{7-system.mdx => 4-system.mdx} | 0
.../docs/schematics/2-schematics/5-docker.mdx | 70 ---
docs/guides/cli/1-overview.mdx | 2 +-
e2e/helpers/write-project-config.ts | 36 ++
libs/schematics/README.md | 7 +-
libs/schematics/e2e/application.test.ts | 170 -------
libs/schematics/e2e/configuration.test.ts | 108 ----
libs/schematics/e2e/docker.test.ts | 45 --
libs/schematics/e2e/part-base.test.ts | 122 -----
libs/schematics/e2e/part-main.test.ts | 262 ----------
libs/schematics/e2e/project.test.ts | 477 ++++++++++++++++++
libs/schematics/e2e/workspace.test.ts | 265 ++++++++++
libs/schematics/src/collection.json | 31 +-
libs/schematics/src/defaults.ts | 22 +-
.../libs/application/application.factory.ts | 87 ----
.../libs/application/application.options.d.ts | 56 --
.../libs/application/application.schema.d.ts | 61 ---
.../libs/application/files/js/.prettierignore | 8 -
.../src/libs/application/files/js/README.md | 166 ------
.../application/files/js/eslint.config.js | 11 -
.../files/js/package.json.template | 48 --
.../files/js/pnpm-workspace.yaml.template | 3 -
.../application/files/js/prettier.config.js | 6 -
.../libs/application/files/ts/.prettierignore | 8 -
.../src/libs/application/files/ts/README.md | 166 ------
.../src/libs/application/files/ts/__dot__env | 6 -
.../application/files/ts/eslint.config.js | 15 -
.../files/ts/package.json.template | 52 --
.../files/ts/pnpm-workspace.yaml.template | 3 -
.../application/files/ts/prettier.config.js | 6 -
.../src/libs/application/schema.json | 70 ---
.../configuration/configuration.factory.ts | 94 ----
.../configuration/configuration.options.d.ts | 1 -
.../configuration/configuration.schema.d.ts | 26 -
.../configuration/files/nanoforge.config.json | 1 -
.../src/libs/configuration/schema.json | 34 --
.../src/libs/docker/docker.factory.ts | 36 --
.../src/libs/docker/docker.options.d.ts | 6 -
.../src/libs/docker/docker.schema.d.ts | 11 -
.../src/libs/docker/files/.dockerignore | 1 -
.../src/libs/docker/files/Dockerfile | 25 -
libs/schematics/src/libs/docker/schema.json | 19 -
.../js/__nanoforgeFolder__/client.save.json | 77 ---
.../js/__nanoforgeFolder__/server.save.json | 49 --
.../__part__/components/example.component.js | 72 ---
.../files/js/__part__/init/after-init.js | 12 -
.../js/__part__/init/after-registry-init.js | 14 -
.../files/js/__part__/init/after-run.js | 12 -
.../files/js/__part__/init/before-init.js | 13 -
.../js/__part__/init/before-registry-init.js | 14 -
.../files/js/__part__/init/before-run.js | 12 -
.../js/__part__/systems/example.system.js | 44 --
.../ts/__nanoforgeFolder__/client.save.json | 77 ---
.../ts/__nanoforgeFolder__/server.save.json | 49 --
.../__part__/components/example.component.ts | 55 --
.../files/ts/__part__/init/after-init.ts | 5 -
.../ts/__part__/init/after-registry-init.ts | 6 -
.../files/ts/__part__/init/after-run.ts | 5 -
.../files/ts/__part__/init/before-init.ts | 5 -
.../ts/__part__/init/before-registry-init.ts | 6 -
.../files/ts/__part__/init/before-run.ts | 5 -
.../ts/__part__/systems/example.system.ts | 31 --
.../src/libs/part-base/part-base.factory.ts | 51 --
.../src/libs/part-base/part-base.options.d.ts | 21 -
.../src/libs/part-base/part-base.schema.d.ts | 26 -
.../schematics/src/libs/part-base/schema.json | 35 --
.../src/libs/part-main/part-main.factory.ts | 84 ---
.../src/libs/part-main/part-main.options.d.ts | 1 -
.../src/libs/part-main/part-main.schema.d.ts | 36 --
.../schematics/src/libs/part-main/schema.json | 43 --
.../src/libs/project/files/js/.dockerignore | 5 +
.../src/libs/project/files/js/Dockerfile | 39 ++
.../src/libs/project/files/js/README.md | 53 ++
.../project/files/js/assets/__dot__gitkeep | 0
.../src/libs/project/files/js/jsconfig.json | 6 +
.../libs/project/files/js/nanoforge.config.js | 6 +
.../project/files/js/package.json.template | 21 +
.../files/js/pnpm-workspace.yaml.template | 3 +
.../drawable-circle-2d.component.js | 404 +++++++++++++++
.../src/components/position-2d.component.js | 42 ++
.../src/libs/project/files/js/src/main.js | 43 ++
.../files/js/src/systems/draw-2d.system.js | 33 ++
.../files/js/src/systems/move-2d.system.js | 34 ++
.../js/src/systems/position-sync.system.js | 35 ++
.../src/libs/project/files/ts/.dockerignore | 5 +
.../src/libs/project/files/ts/Dockerfile | 39 ++
.../src/libs/project/files/ts/README.md | 53 ++
.../files/js => project/files/ts}/__dot__env | 0
.../js => project/files/ts}/__dot__gitignore | 0
.../project/files/ts/assets/__dot__gitkeep | 0
.../libs/project/files/ts/nanoforge.config.ts | 5 +
.../project/files/ts/package.json.template | 21 +
.../files/ts/pnpm-workspace.yaml.template | 3 +
.../drawable-circle-2d.component.ts | 367 ++++++++++++++
.../src/components/position-2d.component.ts | 35 ++
.../src/libs/project/files/ts/src/main.ts | 39 ++
.../files/ts/src/systems/draw-2d.system.ts | 25 +
.../files/ts/src/systems/move-2d.system.ts | 26 +
.../ts/src/systems/position-sync.system.ts | 27 +
.../files/ts/tsconfig.json | 2 +-
.../src/libs/project/project.factory.ts | 135 +++++
.../src/libs/project/project.options.d.ts | 92 ++++
.../src/libs/project/project.schema.d.ts | 87 ++++
libs/schematics/src/libs/project/schema.json | 83 +++
.../src/libs/workspace/files/js/.dockerignore | 5 +
.../src/libs/workspace/files/js/Dockerfile | 39 ++
.../src/libs/workspace/files/js/README.md | 51 ++
.../libs/workspace/files/js/__dot__gitignore | 137 +++++
.../files/js/jsconfig.json | 2 +-
.../workspace/files/js/nanoforge.config.js | 6 +
.../workspace/files/js/package.json.template | 19 +
.../files/js/pnpm-workspace.yaml.template | 6 +
.../src/libs/workspace/files/ts/.dockerignore | 5 +
.../src/libs/workspace/files/ts/Dockerfile | 39 ++
.../src/libs/workspace/files/ts/README.md | 51 ++
.../files/ts/__dot__gitignore | 6 +-
.../workspace/files/ts/nanoforge.config.ts | 6 +
.../workspace/files/ts/package.json.template | 20 +
.../files/ts/pnpm-workspace.yaml.template | 6 +
.../src/libs/workspace/files/ts/tsconfig.json | 40 ++
.../schematics/src/libs/workspace/schema.json | 45 ++
.../src/libs/workspace/workspace.factory.ts | 84 +++
.../src/libs/workspace/workspace.options.d.ts | 44 ++
.../src/libs/workspace/workspace.schema.d.ts | 41 ++
.../utils/config/config.declarator.spec.ts | 38 --
.../src/utils/config/config.declarator.ts | 10 -
.../src/utils/config/config.finder.spec.ts | 72 ---
.../src/utils/config/config.finder.ts | 33 --
.../src/utils/config/config.type.ts | 31 --
libs/schematics/src/utils/main/const.ts | 10 -
libs/schematics/src/utils/main/enums.ts | 8 -
.../src/utils/main/main-functions.spec.ts | 135 -----
.../src/utils/main/main-functions.ts | 61 ---
libs/schematics/src/utils/main/main-utils.ts | 7 -
.../src/utils/main/main.generator.spec.ts | 222 --------
.../src/utils/main/main.generator.ts | 224 --------
libs/schematics/src/utils/main/save.type.ts | 38 --
libs/schematics/src/utils/registry.spec.ts | 106 ++++
libs/schematics/src/utils/registry.ts | 106 ++++
libs/schematics/tsdown.config.ts | 7 +-
148 files changed, 3612 insertions(+), 3934 deletions(-)
delete mode 100644 docs/docs/schematics/2-schematics/1-application.mdx
create mode 100644 docs/docs/schematics/2-schematics/1-workspace.mdx
delete mode 100644 docs/docs/schematics/2-schematics/2-configuration.mdx
create mode 100644 docs/docs/schematics/2-schematics/2-project.mdx
rename docs/docs/schematics/2-schematics/{6-component.mdx => 3-component.mdx} (100%)
delete mode 100644 docs/docs/schematics/2-schematics/3-part-base.mdx
delete mode 100644 docs/docs/schematics/2-schematics/4-part-main.mdx
rename docs/docs/schematics/2-schematics/{7-system.mdx => 4-system.mdx} (100%)
delete mode 100644 docs/docs/schematics/2-schematics/5-docker.mdx
create mode 100644 e2e/helpers/write-project-config.ts
delete mode 100644 libs/schematics/e2e/application.test.ts
delete mode 100644 libs/schematics/e2e/configuration.test.ts
delete mode 100644 libs/schematics/e2e/docker.test.ts
delete mode 100644 libs/schematics/e2e/part-base.test.ts
delete mode 100644 libs/schematics/e2e/part-main.test.ts
create mode 100644 libs/schematics/e2e/project.test.ts
create mode 100644 libs/schematics/e2e/workspace.test.ts
delete mode 100644 libs/schematics/src/libs/application/application.factory.ts
delete mode 100644 libs/schematics/src/libs/application/application.options.d.ts
delete mode 100644 libs/schematics/src/libs/application/application.schema.d.ts
delete mode 100644 libs/schematics/src/libs/application/files/js/.prettierignore
delete mode 100644 libs/schematics/src/libs/application/files/js/README.md
delete mode 100644 libs/schematics/src/libs/application/files/js/eslint.config.js
delete mode 100644 libs/schematics/src/libs/application/files/js/package.json.template
delete mode 100644 libs/schematics/src/libs/application/files/js/pnpm-workspace.yaml.template
delete mode 100644 libs/schematics/src/libs/application/files/js/prettier.config.js
delete mode 100644 libs/schematics/src/libs/application/files/ts/.prettierignore
delete mode 100644 libs/schematics/src/libs/application/files/ts/README.md
delete mode 100644 libs/schematics/src/libs/application/files/ts/__dot__env
delete mode 100644 libs/schematics/src/libs/application/files/ts/eslint.config.js
delete mode 100644 libs/schematics/src/libs/application/files/ts/package.json.template
delete mode 100644 libs/schematics/src/libs/application/files/ts/pnpm-workspace.yaml.template
delete mode 100644 libs/schematics/src/libs/application/files/ts/prettier.config.js
delete mode 100644 libs/schematics/src/libs/application/schema.json
delete mode 100644 libs/schematics/src/libs/configuration/configuration.factory.ts
delete mode 100644 libs/schematics/src/libs/configuration/configuration.options.d.ts
delete mode 100644 libs/schematics/src/libs/configuration/configuration.schema.d.ts
delete mode 100644 libs/schematics/src/libs/configuration/files/nanoforge.config.json
delete mode 100644 libs/schematics/src/libs/configuration/schema.json
delete mode 100644 libs/schematics/src/libs/docker/docker.factory.ts
delete mode 100644 libs/schematics/src/libs/docker/docker.options.d.ts
delete mode 100644 libs/schematics/src/libs/docker/docker.schema.d.ts
delete mode 100644 libs/schematics/src/libs/docker/files/.dockerignore
delete mode 100644 libs/schematics/src/libs/docker/files/Dockerfile
delete mode 100644 libs/schematics/src/libs/docker/schema.json
delete mode 100644 libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/client.save.json
delete mode 100644 libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/server.save.json
delete mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/components/example.component.js
delete mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/after-init.js
delete mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/after-registry-init.js
delete mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/after-run.js
delete mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/before-init.js
delete mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/before-registry-init.js
delete mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/init/before-run.js
delete mode 100644 libs/schematics/src/libs/part-base/files/js/__part__/systems/example.system.js
delete mode 100644 libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/client.save.json
delete mode 100644 libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/server.save.json
delete mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/components/example.component.ts
delete mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/after-init.ts
delete mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/after-registry-init.ts
delete mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/after-run.ts
delete mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/before-init.ts
delete mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/before-registry-init.ts
delete mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/init/before-run.ts
delete mode 100644 libs/schematics/src/libs/part-base/files/ts/__part__/systems/example.system.ts
delete mode 100644 libs/schematics/src/libs/part-base/part-base.factory.ts
delete mode 100644 libs/schematics/src/libs/part-base/part-base.options.d.ts
delete mode 100644 libs/schematics/src/libs/part-base/part-base.schema.d.ts
delete mode 100644 libs/schematics/src/libs/part-base/schema.json
delete mode 100644 libs/schematics/src/libs/part-main/part-main.factory.ts
delete mode 100644 libs/schematics/src/libs/part-main/part-main.options.d.ts
delete mode 100644 libs/schematics/src/libs/part-main/part-main.schema.d.ts
delete mode 100644 libs/schematics/src/libs/part-main/schema.json
create mode 100644 libs/schematics/src/libs/project/files/js/.dockerignore
create mode 100644 libs/schematics/src/libs/project/files/js/Dockerfile
create mode 100644 libs/schematics/src/libs/project/files/js/README.md
create mode 100644 libs/schematics/src/libs/project/files/js/assets/__dot__gitkeep
create mode 100644 libs/schematics/src/libs/project/files/js/jsconfig.json
create mode 100644 libs/schematics/src/libs/project/files/js/nanoforge.config.js
create mode 100644 libs/schematics/src/libs/project/files/js/package.json.template
create mode 100644 libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template
create mode 100644 libs/schematics/src/libs/project/files/js/src/components/drawable-circle-2d.component.js
create mode 100644 libs/schematics/src/libs/project/files/js/src/components/position-2d.component.js
create mode 100644 libs/schematics/src/libs/project/files/js/src/main.js
create mode 100644 libs/schematics/src/libs/project/files/js/src/systems/draw-2d.system.js
create mode 100644 libs/schematics/src/libs/project/files/js/src/systems/move-2d.system.js
create mode 100644 libs/schematics/src/libs/project/files/js/src/systems/position-sync.system.js
create mode 100644 libs/schematics/src/libs/project/files/ts/.dockerignore
create mode 100644 libs/schematics/src/libs/project/files/ts/Dockerfile
create mode 100644 libs/schematics/src/libs/project/files/ts/README.md
rename libs/schematics/src/libs/{application/files/js => project/files/ts}/__dot__env (100%)
rename libs/schematics/src/libs/{application/files/js => project/files/ts}/__dot__gitignore (100%)
create mode 100644 libs/schematics/src/libs/project/files/ts/assets/__dot__gitkeep
create mode 100644 libs/schematics/src/libs/project/files/ts/nanoforge.config.ts
create mode 100644 libs/schematics/src/libs/project/files/ts/package.json.template
create mode 100644 libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template
create mode 100644 libs/schematics/src/libs/project/files/ts/src/components/drawable-circle-2d.component.ts
create mode 100644 libs/schematics/src/libs/project/files/ts/src/components/position-2d.component.ts
create mode 100644 libs/schematics/src/libs/project/files/ts/src/main.ts
create mode 100644 libs/schematics/src/libs/project/files/ts/src/systems/draw-2d.system.ts
create mode 100644 libs/schematics/src/libs/project/files/ts/src/systems/move-2d.system.ts
create mode 100644 libs/schematics/src/libs/project/files/ts/src/systems/position-sync.system.ts
rename libs/schematics/src/libs/{application => project}/files/ts/tsconfig.json (96%)
create mode 100644 libs/schematics/src/libs/project/project.factory.ts
create mode 100644 libs/schematics/src/libs/project/project.options.d.ts
create mode 100644 libs/schematics/src/libs/project/project.schema.d.ts
create mode 100644 libs/schematics/src/libs/project/schema.json
create mode 100644 libs/schematics/src/libs/workspace/files/js/.dockerignore
create mode 100644 libs/schematics/src/libs/workspace/files/js/Dockerfile
create mode 100644 libs/schematics/src/libs/workspace/files/js/README.md
create mode 100644 libs/schematics/src/libs/workspace/files/js/__dot__gitignore
rename libs/schematics/src/libs/{application => workspace}/files/js/jsconfig.json (95%)
create mode 100644 libs/schematics/src/libs/workspace/files/js/nanoforge.config.js
create mode 100644 libs/schematics/src/libs/workspace/files/js/package.json.template
create mode 100644 libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template
create mode 100644 libs/schematics/src/libs/workspace/files/ts/.dockerignore
create mode 100644 libs/schematics/src/libs/workspace/files/ts/Dockerfile
create mode 100644 libs/schematics/src/libs/workspace/files/ts/README.md
rename libs/schematics/src/libs/{application => workspace}/files/ts/__dot__gitignore (97%)
create mode 100644 libs/schematics/src/libs/workspace/files/ts/nanoforge.config.ts
create mode 100644 libs/schematics/src/libs/workspace/files/ts/package.json.template
create mode 100644 libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template
create mode 100644 libs/schematics/src/libs/workspace/files/ts/tsconfig.json
create mode 100644 libs/schematics/src/libs/workspace/schema.json
create mode 100644 libs/schematics/src/libs/workspace/workspace.factory.ts
create mode 100644 libs/schematics/src/libs/workspace/workspace.options.d.ts
create mode 100644 libs/schematics/src/libs/workspace/workspace.schema.d.ts
delete mode 100644 libs/schematics/src/utils/config/config.declarator.spec.ts
delete mode 100644 libs/schematics/src/utils/config/config.declarator.ts
delete mode 100644 libs/schematics/src/utils/config/config.finder.spec.ts
delete mode 100644 libs/schematics/src/utils/config/config.finder.ts
delete mode 100644 libs/schematics/src/utils/config/config.type.ts
delete mode 100644 libs/schematics/src/utils/main/const.ts
delete mode 100644 libs/schematics/src/utils/main/enums.ts
delete mode 100644 libs/schematics/src/utils/main/main-functions.spec.ts
delete mode 100644 libs/schematics/src/utils/main/main-functions.ts
delete mode 100644 libs/schematics/src/utils/main/main-utils.ts
delete mode 100644 libs/schematics/src/utils/main/main.generator.spec.ts
delete mode 100644 libs/schematics/src/utils/main/main.generator.ts
delete mode 100644 libs/schematics/src/utils/main/save.type.ts
create mode 100644 libs/schematics/src/utils/registry.spec.ts
create mode 100644 libs/schematics/src/utils/registry.ts
diff --git a/docs/docs/schematics/1-overview.mdx b/docs/docs/schematics/1-overview.mdx
index a031fec..0f100bb 100644
--- a/docs/docs/schematics/1-overview.mdx
+++ b/docs/docs/schematics/1-overview.mdx
@@ -33,28 +33,18 @@ schematics @nanoforge-dev/schematics: [options]
## Available schematics
-
- Scaffold a complete NanoForge project with `package.json`, TypeScript or JavaScript config,
- linting, and optional server setup.
+
+ Scaffold the root of a NanoForge monorepo: `nanoforge.config.ts`, `package.json`, TypeScript or
+ JavaScript config, and linting.
-
- Generate a `nanoforge.config.json` configuration file consumed by the NanoForge CLI.
+
+ Generate a client or server project under `apps/` inside a workspace, including its
+ `main.ts` entry point, example component/system, and optional Dockerfile.
-
- Create the base folder structure for a client or server part, including example component,
- system, save file, and optional lifecycle hooks.
-
-
- Generate the `main.ts` entry point for a client or server part by reading the `.nanoforge` save
- file.
-
-
- Generate a production-ready `Dockerfile` and `.dockerignore` tailored to your package manager.
-
-
+
Scaffold an ECS component class with an editor manifest for use in the NanoForge editor.
-
+
Scaffold an ECS system function with an editor manifest for use in the NanoForge editor.
diff --git a/docs/docs/schematics/2-schematics/1-application.mdx b/docs/docs/schematics/2-schematics/1-application.mdx
deleted file mode 100644
index 9554bc3..0000000
--- a/docs/docs/schematics/2-schematics/1-application.mdx
+++ /dev/null
@@ -1,80 +0,0 @@
----
-title: Application
-description: Generate a complete NanoForge application scaffold
----
-
-import { ToolTip } from "mintlify/components";
-
-## Overview
-
-The `application` schematic creates a fully structured NanoForge project with all the base files needed to start development — `package.json`, linting configuration, TypeScript or JavaScript setup, and optionally `.env` for server-enabled apps.
-
-## Usage
-
-```bash
-nf new
-```
-
-Or via Angular DevKit schematics directly:
-
-```bash
-schematics @nanoforge-dev/schematics:application --name=my-app
-```
-
-## Options
-
-| Option | Type | Default | Description |
-| ---------------- | ------------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `name` | `string` | `nanoforge-app` | The name of the application |
-| `version` | `string` | `0.0.0` | Initial version in `package.json` |
-| `author` | `string` | `""` | Author field in `package.json` |
-| `description` | `string` | `""` | Description field in `package.json` |
-| `directory` | `string` | `name` | Destination directory for the generated files |
-| `language` | `"ts" \| "js"` | `ts` | Language to use for generated source files |
-| `strict` | `boolean` | `true` | Enable strict mode |
-| `lint` | `boolean` | `true` | Generate linting configuration files |
-| `packageManager` | `"npm" \| "yarn" \| "pnpm" \| "bun"` | `npm` | Package manager to target in scripts and lockfile references |
-| `server` | `boolean` | `false` | Configure a server for the application |
-| `editor` | `boolean` | `false` | Include editor dependencies |
-| `initFunctions` | `boolean` | `false` | Add init function lifecycle hooks |
-
-## Generated structure
-
-```
-my-app/
-├── package.json
-├── tsconfig.json # TypeScript only
-├── jsconfig.json # JavaScript only
-├── eslint.config.js # if lint: true
-├── prettier.config.js # if lint: true
-├── .prettierignore # if lint: true
-├── .env # if server: true
-├── .gitignore
-└── README.md
-```
-
-## Examples
-
-**Minimal TypeScript app:**
-
-```bash
-schematics @nanoforge-dev/schematics:application --name=my-game
-```
-
-**JavaScript app with pnpm and server:**
-
-```bash
-schematics @nanoforge-dev/schematics:application \
- --name=my-game \
- --language=js \
- --packageManager=pnpm \
- --server=true
-```
-
-**TypeScript app without linting:**
-
-```bash
-schematics @nanoforge-dev/schematics:application \
- --name=my-game \
- --lint=false
-```
diff --git a/docs/docs/schematics/2-schematics/1-workspace.mdx b/docs/docs/schematics/2-schematics/1-workspace.mdx
new file mode 100644
index 0000000..ce94053
--- /dev/null
+++ b/docs/docs/schematics/2-schematics/1-workspace.mdx
@@ -0,0 +1,69 @@
+---
+title: Workspace
+description: Generate a NanoForge workspace (monorepo root)
+---
+
+import { Tooltip } from "mintlify/components";
+
+## Overview
+
+The `workspace` schematic creates the root of a NanoForge monorepo: the `nanoforge.config.ts` workspace declaration, `package.json`, and TypeScript or JavaScript base config. A workspace holds one or more projects generated with the [`project`](./2-project) schematic under `apps/`.
+
+`@nanoforge-dev/cli`, `nanoforge` and (for TypeScript) `typescript` are pinned to the newest registry version published at least 48h ago at generation time, mirroring pnpm's own `minimumReleaseAge` supply-chain protection so a freshly (and potentially compromised) published release isn't picked up immediately. Non-nanoforge packages like `typescript` are pinned to a specific version rather than a range; `typescript` is also kept on its `6.x` line rather than jumping to the `7.x` native rewrite.
+
+## Usage
+
+```bash
+schematics @nanoforge-dev/schematics:workspace --name=my-game
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| ---------------- | ------------------------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `name` | `string` | `nanoforge-workspace` | The name of the workspace |
+| `directory` | `string` | `name` | Destination directory for the generated files |
+| `language` | `"ts" \| "js"` | `ts` | Language to use for generated source files |
+| `strict` | `boolean` | `true` | Enable strict mode |
+| `packageManager` | `"npm" \| "yarn" \| "pnpm" \| "bun"` | `npm` | Package manager to target |
+| `allowBuilds` | `string[]` | `[]` | Packages allowed to run install/build scripts, in addition to `bun` |
+
+## Generated structure
+
+```
+my-game/
+├── package.json
+├── tsconfig.json # TypeScript only
+├── tsconfig.spec.json # TypeScript only
+├── jsconfig.json # JavaScript only
+├── pnpm-workspace.yaml # if packageManager: pnpm — allowBuilds map
+├── nanoforge.config.ts # (or .js)
+├── .gitignore
+├── README.md
+└── apps/ # generated with the `project` schematic
+```
+
+## Examples
+
+**Minimal TypeScript workspace:**
+
+```bash
+schematics @nanoforge-dev/schematics:workspace --name=my-game
+```
+
+**pnpm workspace:**
+
+```bash
+schematics @nanoforge-dev/schematics:workspace \
+ --name=my-game \
+ --packageManager=pnpm
+```
+
+**pnpm workspace allowing an extra dependency's build scripts (bun is always allowed):**
+
+```bash
+schematics @nanoforge-dev/schematics:workspace \
+ --name=my-game \
+ --packageManager=pnpm \
+ --allowBuilds=esbuild
+```
diff --git a/docs/docs/schematics/2-schematics/2-configuration.mdx b/docs/docs/schematics/2-schematics/2-configuration.mdx
deleted file mode 100644
index 8514f09..0000000
--- a/docs/docs/schematics/2-schematics/2-configuration.mdx
+++ /dev/null
@@ -1,50 +0,0 @@
----
-title: Configuration
-description: Generate a NanoForge configuration file
----
-
-import { Tooltip } from "mintlify/components";
-
-## Overview
-
-The `configuration` schematic creates a `nanoforge.config.json` file at the root of your project. This file is the central configuration consumed by the NanoForge CLI for builds, starts, and other operations.
-
-## Usage
-
-```bash
-schematics @nanoforge-dev/schematics:configuration
-```
-
-## Options
-
-| Option | Type | Default | Description |
-| --------------- | -------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `name` | `string` | `nanoforge-app` | The name of the application |
-| `directory` | `string` | `.` | Destination directory |
-| `server` | `boolean` | `false` | Include server configuration |
-| `language` | `"ts" \| "js"` | `ts` | Language used in the project |
-| `initFunctions` | `boolean` | `false` | Reference init functions in configuration |
-
-## Generated file
-
-```
-nanoforge.config.json
-```
-
-The generated file is an empty JSON object `{}` that you populate with the fields the NanoForge CLI expects. Refer to the [CLI documentation](https://github.com/NanoForge-dev/CLI) for the full list of supported fields.
-
-## Examples
-
-**Generate configuration at the project root:**
-
-```bash
-schematics @nanoforge-dev/schematics:configuration --name=my-game
-```
-
-**Generate configuration for a server-enabled project:**
-
-```bash
-schematics @nanoforge-dev/schematics:configuration \
- --name=my-game \
- --server=true
-```
diff --git a/docs/docs/schematics/2-schematics/2-project.mdx b/docs/docs/schematics/2-schematics/2-project.mdx
new file mode 100644
index 0000000..198e11f
--- /dev/null
+++ b/docs/docs/schematics/2-schematics/2-project.mdx
@@ -0,0 +1,98 @@
+---
+title: Project
+description: Generate a NanoForge client or server project inside a workspace
+---
+
+import { Tooltip } from "mintlify/components";
+
+## Overview
+
+The `project` schematic generates a complete client or server project under `directory` (e.g. `apps/client`) inside a [`workspace`](./1-workspace): its own `package.json`, `nanoforge.config.ts`, `tsconfig.json`, example ECS component and system, `src/main.ts` entry point, and optionally a `Dockerfile` and lifecycle init functions. `directory` is the full destination path — it isn't appended with `part` automatically, so generating a client and a server side by side means passing e.g. `--directory=apps/client` and `--directory=apps/server` in two separate invocations.
+
+For a standalone project (`workspace: false`), engine dependency versions are pinned to the newest release published at least 48h ago, mirroring pnpm's own `minimumReleaseAge` supply-chain protection. `@nanoforge-dev/ecs` and `@nanoforge-dev/network` are resolved directly under those same names — no aliasing to a different registry package. A standalone project also generates its own `allowBuilds` config (`pnpm-workspace.yaml`'s `allowBuilds` map, `package.json`'s `allowScripts` or `trustedDependencies`, depending on `packageManager`) since it has no parent workspace root to provide one — see `allowBuilds` below.
+
+## Usage
+
+```bash
+schematics @nanoforge-dev/schematics:project --part=client --workspaceName=my-game --directory=apps/client
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| ---------------- | ------------------------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `part` | `"client" \| "server"` | _(required)_ | The type of project to generate |
+| `workspaceName` | `string` | _(required)_ | Name of the parent workspace |
+| `directory` | `string` | `apps` | Full destination path for this project |
+| `language` | `"ts" \| "js"` | `ts` | Language to use for generated source files |
+| `strict` | `boolean` | `true` | Enable strict mode |
+| `packageManager` | `"npm" \| "yarn" \| "pnpm" \| "bun"` | `npm` | Package manager to target |
+| `workspace` | `boolean` | `true` | Generated inside an existing NanoForge workspace |
+| `initFunctions` | `boolean` | `false` | Add lifecycle init function hooks |
+| `hasServer` | `boolean` | `false` | Sync the client's position with a companion server |
+| `docker` | `boolean` | `false` | Generate a Dockerfile and .dockerignore |
+| `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[]` | `[]` | Packages allowed to run install/build scripts (standalone projects only) |
+
+## Generated structure
+
+```
+/
+├── package.json
+├── tsconfig.json # TypeScript only
+├── jsconfig.json # JavaScript only
+├── nanoforge.config.ts # (or .js)
+├── pnpm-workspace.yaml # if !workspace && packageManager: pnpm && allowBuilds.length > 0
+├── Dockerfile # if docker: true
+├── .dockerignore # if docker: true
+├── src/
+│ ├── main.ts
+│ ├── components/
+│ │ ├── position-2d.component.ts # both parts
+│ │ └── drawable-circle-2d.component.ts # client only
+│ └── systems/
+│ ├── draw-2d.system.ts # client only
+│ ├── position-sync.system.ts # client only, if hasServer: true
+│ └── move-2d.system.ts # server only
+└── assets/
+```
+
+## Examples
+
+**Generate a client project:**
+
+```bash
+schematics @nanoforge-dev/schematics:project --part=client --workspaceName=my-game --directory=apps/client
+```
+
+**Generate a server project with Docker, syncing position with the client:**
+
+```bash
+schematics @nanoforge-dev/schematics:project \
+ --part=server \
+ --workspaceName=my-game \
+ --directory=apps/server \
+ --docker=true
+```
+
+**Generate a client that syncs its position from that server:**
+
+```bash
+schematics @nanoforge-dev/schematics:project \
+ --part=client \
+ --workspaceName=my-game \
+ --directory=apps/client \
+ --hasServer=true
+```
+
+**Generate a JavaScript client depending on a shared lib:**
+
+```bash
+schematics @nanoforge-dev/schematics:project \
+ --part=client \
+ --workspaceName=my-game \
+ --directory=apps/client \
+ --language=js \
+ --libs=../../libs/shared
+```
diff --git a/docs/docs/schematics/2-schematics/6-component.mdx b/docs/docs/schematics/2-schematics/3-component.mdx
similarity index 100%
rename from docs/docs/schematics/2-schematics/6-component.mdx
rename to docs/docs/schematics/2-schematics/3-component.mdx
diff --git a/docs/docs/schematics/2-schematics/3-part-base.mdx b/docs/docs/schematics/2-schematics/3-part-base.mdx
deleted file mode 100644
index eec16aa..0000000
--- a/docs/docs/schematics/2-schematics/3-part-base.mdx
+++ /dev/null
@@ -1,115 +0,0 @@
----
-title: Part Base
-description: Generate the base structure for a NanoForge client or server part
----
-
-import { Tooltip } from "mintlify/components";
-
-## Overview
-
-The `part-base` schematic generates the foundational folder structure for a NanoForge **client** or **server** part. It creates example components and systems, the `.nanoforge` save file used to track ECS state, and optionally lifecycle init functions.
-
-## Usage
-
-```bash
-schematics @nanoforge-dev/schematics:part-base --part=client
-```
-
-## Options
-
-| Option | Type | Default | Description |
-| --------------- | ---------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `part` | `"client" \| "server"` | _(required)_ | The part of the application to generate |
-| `directory` | `string` | `.` | Destination directory for the generated files |
-| `language` | `"ts" \| "js"` | `ts` | Language to use for generated source files |
-| `server` | `boolean` | `false` | Whether the application has a server |
-| `initFunctions` | `boolean` | `false` | Add lifecycle init function hooks |
-
-## Generated structure
-
-```
-{directory}/
-├── {part}/
-│ ├── components/
-│ │ └── example.component.ts
-│ ├── systems/
-│ │ └── example.system.ts
-│ └── init/ # if initFunctions: true
-│ ├── before-init.ts
-│ ├── after-init.ts
-│ ├── before-registry-init.ts
-│ ├── after-registry-init.ts
-│ ├── before-run.ts
-│ └── after-run.ts
-└── .nanoforge/
- └── {part}.save.json
-```
-
-## Save file
-
-The `.nanoforge/{part}.save.json` file is the source of truth for ECS state. It lists the libraries, components, systems, and entities registered for this part. The `part-main` schematic reads this file to generate the `main.ts` entry point.
-
-```json
-{
- "libraries": [ ... ],
- "components": [
- {
- "name": "ExampleComponent",
- "path": "./components/example.component.ts",
- "paramsNames": ["paramA", "paramB", "paramC"]
- }
- ],
- "systems": [
- {
- "name": "exampleSystem",
- "path": "./systems/example.system.ts"
- }
- ],
- "entities": [
- {
- "id": "exampleEntity",
- "components": {
- "ExampleComponent": { "paramA": "example", "paramB": 10 }
- }
- }
- ]
-}
-```
-
-## Init functions
-
-When `initFunctions` is `true`, the following lifecycle hooks are scaffolded:
-
-| File | Hook | Called |
-| ------------------------- | -------------------- | ----------------------------------- |
-| `before-init.ts` | `beforeInit` | Before the app initializes |
-| `after-init.ts` | `afterInit` | After the app initializes |
-| `before-registry-init.ts` | `beforeRegistryInit` | Before the ECS registry initializes |
-| `after-registry-init.ts` | `afterRegistryInit` | After the ECS registry initializes |
-| `before-run.ts` | `beforeRun` | Before the game loop starts |
-| `after-run.ts` | `afterRun` | After the game loop ends |
-
-## Examples
-
-**Generate a TypeScript client part:**
-
-```bash
-schematics @nanoforge-dev/schematics:part-base --part=client
-```
-
-**Generate a server part with init hooks:**
-
-```bash
-schematics @nanoforge-dev/schematics:part-base \
- --part=server \
- --initFunctions=true
-```
-
-**Generate a JavaScript client in a specific directory:**
-
-```bash
-schematics @nanoforge-dev/schematics:part-base \
- --part=client \
- --language=js \
- --directory=./my-game
-```
diff --git a/docs/docs/schematics/2-schematics/4-part-main.mdx b/docs/docs/schematics/2-schematics/4-part-main.mdx
deleted file mode 100644
index 60a88dc..0000000
--- a/docs/docs/schematics/2-schematics/4-part-main.mdx
+++ /dev/null
@@ -1,77 +0,0 @@
----
-title: Part Main
-description: Generate the main entry point for a NanoForge client or server part
----
-
-import { Tooltip } from "mintlify/components";
-
-## Overview
-
-The `part-main` schematic reads the `.nanoforge/{part}.save.json` file produced by `part-base` and generates a fully wired `main.ts` (or `main.js`) entry point. It registers all libraries, components, systems, and entities declared in the save file.
-
-
- This schematic must be run after `part-base`.
-
-
-## Usage
-
-```bash
-schematics @nanoforge-dev/schematics:part-main --part=client
-```
-
-## Options
-
-| Option | Type | Default | Description |
-| --------------- | ---------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `part` | `"client" \| "server"` | _(required)_ | The part of the application to generate |
-| `directory` | `string` | `.` | Project root directory |
-| `language` | `"ts" \| "js"` | `ts` | Language of the generated entry point |
-| `initFunctions` | `boolean` | `false` | Wire init lifecycle functions into the main file |
-| `saveFile` | `string` | — | Custom path to the save JSON file |
-| `outFile` | `string` | — | Custom output path for the generated main file |
-| `editor` | `boolean` | `false` | Generate the editor variant of the main file |
-
-## Output path resolution
-
-The generated file is placed at one of the following paths, in priority order:
-
-1. `outFile` — if provided explicitly
-2. `.nanoforge/editor/{part}/main.{language}` — if `editor: true`
-3. `{part}/main.{language}` — default
-
-## How it works
-
-`part-main` reads `{directory}/.nanoforge/{part}.save.json`, builds the full application bootstrap code, then writes the result as the entry point. Every time you add a component, system, or entity through the NanoForge CLI or editor, regenerating `part-main` keeps the entry point in sync with the save file.
-
-## Examples
-
-**Generate a client main file:**
-
-```bash
-schematics @nanoforge-dev/schematics:part-main --part=client
-```
-
-**Generate a server main file with init hooks:**
-
-```bash
-schematics @nanoforge-dev/schematics:part-main \
- --part=server \
- --initFunctions=true
-```
-
-**Generate with a custom save file and output path:**
-
-```bash
-schematics @nanoforge-dev/schematics:part-main \
- --part=client \
- --saveFile=./custom.save.json \
- --outFile=./src/client/main.ts
-```
-
-**Regenerate the editor entry point:**
-
-```bash
-schematics @nanoforge-dev/schematics:part-main \
- --part=client \
- --editor=true
-```
diff --git a/docs/docs/schematics/2-schematics/7-system.mdx b/docs/docs/schematics/2-schematics/4-system.mdx
similarity index 100%
rename from docs/docs/schematics/2-schematics/7-system.mdx
rename to docs/docs/schematics/2-schematics/4-system.mdx
diff --git a/docs/docs/schematics/2-schematics/5-docker.mdx b/docs/docs/schematics/2-schematics/5-docker.mdx
deleted file mode 100644
index ce32b7d..0000000
--- a/docs/docs/schematics/2-schematics/5-docker.mdx
+++ /dev/null
@@ -1,70 +0,0 @@
----
-title: Docker
-description: Generate a Dockerfile for a NanoForge application
----
-
-import { Tooltip } from "mintlify/components";
-
-## Overview
-
-The `docker` schematic generates a production-ready `Dockerfile` and a `.dockerignore` at the specified directory. The Dockerfile is tailored to the package manager you use — selecting the correct base image, lockfile, and install command automatically.
-
-## Usage
-
-```bash
-schematics @nanoforge-dev/schematics:docker
-```
-
-## Options
-
-| Option | Type | Default | Description |
-| ---------------- | ------------------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `directory` | `string` | `.` | Destination directory for the generated files |
-| `packageManager` | `"npm" \| "yarn" \| "pnpm" \| "bun"` | `npm` | Package manager used by the application |
-
-## Generated files
-
-```
-{directory}/
-├── Dockerfile
-└── .dockerignore
-```
-
-## Dockerfile behaviour by package manager
-
-| Package manager | Base image | Install command |
-| --------------- | -------------- | -------------------------------------------------- |
-| `npm` | `node:25-slim` | `npm install --no-lockfile` |
-| `yarn` | `node:25-slim` | `yarn install --frozen-lockfile` |
-| `pnpm` | `node:25-slim` | `pnpm install --frozen-lockfile --allow-build=bun` |
-| `bun` | `oven/bun:1.3` | `bun install --frozen-lockfile` |
-
-The Dockerfile follows a standard multi-step approach:
-
-1. Copy `package.json` and the lockfile.
-2. Install dependencies with frozen lockfile for reproducible builds.
-3. Copy the rest of the source.
-4. Run `build`.
-5. Start with `start`.
-
-## Examples
-
-**Generate a Dockerfile for an npm project:**
-
-```bash
-schematics @nanoforge-dev/schematics:docker
-```
-
-**Generate a Dockerfile for a pnpm project:**
-
-```bash
-schematics @nanoforge-dev/schematics:docker --packageManager=pnpm
-```
-
-**Generate in a specific directory:**
-
-```bash
-schematics @nanoforge-dev/schematics:docker \
- --packageManager=bun \
- --directory=./my-game
-```
diff --git a/docs/guides/cli/1-overview.mdx b/docs/guides/cli/1-overview.mdx
index eec3f3a..0f987de 100644
--- a/docs/guides/cli/1-overview.mdx
+++ b/docs/guides/cli/1-overview.mdx
@@ -5,7 +5,7 @@ description: Task-oriented walkthroughs for common NanoForge CLI workflows
import { Card, CardGroup } from "mintlify/components";
-The [Commands reference](../../docs/commands/1-overview) documents what each command does. These guides
+The [Commands reference](/docs/commands/1-overview) documents what each command does. These guides
take the opposite angle: they follow the **paths a user actually walks** when building a game with
the CLI, stitching commands together end to end.
diff --git a/e2e/helpers/write-project-config.ts b/e2e/helpers/write-project-config.ts
new file mode 100644
index 0000000..e7d7953
--- /dev/null
+++ b/e2e/helpers/write-project-config.ts
@@ -0,0 +1,36 @@
+import { writeFileSync } from "node:fs";
+import { join } from "node:path";
+
+interface WriteProjectConfigOptions {
+ name: string;
+ language: "ts" | "js";
+ initFunctions?: boolean;
+ server?: boolean;
+}
+
+/**
+ * `nf new` still scaffolds a `nanoforge.config.json` (schematics is out of scope for this
+ * change), but the CLI's config loader now only reads `.ts`/`.js` config modules. Tests that
+ * scaffold a project and then run `build`/`generate`/`create` against it need a hand-written
+ * `nanoforge.config.ts` alongside the scaffolded JSON — everything not set here falls back to
+ * the CLI's own defaults, matching what the scaffolded JSON used to rely on too.
+ */
+export const writeProjectConfig = (
+ appDir: string,
+ { name, language, initFunctions = false, server = false }: WriteProjectConfigOptions,
+) => {
+ const content = `export default ${JSON.stringify(
+ {
+ name,
+ language,
+ initFunctions,
+ client: { type: "client", enable: true },
+ server: { type: "server", enable: server },
+ ssl: { enable: false },
+ },
+ null,
+ 2,
+ )};\n`;
+
+ writeFileSync(join(appDir, "nanoforge.config.ts"), content);
+};
diff --git a/libs/schematics/README.md b/libs/schematics/README.md
index 43f1d5f..f7ec6de 100644
--- a/libs/schematics/README.md
+++ b/libs/schematics/README.md
@@ -38,13 +38,10 @@ nf new
This repository provide multiples schematics, usable with Angular Devkit schematics.
-- `application` : Base of a Nanoforge app
-- `configuration` : Template of `nanoforge.config.json` with premade fields
-- `part-base` : Base of Nanoforge client or server
-- `part-main` : Client or server `main.ts` from a config
+- `workspace` : Root of a NanoForge monorepo (`nanoforge.config.ts`, `package.json`, tooling)
+- `project` : Client or server project under `apps/`, including `main.ts`, example component/system, and an optional Dockerfile
- `component` : ECS component class for a client or server part
- `system` : ECS system function for a client or server part
-- `docker` : Dockerfile and `.dockerignore` tailored to your package manager
## Contributing
diff --git a/libs/schematics/e2e/application.test.ts b/libs/schematics/e2e/application.test.ts
deleted file mode 100644
index aa0e92f..0000000
--- a/libs/schematics/e2e/application.test.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
-import { resolve } from "node:path";
-import { beforeAll, describe, expect, it } from "vitest";
-
-const collectionPath = resolve(__dirname, "../dist/collection.json");
-
-describe("application schematic", () => {
- const runner = new SchematicTestRunner("schematics", collectionPath);
-
- describe("with TypeScript (default)", () => {
- let tree: UnitTestTree;
- let packageJson: any;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("application", { name: "my-app" });
- packageJson = JSON.parse(tree.readContent("/my-app/package.json"));
- });
-
- it("should generate project files", () => {
- expect(tree.files).toContain("/my-app/package.json");
- expect(tree.files).toContain("/my-app/tsconfig.json");
- expect(tree.files).toContain("/my-app/eslint.config.js");
- expect(tree.files).toContain("/my-app/prettier.config.js");
- expect(tree.files).toContain("/my-app/README.md");
- expect(tree.files).toContain("/my-app/.gitignore");
- expect(tree.files).not.toContain("/my-app/.env");
- expect(tree.files).not.toContain("/my-app/pnpm-workspace.yaml");
- });
-
- it("should set the project name in package.json", () => {
- expect(packageJson.name).toBe("my-app");
- });
-
- it("should not include ecs-server by default", () => {
- expect(packageJson.devDependencies["@nanoforge-dev/ecs-server"]).toBeUndefined();
- });
-
- it("should not include pnpm config by default", () => {
- expect(packageJson).not.toHaveProperty("pnpm");
- });
-
- it("should not include init functions docs by default", () => {
- expect(tree.readContent("/my-app/README.md")).not.toContain(
- "│ └── init/ # Lifecycle hooks (before-init, after-run, …)",
- );
- });
- });
-
- describe("with JavaScript", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("application", {
- name: "js-app",
- language: "js",
- });
- });
-
- it("should generate JS project files", () => {
- expect(tree.files).toContain("/js-app/package.json");
- expect(tree.files).toContain("/js-app/jsconfig.json");
- expect(tree.files).not.toContain("/js-app/tsconfig.json");
- expect(tree.files).not.toContain("/js-app/.env");
- });
- });
-
- describe("with JavaScript and no lint", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("application", {
- name: "js-no-lint-app",
- language: "js",
- lint: false,
- });
- });
-
- it("should generate JS project files without lint files", () => {
- expect(tree.files).toContain("/js-no-lint-app/package.json");
- expect(tree.files).not.toContain("/js-no-lint-app/eslint.config.json");
- expect(tree.files).not.toContain("/js-no-lint-app/prettier.config.json");
- expect(tree.files).not.toContain("/js-no-lint-app/.prettierignore");
- expect(tree.files).not.toContain("/js-no-lint-app/jsconfig.json");
- expect(tree.files).not.toContain("/js-no-lint-app/tsconfig.json");
- });
- });
-
- describe("with server enabled", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("application", {
- name: "server-app",
- server: true,
- });
- });
-
- it("should include env file", () => {
- expect(tree.files).toContain("/server-app/.env");
- });
-
- it("should include ecs-server dependency", () => {
- const packageJson = JSON.parse(tree.readContent("/server-app/package.json"));
- expect(packageJson.devDependencies["@nanoforge-dev/ecs-server"]).toBeDefined();
- });
- });
-
- describe("with custom options", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("application", {
- name: "custom-app",
- version: "1.2.3",
- author: "Test Author",
- description: "A test project",
- editor: true,
- });
- });
-
- it("should apply custom options to package.json", () => {
- const packageJson = JSON.parse(tree.readContent("/custom-app/package.json"));
- expect(packageJson.version).toBe("1.2.3");
- expect(packageJson.author).toBe("Test Author");
- expect(packageJson.description).toBe("A test project");
- expect(packageJson.devDependencies["@nanoforge-dev/core-editor"]).toBeDefined();
- expect(packageJson.devDependencies["@nanoforge-dev/graphics-2d-editor"]).toBeDefined();
- });
- });
-
- describe("with custom directory", () => {
- it("should generate files in the specified directory", async () => {
- const tree = await runner.runSchematic("application", {
- name: "my-app",
- directory: "custom-dir",
- });
- expect(tree.files).toContain("/custom-dir/package.json");
- });
- });
-
- describe("with pnpm", () => {
- it("should generate specifications of pnpm", async () => {
- const tree = await runner.runSchematic("application", {
- name: "pnpm-app",
- packageManager: "pnpm",
- server: true,
- });
- expect(tree.files).toContain("/pnpm-app/package.json");
- expect(tree.files).toContain("/pnpm-app/pnpm-workspace.yaml");
- const packageJson = JSON.parse(tree.readContent("/pnpm-app/package.json"));
- expect(packageJson).toHaveProperty("pnpm", {
- neverBuiltDependencies: [],
- });
- });
- });
-
- describe("with init functions", () => {
- it("should generate specifications of init functions", async () => {
- const tree = await runner.runSchematic("application", {
- name: "init-functions-app",
- initFunctions: true,
- });
- expect(tree.files).toContain("/init-functions-app/README.md");
- const content = tree.readContent("/init-functions-app/README.md");
- expect(content).toContain(
- "│ └── init/ # Lifecycle hooks (before-init, after-run, …)",
- );
- });
- });
-});
diff --git a/libs/schematics/e2e/configuration.test.ts b/libs/schematics/e2e/configuration.test.ts
deleted file mode 100644
index 474e9cc..0000000
--- a/libs/schematics/e2e/configuration.test.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
-import { resolve } from "node:path";
-import { beforeAll, describe, expect, it } from "vitest";
-
-const collectionPath = resolve(__dirname, "../dist/collection.json");
-
-describe("configuration schematic", () => {
- const runner = new SchematicTestRunner("schematics", collectionPath);
-
- describe("on an empty tree", () => {
- let tree: UnitTestTree;
- let config: Record;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("configuration", {
- name: "my-name",
- });
- config = JSON.parse(tree.readContent("/nanoforge.config.json"));
- });
-
- it("should generate nanoforge.config.json", () => {
- expect(tree.files).toContain("/nanoforge.config.json");
- });
-
- it("should create a valid JSON config file", () => {
- expect(() => JSON.parse(tree.readContent("/nanoforge.config.json"))).not.toThrow();
- });
-
- it("should include default client build config", () => {
- expect(config.name).toBe("my-name");
- expect(config.language).toBe("ts");
- expect(config.initFunctions).not.toBeDefined();
- });
-
- it("should include default client build config", () => {
- expect(config.client).toBeDefined();
- expect(config.client.enable).toBe(true);
- });
-
- it("should have server disabled", () => {
- expect(config.server.enable).toBe(false);
- });
- });
-
- describe("with js", () => {
- let tree: UnitTestTree;
- let config: Record;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("configuration", {
- name: "my-name",
- language: "js",
- initFunctions: true,
- });
- config = JSON.parse(tree.readContent("/nanoforge.config.json"));
- });
-
- it("should generate nanoforge.config.json", () => {
- expect(tree.files).toContain("/nanoforge.config.json");
- });
-
- it("should include default client build config", () => {
- expect(config.name).toBe("my-name");
- expect(config.language).toBe("js");
- expect(config.initFunctions).toBe(true);
- });
-
- it("should include default client build config", () => {
- expect(config.client).toBeDefined();
- expect(config.client.build.entry).toBe("client/main.js");
- });
-
- it("should include default editor config", () => {
- expect(config.client.editor.entry).toBe(".nanoforge/editor/client/main.js");
- });
- });
-
- describe("with server enabled", () => {
- let tree: UnitTestTree;
- let config: Record;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("configuration", {
- server: true,
- });
- config = JSON.parse(tree.readContent("/nanoforge.config.json"));
- });
-
- it("should generate config file", () => {
- expect(tree.files).toContain("/nanoforge.config.json");
- });
-
- it("should include server config with enable flag", () => {
- expect(config.server).toBeDefined();
- expect(config.server.enable).toBe(true);
- });
- });
-
- describe("with custom directory", () => {
- it("should generate config in the specified directory", async () => {
- const tree = await runner.runSchematic("configuration", {
- name: "my-app",
- directory: "custom-dir",
- });
- expect(tree.files).toContain("/custom-dir/nanoforge.config.json");
- });
- });
-});
diff --git a/libs/schematics/e2e/docker.test.ts b/libs/schematics/e2e/docker.test.ts
deleted file mode 100644
index f77fc19..0000000
--- a/libs/schematics/e2e/docker.test.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
-import { resolve } from "node:path";
-import { beforeAll, describe, expect, it } from "vitest";
-
-const collectionPath = resolve(__dirname, "../dist/collection.json");
-
-describe("docker schematic", () => {
- const runner = new SchematicTestRunner("schematics", collectionPath);
-
- describe("with npm package manager", () => {
- let tree: UnitTestTree;
-
- describe.each(["npm", "yarn", "pnpm", "bun"] as const)(
- "with ${packageManager} package manager",
- (packageManager) => {
- beforeAll(async () => {
- tree = await runner.runSchematic("docker", {
- directory: `${packageManager}-test-app`,
- packageManager,
- });
- });
-
- it("should create Dockerfile", () => {
- expect(tree.files).toContain(`/${packageManager}-test-app/Dockerfile`);
- expect(tree.files).toContain(`/${packageManager}-test-app/.dockerignore`);
- });
-
- it("should create docker file with install command", () => {
- const content = tree.readContent(`/${packageManager}-test-app/Dockerfile`);
- expect(content).toContain(`RUN ${packageManager} install`);
- });
-
- it("should create docker file with build command", () => {
- const content = tree.readContent(`/${packageManager}-test-app/Dockerfile`);
- expect(content).toContain(`RUN ${packageManager} run build`);
- });
-
- it("should create docker file with start command", () => {
- const content = tree.readContent(`/${packageManager}-test-app/Dockerfile`);
- expect(content).toContain(`CMD ["${packageManager}", "run", "start"]`);
- });
- },
- );
- });
-});
diff --git a/libs/schematics/e2e/part-base.test.ts b/libs/schematics/e2e/part-base.test.ts
deleted file mode 100644
index 0e917f3..0000000
--- a/libs/schematics/e2e/part-base.test.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
-import { resolve } from "node:path";
-import { beforeAll, describe, expect, it } from "vitest";
-
-const collectionPath = resolve(__dirname, "../dist/collection.json");
-
-describe("part-base schematic", () => {
- const runner = new SchematicTestRunner("schematics", collectionPath);
-
- describe("client part with TypeScript", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("part-base", {
- directory: "my-app",
- part: "client",
- language: "ts",
- });
- });
-
- it("should generate client directory structure", () => {
- expect(tree.files).toContain("/my-app/client/components/example.component.ts");
- expect(tree.files).toContain("/my-app/client/systems/example.system.ts");
- });
-
- it("should generate the client save file", () => {
- expect(tree.files).toContain("/my-app/.nanoforge/client.save.json");
- });
-
- it("should not generate server save file", () => {
- expect(tree.files).not.toContain("/my-app/.nanoforge/server.save.json");
- });
-
- it("should not generate init functions by default", () => {
- const initFiles = tree.files.filter((f) => f.includes("/init/"));
- expect(initFiles).toHaveLength(0);
- });
-
- it("should generate a valid save file", () => {
- const save = JSON.parse(tree.readContent("/my-app/.nanoforge/client.save.json"));
- expect(save.libraries).toBeDefined();
- expect(save.components).toBeDefined();
- expect(save.systems).toBeDefined();
- expect(save.entities).toBeDefined();
- });
- });
-
- describe("server part with TypeScript", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("part-base", {
- directory: "my-app",
- part: "server",
- language: "ts",
- });
- });
-
- it("should generate server directory structure", () => {
- expect(tree.files).toContain("/my-app/server/components/example.component.ts");
- expect(tree.files).toContain("/my-app/server/systems/example.system.ts");
- });
-
- it("should generate the server save file", () => {
- expect(tree.files).toContain("/my-app/.nanoforge/server.save.json");
- });
-
- it("should not generate client save file", () => {
- expect(tree.files).not.toContain("/my-app/.nanoforge/client.save.json");
- });
- });
-
- describe("with init functions enabled", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("part-base", {
- directory: "my-app",
- part: "client",
- language: "ts",
- initFunctions: true,
- });
- });
-
- it("should generate init function files", () => {
- expect(tree.files).toContain("/my-app/client/init/before-init.ts");
- expect(tree.files).toContain("/my-app/client/init/after-init.ts");
- expect(tree.files).toContain("/my-app/client/init/before-registry-init.ts");
- expect(tree.files).toContain("/my-app/client/init/after-registry-init.ts");
- expect(tree.files).toContain("/my-app/client/init/before-run.ts");
- expect(tree.files).toContain("/my-app/client/init/after-run.ts");
- });
- });
-
- describe("with JavaScript", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("part-base", {
- directory: "my-app",
- part: "client",
- language: "js",
- });
- });
-
- it("should generate JS files", () => {
- expect(tree.files).toContain("/my-app/client/components/example.component.js");
- expect(tree.files).toContain("/my-app/client/systems/example.system.js");
- });
- });
-
- describe("with custom directory", () => {
- it("should generate files in the specified directory", async () => {
- const tree = await runner.runSchematic("part-base", {
- name: "my-app",
- part: "client",
- directory: "custom-dir",
- });
- expect(tree.files).toContain("/custom-dir/client/components/example.component.ts");
- });
- });
-});
diff --git a/libs/schematics/e2e/part-main.test.ts b/libs/schematics/e2e/part-main.test.ts
deleted file mode 100644
index 736f8b6..0000000
--- a/libs/schematics/e2e/part-main.test.ts
+++ /dev/null
@@ -1,262 +0,0 @@
-import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
-import { mkdirSync, rmSync, writeFileSync } from "node:fs";
-import { resolve } from "node:path";
-import { afterAll, beforeAll, describe, expect, it } from "vitest";
-
-import { type Save } from "@utils/main/save.type";
-
-const collectionPath = resolve(__dirname, "../dist/collection.json");
-const tmpDir = resolve(__dirname, "../.tmp-e2e");
-
-const clientSave: Save = {
- libraries: [
- {
- id: "ecsLibrary",
- type: "component-system",
- name: "ECSClientLibrary",
- path: "@nanoforge-dev/ecs-client",
- },
- {
- id: "graphicsLibrary",
- type: "graphics",
- name: "Graphics2DLibrary",
- path: "@nanoforge-dev/graphics-2d",
- },
- ],
- components: [
- {
- name: "ExampleComponent",
- path: "./components/example.component.ts",
- paramsNames: ["exampleValueName", "exampleNum", "__RESERVED_ASSET_exampleAsset"],
- },
- ],
- systems: [{ name: "exampleSystem", path: "./systems/example.system.js" }],
- entities: [
- {
- id: "player",
- components: {
- ExampleComponent: {
- exampleValueName: "test",
- exampleNum: 5,
- exampleAsset: "/path/to/asset",
- },
- },
- },
- ],
-};
-
-const serverSave: Save = {
- libraries: [
- {
- id: "ecsLibrary",
- type: "component-system",
- name: "ECSServerLibrary",
- path: "@nanoforge-dev/ecs-server",
- },
- ],
- components: [],
- systems: [],
- entities: [],
-};
-
-describe("part-main schematic", () => {
- const runner = new SchematicTestRunner("schematics", collectionPath);
-
- beforeAll(() => {
- mkdirSync(tmpDir, { recursive: true });
- writeFileSync(resolve(tmpDir, "client.save.json"), JSON.stringify(clientSave));
- writeFileSync(resolve(tmpDir, "server.save.json"), JSON.stringify(serverSave));
- });
-
- afterAll(() => {
- rmSync(tmpDir, { recursive: true, force: true });
- });
-
- describe("client main with TypeScript", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("part-main", {
- directory: "my-app",
- part: "client",
- language: "ts",
- saveFile: resolve(tmpDir, "client.save.json"),
- });
- });
-
- it("should generate client/main.ts", () => {
- expect(tree.files).toContain("/my-app/client/main.ts");
- });
-
- it("should contain NanoforgeFactory.createClient()", () => {
- const content = tree.readContent("/my-app/client/main.ts");
- expect(content).toContain("NanoforgeFactory.createClient()");
- });
-
- it("should contain typed main function", () => {
- const content = tree.readContent("/my-app/client/main.ts");
- expect(content).toContain("export async function main(options: IRunOptions)");
- });
-
- it("should import and instantiate libraries", () => {
- const content = tree.readContent("/my-app/client/main.ts");
- expect(content).toContain('import { ECSClientLibrary } from "@nanoforge-dev/ecs-client"');
- expect(content).toContain('import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d"');
- expect(content).toContain("const ecsLibrary = new ECSClientLibrary();");
- expect(content).toContain("const graphicsLibrary = new Graphics2DLibrary();");
- });
-
- it("should register entities and systems", () => {
- const content = tree.readContent("/my-app/client/main.ts");
- expect(content).toContain("const player = registry.spawnEntity();");
- expect(content).toContain(
- 'registry.addComponent(player, new ExampleComponent("test", 5, assetManagerLibrary.getAsset("/path/to/asset")));',
- );
- expect(content).toContain("registry.addSystem(exampleSystem);");
- });
-
- it("should not contain editor libs", () => {
- const content = tree.readContent("/my-app/client/main.ts");
- expect(content).not.toContain("@nanoforge-dev/graphics-2d-editor");
- });
- });
-
- describe("server main with TypeScript", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("part-main", {
- directory: "my-app",
- part: "server",
- language: "ts",
- saveFile: resolve(tmpDir, "server.save.json"),
- });
- });
-
- it("should generate server/main.ts", () => {
- expect(tree.files).toContain("/my-app/server/main.ts");
- });
-
- it("should contain NanoforgeFactory.createServer()", () => {
- const content = tree.readContent("/my-app/server/main.ts");
- expect(content).toContain("NanoforgeFactory.createServer()");
- });
- });
-
- describe("client main with JavaScript", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("part-main", {
- directory: "my-app",
- part: "client",
- language: "js",
- saveFile: resolve(tmpDir, "client.save.json"),
- });
- });
-
- it("should generate client/main.js", () => {
- expect(tree.files).toContain("/my-app/client/main.js");
- });
-
- it("should not contain TypeScript type annotations", () => {
- const content = tree.readContent("/my-app/client/main.js");
- expect(content).not.toContain("IRunOptions");
- expect(content).toContain("export async function main(options)");
- });
- });
-
- describe("with init functions enabled", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("part-main", {
- directory: "my-app",
- part: "client",
- language: "ts",
- initFunctions: true,
- saveFile: resolve(tmpDir, "client.save.json"),
- });
- });
-
- it("should import init functions", () => {
- const content = tree.readContent("/my-app/client/main.ts");
- expect(content).toContain('import { beforeInit } from "./init/before-init"');
- expect(content).toContain('import { afterRun } from "./init/after-run"');
- });
-
- it("should call init functions in the main body", () => {
- const content = tree.readContent("/my-app/client/main.ts");
- expect(content).toContain("await beforeInit(app);");
- expect(content).toContain("await afterInit(app);");
- expect(content).toContain("await beforeRegistryInit(app, registry);");
- expect(content).toContain("await afterRegistryInit(app, registry);");
- expect(content).toContain("await beforeRun(app);");
- expect(content).toContain("await afterRun(app);");
- });
- });
-
- describe("editor mode", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("part-main", {
- directory: "my-app",
- part: "client",
- language: "ts",
- editor: true,
- saveFile: resolve(tmpDir, "client.save.json"),
- });
- });
-
- it("should generate the editor main file under .nanoforge/editor", () => {
- expect(tree.files).toContain("/my-app/.nanoforge/editor/client/main.ts");
- });
-
- it("should import from @nanoforge-dev/core-editor", () => {
- const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
- expect(content).toContain('from "@nanoforge-dev/core-editor"');
- });
-
- it("should use IEditorRunOptions type", () => {
- const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
- expect(content).toContain("IEditorRunOptions");
- expect(content).toContain("export async function main(options: IEditorRunOptions)");
- });
-
- it("should use entity paramsValues from editor save", () => {
- const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
- expect(content).toContain(
- 'options.editor.save.entities[0].components["ExampleComponent"]["exampleValueName"]',
- );
- expect(content).toContain(
- 'assetManagerLibrary.getAsset(options.editor.save.entities[0].components["ExampleComponent"]["exampleAsset"])',
- );
- });
-
- it("should import components and systems with relative path to root", () => {
- const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
- expect(content).toContain(
- 'import { ExampleComponent } from "../../../client/components/example.component"',
- );
- expect(content).toContain(
- 'import { exampleSystem } from "../../../client/systems/example.system"',
- );
- });
-
- it("should create a entityId reserved component", () => {
- const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
- expect(content).toContain(
- 'registry.addComponent(player, { name: "__RESERVED_entityId", entityId: "player" });',
- );
- });
-
- it("should init graphics editor", () => {
- const content = tree.readContent("/my-app/.nanoforge/editor/client/main.ts");
- expect(content).toContain(
- 'import { Graphics2DEditorLibrary } from "@nanoforge-dev/graphics-2d-editor"',
- );
- expect(content).toContain("const graphicsEditorLibrary = new Graphics2DEditorLibrary();");
- });
- });
-});
diff --git a/libs/schematics/e2e/project.test.ts b/libs/schematics/e2e/project.test.ts
new file mode 100644
index 0000000..340fa1f
--- /dev/null
+++ b/libs/schematics/e2e/project.test.ts
@@ -0,0 +1,477 @@
+import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
+import { resolve } from "node:path";
+import { beforeAll, describe, expect, it } from "vitest";
+
+const collectionPath = resolve(__dirname, "../dist/collection.json");
+
+// The factory resolves real registry versions (respecting a 48h trust
+// window) for standalone projects, so we can't assert an exact pinned
+// number here — only that a registry-resolved value or a workspace-linking
+// protocol came back.
+// Includes semver-range fallbacks (e.g. "^2") so this still passes when the
+// registry is unreachable and the fetcher falls back to its default.
+const RESOLVED_VERSION = /^(\d+\.\d+\.\d+|workspace:\*|\*|\^\d+(\.\d+){0,2})$/;
+
+describe("project schematic", () => {
+ const runner = new SchematicTestRunner("schematics", collectionPath);
+
+ describe("client project with TypeScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "my-workspace",
+ directory: "apps/client",
+ });
+ });
+
+ it("should generate the project structure under apps/client", () => {
+ expect(tree.files).toContain("/apps/client/package.json");
+ expect(tree.files).toContain("/apps/client/tsconfig.json");
+ expect(tree.files).toContain("/apps/client/nanoforge.config.ts");
+ expect(tree.files).toContain("/apps/client/src/main.ts");
+ expect(tree.files).toContain("/apps/client/src/components/drawable-circle-2d.component.ts");
+ expect(tree.files).toContain("/apps/client/src/components/position-2d.component.ts");
+ expect(tree.files).toContain("/apps/client/src/systems/draw-2d.system.ts");
+ expect(tree.files).toContain("/apps/client/assets/.gitkeep");
+ });
+
+ it("should derive the package name from workspaceName and part", () => {
+ const packageJson = JSON.parse(tree.readContent("/apps/client/package.json"));
+ expect(packageJson.name).toBe("my-workspace-client");
+ });
+
+ it("should include client-only dependencies", () => {
+ const packageJson = JSON.parse(tree.readContent("/apps/client/package.json"));
+ expect(packageJson.devDependencies["@nanoforge-dev/graphics-2d"]).toBeDefined();
+ expect(packageJson.devDependencies["@nanoforge-dev/input"]).toBeDefined();
+ });
+
+ it("should declare a client-typed nanoforge.config.ts", () => {
+ const content = tree.readContent("/apps/client/nanoforge.config.ts");
+ expect(content).toContain('type: "client"');
+ });
+
+ it("should generate a client entry point", () => {
+ const content = tree.readContent("/apps/client/src/main.ts");
+ expect(content).toContain("NanoforgeFactory.createClient({ tickRate: 60 })");
+ expect(content).toContain('from "@nanoforge-dev/ecs/client"');
+ expect(content).toContain("Graphics2DLibrary");
+ expect(content).toContain("InputLibrary");
+ expect(content).toContain("NetworkClientLibrary");
+ });
+
+ it("should access the graphics library directly off ctx, not through ctx.libs", () => {
+ const content = tree.readContent("/apps/client/src/systems/draw-2d.system.ts");
+ expect(content).toContain("ctx.graphics");
+ expect(content).not.toContain("ctx.libs");
+ });
+
+ it("should not generate Dockerfile or init functions by default", () => {
+ expect(tree.files).not.toContain("/apps/client/Dockerfile");
+ expect(tree.files).not.toContain("/apps/client/.dockerignore");
+ const initFiles = tree.files.filter((f) => f.includes("/init/"));
+ expect(initFiles).toHaveLength(0);
+ });
+ });
+
+ describe("server project with TypeScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("project", {
+ part: "server",
+ workspaceName: "my-workspace",
+ directory: "apps/server",
+ });
+ });
+
+ it("should generate the project structure under apps/server", () => {
+ expect(tree.files).toContain("/apps/server/package.json");
+ expect(tree.files).toContain("/apps/server/src/main.ts");
+ });
+
+ it("should not include client-only dependencies", () => {
+ const packageJson = JSON.parse(tree.readContent("/apps/server/package.json"));
+ expect(packageJson.devDependencies["@nanoforge-dev/graphics-2d"]).toBeUndefined();
+ expect(packageJson.devDependencies["@nanoforge-dev/input"]).toBeUndefined();
+ });
+
+ it("should generate a server entry point", () => {
+ const content = tree.readContent("/apps/server/src/main.ts");
+ expect(content).toContain("NanoforgeFactory.createServer({ tickRate: 60 })");
+ expect(content).toContain('from "@nanoforge-dev/ecs/server"');
+ expect(content).not.toContain("Graphics2DLibrary");
+ expect(content).toContain("NetworkServerLibrary");
+ expect(content).toContain("move2D");
+ expect(content).toContain("new Position2D(500, 500)");
+ });
+
+ it("should generate a server-side position component and movement system", () => {
+ expect(tree.files).toContain("/apps/server/src/components/position-2d.component.ts");
+ expect(tree.files).toContain("/apps/server/src/systems/move-2d.system.ts");
+ expect(tree.files).not.toContain(
+ "/apps/server/src/components/drawable-circle-2d.component.ts",
+ );
+ expect(tree.files).not.toContain("/apps/server/src/systems/draw-2d.system.ts");
+ });
+ });
+
+ describe("with a custom name", () => {
+ it("replaces part in the package name when workspaceName is set", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "my-workspace",
+ name: "frontend",
+ directory: "custom-name-apps/client",
+ });
+ const packageJson = JSON.parse(tree.readContent("/custom-name-apps/client/package.json"));
+ expect(packageJson.name).toBe("my-workspace-frontend");
+ });
+
+ it("becomes the whole package name when workspaceName is absent", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ name: "standalone-app",
+ directory: "custom-name-standalone-apps/client",
+ });
+ const packageJson = JSON.parse(
+ tree.readContent("/custom-name-standalone-apps/client/package.json"),
+ );
+ expect(packageJson.name).toBe("standalone-app");
+ });
+
+ it("falls back to a default app name when neither workspaceName nor name is set", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ directory: "custom-name-default-apps/client",
+ });
+ const packageJson = JSON.parse(
+ tree.readContent("/custom-name-default-apps/client/package.json"),
+ );
+ expect(packageJson.name).toBe("nanoforge-app");
+ });
+ });
+
+ describe("with hasServer", () => {
+ it("keeps the client's position local by default (hasServer omitted)", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "no-server-workspace",
+ directory: "no-server-apps/client",
+ });
+ expect(tree.files).not.toContain(
+ "/no-server-apps/client/src/systems/position-sync.system.ts",
+ );
+ const content = tree.readContent("/no-server-apps/client/src/main.ts");
+ expect(content).not.toContain("positionSync");
+ });
+
+ it("syncs the client's position from the server when hasServer is true", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "has-server-workspace",
+ directory: "has-server-apps/client",
+ hasServer: true,
+ });
+ expect(tree.files).toContain("/has-server-apps/client/src/systems/position-sync.system.ts");
+ const content = tree.readContent("/has-server-apps/client/src/main.ts");
+ expect(content).toContain('import { positionSync } from "./systems/position-sync.system"');
+ expect(content).toContain("registry.addSystem(positionSync)");
+ });
+
+ it("is ignored for a server project - position handling is always active there", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "server",
+ workspaceName: "server-has-server-workspace",
+ directory: "server-has-server-apps/server",
+ hasServer: false,
+ });
+ expect(tree.files).toContain("/server-has-server-apps/server/src/systems/move-2d.system.ts");
+ });
+ });
+
+ describe("with docker enabled", () => {
+ it("should generate Dockerfile and .dockerignore for a standalone project", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "docker-workspace",
+ directory: "docker-apps/client",
+ docker: true,
+ workspace: false,
+ });
+ expect(tree.files).toContain("/docker-apps/client/Dockerfile");
+ expect(tree.files).toContain("/docker-apps/client/.dockerignore");
+ });
+
+ it("should not generate a Dockerfile when the project is inside a workspace", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "docker-in-workspace",
+ directory: "docker-in-workspace-apps/client",
+ docker: true,
+ workspace: true,
+ });
+ expect(tree.files).not.toContain("/docker-in-workspace-apps/client/Dockerfile");
+ expect(tree.files).not.toContain("/docker-in-workspace-apps/client/.dockerignore");
+ });
+
+ describe.each(["npm", "yarn", "pnpm", "bun"] as const)(
+ "with %s package manager",
+ (packageManager) => {
+ it("should branch Dockerfile content on the package manager", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: `${packageManager}-workspace`,
+ directory: `${packageManager}-apps/client`,
+ docker: true,
+ workspace: false,
+ packageManager,
+ });
+ const content = tree.readContent(`/${packageManager}-apps/client/Dockerfile`);
+ const installCommand = packageManager === "npm" ? "npm ci" : `${packageManager} install`;
+ expect(content).toContain(`RUN ${installCommand}`);
+ expect(content).toContain(`RUN ${packageManager} run build`);
+ expect(content).toContain(`CMD ["${packageManager}", "run", "start"]`);
+ });
+ },
+ );
+ });
+
+ describe("dependency versioning via the workspace field", () => {
+ describe.each(["npm", "yarn", "pnpm", "bun"] as const)(
+ "with %s package manager",
+ (packageManager) => {
+ it("uses the workspace-linking protocol by default", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: `${packageManager}-dep-workspace`,
+ directory: `${packageManager}-dep-apps/client`,
+ packageManager,
+ });
+ const packageJson = JSON.parse(
+ tree.readContent(`/${packageManager}-dep-apps/client/package.json`),
+ );
+ const expected = packageManager === "npm" ? "*" : "workspace:*";
+ expect(packageJson.devDependencies["@nanoforge-dev/ecs"]).toBe(expected);
+ expect(packageJson.devDependencies["nanoforge"]).toBe(expected);
+ });
+
+ it("resolves a real registry version when workspace is false", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: `${packageManager}-standalone-workspace`,
+ directory: `${packageManager}-standalone-apps/client`,
+ packageManager,
+ workspace: false,
+ });
+ const packageJson = JSON.parse(
+ tree.readContent(`/${packageManager}-standalone-apps/client/package.json`),
+ );
+ expect(packageJson.devDependencies["@nanoforge-dev/ecs"]).toMatch(RESOLVED_VERSION);
+ expect(packageJson.devDependencies["nanoforge"]).toMatch(RESOLVED_VERSION);
+ });
+ },
+ );
+
+ it("produces valid JSON for a server project", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "server",
+ workspaceName: "valid-json-workspace",
+ directory: "valid-json-apps/server",
+ });
+ expect(() =>
+ JSON.parse(tree.readContent("/valid-json-apps/server/package.json")),
+ ).not.toThrow();
+ });
+ });
+
+ describe("with init functions enabled", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "init-workspace",
+ directory: "init-apps/client",
+ initFunctions: true,
+ });
+ });
+
+ it("should generate init function files", () => {
+ expect(tree.files).toContain("/init-apps/client/src/init/before-init.ts");
+ expect(tree.files).toContain("/init-apps/client/src/init/after-init.ts");
+ expect(tree.files).toContain("/init-apps/client/src/init/before-registry-init.ts");
+ expect(tree.files).toContain("/init-apps/client/src/init/after-registry-init.ts");
+ expect(tree.files).toContain("/init-apps/client/src/init/before-run.ts");
+ expect(tree.files).toContain("/init-apps/client/src/init/after-run.ts");
+ });
+
+ it("should wire the init functions into main.ts", () => {
+ const content = tree.readContent("/init-apps/client/src/main.ts");
+ expect(content).toContain('import { beforeInit } from "./init/before-init"');
+ expect(content).toContain("await beforeInit(app);");
+ expect(content).toContain("await afterRun(app);");
+ });
+ });
+
+ describe("with JavaScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "js-workspace",
+ directory: "js-apps/client",
+ language: "js",
+ });
+ });
+
+ it("should generate JS files", () => {
+ expect(tree.files).toContain("/js-apps/client/src/main.js");
+ expect(tree.files).toContain(
+ "/js-apps/client/src/components/drawable-circle-2d.component.js",
+ );
+ expect(tree.files).toContain("/js-apps/client/jsconfig.json");
+ expect(tree.files).not.toContain("/js-apps/client/tsconfig.json");
+ });
+ });
+
+ describe("with libs", () => {
+ it("should list libs in nanoforge.config.ts", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "libs-workspace",
+ directory: "libs-apps/client",
+ libs: ["../../libs/shared"],
+ });
+ const content = tree.readContent("/libs-apps/client/nanoforge.config.ts");
+ expect(content).toContain('libs: ["../../libs/shared"]');
+ });
+ });
+
+ describe("@nanoforge-dev/ecs and @nanoforge-dev/network version resolution", () => {
+ it("still writes the ecs/network keys even though they're resolved via their real per-part packages", async () => {
+ // @nanoforge-dev/ecs and @nanoforge-dev/network aren't published under
+ // those names (see ecs-lib/ecs-client/ecs-server and
+ // network-client/network-server) — the factory aliases to the real
+ // package to resolve a trustworthy version, but keeps writing the
+ // existing (unchanged) `ecs`/`network` keys.
+ const client = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "ecs-alias-client-workspace",
+ directory: "ecs-alias-client-apps/client",
+ workspace: false,
+ });
+ const clientPackageJson = JSON.parse(
+ client.readContent("/ecs-alias-client-apps/client/package.json"),
+ );
+ expect(clientPackageJson.devDependencies["@nanoforge-dev/ecs"]).toMatch(RESOLVED_VERSION);
+ expect(clientPackageJson.devDependencies["@nanoforge-dev/network"]).toMatch(RESOLVED_VERSION);
+
+ const server = await runner.runSchematic("project", {
+ part: "server",
+ workspaceName: "ecs-alias-server-workspace",
+ directory: "ecs-alias-server-apps/server",
+ workspace: false,
+ });
+ const serverPackageJson = JSON.parse(
+ server.readContent("/ecs-alias-server-apps/server/package.json"),
+ );
+ expect(serverPackageJson.devDependencies["@nanoforge-dev/ecs"]).toMatch(RESOLVED_VERSION);
+ expect(serverPackageJson.devDependencies["@nanoforge-dev/network"]).toMatch(RESOLVED_VERSION);
+ });
+ });
+
+ describe("packageManager (Corepack) field", () => {
+ it("is never set on a generated project's package.json", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "no-corepack-workspace",
+ directory: "no-corepack-apps/client",
+ workspace: false,
+ packageManager: "pnpm",
+ });
+ const packageJson = JSON.parse(tree.readContent("/no-corepack-apps/client/package.json"));
+ expect(packageJson.packageManager).toBeUndefined();
+ });
+ });
+
+ describe("allowBuilds (standalone projects are their own root)", () => {
+ it("is ignored when generated inside a workspace, even if passed", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "server",
+ workspaceName: "in-workspace-allow-builds",
+ directory: "in-workspace-allow-builds-apps/server",
+ packageManager: "pnpm",
+ allowBuilds: ["wrtc"],
+ });
+ expect(tree.files).not.toContain(
+ "/in-workspace-allow-builds-apps/server/pnpm-workspace.yaml",
+ );
+ const packageJson = JSON.parse(
+ tree.readContent("/in-workspace-allow-builds-apps/server/package.json"),
+ );
+ expect(packageJson).not.toHaveProperty("allowScripts");
+ expect(packageJson).not.toHaveProperty("trustedDependencies");
+ });
+
+ it("writes a pnpm-workspace.yaml allowBuilds map for a standalone pnpm project", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "server",
+ workspaceName: "standalone-allow-builds",
+ directory: "standalone-allow-builds-apps/server",
+ workspace: false,
+ packageManager: "pnpm",
+ allowBuilds: ["wrtc"],
+ });
+ const pnpmWorkspaceYaml = tree.readContent(
+ "/standalone-allow-builds-apps/server/pnpm-workspace.yaml",
+ );
+ expect(pnpmWorkspaceYaml).toContain("allowBuilds:");
+ expect(pnpmWorkspaceYaml).toContain("wrtc: true");
+ });
+
+ it("does not generate pnpm-workspace.yaml for a standalone pnpm project with no allowBuilds", async () => {
+ const tree = await runner.runSchematic("project", {
+ part: "client",
+ workspaceName: "standalone-no-allow-builds",
+ directory: "standalone-no-allow-builds-apps/client",
+ workspace: false,
+ packageManager: "pnpm",
+ });
+ expect(tree.files).not.toContain(
+ "/standalone-no-allow-builds-apps/client/pnpm-workspace.yaml",
+ );
+ });
+
+ it("writes allowScripts/trustedDependencies for a standalone npm/bun project", async () => {
+ const npmTree = await runner.runSchematic("project", {
+ part: "server",
+ workspaceName: "standalone-npm-allow-builds",
+ directory: "standalone-npm-allow-builds-apps/server",
+ workspace: false,
+ packageManager: "npm",
+ allowBuilds: ["wrtc"],
+ });
+ const npmPackageJson = JSON.parse(
+ npmTree.readContent("/standalone-npm-allow-builds-apps/server/package.json"),
+ );
+ expect(npmPackageJson.allowScripts).toEqual({ wrtc: true });
+
+ const bunTree = await runner.runSchematic("project", {
+ part: "server",
+ workspaceName: "standalone-bun-allow-builds",
+ directory: "standalone-bun-allow-builds-apps/server",
+ workspace: false,
+ packageManager: "bun",
+ allowBuilds: ["wrtc"],
+ });
+ const bunPackageJson = JSON.parse(
+ bunTree.readContent("/standalone-bun-allow-builds-apps/server/package.json"),
+ );
+ expect(bunPackageJson.trustedDependencies).toEqual(["wrtc"]);
+ });
+ });
+});
diff --git a/libs/schematics/e2e/workspace.test.ts b/libs/schematics/e2e/workspace.test.ts
new file mode 100644
index 0000000..6356e6b
--- /dev/null
+++ b/libs/schematics/e2e/workspace.test.ts
@@ -0,0 +1,265 @@
+import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing";
+import { resolve } from "node:path";
+import { beforeAll, describe, expect, it } from "vitest";
+
+const collectionPath = resolve(__dirname, "../dist/collection.json");
+
+// The factory resolves real registry versions (respecting a 48h trust
+// window), so we can't assert exact pinned numbers here — only that a
+// registry-resolved value or a workspace-linking protocol came back.
+// Includes semver-range fallbacks (e.g. "latest") so this still passes when
+// the registry is unreachable and the fetcher falls back to its default.
+const RESOLVED_VERSION = /^(\d+\.\d+\.\d+|workspace:\*|\*|latest|\^\d+(\.\d+){0,2})$/;
+
+describe("workspace schematic", () => {
+ const runner = new SchematicTestRunner("schematics", collectionPath);
+
+ describe("with TypeScript (default)", () => {
+ let tree: UnitTestTree;
+ let packageJson: any;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("workspace", { name: "my-workspace" });
+ packageJson = JSON.parse(tree.readContent("/my-workspace/package.json"));
+ });
+
+ it("should generate workspace files", () => {
+ expect(tree.files).toContain("/my-workspace/package.json");
+ expect(tree.files).toContain("/my-workspace/tsconfig.json");
+ expect(tree.files).toContain("/my-workspace/tsconfig.spec.json");
+ expect(tree.files).toContain("/my-workspace/.gitignore");
+ expect(tree.files).toContain("/my-workspace/nanoforge.config.ts");
+ expect(tree.files).toContain("/my-workspace/README.md");
+ expect(tree.files).not.toContain("/my-workspace/pnpm-workspace.yaml");
+ });
+
+ it("does not generate any lint tooling files or the prettier/eslint dependencies", () => {
+ expect(tree.files).not.toContain("/my-workspace/eslint.config.js");
+ expect(tree.files).not.toContain("/my-workspace/prettier.config.js");
+ expect(tree.files).not.toContain("/my-workspace/.prettierignore");
+ expect(packageJson.devDependencies["prettier"]).toBeUndefined();
+ expect(packageJson.devDependencies["eslint"]).toBeUndefined();
+ expect(packageJson.scripts.lint).toBeUndefined();
+ expect(packageJson.scripts.format).toBeUndefined();
+ });
+
+ it("should set the workspace name in package.json", () => {
+ expect(packageJson.name).toBe("my-workspace");
+ });
+
+ it("should declare npm workspaces in package.json (apps only, no libs)", () => {
+ expect(packageJson.workspaces).toEqual(["apps/*"]);
+ });
+
+ it("does not include @nanoforge-dev/core", () => {
+ expect(packageJson.devDependencies["@nanoforge-dev/core"]).toBeUndefined();
+ });
+
+ it("should not include pnpm config by default", () => {
+ expect(packageJson).not.toHaveProperty("pnpm");
+ });
+
+ it("should resolve real versions for cli/nanoforge/typescript", () => {
+ expect(packageJson.devDependencies["@nanoforge-dev/cli"]).toMatch(RESOLVED_VERSION);
+ expect(packageJson.devDependencies["nanoforge"]).toMatch(RESOLVED_VERSION);
+ expect(packageJson.devDependencies["typescript"]).toMatch(RESOLVED_VERSION);
+ });
+
+ it("pins typescript to a specific version, not a range", () => {
+ expect(packageJson.devDependencies["typescript"]).toMatch(/^\d+\.\d+\.\d+$/);
+ });
+
+ it("does not jump typescript to its major-7 native rewrite", () => {
+ const version = packageJson.devDependencies["typescript"] as string;
+ expect(version.split(".")[0]).toBe("6");
+ });
+
+ it("should declare a workspace-typed nanoforge.config.ts with apps only (no libs)", () => {
+ const content = tree.readContent("/my-workspace/nanoforge.config.ts");
+ expect(content).toContain('type: "workspace"');
+ expect(content).toContain('packages: ["apps/*"]');
+ });
+
+ it("does not set a packageManager (Corepack) field", () => {
+ expect(packageJson.packageManager).toBeUndefined();
+ });
+ });
+
+ describe("with JavaScript", () => {
+ let tree: UnitTestTree;
+
+ beforeAll(async () => {
+ tree = await runner.runSchematic("workspace", {
+ name: "js-workspace",
+ language: "js",
+ });
+ });
+
+ it("should generate JS workspace files", () => {
+ expect(tree.files).toContain("/js-workspace/package.json");
+ expect(tree.files).toContain("/js-workspace/jsconfig.json");
+ expect(tree.files).toContain("/js-workspace/nanoforge.config.js");
+ expect(tree.files).not.toContain("/js-workspace/tsconfig.json");
+ expect(tree.files).not.toContain("/js-workspace/nanoforge.config.ts");
+ });
+
+ it("has the same package.json shape as the TypeScript workspace, minus typescript itself", () => {
+ const packageJson = JSON.parse(tree.readContent("/js-workspace/package.json"));
+ expect(packageJson.devDependencies["typescript"]).toBeUndefined();
+ expect(packageJson.devDependencies["@nanoforge-dev/cli"]).toMatch(RESOLVED_VERSION);
+ expect(packageJson.devDependencies["@nanoforge-dev/core"]).toBeUndefined();
+ expect(packageJson.devDependencies["prettier"]).toBeUndefined();
+ expect(packageJson.version).toBeUndefined();
+ expect(packageJson.author).toBeUndefined();
+ expect(packageJson.description).toBeUndefined();
+ expect(packageJson).not.toHaveProperty("lint-staged");
+ expect(packageJson.workspaces).toEqual(["apps/*"]);
+ });
+
+ it("does not generate any lint tooling files (same as TypeScript)", () => {
+ expect(tree.files).not.toContain("/js-workspace/eslint.config.js");
+ expect(tree.files).not.toContain("/js-workspace/prettier.config.js");
+ expect(tree.files).not.toContain("/js-workspace/.prettierignore");
+ });
+ });
+
+ describe("with custom directory", () => {
+ it("should generate files in the specified directory", async () => {
+ const tree = await runner.runSchematic("workspace", {
+ name: "my-workspace",
+ directory: "custom-dir",
+ });
+ expect(tree.files).toContain("/custom-dir/package.json");
+ });
+ });
+
+ describe("with pnpm", () => {
+ it("should generate pnpm-specific files and no npm workspaces field", async () => {
+ const tree = await runner.runSchematic("workspace", {
+ name: "pnpm-workspace",
+ packageManager: "pnpm",
+ });
+ expect(tree.files).toContain("/pnpm-workspace/pnpm-workspace.yaml");
+ const packageJson = JSON.parse(tree.readContent("/pnpm-workspace/package.json"));
+ // pnpm 10+ no longer reads the "pnpm" field from package.json (it warns and
+ // ignores it); build-script approval now lives in pnpm-workspace.yaml's
+ // `allowBuilds` map instead. See https://pnpm.io/settings.
+ expect(packageJson).not.toHaveProperty("pnpm");
+ expect(packageJson.workspaces).toBeUndefined();
+ expect(packageJson.devDependencies["@nanoforge-dev/cli"]).toBe("workspace:*");
+ });
+
+ it("should generate a pnpm-workspace.yaml with apps only (no libs)", async () => {
+ const tree = await runner.runSchematic("workspace", {
+ name: "pnpm-packages-workspace",
+ packageManager: "pnpm",
+ });
+ const pnpmWorkspaceYaml = tree.readContent("/pnpm-packages-workspace/pnpm-workspace.yaml");
+ expect(pnpmWorkspaceYaml).toContain('- "apps/*"');
+ expect(pnpmWorkspaceYaml).not.toContain("libs/*");
+ });
+
+ it("always allows bun's build script in pnpm-workspace.yaml's allowBuilds map", async () => {
+ const tree = await runner.runSchematic("workspace", {
+ name: "pnpm-default-allow-builds-workspace",
+ packageManager: "pnpm",
+ });
+ const pnpmWorkspaceYaml = tree.readContent(
+ "/pnpm-default-allow-builds-workspace/pnpm-workspace.yaml",
+ );
+ expect(pnpmWorkspaceYaml).toContain("allowBuilds:");
+ expect(pnpmWorkspaceYaml).toContain("bun: true");
+ });
+
+ it("adds user-requested packages to pnpm-workspace.yaml's allowBuilds map alongside bun", async () => {
+ const tree = await runner.runSchematic("workspace", {
+ name: "pnpm-allow-builds-workspace",
+ packageManager: "pnpm",
+ allowBuilds: ["esbuild"],
+ });
+ const pnpmWorkspaceYaml = tree.readContent(
+ "/pnpm-allow-builds-workspace/pnpm-workspace.yaml",
+ );
+ expect(pnpmWorkspaceYaml).toContain("bun: true");
+ expect(pnpmWorkspaceYaml).toContain("esbuild: true");
+ });
+ });
+
+ describe("with npm", () => {
+ it("always allows bun's build script via package.json's allowScripts", async () => {
+ const tree = await runner.runSchematic("workspace", {
+ name: "npm-allow-scripts-workspace",
+ packageManager: "npm",
+ allowBuilds: ["esbuild"],
+ });
+ expect(tree.files).not.toContain("/npm-allow-scripts-workspace/pnpm-workspace.yaml");
+ const packageJson = JSON.parse(tree.readContent("/npm-allow-scripts-workspace/package.json"));
+ expect(packageJson.allowScripts).toEqual({ bun: true, esbuild: true });
+ });
+ });
+
+ describe("with bun", () => {
+ it("always allows bun's build script via package.json's trustedDependencies", async () => {
+ const tree = await runner.runSchematic("workspace", {
+ name: "bun-trusted-deps-workspace",
+ packageManager: "bun",
+ allowBuilds: ["esbuild"],
+ });
+ expect(tree.files).not.toContain("/bun-trusted-deps-workspace/pnpm-workspace.yaml");
+ const packageJson = JSON.parse(tree.readContent("/bun-trusted-deps-workspace/package.json"));
+ expect(packageJson.trustedDependencies).toEqual(["bun", "esbuild"]);
+ });
+ });
+
+ describe("with yarn", () => {
+ it("should not generate pnpm-workspace.yaml, no script-allow field, and pinned versions", async () => {
+ const tree = await runner.runSchematic("workspace", {
+ name: "yarn-workspace",
+ packageManager: "yarn",
+ });
+ expect(tree.files).not.toContain("/yarn-workspace/pnpm-workspace.yaml");
+ const packageJson = JSON.parse(tree.readContent("/yarn-workspace/package.json"));
+ expect(packageJson).not.toHaveProperty("allowScripts");
+ expect(packageJson).not.toHaveProperty("trustedDependencies");
+ expect(packageJson.workspaces).toEqual(["apps/*"]);
+ expect(packageJson.devDependencies["@nanoforge-dev/cli"]).toMatch(RESOLVED_VERSION);
+ });
+ });
+
+ describe("with docker disabled (default)", () => {
+ it("should not generate a Dockerfile or .dockerignore", async () => {
+ const tree = await runner.runSchematic("workspace", { name: "no-docker-workspace" });
+ expect(tree.files).not.toContain("/no-docker-workspace/Dockerfile");
+ expect(tree.files).not.toContain("/no-docker-workspace/.dockerignore");
+ });
+ });
+
+ describe("with docker enabled", () => {
+ it("should generate Dockerfile and .dockerignore", async () => {
+ const tree = await runner.runSchematic("workspace", {
+ name: "docker-workspace",
+ docker: true,
+ });
+ expect(tree.files).toContain("/docker-workspace/Dockerfile");
+ expect(tree.files).toContain("/docker-workspace/.dockerignore");
+ });
+
+ describe.each(["npm", "yarn", "pnpm", "bun"] as const)(
+ "with %s package manager",
+ (packageManager) => {
+ it("should branch Dockerfile content on the package manager", async () => {
+ const tree = await runner.runSchematic("workspace", {
+ name: `${packageManager}-docker-workspace`,
+ docker: true,
+ packageManager,
+ });
+ const content = tree.readContent(`/${packageManager}-docker-workspace/Dockerfile`);
+ const installCommand = packageManager === "npm" ? "npm ci" : `${packageManager} install`;
+ expect(content).toContain(`RUN ${installCommand}`);
+ expect(content).toContain(`RUN ${packageManager} run build`);
+ expect(content).toContain(`CMD ["${packageManager}", "run", "start"]`);
+ });
+ },
+ );
+ });
+});
diff --git a/libs/schematics/src/collection.json b/libs/schematics/src/collection.json
index 19462ce..3fdca2d 100644
--- a/libs/schematics/src/collection.json
+++ b/libs/schematics/src/collection.json
@@ -1,30 +1,15 @@
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
- "application": {
- "factory": "./libs/application/application.factory#main",
- "description": "Create a NanoForge Base application.",
- "schema": "./libs/application/schema.json"
+ "workspace": {
+ "factory": "./libs/workspace/workspace.factory#main",
+ "description": "Create a NanoForge workspace (monorepo root: config, tooling, package manifest).",
+ "schema": "./libs/workspace/schema.json"
},
- "configuration": {
- "factory": "./libs/configuration/configuration.factory#main",
- "description": "Create a NanoForge Configuration.",
- "schema": "./libs/configuration/schema.json"
- },
- "part-base": {
- "factory": "./libs/part-base/part-base.factory#main",
- "description": "Create a NanoForge Base for client or server.",
- "schema": "./libs/part-base/schema.json"
- },
- "part-main": {
- "factory": "./libs/part-main/part-main.factory#main",
- "description": "Create a Main file for client or server.",
- "schema": "./libs/part-main/schema.json"
- },
- "docker": {
- "factory": "./libs/docker/docker.factory#main",
- "description": "Create a Dockerfile for the application.",
- "schema": "./libs/docker/schema.json"
+ "project": {
+ "factory": "./libs/project/project.factory#main",
+ "description": "Create a NanoForge client or server project under apps/.",
+ "schema": "./libs/project/schema.json"
},
"component": {
"factory": "./libs/component/component.factory#main",
diff --git a/libs/schematics/src/defaults.ts b/libs/schematics/src/defaults.ts
index 5c44c97..f4621c9 100644
--- a/libs/schematics/src/defaults.ts
+++ b/libs/schematics/src/defaults.ts
@@ -1,6 +1,22 @@
export const DEFAULT_APP_NAME = "nanoforge-app";
-export const DEFAULT_VERSION = "0.0.0";
-export const DEFAULT_AUTHOR = "";
-export const DEFAULT_DESCRIPTION = "";
export const DEFAULT_LANGUAGE = "ts";
export const DEFAULT_PACKAGE_MANAGER = "npm";
+export const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = "^2";
+export const DEFAULT_ENGINE_VERSION = "^2";
+
+/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */
+export const DEFAULT_CLI_DEPENDENCY_VERSION = "latest";
+
+/**
+ * Fallback used when the registry lookup for `typescript` fails. A specific
+ * version, not a range — non-nanoforge packages are pinned exactly.
+ */
+export const DEFAULT_TYPESCRIPT_VERSION = "6.0.3";
+
+/**
+ * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when
+ * resolving a pinned dependency version at generation time, only consider
+ * versions published at least this long ago, so a freshly (and potentially
+ * maliciously) published release isn't picked up immediately.
+ */
+export const MIN_RELEASE_AGE_HOURS = 48;
diff --git a/libs/schematics/src/libs/application/application.factory.ts b/libs/schematics/src/libs/application/application.factory.ts
deleted file mode 100644
index 5e0bb21..0000000
--- a/libs/schematics/src/libs/application/application.factory.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { type Path, join, normalize, strings } from "@angular-devkit/core";
-import {
- type Rule,
- type SchematicContext,
- type Source,
- type Tree,
- apply,
- mergeWith,
- move,
- renameTemplateFiles,
- template,
- url,
-} from "@angular-devkit/schematics";
-import { firstValueFrom } from "rxjs";
-
-import {
- DEFAULT_APP_NAME,
- DEFAULT_AUTHOR,
- DEFAULT_DESCRIPTION,
- DEFAULT_LANGUAGE,
- DEFAULT_PACKAGE_MANAGER,
- DEFAULT_VERSION,
-} from "~/defaults";
-
-import { type ApplicationOptions } from "./application.options";
-import { type ApplicationSchema } from "./application.schema";
-
-const transform = (schema: ApplicationSchema): ApplicationOptions => {
- return {
- name: schema.name ?? DEFAULT_APP_NAME,
- version: schema.version ?? DEFAULT_VERSION,
- author: schema.author ?? DEFAULT_AUTHOR,
- description: schema.description ?? DEFAULT_DESCRIPTION,
- language: schema.language ?? DEFAULT_LANGUAGE,
- strict: schema.strict ?? true,
- lint: schema.lint ?? true,
- packageManager: schema.packageManager ?? DEFAULT_PACKAGE_MANAGER,
- server: schema.server ?? false,
- editor: schema.editor ?? false,
- initFunctions: schema.initFunctions ?? false,
- };
-};
-
-const generate = (options: ApplicationOptions, path: string): Source => {
- return apply(url(join("./files" as Path, options.language)), [
- template({
- dot: ".",
- ...strings,
- ...options,
- }),
- renameTemplateFiles(),
- move(normalize(path)),
- ]);
-};
-
-export const main = (schema: ApplicationSchema): Rule => {
- const options = transform(schema);
- const path = schema.directory ?? options.name;
-
- return async (baseTree: Tree, context: SchematicContext) => {
- let tree = await mergeWith(generate(options, path))(baseTree, context);
- if (!tree) return tree;
- if (!("delete" in tree)) {
- if (typeof tree === "function") return tree;
- tree = await firstValueFrom(tree);
- }
-
- const basePath = join("/" as Path, path);
-
- if (!options.lint) {
- tree.delete(join(basePath, "eslint.config.js"));
- tree.delete(join(basePath, "prettier.config.js"));
- tree.delete(join(basePath, ".prettierignore"));
- if (options.language === "js") tree.delete(join(basePath, "jsconfig.json"));
- }
-
- if (!options.server) {
- tree.delete(join(basePath, ".env"));
- }
-
- if (options.packageManager !== "pnpm") {
- tree.delete(join(basePath, "pnpm-workspace.yaml"));
- }
-
- return tree;
- };
-};
diff --git a/libs/schematics/src/libs/application/application.options.d.ts b/libs/schematics/src/libs/application/application.options.d.ts
deleted file mode 100644
index 71201d8..0000000
--- a/libs/schematics/src/libs/application/application.options.d.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-export interface ApplicationOptions {
- /**
- * NanoForge application name
- */
- name: string;
-
- /**
- * NanoForge application version
- */
- version: string;
-
- /**
- * NanoForge application author
- */
- author: string;
-
- /**
- * NanoForge application description
- */
- description: string;
-
- /**
- * NanoForge Application language
- */
- language: "js" | "ts";
-
- /**
- * With strict mode (TypeScript or JavaScript but only on compatible editor)
- */
- strict: boolean;
-
- /**
- * Generate lint files
- */
- lint: boolean;
-
- /**
- * The used package manager
- */
- packageManager: "npm" | "yarn" | "pnpm" | "bun";
-
- /**
- * Configure a server for the application
- */
- server: boolean;
-
- /**
- * Add editor dependencies
- */
- editor: boolean;
-
- /**
- * Add init functions to the application
- */
- initFunctions: boolean;
-}
diff --git a/libs/schematics/src/libs/application/application.schema.d.ts b/libs/schematics/src/libs/application/application.schema.d.ts
deleted file mode 100644
index 60579c3..0000000
--- a/libs/schematics/src/libs/application/application.schema.d.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-export interface ApplicationSchema {
- /**
- * NanoForge application name
- */
- name: string;
-
- /**
- * NanoForge application version
- */
- version?: string;
-
- /**
- * NanoForge application author
- */
- author?: string;
-
- /**
- * NanoForge application description
- */
- description?: string;
-
- /**
- * NanoForge application destination directory
- */
- directory?: string;
-
- /**
- * NanoForge Application language
- */
- language?: "js" | "ts";
-
- /**
- * With strict mode (TypeScript or JavaScript but only on compatible editor)
- */
- strict?: boolean;
-
- /**
- * Generate lint files
- */
- lint?: boolean;
-
- /**
- * The used package manager
- */
- packageManager?: "npm" | "yarn" | "pnpm" | "bun";
-
- /**
- * Configure a server for the application
- */
- server?: boolean;
-
- /**
- * Add editor dependencies
- */
- editor: boolean;
-
- /**
- * Add init functions to the application
- */
- initFunctions: boolean;
-}
diff --git a/libs/schematics/src/libs/application/files/js/.prettierignore b/libs/schematics/src/libs/application/files/js/.prettierignore
deleted file mode 100644
index 2f0ede7..0000000
--- a/libs/schematics/src/libs/application/files/js/.prettierignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Ignore files for PNPM, NPM and YARN
-pnpm-lock.yaml
-package-lock.json
-yarn.lock
-bun.lock
-
-/node_modules
-/.nanoforge
diff --git a/libs/schematics/src/libs/application/files/js/README.md b/libs/schematics/src/libs/application/files/js/README.md
deleted file mode 100644
index 94b63de..0000000
--- a/libs/schematics/src/libs/application/files/js/README.md
+++ /dev/null
@@ -1,166 +0,0 @@
-# <%= name %>
-
----
-
-## Prerequisites
-
-- [Node.js](https://nodejs.org/) 18+
-- Package manager: **<%= packageManager %>**
-- [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `<%= packageManager %> install -g @nanoforge-dev/cli`
-
----
-
-## Installation
-
-```bash
-<%= packageManager %> install
-```
-
----
-
-## Development
-
-Start the game in watch mode with hot reload:
-
-```bash
-nf dev
-```
-
-<% if (editor) { %>Open the visual editor (optional — lets you edit entities, components and systems without touching code):
-```bash
-nf editor
-```
-
-<% } %>---
-
-## Project structure
-
-```
-<%= name %>/
-├── .nanoforge/ # NanoForge internal save files (do not edit manually)
-├── client/ # Client-side code (runs in the browser)
-│ ├── static/ # Static assets
-│ ├── components/ # ECS components
-│ ├── systems/ # ECS systems<% if (initFunctions) { %>
-│ └── init/ # Lifecycle hooks (before-init, after-run, …)<% } %><% if (server) { %>
-├── server/ # Server-side code
-│ ├── static/
-│ ├── components/
-│ ├── systems/<% if (initFunctions) { %>
-│ └── init/<% } %><% } %>
-├── package.json
-├── jsconfig.json
-└── nanoforge.config.json # Game configuration
-```
-
----
-
-## Modifying the game
-
-### Add a component
-
-```bash
-nf generate component --part client
-```
-
-A component is a plain class that holds data for an entity. Edit the generated file in `client/components/`:
-
-```js
-export class MyComponent {
- constructor(speed) {
- this.speed = speed;
- }
-}
-
-export default MyComponent.name;
-```
-
-### Add a system
-
-```bash
-nf generate system --part client
-```
-
-A system is a function that runs every tick and operates on entities that have specific components. Edit the generated file in `client/systems/`:
-
-```js
-import { MyComponent } from "../components/my.component.js";
-
-export const mySystem = (registry, ctx) => {
- const entities = registry.getZipper([MyComponent]);
-
- entities.forEach((entity) => {
- entity.MyComponent.speed += 1;
- });
-};
-
-export default mySystem.name;
-```
-
-<% if (server) { %>
-### Server-side code
-
-Server components and systems live in `server/` and are imported from `@nanoforge-dev/ecs-server`. They follow the same pattern as the client side.
-
-Configure the server connection in `.env`:
-
-| Variable | Default | Description |
-|---------------------------------------|-------------|---------------------------------|
-| `NANOFORGE_CLIENT_SERVER_ADDRESS` | `127.0.0.1` | Server address (client-side) |
-| `NANOFORGE_CLIENT_SERVER_TCP_PORT` | `4444` | TCP port the client connects to |
-| `NANOFORGE_CLIENT_SERVER_UDP_PORT` | `4445` | UDP port the client connects to |
-| `NANOFORGE_SERVER_LISTENING_TCP_PORT` | `4444` | TCP port the server listens on |
-| `NANOFORGE_SERVER_LISTENING_UDP_PORT` | `4445` | UDP port the server listens on |
-
-<% } %>---
-
-## Build
-
-Compile the game for production:
-
-```bash
-<%= packageManager %> run build
-# or
-nf build
-```
-
-Output is written to the `.nanoforge/` directory.
-
----
-
-## Run
-
-Start the game server and open it in a browser:
-
-```bash
-<%= packageManager %> run start
-# or
-nf start
-```
-
-<% if (lint) { %>---
-
-## Code quality
-
-```bash
-# Check formatting and linting
-<%= packageManager %> run lint
-
-# Auto-fix formatting and linting
-<%= packageManager %> run format
-```
-
-<% } %>---
-
-## CLI reference
-
-| Command | Description |
-|--------------------------------|-------------------------------------------|
-| `nf dev` | Start in development mode with hot reload |
-| `nf build` | Build for production |
-| `nf start` | Run the built game |
-| `nf generate component ` | Scaffold a new component |
-| `nf generate system ` | Scaffold a new system |<% if (editor) { %>
-| `nf editor` | Open the visual editor |<% } %>
-
-Full CLI documentation:
diff --git a/libs/schematics/src/libs/application/files/js/eslint.config.js b/libs/schematics/src/libs/application/files/js/eslint.config.js
deleted file mode 100644
index a90e742..0000000
--- a/libs/schematics/src/libs/application/files/js/eslint.config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import pluginJs from "@eslint/js";
-import eslintConfigPrettier from "eslint-config-prettier";
-import globals from "globals";
-
-export default [
- { files: ["{client,server}/**/*.{js}"] },
- { languageOptions: { globals: globals.node } },
-
- pluginJs.configs.recommended,
- eslintConfigPrettier,
-];
diff --git a/libs/schematics/src/libs/application/files/js/package.json.template b/libs/schematics/src/libs/application/files/js/package.json.template
deleted file mode 100644
index 4d2bd37..0000000
--- a/libs/schematics/src/libs/application/files/js/package.json.template
+++ /dev/null
@@ -1,48 +0,0 @@
-{
- "name": "<%= name %>",
- "version": "<%= version %>",
- "description": "<%= description %>",
- "author": "<%= author %>",
- "scripts": {
- "build": "nf build",
- "start": "nf start"<% if (lint) { %>,
- "lint": "prettier --check . && eslint --format=pretty \"{client,server}/**/*.js\"",
- "format": "prettier --write . && eslint --fix --format=pretty \"{client,server}/**/*.js\""<% } %>
- },
- "devDependencies": {<% if (lint) { %>
- "@eslint/js": "^9.39.0",<% } %>
- "@nanoforge-dev/asset-manager": "^1",
- "@nanoforge-dev/cli": "latest",
- "@nanoforge-dev/common": "^1",
- "@nanoforge-dev/config": "^1",
- "@nanoforge-dev/core": "^1",<% if (editor) { %>
- "@nanoforge-dev/core-editor": "^1",<% } %>
- "@nanoforge-dev/ecs-client": "^1",<% if (server) { %>
- "@nanoforge-dev/ecs-server": "^1",<% } %>
- "@nanoforge-dev/graphics-2d": "^1",<% if (editor) { %>
- "@nanoforge-dev/graphics-2d-editor": "^1",<% } %>
- "@nanoforge-dev/input": "^1",
- "@nanoforge-dev/music": "^1",<% if (server) { %>
- "@nanoforge-dev/network-client": "^1",
- "@nanoforge-dev/network-server": "^1",<% } %>
- "@nanoforge-dev/sound": "^1"<% if (lint) { %>,
- "eslint": "^9.39.0",
- "eslint-config-prettier": "^10.1.8",
- "eslint-plugin-prettier": "^5.5.4",
- "globals": "^16.5.0",
- "lint-staged": "^16.2.6",
- "prettier": "^3.6.2"<% } %>
- },<% if (lint) { %>
- "lint-staged": {
- "**/*.js": [
- "prettier --write"
- ],
- "{client,server}/**/*.js": [
- "eslint --fix"
- ]
- },<% } %><% if (packageManager === "pnpm") { %>
- "pnpm": {
- "neverBuiltDependencies": []
- },<% } %>
- "private": true
-}
diff --git a/libs/schematics/src/libs/application/files/js/pnpm-workspace.yaml.template b/libs/schematics/src/libs/application/files/js/pnpm-workspace.yaml.template
deleted file mode 100644
index 236b123..0000000
--- a/libs/schematics/src/libs/application/files/js/pnpm-workspace.yaml.template
+++ /dev/null
@@ -1,3 +0,0 @@
-allowBuilds:
- bun: true<% if (server) { %>
- wrtc: true<% } %>
diff --git a/libs/schematics/src/libs/application/files/js/prettier.config.js b/libs/schematics/src/libs/application/files/js/prettier.config.js
deleted file mode 100644
index 8e5015d..0000000
--- a/libs/schematics/src/libs/application/files/js/prettier.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- useTabs: false,
- singleQuote: false,
- trailingComma: "all",
- printWidth: 100,
-};
diff --git a/libs/schematics/src/libs/application/files/ts/.prettierignore b/libs/schematics/src/libs/application/files/ts/.prettierignore
deleted file mode 100644
index 2f0ede7..0000000
--- a/libs/schematics/src/libs/application/files/ts/.prettierignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Ignore files for PNPM, NPM and YARN
-pnpm-lock.yaml
-package-lock.json
-yarn.lock
-bun.lock
-
-/node_modules
-/.nanoforge
diff --git a/libs/schematics/src/libs/application/files/ts/README.md b/libs/schematics/src/libs/application/files/ts/README.md
deleted file mode 100644
index 44878e2..0000000
--- a/libs/schematics/src/libs/application/files/ts/README.md
+++ /dev/null
@@ -1,166 +0,0 @@
-# <%= name %>
-
----
-
-## Prerequisites
-
-- [Node.js](https://nodejs.org/) 18+
-- Package manager: **<%= packageManager %>**
-- [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `<%= packageManager %> install -g @nanoforge-dev/cli`
-
----
-
-## Installation
-
-```bash
-<%= packageManager %> install
-```
-
----
-
-## Development
-
-Start the game in watch mode with hot reload:
-
-```bash
-nf dev
-```
-
-<% if (editor) { %>Open the visual editor (optional — lets you edit entities, components and systems without touching code):
-```bash
-nf editor
-```
-
-<% } %>---
-
-## Project structure
-
-```
-<%= name %>/
-├── .nanoforge/ # NanoForge internal save files (do not edit manually)
-├── client/ # Client-side code (runs in the browser)
-│ ├── static/ # Static assets
-│ ├── components/ # ECS components
-│ ├── systems/ # ECS systems<% if (initFunctions) { %>
-│ └── init/ # Lifecycle hooks (before-init, after-run, …)<% } %><% if (server) { %>
-├── server/ # Server-side code
-│ ├── static/
-│ ├── components/
-│ ├── systems/<% if (initFunctions) { %>
-│ └── init/<% } %><% } %>
-├── package.json
-├── tsconfig.json
-└── nanoforge.config.json # Game configuration
-```
-
----
-
-## Modifying the game
-
-### Add a component
-
-```bash
-nf generate component --part client
-```
-
-A component is a plain class that holds data for an entity. Edit the generated file in `client/components/`:
-
-```ts
-export class MyComponent {
- constructor(public speed: number) {}
-}
-
-export default MyComponent.name;
-```
-
-### Add a system
-
-```bash
-nf generate system --part client
-```
-
-A system is a function that runs every tick and operates on entities that have specific components. Edit the generated file in `client/systems/`:
-
-```ts
-import { type Context } from "@nanoforge-dev/common";
-import { type Registry } from "@nanoforge-dev/ecs-client";
-import { MyComponent } from "../components/my.component";
-
-export const mySystem = (registry: Registry, ctx: Context) => {
- const entities = registry.getZipper([MyComponent]);
-
- entities.forEach((entity) => {
- entity.MyComponent.speed += 1;
- });
-};
-
-export default mySystem.name;
-```
-
-<% if (server) { %>
-### Server-side code
-
-Server components and systems live in `server/` and are imported from `@nanoforge-dev/ecs-server`. They follow the same pattern as the client side.
-
-Configure the server connection in `.env`:
-
-| Variable | Default | Description |
-|---------------------------------------|-------------|---------------------------------|
-| `NANOFORGE_CLIENT_SERVER_ADDRESS` | `127.0.0.1` | Server address (client-side) |
-| `NANOFORGE_CLIENT_SERVER_TCP_PORT` | `4444` | TCP port the client connects to |
-| `NANOFORGE_CLIENT_SERVER_UDP_PORT` | `4445` | UDP port the client connects to |
-| `NANOFORGE_SERVER_LISTENING_TCP_PORT` | `4444` | TCP port the server listens on |
-| `NANOFORGE_SERVER_LISTENING_UDP_PORT` | `4445` | UDP port the server listens on |
-
-<% } %>---
-
-## Build
-
-Compile the game for production:
-
-```bash
-<%= packageManager %> run build
-# or
-nf build
-```
-
-Output is written to the `.nanoforge/` directory.
-
----
-
-## Run
-
-Start the game server and open it in a browser:
-
-```bash
-<%= packageManager %> run start
-# or
-nf start
-```
-
-<% if (lint) { %>---
-
-## Code quality
-
-```bash
-# Check formatting and linting
-<%= packageManager %> run lint
-
-# Auto-fix formatting and linting
-<%= packageManager %> run format
-```
-
-<% } %>---
-
-## CLI reference
-
-| Command | Description |
-|--------------------------------|-------------------------------------------|
-| `nf dev` | Start in development mode with hot reload |
-| `nf build` | Build for production |
-| `nf start` | Run the built game |
-| `nf generate component ` | Scaffold a new component |
-| `nf generate system ` | Scaffold a new system |<% if (editor) { %>
-| `nf editor` | Open the visual editor |<% } %>
-
-Full CLI documentation:
diff --git a/libs/schematics/src/libs/application/files/ts/__dot__env b/libs/schematics/src/libs/application/files/ts/__dot__env
deleted file mode 100644
index 75b4595..0000000
--- a/libs/schematics/src/libs/application/files/ts/__dot__env
+++ /dev/null
@@ -1,6 +0,0 @@
-NANOFORGE_CLIENT_SERVER_TCP_PORT=4444
-NANOFORGE_CLIENT_SERVER_UDP_PORT=4445
-NANOFORGE_CLIENT_SERVER_ADDRESS=127.0.0.1
-
-NANOFORGE_SERVER_LISTENING_TCP_PORT=4444
-NANOFORGE_SERVER_LISTENING_UDP_PORT=4445
diff --git a/libs/schematics/src/libs/application/files/ts/eslint.config.js b/libs/schematics/src/libs/application/files/ts/eslint.config.js
deleted file mode 100644
index 2f3639d..0000000
--- a/libs/schematics/src/libs/application/files/ts/eslint.config.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import pluginJs from "@eslint/js";
-import eslintConfigPrettier from "eslint-config-prettier";
-import globals from "globals";
-import tseslint from "typescript-eslint";
-
-export default [
- { files: ["{client,server}/**/*.{ts}"] },
- { languageOptions: { globals: globals.node } },
-
- pluginJs.configs.recommended,
- ...tseslint.configs.recommended,
- ...tseslint.configs.strict,
- eslintConfigPrettier,
- { ignores: ["**/*.js", "**/*.d.ts"] },
-];
diff --git a/libs/schematics/src/libs/application/files/ts/package.json.template b/libs/schematics/src/libs/application/files/ts/package.json.template
deleted file mode 100644
index 3f4a160..0000000
--- a/libs/schematics/src/libs/application/files/ts/package.json.template
+++ /dev/null
@@ -1,52 +0,0 @@
-{
- "name": "<%= name %>",
- "version": "<%= version %>",
- "description": "<%= description %>",
- "author": "<%= author %>",
- "scripts": {
- "build": "nf build",
- "start": "nf start"<% if (lint) { %>,
- "lint": "prettier --check . && eslint --format=pretty \"{client,server}/**/*.ts\"",
- "format": "prettier --write . && eslint --fix --format=pretty \"{client,server}/**/*.ts\""<% } %>
- },
- "devDependencies": {<% if (lint) { %>
- "@eslint/js": "^9.39.0",<% } %>
- "@nanoforge-dev/asset-manager": "^1",
- "@nanoforge-dev/cli": "latest",
- "@nanoforge-dev/common": "^1",
- "@nanoforge-dev/config": "^1",
- "@nanoforge-dev/core": "^1",<% if (editor) { %>
- "@nanoforge-dev/core-editor": "^1",<% } %>
- "@nanoforge-dev/ecs-client": "^1",<% if (server) { %>
- "@nanoforge-dev/ecs-server": "^1",<% } %>
- "@nanoforge-dev/graphics-2d": "^1",<% if (editor) { %>
- "@nanoforge-dev/graphics-2d-editor": "^1",<% } %>
- "@nanoforge-dev/input": "^1",
- "@nanoforge-dev/music": "^1",<% if (server) { %>
- "@nanoforge-dev/network-client": "^1",
- "@nanoforge-dev/network-server": "^1",<% } %>
- "@nanoforge-dev/sound": "^1",<% if (lint) { %>
- "@typescript-eslint/eslint-plugin": "^8.46.2",
- "@typescript-eslint/parser": "^8.46.2",
- "eslint": "^9.39.0",
- "eslint-config-prettier": "^10.1.8",
- "eslint-plugin-prettier": "^5.5.4",
- "globals": "^16.5.0",
- "lint-staged": "^16.2.6",
- "prettier": "^3.6.2",<% } %>
- "typescript": "^5.9.3"<% if (lint) { %>,
- "typescript-eslint": "^8.46.2"<% } %>
- },<% if (lint) { %>
- "lint-staged": {
- "**/*.ts": [
- "prettier --write"
- ],
- "{client,server}/**/*.ts": [
- "eslint --fix"
- ]
- },<% } %><% if (packageManager === "pnpm") { %>
- "pnpm": {
- "neverBuiltDependencies": []
- },<% } %>
- "private": true
-}
diff --git a/libs/schematics/src/libs/application/files/ts/pnpm-workspace.yaml.template b/libs/schematics/src/libs/application/files/ts/pnpm-workspace.yaml.template
deleted file mode 100644
index 236b123..0000000
--- a/libs/schematics/src/libs/application/files/ts/pnpm-workspace.yaml.template
+++ /dev/null
@@ -1,3 +0,0 @@
-allowBuilds:
- bun: true<% if (server) { %>
- wrtc: true<% } %>
diff --git a/libs/schematics/src/libs/application/files/ts/prettier.config.js b/libs/schematics/src/libs/application/files/ts/prettier.config.js
deleted file mode 100644
index 8e5015d..0000000
--- a/libs/schematics/src/libs/application/files/ts/prettier.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- useTabs: false,
- singleQuote: false,
- trailingComma: "all",
- printWidth: 100,
-};
diff --git a/libs/schematics/src/libs/application/schema.json b/libs/schematics/src/libs/application/schema.json
deleted file mode 100644
index d570149..0000000
--- a/libs/schematics/src/libs/application/schema.json
+++ /dev/null
@@ -1,70 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema",
- "$id": "SchematicsNanoForgeBaseApplication",
- "title": "NanoForge Base Application Options Schema",
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "The name of the application",
- "default": "nanoforge-app"
- },
- "version": {
- "type": "string",
- "description": "NanoForge application version",
- "default": "0.0.0"
- },
- "author": {
- "type": "string",
- "description": "NanoForge application author",
- "default": ""
- },
- "description": {
- "type": "string",
- "description": "NanoForge application description",
- "default": ""
- },
- "directory": {
- "type": "string",
- "description": "NanoForge application destination directory"
- },
- "language": {
- "type": "string",
- "enum": ["ts", "js"],
- "description": "NanoForge application language",
- "default": "ts"
- },
- "strict": {
- "type": "boolean",
- "description": "With strict mode (TypeScript or JavaScript but only on compatible editor)",
- "default": true
- },
- "lint": {
- "type": "boolean",
- "description": "Generate lint files",
- "default": true
- },
- "packageManager": {
- "type": "string",
- "enum": ["npm", "yarn", "pnpm", "bun"],
- "description": "NanoForge application package manager",
- "default": "npm"
- },
- "server": {
- "type": "boolean",
- "description": "Configure a server for the application",
- "default": false
- },
- "editor": {
- "type": "boolean",
- "description": "Add editor dependencies",
- "default": false
- },
- "initFunctions": {
- "type": "boolean",
- "description": "Add init functions to the application",
- "default": false
- }
- },
- "required": ["name"]
-}
diff --git a/libs/schematics/src/libs/configuration/configuration.factory.ts b/libs/schematics/src/libs/configuration/configuration.factory.ts
deleted file mode 100644
index a4a0a17..0000000
--- a/libs/schematics/src/libs/configuration/configuration.factory.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { type Path, join, normalize, strings } from "@angular-devkit/core";
-import {
- type Rule,
- type Source,
- type Tree,
- apply,
- branchAndMerge,
- chain,
- mergeWith,
- move,
- template,
- url,
-} from "@angular-devkit/schematics";
-
-import { ConfigDeclarator } from "@utils/config/config.declarator";
-import { ConfigFinder } from "@utils/config/config.finder";
-import { type Config } from "@utils/config/config.type";
-import { type DeepPartial } from "@utils/types";
-
-import { type ConfigurationOptions } from "./configuration.options";
-import { type ConfigurationSchema } from "./configuration.schema";
-
-const transform = (schema: ConfigurationSchema): ConfigurationOptions => {
- void schema;
-
- return {};
-};
-
-const generate = (options: ConfigurationOptions, path: string): Source => {
- return apply(url(join("./files" as Path)), [
- template({
- ...strings,
- ...options,
- }),
- move(normalize(path)),
- ]);
-};
-
-const addConfiguration = (options: DeepPartial, path: Path) => {
- return (tree: Tree) => {
- const config = new ConfigFinder(tree).find(path);
- if (!config) return tree;
-
- const content = tree.read(config)?.toString();
- const declarator = new ConfigDeclarator();
-
- if (!content) return tree;
-
- tree.overwrite(config, declarator.declare(content, options));
- return tree;
- };
-};
-
-const getConfig = (schema: ConfigurationSchema): DeepPartial => {
- const res: DeepPartial = {
- name: schema.name,
- language: schema.language,
- client: {
- enable: true,
- },
- server: {
- enable: schema.server ?? false,
- },
- };
-
- if (schema.initFunctions) {
- res["initFunctions"] = true;
- }
-
- if (schema.language === "js") {
- if ("client" in res && res.client) {
- res.client["build"] = { entry: "client/main.js" };
- res.client["editor"] = { entry: ".nanoforge/editor/client/main.js" };
- }
- if (schema.server && "server" in res && res.server) {
- res.server["build"] = { entry: "server/main.js" };
- res.server["editor"] = { entry: ".nanoforge/editor/client/main.js" };
- }
- }
-
- return res;
-};
-
-export const main = (schema: ConfigurationSchema): Rule => {
- const options = transform(schema);
- const directory = schema.directory;
-
- return branchAndMerge(
- chain([
- mergeWith(generate(options, directory)),
- addConfiguration(getConfig(schema), directory as Path),
- ]),
- );
-};
diff --git a/libs/schematics/src/libs/configuration/configuration.options.d.ts b/libs/schematics/src/libs/configuration/configuration.options.d.ts
deleted file mode 100644
index 3061ac5..0000000
--- a/libs/schematics/src/libs/configuration/configuration.options.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-export interface ConfigurationOptions {}
diff --git a/libs/schematics/src/libs/configuration/configuration.schema.d.ts b/libs/schematics/src/libs/configuration/configuration.schema.d.ts
deleted file mode 100644
index fb569f6..0000000
--- a/libs/schematics/src/libs/configuration/configuration.schema.d.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-export interface ConfigurationSchema {
- /**
- * The name of the application
- */
- name: string;
-
- /**
- * NanoForge application destination directory
- */
- directory: string;
-
- /**
- * Configure a server for the application
- */
- server: boolean;
-
- /**
- * NanoForge Application language
- */
- language: "js" | "ts";
-
- /**
- * Add init functions to the application
- */
- initFunctions: boolean;
-}
diff --git a/libs/schematics/src/libs/configuration/files/nanoforge.config.json b/libs/schematics/src/libs/configuration/files/nanoforge.config.json
deleted file mode 100644
index 0967ef4..0000000
--- a/libs/schematics/src/libs/configuration/files/nanoforge.config.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/libs/schematics/src/libs/configuration/schema.json b/libs/schematics/src/libs/configuration/schema.json
deleted file mode 100644
index 427d3da..0000000
--- a/libs/schematics/src/libs/configuration/schema.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema",
- "$id": "SchematicsNanoForgeConfiguration",
- "title": "NanoForge Base Application Options Schema",
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "The name of the application",
- "default": "nanoforge-app"
- },
- "directory": {
- "type": "string",
- "description": "NanoForge application destination directory",
- "default": "."
- },
- "server": {
- "type": "boolean",
- "description": "Server configuration",
- "default": false
- },
- "language": {
- "type": "string",
- "enum": ["ts", "js"],
- "description": "NanoForge application language",
- "default": "ts"
- },
- "initFunctions": {
- "type": "boolean",
- "description": "Add init functions to the main application",
- "default": false
- }
- }
-}
diff --git a/libs/schematics/src/libs/docker/docker.factory.ts b/libs/schematics/src/libs/docker/docker.factory.ts
deleted file mode 100644
index 34909a4..0000000
--- a/libs/schematics/src/libs/docker/docker.factory.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { type Path, normalize, strings } from "@angular-devkit/core";
-import {
- type Rule,
- type Source,
- apply,
- mergeWith,
- move,
- template,
- url,
-} from "@angular-devkit/schematics";
-
-import { DEFAULT_PACKAGE_MANAGER } from "~/defaults";
-
-import { type DockerOptions } from "./docker.options";
-import { type DockerSchema } from "./docker.schema";
-
-const transform = (schema: DockerSchema): DockerOptions => {
- return {
- packageManager: schema.packageManager ?? DEFAULT_PACKAGE_MANAGER,
- };
-};
-
-const generate = (options: DockerOptions, path: string): Source => {
- return apply(url("./files" as Path), [
- template({
- ...strings,
- ...options,
- }),
- move(normalize(path)),
- ]);
-};
-
-export const main = (schema: DockerSchema): Rule => {
- const options = transform(schema);
- return mergeWith(generate(options, schema.directory));
-};
diff --git a/libs/schematics/src/libs/docker/docker.options.d.ts b/libs/schematics/src/libs/docker/docker.options.d.ts
deleted file mode 100644
index 489d255..0000000
--- a/libs/schematics/src/libs/docker/docker.options.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export interface DockerOptions {
- /**
- * NanoForge application package manager
- */
- packageManager: "npm" | "yarn" | "pnpm" | "bun";
-}
diff --git a/libs/schematics/src/libs/docker/docker.schema.d.ts b/libs/schematics/src/libs/docker/docker.schema.d.ts
deleted file mode 100644
index f3c29da..0000000
--- a/libs/schematics/src/libs/docker/docker.schema.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export interface DockerSchema {
- /**
- * Target directory for generated Docker files
- */
- directory: string;
-
- /**
- * NanoForge application package manager
- */
- packageManager: "npm" | "yarn" | "pnpm" | "bun";
-}
diff --git a/libs/schematics/src/libs/docker/files/.dockerignore b/libs/schematics/src/libs/docker/files/.dockerignore
deleted file mode 100644
index c2658d7..0000000
--- a/libs/schematics/src/libs/docker/files/.dockerignore
+++ /dev/null
@@ -1 +0,0 @@
-node_modules/
diff --git a/libs/schematics/src/libs/docker/files/Dockerfile b/libs/schematics/src/libs/docker/files/Dockerfile
deleted file mode 100644
index 62571d5..0000000
--- a/libs/schematics/src/libs/docker/files/Dockerfile
+++ /dev/null
@@ -1,25 +0,0 @@
-<% if (packageManager === 'bun') { %>FROM oven/bun:1.3<% } else { %>FROM node:25-slim<% } %>
-<% if (packageManager === 'pnpm') { %>
-ENV PNPM_HOME="/pnpm"
-ENV PATH="$PNPM_HOME:$PATH"
-RUN npm install -g pnpm
-<% } %><% if (packageManager === 'yarn') { %>
-RUN npm install -g yarn
-<% } %>
-WORKDIR /app
-
-COPY package.json ./
-<% if (packageManager === 'npm') { %>COPY package-lock.json ./
-<% } else if (packageManager === 'yarn') { %>COPY yarn.lock ./
-<% } else if (packageManager === 'pnpm') { %>COPY pnpm-lock.yaml ./
-<% } else if (packageManager === 'bun') { %>COPY bun.lock ./
-<% } %>
-<% if (packageManager === 'npm') { %>RUN npm install --no-lockfile
-<% } else if (packageManager === 'pnpm') { %>RUN pnpm install --frozen-lockfile --allow-build=bun
-<% } else { %>RUN <%= packageManager %> install --frozen-lockfile
-<% } %>
-COPY . .
-
-RUN <%= packageManager %> run build
-
-CMD ["<%= packageManager %>", "run", "start"]
diff --git a/libs/schematics/src/libs/docker/schema.json b/libs/schematics/src/libs/docker/schema.json
deleted file mode 100644
index 5055696..0000000
--- a/libs/schematics/src/libs/docker/schema.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema",
- "$id": "SchematicsNanoForgeDocker",
- "title": "NanoForge Base Docker Options Schema",
- "type": "object",
- "properties": {
- "directory": {
- "type": "string",
- "description": "NanoForge application destination directory",
- "default": "."
- },
- "packageManager": {
- "type": "string",
- "enum": ["npm", "yarn", "pnpm", "bun"],
- "description": "NanoForge application package manager",
- "default": "npm"
- }
- }
-}
diff --git a/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/client.save.json b/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/client.save.json
deleted file mode 100644
index 843e417..0000000
--- a/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/client.save.json
+++ /dev/null
@@ -1,77 +0,0 @@
-{
- "libraries": [
- {
- "id": "assetManagerLibrary",
- "type": "asset-manager",
- "name": "AssetManagerLibrary",
- "path": "@nanoforge-dev/asset-manager",
- "params": []
- },
- {
- "id": "ecsLibrary",
- "type": "component-system",
- "name": "ECSClientLibrary",
- "path": "@nanoforge-dev/ecs-client",
- "params": []
- },
- {
- "id": "graphicsLibrary",
- "type": "graphics",
- "name": "Graphics2DLibrary",
- "path": "@nanoforge-dev/graphics-2d",
- "params": []
- },
- {
- "id": "inputLibrary",
- "type": "input",
- "name": "InputLibrary",
- "path": "@nanoforge-dev/input",
- "params": []
- },
- {
- "id": "musicLibrary",
- "type": "music",
- "name": "MusicLibrary",
- "path": "@nanoforge-dev/music",
- "params": []
- },<% if (server) { %>
- {
- "id": "networkLibrary",
- "type": "network",
- "name": "NetworkClientLibrary",
- "path": "@nanoforge-dev/network-client",
- "params": []
- },<% } %>
- {
- "id": "soundLibrary",
- "type": "sound",
- "name": "SoundLibrary",
- "path": "@nanoforge-dev/sound",
- "params": []
- }
- ],
- "components": [
- {
- "name": "ExampleComponent",
- "path": "./components/example.component.js",
- "paramsNames": ["paramA", "paramB", "paramC"]
- }
- ],
- "systems": [
- {
- "name": "exampleSystem",
- "path": "./systems/example.system.js"
- }
- ],
- "entities": [
- {
- "id": "exampleEntity",
- "components": {
- "ExampleComponent": {
- "paramA": "example",
- "paramB": 10
- }
- }
- }
- ]
-}
diff --git a/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/server.save.json b/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/server.save.json
deleted file mode 100644
index ba77067..0000000
--- a/libs/schematics/src/libs/part-base/files/js/__nanoforgeFolder__/server.save.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "libraries": [
- {
- "id": "assetManagerLibrary",
- "type": "asset-manager",
- "name": "AssetManagerLibrary",
- "path": "@nanoforge-dev/asset-manager",
- "params": []
- },
- {
- "id": "ecsLibrary",
- "type": "component-system",
- "name": "ECSServerLibrary",
- "path": "@nanoforge-dev/ecs-server",
- "params": []
- }<% if (server) { %>,
- {
- "id": "networkLibrary",
- "type": "network",
- "name": "NetworkServerLibrary",
- "path": "@nanoforge-dev/network-server",
- "params": []
- }<% } %>
- ],
- "components": [
- {
- "name": "ExampleComponent",
- "path": "./components/example.component.js",
- "paramsNames": ["paramA", "paramB", "paramC"]
- }
- ],
- "systems": [
- {
- "name": "exampleSystem",
- "path": "./systems/example.system.js"
- }
- ],
- "entities": [
- {
- "id": "exampleEntity",
- "components": {
- "ExampleComponent": {
- "paramA": "example",
- "paramB": 10
- }
- }
- }
- ]
-}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/components/example.component.js b/libs/schematics/src/libs/part-base/files/js/__part__/components/example.component.js
deleted file mode 100644
index bf9728b..0000000
--- a/libs/schematics/src/libs/part-base/files/js/__part__/components/example.component.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * @typedef {import("@nanoforge-dev/ecs-<%= part %>").EditorComponentManifest} EditorComponentManifest
- */
-
-export class ExampleComponent {
- static name = "ExampleComponent";
- name = this.constructor.name;
-
- paramA;
- paramB;
- paramC;
-
- /**
- * Example component constructor
- * @param {string} paramA
- * @param {number} paramB
- * @param {boolean} [paramC = false]
- */
- constructor(paramA, paramB, paramC = false) {
- this.paramA = paramA;
- this.paramB = paramB;
- this.paramC = paramC;
- }
-
- get foo() {
- return "bar";
- }
-
- get paramAOrDefault() {
- return this.paramC ? this.paramA : "default";
- }
-
- addOne() {
- this.paramB += 1;
- }
-}
-
-// * Required to generate code
-export default ExampleComponent.name;
-
-/**
- * Editor component manifest
- * * Required for the editor to display the component and generate code
- * @type {EditorComponentManifest}
- */
-export const EDITOR_COMPONENT_MANIFEST = {
- name: "Example",
- description: "Example component description",
- params: [
- {
- type: "string",
- name: "paramA",
- description: "Param A description",
- example: "Example value",
- },
- {
- type: "number",
- name: "paramB",
- description: "Param B description",
- example: 3,
- },
- {
- type: "boolean",
- name: "paramC",
- description: "Param C description",
- example: true,
- default: false,
- // Not required because it has a default value
- optional: true,
- },
- ],
-};
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/after-init.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/after-init.js
deleted file mode 100644
index 74d2124..0000000
--- a/libs/schematics/src/libs/part-base/files/js/__part__/init/after-init.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
- */
-
-/**
- * Actions that are executed after the application initialization
- * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
- * @returns {Promise}
- */
-export async function afterInit(app) {
- // Actions after app initialization
-}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/after-registry-init.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/after-registry-init.js
deleted file mode 100644
index 8de3bf6..0000000
--- a/libs/schematics/src/libs/part-base/files/js/__part__/init/after-registry-init.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
- * @typedef {import("@nanoforge-dev/ecs").Registry} Registry
- */
-
-/**
- * Actions that are executed after the registry components, entities and systems initialization
- * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
- * @param {Registry} registry - ECS registry instance
- * @returns {Promise}
- */
-export async function afterRegistryInit(app, registry) {
- // Actions after registry components, entities and systems initialization
-}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/after-run.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/after-run.js
deleted file mode 100644
index e21bbca..0000000
--- a/libs/schematics/src/libs/part-base/files/js/__part__/init/after-run.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
- */
-
-/**
- * Actions that are executed when the application ended
- * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
- * @returns {Promise}
- */
-export async function afterRun(app) {
- // Actions after app ended
-}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/before-init.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/before-init.js
deleted file mode 100644
index 602b388..0000000
--- a/libs/schematics/src/libs/part-base/files/js/__part__/init/before-init.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
- */
-
-/**
- * Actions that are executed before the application initialization
- * But are executed after base libraries addition
- * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
- * @returns {Promise}
- */
-export async function beforeInit(app) {
- // Actions before app initialization
-}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/before-registry-init.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/before-registry-init.js
deleted file mode 100644
index ae6c374..0000000
--- a/libs/schematics/src/libs/part-base/files/js/__part__/init/before-registry-init.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
- * @typedef {import("@nanoforge-dev/ecs").Registry} Registry
- */
-
-/**
- * Actions that are executed before the registry components, entities and systems initialization
- * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
- * @param {Registry} registry - ECS registry instance
- * @returns {Promise}
- */
-export async function beforeRegistryInit(app, registry) {
- // Actions before registry components, entities and systems initialization
-}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/init/before-run.js b/libs/schematics/src/libs/part-base/files/js/__part__/init/before-run.js
deleted file mode 100644
index e77e47a..0000000
--- a/libs/schematics/src/libs/part-base/files/js/__part__/init/before-run.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * @typedef {import("@nanoforge-dev/core").<%= appClass %>} <%= appClass %>
- */
-
-/**
- * Actions that are executed before the application started
- * @param {<%= appClass %>} app - Nanoforge <%= part %> instance
- * @returns {Promise}
- */
-export async function beforeRun(app) {
- // Actions before app started
-}
diff --git a/libs/schematics/src/libs/part-base/files/js/__part__/systems/example.system.js b/libs/schematics/src/libs/part-base/files/js/__part__/systems/example.system.js
deleted file mode 100644
index c178bfa..0000000
--- a/libs/schematics/src/libs/part-base/files/js/__part__/systems/example.system.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * @typedef {import("@nanoforge-dev/common").Context} Context
- * @typedef {import("@nanoforge-dev/ecs-<%= part %>").EditorSystemManifest} EditorSystemManifest
- * @typedef {import("@nanoforge-dev/ecs-<%= part %>").Registry} Registry
- */
-
-import { ExampleComponent } from "../components/example.component";
-
-/**
- * Example system
- * This system end the game when paramB reaches 0 for any entity with ExampleComponent
- * @param {Registry} registry - ECS registry instance
- * @param {Context} ctx - Nanoforge <%= part %> instance
- */
-export const exampleSystem = (registry, ctx) => {
- const entities = registry.getZipper([ExampleComponent]);
-
- entities.forEach((entity) => {
- if (entity.ExampleComponent.paramA === "end") {
- ctx.app.setIsRunning(false);
- return;
- }
-
- if (entity.ExampleComponent.paramB === 0) entity.ExampleComponent.paramA = "end";
-
- if (entity.ExampleComponent.paramB >= 0)
- entity.ExampleComponent.paramB = entity.ExampleComponent.paramB - 1;
- });
-};
-
-// * Required to generate code
-export default exampleSystem.name;
-
-/**
- * Editor component manifest
- * * Required for the editor to display the system and generate code
- * @type {EditorSystemManifest}
- */
-export const EDITOR_SYSTEM_MANIFEST = {
- name: "Example",
- description:
- "This system end the game when paramB reaches 0 for any entity with ExampleComponent",
- dependencies: ["ExampleComponent"],
-};
diff --git a/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/client.save.json b/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/client.save.json
deleted file mode 100644
index c3be787..0000000
--- a/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/client.save.json
+++ /dev/null
@@ -1,77 +0,0 @@
-{
- "libraries": [
- {
- "id": "assetManagerLibrary",
- "type": "asset-manager",
- "name": "AssetManagerLibrary",
- "path": "@nanoforge-dev/asset-manager",
- "params": []
- },
- {
- "id": "ecsLibrary",
- "type": "component-system",
- "name": "ECSClientLibrary",
- "path": "@nanoforge-dev/ecs-client",
- "params": []
- },
- {
- "id": "graphicsLibrary",
- "type": "graphics",
- "name": "Graphics2DLibrary",
- "path": "@nanoforge-dev/graphics-2d",
- "params": []
- },
- {
- "id": "inputLibrary",
- "type": "input",
- "name": "InputLibrary",
- "path": "@nanoforge-dev/input",
- "params": []
- },
- {
- "id": "musicLibrary",
- "type": "music",
- "name": "MusicLibrary",
- "path": "@nanoforge-dev/music",
- "params": []
- },<% if (server) { %>
- {
- "id": "networkLibrary",
- "type": "network",
- "name": "NetworkClientLibrary",
- "path": "@nanoforge-dev/network-client",
- "params": []
- },<% } %>
- {
- "id": "soundLibrary",
- "type": "sound",
- "name": "SoundLibrary",
- "path": "@nanoforge-dev/sound",
- "params": []
- }
- ],
- "components": [
- {
- "name": "ExampleComponent",
- "path": "./components/example.component.ts",
- "paramsNames": ["paramA", "paramB", "paramC"]
- }
- ],
- "systems": [
- {
- "name": "exampleSystem",
- "path": "./systems/example.system.ts"
- }
- ],
- "entities": [
- {
- "id": "exampleEntity",
- "components": {
- "ExampleComponent": {
- "paramA": "example",
- "paramB": 10
- }
- }
- }
- ]
-}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/server.save.json b/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/server.save.json
deleted file mode 100644
index 47f7bba..0000000
--- a/libs/schematics/src/libs/part-base/files/ts/__nanoforgeFolder__/server.save.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "libraries": [
- {
- "id": "assetManagerLibrary",
- "type": "asset-manager",
- "name": "AssetManagerLibrary",
- "path": "@nanoforge-dev/asset-manager",
- "params": []
- },
- {
- "id": "ecsLibrary",
- "type": "component-system",
- "name": "ECSServerLibrary",
- "path": "@nanoforge-dev/ecs-server",
- "params": []
- }<% if (server) { %>,
- {
- "id": "networkLibrary",
- "type": "network",
- "name": "NetworkServerLibrary",
- "path": "@nanoforge-dev/network-server",
- "params": []
- }<% } %>
- ],
- "components": [
- {
- "name": "ExampleComponent",
- "path": "./components/example.component.ts",
- "paramsNames": ["paramA", "paramB", "paramC"]
- }
- ],
- "systems": [
- {
- "name": "exampleSystem",
- "path": "./systems/example.system.ts"
- }
- ],
- "entities": [
- {
- "id": "exampleEntity",
- "components": {
- "ExampleComponent": {
- "paramA": "example",
- "paramB": 10
- }
- }
- }
- ]
-}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/components/example.component.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/components/example.component.ts
deleted file mode 100644
index 4f98934..0000000
--- a/libs/schematics/src/libs/part-base/files/ts/__part__/components/example.component.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { type EditorComponentManifest } from "@nanoforge-dev/ecs-<%= part %>";
-
-export class ExampleComponent {
- name = this.constructor.name;
-
- constructor(
- public paramA: string,
- public paramB: number,
- public paramC: boolean = false,
- ) {}
-
- get foo() {
- return "bar";
- }
-
- get paramAOrDefault() {
- return this.paramC ? this.paramA : "default";
- }
-
- addOne() {
- this.paramB += 1;
- }
-}
-
-// * Required to generate code
-export default ExampleComponent.name;
-
-// * Required for the editor to display the component and generate code
-export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = {
- name: "Example",
- description: "Example component description",
- params: [
- {
- type: "string",
- name: "paramA",
- description: "Param A description",
- example: "Example value",
- },
- {
- type: "number",
- name: "paramB",
- description: "Param B description",
- example: 3,
- },
- {
- type: "boolean",
- name: "paramC",
- description: "Param C description",
- example: true,
- default: false,
- // Not required because it has a default value
- optional: true,
- },
- ],
-};
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-init.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-init.ts
deleted file mode 100644
index c5c3cee..0000000
--- a/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-init.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { type <%= appClass %> } from "@nanoforge-dev/core";
-
-export async function afterInit(app: <%= appClass %>) {
- // Actions after app initialization
-}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-registry-init.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-registry-init.ts
deleted file mode 100644
index 8233635..0000000
--- a/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-registry-init.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { type <%= appClass %> } from "@nanoforge-dev/core";
-import { type Registry } from "@nanoforge-dev/ecs";
-
-export async function afterRegistryInit(app: <%= appClass %>, registry: Registry) {
- // Actions after registry components, entities and systems initialization
-}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-run.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-run.ts
deleted file mode 100644
index 1d72689..0000000
--- a/libs/schematics/src/libs/part-base/files/ts/__part__/init/after-run.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { type <%= appClass %> } from "@nanoforge-dev/core";
-
-export async function afterRun(app: <%= appClass %>) {
- // Actions after app ended
-}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-init.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-init.ts
deleted file mode 100644
index b45b2b8..0000000
--- a/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-init.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { type <%= appClass %> } from "@nanoforge-dev/core";
-
-export async function beforeInit(app: <%= appClass %>) {
- // Actions before app initialization
-}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-registry-init.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-registry-init.ts
deleted file mode 100644
index 91eda4a..0000000
--- a/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-registry-init.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { type <%= appClass %> } from "@nanoforge-dev/core";
-import { type Registry } from "@nanoforge-dev/ecs";
-
-export async function beforeRegistryInit(app: <%= appClass %>, registry: Registry) {
- // Actions before registry components, entities and systems initialization
-}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-run.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-run.ts
deleted file mode 100644
index 2e0c679..0000000
--- a/libs/schematics/src/libs/part-base/files/ts/__part__/init/before-run.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { type <%= appClass %> } from "@nanoforge-dev/core";
-
-export async function beforeRun(app: <%= appClass %>) {
- // Actions before app started
-}
diff --git a/libs/schematics/src/libs/part-base/files/ts/__part__/systems/example.system.ts b/libs/schematics/src/libs/part-base/files/ts/__part__/systems/example.system.ts
deleted file mode 100644
index 10162b1..0000000
--- a/libs/schematics/src/libs/part-base/files/ts/__part__/systems/example.system.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { type Context } from "@nanoforge-dev/common";
-import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs-<%= part %>";
-
-import { ExampleComponent } from "../components/example.component";
-
-export const exampleSystem = (registry: Registry, ctx: Context) => {
- const entities = registry.getZipper([ExampleComponent]);
-
- entities.forEach((entity) => {
- if (entity.ExampleComponent.paramA === "end") {
- ctx.app.setIsRunning(false);
- return;
- }
-
- if (entity.ExampleComponent.paramB === 0) entity.ExampleComponent.paramA = "end";
-
- if (entity.ExampleComponent.paramB >= 0)
- entity.ExampleComponent.paramB = entity.ExampleComponent.paramB - 1;
- });
-};
-
-// * Required to generate code
-export default exampleSystem.name;
-
-// * Required for the editor to display the system and generate code
-export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = {
- name: "Example",
- description:
- "This system end the game when paramB reaches 0 for any entity with ExampleComponent",
- dependencies: ["ExampleComponent"],
-};
diff --git a/libs/schematics/src/libs/part-base/part-base.factory.ts b/libs/schematics/src/libs/part-base/part-base.factory.ts
deleted file mode 100644
index 2144656..0000000
--- a/libs/schematics/src/libs/part-base/part-base.factory.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { type Path, join, normalize, strings } from "@angular-devkit/core";
-import {
- type Rule,
- type Source,
- apply,
- filter,
- mergeWith,
- move,
- template,
- url,
-} from "@angular-devkit/schematics";
-
-import { type PartBaseOptions } from "./part-base.options";
-import { type PartBaseSchema } from "./part-base.schema";
-
-const transform = (schema: PartBaseSchema): PartBaseOptions => {
- return {
- part: schema.part,
- server: schema.server,
- appClass: schema.part === "client" ? "NanoforgeClient" : "NanoforgeServer",
- nanoforgeFolder: ".nanoforge",
- };
-};
-
-const generate = (
- options: PartBaseOptions,
- path: string,
- initFunctions: boolean,
- language: "ts" | "js",
-): Source => {
- const rules = [
- template({
- ...strings,
- ...options,
- }),
- move(normalize(path)),
- filter((path) => {
- const splited = path.split("/");
- return splited.at(-2) !== ".nanoforge" || splited.at(-1) === `${options.part}.save.json`;
- }),
- ];
- if (!initFunctions) rules.push(filter((path) => path.split("/").at(-2) !== "init"));
-
- return apply(url(join("./files" as Path, language)), rules);
-};
-
-export const main = (schema: PartBaseSchema): Rule => {
- const options = transform(schema);
-
- return mergeWith(generate(options, schema.directory, schema.initFunctions, schema.language));
-};
diff --git a/libs/schematics/src/libs/part-base/part-base.options.d.ts b/libs/schematics/src/libs/part-base/part-base.options.d.ts
deleted file mode 100644
index 9e5934b..0000000
--- a/libs/schematics/src/libs/part-base/part-base.options.d.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-export interface PartBaseOptions {
- /**
- * The part of the application to generate
- */
- part: "client" | "server";
-
- /**
- * App class name
- */
- appClass: string;
-
- /**
- * Configure a server for the application
- */
- server: boolean;
-
- /**
- * Name of Nanoforge folder
- */
- nanoforgeFolder: string;
-}
diff --git a/libs/schematics/src/libs/part-base/part-base.schema.d.ts b/libs/schematics/src/libs/part-base/part-base.schema.d.ts
deleted file mode 100644
index d708ed0..0000000
--- a/libs/schematics/src/libs/part-base/part-base.schema.d.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-export interface PartBaseSchema {
- /**
- * NanoForge application destination directory
- */
- directory: string;
-
- /**
- * The part of the application to generate
- */
- part: "client" | "server";
-
- /**
- * NanoForge Application language
- */
- language: "js" | "ts";
-
- /**
- * Add init functions to the application
- */
- initFunctions: boolean;
-
- /**
- * Configure a server for the application
- */
- server: boolean;
-}
diff --git a/libs/schematics/src/libs/part-base/schema.json b/libs/schematics/src/libs/part-base/schema.json
deleted file mode 100644
index 95d3bc1..0000000
--- a/libs/schematics/src/libs/part-base/schema.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema",
- "$id": "SchematicsNanoForgePartBase",
- "title": "NanoForge Part Base Options Schema",
- "type": "object",
- "properties": {
- "directory": {
- "type": "string",
- "description": "NanoForge application destination directory",
- "default": "."
- },
- "part": {
- "type": "string",
- "enum": ["client", "server"],
- "description": "The part of the application to generate"
- },
- "language": {
- "type": "string",
- "enum": ["ts", "js"],
- "description": "NanoForge application language",
- "default": "ts"
- },
- "initFunctions": {
- "type": "boolean",
- "description": "Add init functions to the application",
- "default": false
- },
- "server": {
- "type": "boolean",
- "description": "There is a Server in app",
- "default": false
- }
- },
- "required": ["part"]
-}
diff --git a/libs/schematics/src/libs/part-main/part-main.factory.ts b/libs/schematics/src/libs/part-main/part-main.factory.ts
deleted file mode 100644
index 1bd9770..0000000
--- a/libs/schematics/src/libs/part-main/part-main.factory.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { type Path, join, normalize, strings } from "@angular-devkit/core";
-import {
- type Rule,
- type Source,
- type Tree,
- apply,
- asSource,
- mergeWith,
- move,
- template,
-} from "@angular-devkit/schematics";
-import * as fs from "fs";
-
-import { generateMain } from "@utils/main/main-functions";
-import { type Save, SaveLibraryTypeEnum } from "@utils/main/save.type";
-
-import { type PartMainOptions } from "./part-main.options";
-import { type PartMainSchema } from "./part-main.schema";
-
-const transform = (schema: PartMainSchema): PartMainOptions => {
- void schema;
-
- return {};
-};
-
-const generate = (options: PartMainOptions, path: string, schema: PartMainSchema): Source => {
- const rules = [
- template({
- ...strings,
- ...options,
- }),
- move(normalize(path)),
- ];
-
- return apply(asSource(writeMain(schema, path)), rules);
-};
-
-const getMainPath = (schema: PartMainSchema): string => {
- if (schema.outFile) return schema.outFile;
- if (schema.editor)
- return join(".nanoforge/editor" as Path, schema.part, `main.${schema.language}`);
- return join(schema.part as Path, `main.${schema.language}`);
-};
-
-const writeMain = (schema: PartMainSchema, path: string) => {
- return (tree: Tree) => {
- let save = getSave(path, schema.part, schema.saveFile);
-
- const resPath = getMainPath(schema);
-
- if (schema.editor) {
- save = getEditorSave(save);
- }
-
- const content = generateMain(schema, save);
- fs.rmSync(join(path as Path, resPath), { force: true });
- tree.create(resPath, content);
- return tree;
- };
-};
-
-const getSave = (path: string, part: "client" | "server", saveFile?: string): Save => {
- return JSON.parse(
- fs.readFileSync(saveFile ?? join(path as Path, `.nanoforge/${part}.save.json`), "utf-8"),
- ) as Save;
-};
-
-const getEditorSave = (save: Save): Save => {
- if (save.libraries.some((lib) => lib.type === SaveLibraryTypeEnum.GRAPHICS))
- save.libraries.push({
- id: "graphicsEditorLibrary",
- type: "graphics-editor",
- name: "Graphics2DEditorLibrary",
- path: "@nanoforge-dev/graphics-2d-editor",
- });
-
- return save;
-};
-
-export const main = (schema: PartMainSchema): Rule => {
- const options = transform(schema);
-
- return mergeWith(generate(options, schema.directory, schema));
-};
diff --git a/libs/schematics/src/libs/part-main/part-main.options.d.ts b/libs/schematics/src/libs/part-main/part-main.options.d.ts
deleted file mode 100644
index d16f512..0000000
--- a/libs/schematics/src/libs/part-main/part-main.options.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-export interface PartMainOptions {}
diff --git a/libs/schematics/src/libs/part-main/part-main.schema.d.ts b/libs/schematics/src/libs/part-main/part-main.schema.d.ts
deleted file mode 100644
index f2a474a..0000000
--- a/libs/schematics/src/libs/part-main/part-main.schema.d.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-export interface PartMainSchema {
- /**
- * NanoForge application destination directory
- */
- directory: string;
-
- /**
- * The part of the application to generate
- */
- part: "client" | "server";
-
- /**
- * NanoForge Application language
- */
- language: "js" | "ts";
-
- /**
- * Add init functions to the application
- */
- initFunctions: boolean;
-
- /**
- * Save file path with components and systems in JSON format
- */
- saveFile?: string;
-
- /**
- * Path of the out file
- */
- outFile?: string;
-
- /**
- * Build main editor
- */
- editor: boolean;
-}
diff --git a/libs/schematics/src/libs/part-main/schema.json b/libs/schematics/src/libs/part-main/schema.json
deleted file mode 100644
index d2f84a8..0000000
--- a/libs/schematics/src/libs/part-main/schema.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema",
- "$id": "SchematicsNanoForgePartMain",
- "title": "NanoForge Part Main Options Schema",
- "type": "object",
- "properties": {
- "directory": {
- "type": "string",
- "description": "NanoForge application destination directory",
- "default": "."
- },
- "part": {
- "type": "string",
- "enum": ["client", "server"],
- "description": "The part of the application to generate"
- },
- "language": {
- "type": "string",
- "enum": ["ts", "js"],
- "description": "NanoForge application language",
- "default": "ts"
- },
- "initFunctions": {
- "type": "boolean",
- "description": "Add init functions to the main application",
- "default": false
- },
- "saveFile": {
- "type": "string",
- "description": "Save file path with components and systems in JSON format"
- },
- "outFile": {
- "type": "string",
- "description": "Path of the out file"
- },
- "editor": {
- "type": "boolean",
- "description": "Build main editor",
- "default": false
- }
- },
- "required": ["part"]
-}
diff --git a/libs/schematics/src/libs/project/files/js/.dockerignore b/libs/schematics/src/libs/project/files/js/.dockerignore
new file mode 100644
index 0000000..6769f3a
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/.dockerignore
@@ -0,0 +1,5 @@
+node_modules/
+dist/
+.nanoforge/
+.git/
+.env
diff --git a/libs/schematics/src/libs/project/files/js/Dockerfile b/libs/schematics/src/libs/project/files/js/Dockerfile
new file mode 100644
index 0000000..2badd74
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/Dockerfile
@@ -0,0 +1,39 @@
+<% if (packageManager === "bun") { %>
+FROM oven/bun:1.3 AS base
+<% } else if (packageManager === "pnpm") { %>
+FROM ghcr.io/pnpm/pnpm:12 AS base
+RUN pnpm runtime set node 26 -g
+<% } else { %>
+FROM node:26-slim AS base
+<% } %><% if (packageManager === "yarn") { %>
+RUN npm install -g yarn
+<% } %>
+WORKDIR /app
+
+FROM base AS deps
+
+<% if (packageManager === "npm") { %>COPY package.json package-lock.json ./
+RUN npm ci
+<% } else if (packageManager === "yarn") { %>COPY package.json yarn.lock ./
+RUN yarn install --frozen-lockfile
+<% } else if (packageManager === "pnpm") { %>COPY package.json pnpm-lock.yaml ./
+RUN pnpm install --frozen-lockfile
+<% } else { %>COPY package.json bun.lock ./
+RUN bun install --frozen-lockfile
+<% } %>
+FROM deps AS build
+COPY . .
+RUN <%= packageManager %> run build
+
+<% if (packageManager === "bun") { %>FROM oven/bun:1.3-slim<% } else { %>FROM base<% } %>
+WORKDIR /app
+
+COPY --from=build /app/node_modules ./node_modules
+COPY --from=build /app/dist ./dist
+COPY --from=build /app/package.json ./
+COPY --from=build /app/nanoforge.config.js ./
+<% if (part === "server") { %>
+<% } %><% if (packageManager !== "pnpm") { %>
+USER <% if (packageManager === "bun") { %>bun<% } else { %>node<% } %>
+<% } %><% if (part === "client") { %>EXPOSE 3000
+<% } %>CMD ["<%= packageManager %>", "run", "start"]
diff --git a/libs/schematics/src/libs/project/files/js/README.md b/libs/schematics/src/libs/project/files/js/README.md
new file mode 100644
index 0000000..b068141
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/README.md
@@ -0,0 +1,53 @@
+# <%= packageName %>
+
+A [NanoForge](https://nanoforge.eu) <%= part %> app.
+<% if (!workspace) { %>
+## Prerequisites
+
+- Node.js and <%= packageManager %>
+- The [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `npm install -g @nanoforge-dev/cli`
+
+## Install
+
+```bash
+<%= packageManager %> install
+```
+<% } %>
+## Development
+
+```bash
+nf dev
+```
+
+`nf dev` serves the game and watches your files, so changes are picked up as you work.
+
+## Build & start
+
+```bash
+nf build
+nf start
+```
+
+`nf build` compiles this <%= part %> for production, and `nf start` serves the build.
+<% if (editor) { %>
+This app was generated with editor support. Launch it with:
+
+```bash
+nf editor .
+```
+<% } %><% if (docker) { %>
+A `Dockerfile` is also included to containerize the built app for deployment.
+<% } %>
+## CLI reference
+
+| Command | Description |
+| ------------- | ---------------------------------- |
+| `nf dev` | Develop with hot reload |
+| `nf build` | Build for production |
+| `nf start` | Run a built app |<% if (editor) { %>
+| `nf editor` | Launch the visual editor |<% } %>
+
+## More info
+
+- [Documentation](https://docs.nanoforge.eu) - CLI commands, configuration, and guides
+- [nanoforge.eu](https://nanoforge.eu) - learn more about NanoForge
diff --git a/libs/schematics/src/libs/project/files/js/assets/__dot__gitkeep b/libs/schematics/src/libs/project/files/js/assets/__dot__gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/libs/schematics/src/libs/project/files/js/jsconfig.json b/libs/schematics/src/libs/project/files/js/jsconfig.json
new file mode 100644
index 0000000..3e2e75e
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://json.schemastore.org/jsconfig.json",
+ "extends": "../../jsconfig.json",
+ "include": ["src/**/*.js"],
+ "exclude": ["node_modules", "dist", "../../.nanoforge"]
+}
diff --git a/libs/schematics/src/libs/project/files/js/nanoforge.config.js b/libs/schematics/src/libs/project/files/js/nanoforge.config.js
new file mode 100644
index 0000000..f9f079f
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/nanoforge.config.js
@@ -0,0 +1,6 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "<%= part %>",<% if (libs.length) { %>
+ libs: [<% libs.forEach(function (lib, i) { %>"<%= lib %>"<% if (i < libs.length - 1) { %>, <% } %><% }); %>],<% } %>
+});
diff --git a/libs/schematics/src/libs/project/files/js/package.json.template b/libs/schematics/src/libs/project/files/js/package.json.template
new file mode 100644
index 0000000..64f7c67
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/package.json.template
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "<%= packageName %>",
+ "scripts": {
+ "dev": "nf dev",
+ "build": "nf build",
+ "start": "nf start"
+ },
+ "devDependencies": {
+ "@nanoforge-dev/ecs": "<%= ecsVersion %>",<% if (part === "client") { %>
+ "@nanoforge-dev/graphics-2d": "<%= graphics2dVersion %>",
+ "@nanoforge-dev/input": "<%= inputVersion %>",<% } %>
+ "@nanoforge-dev/network": "<%= networkVersion %>",
+ "nanoforge": "<%= nanoforgeVersion %>"
+ },<% if (packageManager === "npm" && allowBuilds.length > 0) { %>
+ "allowScripts": {<% allowBuilds.forEach(function (pkg, i) { %>
+ "<%= pkg %>": true<% if (i < allowBuilds.length - 1) { %>,<% } %><% }); %>
+ },<% } %><% if (packageManager === "bun" && allowBuilds.length > 0) { %>
+ "trustedDependencies": [<% allowBuilds.forEach(function (pkg, i) { %>"<%= pkg %>"<% if (i < allowBuilds.length - 1) { %>, <% } %><% }); %>],<% } %>
+ "private": true
+}
diff --git a/libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template b/libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template
new file mode 100644
index 0000000..2312ae9
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template
@@ -0,0 +1,3 @@
+<% if (allowBuilds.length > 0) { %>allowBuilds:<% allowBuilds.forEach(function (pkg) { %>
+ <%= pkg %>: true<% }); %>
+<% } %>
diff --git a/libs/schematics/src/libs/project/files/js/src/components/drawable-circle-2d.component.js b/libs/schematics/src/libs/project/files/js/src/components/drawable-circle-2d.component.js
new file mode 100644
index 0000000..2f3a3f5
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/src/components/drawable-circle-2d.component.js
@@ -0,0 +1,404 @@
+/**
+ * @typedef {import("@nanoforge-dev/ecs").EditorComponentManifest} EditorComponentManifest
+ * @typedef {import("@nanoforge-dev/graphics-2d").Layer} Layer
+ * @typedef {import("nanoforge").NfFile} NfFile
+ */
+
+import { Circle } from "@nanoforge-dev/graphics-2d";
+
+export class DrawableCircle2D {
+ name = this.constructor.name;
+ shape;
+
+ /** @type {NfFile | undefined} */
+ #fillPatternImage;
+
+ /**
+ * @param {number} [radius]
+ * @param {boolean} [visible]
+ * @param {string} [id]
+ * @param {number} [opacity]
+ * @param {number} [scaleX]
+ * @param {number} [scaleY]
+ * @param {number} [skewX]
+ * @param {number} [skewY]
+ * @param {number} [rotation]
+ * @param {number} [offsetX]
+ * @param {number} [offsetY]
+ * @param {string} [fillColor]
+ * @param {NfFile} [fillPatternImage]
+ * @param {number} [fillPatternX]
+ * @param {number} [fillPatternY]
+ * @param {number} [fillPatternOffsetX]
+ * @param {number} [fillPatternOffsetY]
+ * @param {number} [fillPatternScaleX]
+ * @param {number} [fillPatternScaleY]
+ * @param {number} [fillPatternRotation]
+ * @param {string} [fillPatternRepeat]
+ */
+ constructor(
+ radius,
+ visible,
+ id,
+ opacity,
+ scaleX,
+ scaleY,
+ skewX,
+ skewY,
+ rotation,
+ offsetX,
+ offsetY,
+ fillColor,
+ fillPatternImage,
+ fillPatternX,
+ fillPatternY,
+ fillPatternOffsetX,
+ fillPatternOffsetY,
+ fillPatternScaleX,
+ fillPatternScaleY,
+ fillPatternRotation,
+ fillPatternRepeat,
+ ) {
+ this.#fillPatternImage = fillPatternImage;
+
+ this.shape = new Circle({
+ radius,
+ visible,
+ id,
+ opacity,
+ scaleX,
+ scaleY,
+ skewX,
+ skewY,
+ rotation,
+ offsetX,
+ offsetY,
+ fill: fillColor,
+ fillPatternImage: fillPatternImage
+ ? Object.assign(new Image(), { src: fillPatternImage.path })
+ : undefined,
+ fillPatternX,
+ fillPatternY,
+ fillPatternOffsetX,
+ fillPatternOffsetY,
+ fillPatternScaleX,
+ fillPatternScaleY,
+ fillPatternRotation,
+ fillPatternRepeat,
+ });
+ }
+
+ get radius() {
+ return this.shape.radius();
+ }
+ set radius(v) {
+ this.shape.radius(v);
+ this.#redraw();
+ }
+ get visible() {
+ return this.shape.visible();
+ }
+ set visible(v) {
+ this.shape.visible(v);
+ this.#redraw();
+ }
+ get id() {
+ return this.shape.id();
+ }
+ set id(v) {
+ this.shape.id(v);
+ this.#redraw();
+ }
+ get opacity() {
+ return this.shape.opacity();
+ }
+ set opacity(v) {
+ this.shape.opacity(v);
+ this.#redraw();
+ }
+ get scaleX() {
+ return this.shape.scaleX();
+ }
+ set scaleX(v) {
+ this.shape.scaleX(v);
+ this.#redraw();
+ }
+ get scaleY() {
+ return this.shape.scaleY();
+ }
+ set scaleY(v) {
+ this.shape.scaleY(v);
+ this.#redraw();
+ }
+ get skewX() {
+ return this.shape.skewX();
+ }
+ set skewX(v) {
+ this.shape.skewX(v);
+ this.#redraw();
+ }
+ get skewY() {
+ return this.shape.skewY();
+ }
+ set skewY(v) {
+ this.shape.skewY(v);
+ this.#redraw();
+ }
+ get rotation() {
+ return this.shape.rotation();
+ }
+ set rotation(v) {
+ this.shape.rotation(v);
+ this.#redraw();
+ }
+ get offsetX() {
+ return this.shape.offsetX();
+ }
+ set offsetX(v) {
+ this.shape.offsetX(v);
+ this.#redraw();
+ }
+ get offsetY() {
+ return this.shape.offsetY();
+ }
+ set offsetY(v) {
+ this.shape.offsetY(v);
+ this.#redraw();
+ }
+ get fillColor() {
+ return this.shape.fill().toString();
+ }
+ set fillColor(v) {
+ this.shape.fill(v);
+ this.#redraw();
+ }
+ /** @returns {NfFile | undefined} */
+ get fillPatternImage() {
+ return this.#fillPatternImage;
+ }
+ /** @param {NfFile} v */
+ set fillPatternImage(v) {
+ this.#fillPatternImage = v;
+ this.shape.fillPatternImage(v ? Object.assign(new Image(), { src: v.path }) : undefined);
+ this.#redraw();
+ }
+ get fillPatternX() {
+ return this.shape.fillPatternX();
+ }
+ set fillPatternX(v) {
+ this.shape.fillPatternX(v);
+ this.#redraw();
+ }
+ get fillPatternY() {
+ return this.shape.fillPatternY();
+ }
+ set fillPatternY(v) {
+ this.shape.fillPatternY(v);
+ this.#redraw();
+ }
+ get fillPatternOffsetX() {
+ return this.shape.fillPatternOffsetX();
+ }
+ set fillPatternOffsetX(v) {
+ this.shape.fillPatternOffsetX(v);
+ this.#redraw();
+ }
+ get fillPatternOffsetY() {
+ return this.shape.fillPatternOffsetY();
+ }
+ set fillPatternOffsetY(v) {
+ this.shape.fillPatternOffsetY(v);
+ this.#redraw();
+ }
+ get fillPatternScaleX() {
+ return this.shape.fillPatternScaleX();
+ }
+ set fillPatternScaleX(v) {
+ this.shape.fillPatternScaleX(v);
+ this.#redraw();
+ }
+ get fillPatternScaleY() {
+ return this.shape.fillPatternScaleY();
+ }
+ set fillPatternScaleY(v) {
+ this.shape.fillPatternScaleY(v);
+ this.#redraw();
+ }
+ get fillPatternRotation() {
+ return this.shape.fillPatternRotation();
+ }
+ set fillPatternRotation(v) {
+ this.shape.fillPatternRotation(v);
+ this.#redraw();
+ }
+ get fillPatternRepeat() {
+ return this.shape.fillPatternRepeat();
+ }
+ set fillPatternRepeat(v) {
+ this.shape.fillPatternRepeat(v);
+ this.#redraw();
+ }
+
+ /** @param {Layer} layer */
+ addToLayer(layer) {
+ if (this.shape.getParent() !== layer) layer.add(this.shape);
+ }
+
+ #redraw() {
+ this.shape.getLayer()?.batchDraw();
+ }
+}
+// * Required to generate code
+export default DrawableCircle2D.name;
+
+// * Required for the editor to display the component and generate code
+/** @type {EditorComponentManifest} */
+export const EDITOR_COMPONENT_MANIFEST = {
+ name: "DrawableCircle2D",
+ description: "Graphically drawable circle 2D",
+ params: [
+ {
+ name: "radius",
+ type: "number",
+ description: "Radius of the circle in pixels",
+ default: 10,
+ optional: false,
+ },
+ {
+ name: "visible",
+ type: "boolean",
+ description: "Controls whether the node is rendered",
+ default: true,
+ optional: false,
+ },
+ {
+ name: "id",
+ type: "string",
+ description: "Unique identifier for the node, used with stage.findOne('#id')",
+ optional: true,
+ },
+ {
+ name: "opacity",
+ type: "number",
+ description: "Opacity from 0 (transparent) to 1 (fully opaque)",
+ default: 1,
+ optional: true,
+ },
+ {
+ name: "scaleX",
+ type: "number",
+ description: "Horizontal scale factor",
+ default: 1,
+ optional: true,
+ },
+ {
+ name: "scaleY",
+ type: "number",
+ description: "Vertical scale factor",
+ default: 1,
+ optional: true,
+ },
+ {
+ name: "skewX",
+ type: "number",
+ default: 0,
+ description: "Horizontal skew in radians, shears along the X axis",
+ optional: true,
+ },
+ {
+ name: "skewY",
+ type: "number",
+ default: 0,
+ description: "Vertical skew in radians, shears along the Y axis",
+ optional: true,
+ },
+ {
+ name: "rotation",
+ type: "number",
+ description:
+ "Rotation angle in degrees applied around the offset point (positive values rotate clockwise)",
+ default: 0,
+ optional: true,
+ },
+ {
+ name: "offsetX",
+ type: "number",
+ description: "Horizontal offset for the transform origin",
+ default: 0,
+ optional: true,
+ },
+ {
+ name: "offsetY",
+ type: "number",
+ description: "Vertical offset for the transform origin",
+ default: 0,
+ optional: true,
+ },
+ {
+ name: "fillColor",
+ type: "string",
+ description:
+ 'Solid fill color as a CSS color string (e.g. "red", "#ff0000", "rgba(255,0,0,0.5)"), or a pre-built CanvasGradient object',
+ default: "red",
+ example: "rgb(255,255,255)",
+ optional: true,
+ },
+ {
+ name: "fillPatternImage",
+ type: "asset",
+ description: "Image element to use as a tiling pattern fill",
+ optional: true,
+ },
+ {
+ name: "fillPatternX",
+ type: "number",
+ description:
+ "Horizontal offset of the pattern origin relative to the shape's top-left corner, in pixels",
+ optional: true,
+ },
+ {
+ name: "fillPatternY",
+ type: "number",
+ description:
+ "Vertical offset of the pattern origin relative to the shape's top-left corner, in pixels",
+ optional: true,
+ },
+ {
+ name: "fillPatternOffsetX",
+ type: "number",
+ description: "Horizontally shifts the pattern image within the tile before repeating",
+ optional: true,
+ },
+ {
+ name: "fillPatternOffsetY",
+ type: "number",
+ description: "Vertically shifts the pattern image within the tile before repeating",
+ optional: true,
+ },
+ {
+ name: "fillPatternScaleX",
+ type: "number",
+ description: "Horizontal scale factor for the pattern tile",
+ optional: true,
+ },
+ {
+ name: "fillPatternScaleY",
+ type: "number",
+ description: "Vertical scale factor for the pattern tile",
+ optional: true,
+ },
+ {
+ name: "fillPatternRotation",
+ type: "number",
+ description: "Rotation of the pattern tile in degrees",
+ optional: true,
+ },
+ {
+ name: "fillPatternRepeat",
+ type: "string",
+ description:
+ 'Repeat mode for the pattern. One of "repeat" (default), "repeat-x", "repeat-y", or "no-repeat"',
+ optional: true,
+ },
+ ],
+};
diff --git a/libs/schematics/src/libs/project/files/js/src/components/position-2d.component.js b/libs/schematics/src/libs/project/files/js/src/components/position-2d.component.js
new file mode 100644
index 0000000..82f4b4f
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/src/components/position-2d.component.js
@@ -0,0 +1,42 @@
+/**
+ * @typedef {import("@nanoforge-dev/ecs").EditorComponentManifest} EditorComponentManifest
+ */
+
+export class Position2D {
+ name = this.constructor.name;
+
+ /**
+ * @param {number} x
+ * @param {number} y
+ */
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+}
+
+// * Required to generate code
+export default Position2D.name;
+
+// * Required for the editor to display the component and generate code
+/** @type {EditorComponentManifest} */
+export const EDITOR_COMPONENT_MANIFEST = {
+ name: "Position2D",
+ description: "Position of an entity in a 2 dimensional space in pixel",
+ params: [
+ {
+ type: "number",
+ name: "x",
+ description: "Horizontal position in pixel",
+ example: 4.2,
+ default: 0,
+ },
+ {
+ type: "number",
+ name: "y",
+ description: "Vertical position in pixel",
+ example: 67,
+ default: 0,
+ },
+ ],
+};
diff --git a/libs/schematics/src/libs/project/files/js/src/main.js b/libs/schematics/src/libs/project/files/js/src/main.js
new file mode 100644
index 0000000..68ec50a
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/src/main.js
@@ -0,0 +1,43 @@
+import { NanoforgeFactory } from "nanoforge";
+import { EcsLibrary } from "@nanoforge-dev/ecs/<%= part %>";<% if (part === "client") { %>
+import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d";
+import { InputLibrary } from "@nanoforge-dev/input";<% } %>
+import { <%= part === "client" ? "NetworkClientLibrary" : "NetworkServerLibrary" %> } from "@nanoforge-dev/network/<%= part %>";
+<% if (part === "client") { %>
+import { DrawableCircle2D } from "./components/drawable-circle-2d.component";<% } %>
+import { Position2D } from "./components/position-2d.component";<% if (part === "client") { %>
+import { draw2D } from "./systems/draw-2d.system";<% if (hasServer) { %>
+import { positionSync } from "./systems/position-sync.system";<% } %><% } else { %>
+import { move2D } from "./systems/move-2d.system";<% } %>
+
+/**
+ * @param {import("nanoforge").<%= part === "client" ? "ClientRunOptions" : "ServerRunOptions" %>} options
+ * @returns {Promise}
+ */
+export const main = async (options) => {
+ const app = NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>({ tickRate: 60 });
+
+ const ecs = new EcsLibrary();
+ app.use(ecs);<% if (part === "client") { %>
+ app.use(new Graphics2DLibrary());
+ app.use(new InputLibrary());<% } %>
+ app.use(new <%= part === "client" ? "NetworkClientLibrary" : "NetworkServerLibrary" %>());
+
+ await app.init(options);
+
+ const registry = ecs.registry;
+<% if (part === "client") { %>
+ const circle = registry.spawnEntity();
+ registry.addComponent(circle, new Position2D(500, 500));
+ registry.addComponent(circle, new DrawableCircle2D(75, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, "red"));
+
+ registry.addSystem(draw2D);<% if (hasServer) { %>
+ registry.addSystem(positionSync);<% } %>
+<% } else { %>
+ const entity = registry.spawnEntity();
+ registry.addComponent(entity, new Position2D(500, 500));
+
+ registry.addSystem(move2D);
+<% } %>
+ await app.run();
+};
diff --git a/libs/schematics/src/libs/project/files/js/src/systems/draw-2d.system.js b/libs/schematics/src/libs/project/files/js/src/systems/draw-2d.system.js
new file mode 100644
index 0000000..2312b4c
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/src/systems/draw-2d.system.js
@@ -0,0 +1,33 @@
+/**
+ * @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest
+ * @typedef {import("@nanoforge-dev/ecs").Registry} Registry
+ * @typedef {import("nanoforge").Context} Context
+ */
+
+import { DrawableCircle2D } from "../components/drawable-circle-2d.component";
+import { Position2D } from "../components/position-2d.component";
+
+/**
+ * @param {Registry} registry
+ * @param {Context} ctx
+ */
+export function draw2D(registry, ctx) {
+ const graphic2d = ctx.graphics;
+
+ const entities = registry.getZipper([Position2D, DrawableCircle2D]);
+
+ entities.forEach(({ Position2D, DrawableCircle2D }) => {
+ DrawableCircle2D.addToLayer(graphic2d.baseLayer);
+ DrawableCircle2D.shape.setPosition(Position2D);
+ });
+}
+// * Required to generate code
+export default draw2D.name;
+
+// * Required for the editor to display the system and generate code
+/** @type {EditorSystemManifest} */
+export const EDITOR_SYSTEM_MANIFEST = {
+ name: "draw2D",
+ description: "Draw every drawable component",
+ dependencies: ["Position2D", "DrawableCircle2D"],
+};
diff --git a/libs/schematics/src/libs/project/files/js/src/systems/move-2d.system.js b/libs/schematics/src/libs/project/files/js/src/systems/move-2d.system.js
new file mode 100644
index 0000000..a619065
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/src/systems/move-2d.system.js
@@ -0,0 +1,34 @@
+/**
+ * @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest
+ * @typedef {import("@nanoforge-dev/ecs").Registry} Registry
+ * @typedef {import("nanoforge").Context} Context
+ */
+
+import { Position2D } from "../components/position-2d.component";
+
+/**
+ * @param {Registry} registry
+ * @param {Context} ctx
+ */
+export function move2D(registry, ctx) {
+ const network = ctx.network;
+ const entities = registry.getZipper([Position2D]);
+
+ entities.forEach(({ Position2D }) => {
+ Position2D.x += 1;
+
+ network.tcp.sendToEverybody(
+ new TextEncoder().encode(JSON.stringify({ x: Position2D.x, y: Position2D.y })),
+ );
+ });
+}
+// * Required to generate code
+export default move2D.name;
+
+// * Required for the editor to display the system and generate code
+/** @type {EditorSystemManifest} */
+export const EDITOR_SYSTEM_MANIFEST = {
+ name: "move2D",
+ description: "Moves the entity and broadcasts its position to every connected client",
+ dependencies: ["Position2D"],
+};
diff --git a/libs/schematics/src/libs/project/files/js/src/systems/position-sync.system.js b/libs/schematics/src/libs/project/files/js/src/systems/position-sync.system.js
new file mode 100644
index 0000000..d9339e0
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/src/systems/position-sync.system.js
@@ -0,0 +1,35 @@
+/**
+ * @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest
+ * @typedef {import("@nanoforge-dev/ecs").Registry} Registry
+ * @typedef {import("nanoforge").Context} Context
+ */
+
+import { Position2D } from "../components/position-2d.component";
+
+/**
+ * @param {Registry} registry
+ * @param {Context} ctx
+ */
+export function positionSync(registry, ctx) {
+ const network = ctx.network;
+ const packets = network.tcp
+ .getReceivedPackets()
+ .map((packet) => JSON.parse(new TextDecoder().decode(packet)));
+
+ const [entity] = registry.getZipper([Position2D]);
+
+ packets.forEach((packet) => {
+ entity.Position2D.x = packet.x;
+ entity.Position2D.y = packet.y;
+ });
+}
+// * Required to generate code
+export default positionSync.name;
+
+// * Required for the editor to display the system and generate code
+/** @type {EditorSystemManifest} */
+export const EDITOR_SYSTEM_MANIFEST = {
+ name: "positionSync",
+ description: "Overwrites the local entity's position with the latest value received from the server",
+ dependencies: ["Position2D"],
+};
diff --git a/libs/schematics/src/libs/project/files/ts/.dockerignore b/libs/schematics/src/libs/project/files/ts/.dockerignore
new file mode 100644
index 0000000..6769f3a
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/.dockerignore
@@ -0,0 +1,5 @@
+node_modules/
+dist/
+.nanoforge/
+.git/
+.env
diff --git a/libs/schematics/src/libs/project/files/ts/Dockerfile b/libs/schematics/src/libs/project/files/ts/Dockerfile
new file mode 100644
index 0000000..70fed63
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/Dockerfile
@@ -0,0 +1,39 @@
+<% if (packageManager === "bun") { %>
+FROM oven/bun:1.3 AS base
+<% } else if (packageManager === "pnpm") { %>
+FROM ghcr.io/pnpm/pnpm:12 AS base
+RUN pnpm runtime set node 26 -g
+<% } else { %>
+FROM node:26-slim AS base
+<% } %><% if (packageManager === "yarn") { %>
+RUN npm install -g yarn
+<% } %>
+WORKDIR /app
+
+FROM base AS deps
+
+<% if (packageManager === "npm") { %>COPY package.json package-lock.json ./
+RUN npm ci
+<% } else if (packageManager === "yarn") { %>COPY package.json yarn.lock ./
+RUN yarn install --frozen-lockfile
+<% } else if (packageManager === "pnpm") { %>COPY package.json pnpm-lock.yaml ./
+RUN pnpm install --frozen-lockfile
+<% } else { %>COPY package.json bun.lock ./
+RUN bun install --frozen-lockfile
+<% } %>
+FROM deps AS build
+COPY . .
+RUN <%= packageManager %> run build
+
+<% if (packageManager === "bun") { %>FROM oven/bun:1.3-slim<% } else { %>FROM base<% } %>
+WORKDIR /app
+
+COPY --from=build /app/node_modules ./node_modules
+COPY --from=build /app/dist ./dist
+COPY --from=build /app/package.json ./
+COPY --from=build /app/nanoforge.config.ts ./
+<% if (part === "server") { %>
+<% } %><% if (packageManager !== "pnpm") { %>
+USER <% if (packageManager === "bun") { %>bun<% } else { %>node<% } %>
+<% } %><% if (part === "client") { %>EXPOSE 3000
+<% } %>CMD ["<%= packageManager %>", "run", "start"]
diff --git a/libs/schematics/src/libs/project/files/ts/README.md b/libs/schematics/src/libs/project/files/ts/README.md
new file mode 100644
index 0000000..b068141
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/README.md
@@ -0,0 +1,53 @@
+# <%= packageName %>
+
+A [NanoForge](https://nanoforge.eu) <%= part %> app.
+<% if (!workspace) { %>
+## Prerequisites
+
+- Node.js and <%= packageManager %>
+- The [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `npm install -g @nanoforge-dev/cli`
+
+## Install
+
+```bash
+<%= packageManager %> install
+```
+<% } %>
+## Development
+
+```bash
+nf dev
+```
+
+`nf dev` serves the game and watches your files, so changes are picked up as you work.
+
+## Build & start
+
+```bash
+nf build
+nf start
+```
+
+`nf build` compiles this <%= part %> for production, and `nf start` serves the build.
+<% if (editor) { %>
+This app was generated with editor support. Launch it with:
+
+```bash
+nf editor .
+```
+<% } %><% if (docker) { %>
+A `Dockerfile` is also included to containerize the built app for deployment.
+<% } %>
+## CLI reference
+
+| Command | Description |
+| ------------- | ---------------------------------- |
+| `nf dev` | Develop with hot reload |
+| `nf build` | Build for production |
+| `nf start` | Run a built app |<% if (editor) { %>
+| `nf editor` | Launch the visual editor |<% } %>
+
+## More info
+
+- [Documentation](https://docs.nanoforge.eu) - CLI commands, configuration, and guides
+- [nanoforge.eu](https://nanoforge.eu) - learn more about NanoForge
diff --git a/libs/schematics/src/libs/application/files/js/__dot__env b/libs/schematics/src/libs/project/files/ts/__dot__env
similarity index 100%
rename from libs/schematics/src/libs/application/files/js/__dot__env
rename to libs/schematics/src/libs/project/files/ts/__dot__env
diff --git a/libs/schematics/src/libs/application/files/js/__dot__gitignore b/libs/schematics/src/libs/project/files/ts/__dot__gitignore
similarity index 100%
rename from libs/schematics/src/libs/application/files/js/__dot__gitignore
rename to libs/schematics/src/libs/project/files/ts/__dot__gitignore
diff --git a/libs/schematics/src/libs/project/files/ts/assets/__dot__gitkeep b/libs/schematics/src/libs/project/files/ts/assets/__dot__gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/libs/schematics/src/libs/project/files/ts/nanoforge.config.ts b/libs/schematics/src/libs/project/files/ts/nanoforge.config.ts
new file mode 100644
index 0000000..b05cbcb
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/nanoforge.config.ts
@@ -0,0 +1,5 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "<%= part %>",
+});
diff --git a/libs/schematics/src/libs/project/files/ts/package.json.template b/libs/schematics/src/libs/project/files/ts/package.json.template
new file mode 100644
index 0000000..64f7c67
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/package.json.template
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "<%= packageName %>",
+ "scripts": {
+ "dev": "nf dev",
+ "build": "nf build",
+ "start": "nf start"
+ },
+ "devDependencies": {
+ "@nanoforge-dev/ecs": "<%= ecsVersion %>",<% if (part === "client") { %>
+ "@nanoforge-dev/graphics-2d": "<%= graphics2dVersion %>",
+ "@nanoforge-dev/input": "<%= inputVersion %>",<% } %>
+ "@nanoforge-dev/network": "<%= networkVersion %>",
+ "nanoforge": "<%= nanoforgeVersion %>"
+ },<% if (packageManager === "npm" && allowBuilds.length > 0) { %>
+ "allowScripts": {<% allowBuilds.forEach(function (pkg, i) { %>
+ "<%= pkg %>": true<% if (i < allowBuilds.length - 1) { %>,<% } %><% }); %>
+ },<% } %><% if (packageManager === "bun" && allowBuilds.length > 0) { %>
+ "trustedDependencies": [<% allowBuilds.forEach(function (pkg, i) { %>"<%= pkg %>"<% if (i < allowBuilds.length - 1) { %>, <% } %><% }); %>],<% } %>
+ "private": true
+}
diff --git a/libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template b/libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template
new file mode 100644
index 0000000..2312ae9
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template
@@ -0,0 +1,3 @@
+<% if (allowBuilds.length > 0) { %>allowBuilds:<% allowBuilds.forEach(function (pkg) { %>
+ <%= pkg %>: true<% }); %>
+<% } %>
diff --git a/libs/schematics/src/libs/project/files/ts/src/components/drawable-circle-2d.component.ts b/libs/schematics/src/libs/project/files/ts/src/components/drawable-circle-2d.component.ts
new file mode 100644
index 0000000..30c4454
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/src/components/drawable-circle-2d.component.ts
@@ -0,0 +1,367 @@
+import type { EditorComponentManifest } from "@nanoforge-dev/ecs";
+import { Circle, type Layer } from "@nanoforge-dev/graphics-2d";
+import type { NfFile } from "nanoforge";
+
+export class DrawableCircle2D {
+ public readonly name: string = this.constructor.name;
+ shape: Circle;
+
+ constructor(
+ radius?: number,
+ visible?: boolean,
+ id?: string,
+ opacity?: number,
+ scaleX?: number,
+ scaleY?: number,
+ skewX?: number,
+ skewY?: number,
+ rotation?: number,
+ offsetX?: number,
+ offsetY?: number,
+ fillColor?: string,
+ private _fillPatternImage?: NfFile,
+ fillPatternX?: number,
+ fillPatternY?: number,
+ fillPatternOffsetX?: number,
+ fillPatternOffsetY?: number,
+ fillPatternScaleX?: number,
+ fillPatternScaleY?: number,
+ fillPatternRotation?: number,
+ fillPatternRepeat?: string,
+ ) {
+ this.shape = new Circle({
+ radius,
+ visible,
+ id,
+ opacity,
+ scaleX,
+ scaleY,
+ skewX,
+ skewY,
+ rotation,
+ offsetX,
+ offsetY,
+ fill: fillColor,
+ fillPatternImage: _fillPatternImage
+ ? Object.assign(new Image(), { src: _fillPatternImage.path })
+ : undefined,
+ fillPatternX,
+ fillPatternY,
+ fillPatternOffsetX,
+ fillPatternOffsetY,
+ fillPatternScaleX,
+ fillPatternScaleY,
+ fillPatternRotation,
+ fillPatternRepeat,
+ });
+ }
+
+ get radius() {
+ return this.shape.radius();
+ }
+ set radius(v: number) {
+ this.shape.radius(v);
+ this.redraw();
+ }
+ get visible() {
+ return this.shape.visible();
+ }
+ set visible(v: boolean) {
+ this.shape.visible(v);
+ this.redraw();
+ }
+ get id() {
+ return this.shape.id();
+ }
+ set id(v: string) {
+ this.shape.id(v);
+ this.redraw();
+ }
+ get opacity() {
+ return this.shape.opacity();
+ }
+ set opacity(v: number) {
+ this.shape.opacity(v);
+ this.redraw();
+ }
+ get scaleX() {
+ return this.shape.scaleX();
+ }
+ set scaleX(v: number) {
+ this.shape.scaleX(v);
+ this.redraw();
+ }
+ get scaleY() {
+ return this.shape.scaleY();
+ }
+ set scaleY(v: number) {
+ this.shape.scaleY(v);
+ this.redraw();
+ }
+ get skewX() {
+ return this.shape.skewX();
+ }
+ set skewX(v: number) {
+ this.shape.skewX(v);
+ this.redraw();
+ }
+ get skewY() {
+ return this.shape.skewY();
+ }
+ set skewY(v: number) {
+ this.shape.skewY(v);
+ this.redraw();
+ }
+ get rotation() {
+ return this.shape.rotation();
+ }
+ set rotation(v: number) {
+ this.shape.rotation(v);
+ this.redraw();
+ }
+ get offsetX() {
+ return this.shape.offsetX();
+ }
+ set offsetX(v: number) {
+ this.shape.offsetX(v);
+ this.redraw();
+ }
+ get offsetY() {
+ return this.shape.offsetY();
+ }
+ set offsetY(v: number) {
+ this.shape.offsetY(v);
+ this.redraw();
+ }
+ get fillColor() {
+ return this.shape.fill().toString();
+ }
+ set fillColor(v: string) {
+ this.shape.fill(v);
+ this.redraw();
+ }
+ get fillPatternImage(): NfFile | undefined {
+ return this._fillPatternImage;
+ }
+ set fillPatternImage(v: NfFile) {
+ this.shape.fillPatternImage(v ? Object.assign(new Image(), { src: v.path }) : undefined);
+ this.redraw();
+ }
+ get fillPatternX() {
+ return this.shape.fillPatternX();
+ }
+ set fillPatternX(v: number) {
+ this.shape.fillPatternX(v);
+ this.redraw();
+ }
+ get fillPatternY() {
+ return this.shape.fillPatternY();
+ }
+ set fillPatternY(v: number) {
+ this.shape.fillPatternY(v);
+ this.redraw();
+ }
+ get fillPatternOffsetX() {
+ return this.shape.fillPatternOffsetX();
+ }
+ set fillPatternOffsetX(v: number) {
+ this.shape.fillPatternOffsetX(v);
+ this.redraw();
+ }
+ get fillPatternOffsetY() {
+ return this.shape.fillPatternOffsetY();
+ }
+ set fillPatternOffsetY(v: number) {
+ this.shape.fillPatternOffsetY(v);
+ this.redraw();
+ }
+ get fillPatternScaleX() {
+ return this.shape.fillPatternScaleX();
+ }
+ set fillPatternScaleX(v: number) {
+ this.shape.fillPatternScaleX(v);
+ this.redraw();
+ }
+ get fillPatternScaleY() {
+ return this.shape.fillPatternScaleY();
+ }
+ set fillPatternScaleY(v: number) {
+ this.shape.fillPatternScaleY(v);
+ this.redraw();
+ }
+ get fillPatternRotation() {
+ return this.shape.fillPatternRotation();
+ }
+ set fillPatternRotation(v: number) {
+ this.shape.fillPatternRotation(v);
+ this.redraw();
+ }
+ get fillPatternRepeat() {
+ return this.shape.fillPatternRepeat();
+ }
+ set fillPatternRepeat(v: string) {
+ this.shape.fillPatternRepeat(v);
+ this.redraw();
+ }
+
+ public addToLayer(layer: Layer): void {
+ if (this.shape.getParent() !== layer) layer.add(this.shape);
+ }
+
+ private redraw() {
+ this.shape.getLayer()?.batchDraw();
+ }
+}
+// * Required to generate code
+export default DrawableCircle2D.name;
+
+// * Required for the editor to display the component and generate code
+export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = {
+ name: "DrawableCircle2D",
+ description: "Graphically drawable circle 2D",
+ params: [
+ {
+ name: "radius",
+ type: "number",
+ description: "Radius of the circle in pixels",
+ default: 10,
+ optional: false,
+ },
+ {
+ name: "visible",
+ type: "boolean",
+ description: "Controls whether the node is rendered",
+ default: true,
+ optional: false,
+ },
+ {
+ name: "id",
+ type: "string",
+ description: "Unique identifier for the node, used with stage.findOne('#id')",
+ optional: true,
+ },
+ {
+ name: "opacity",
+ type: "number",
+ description: "Opacity from 0 (transparent) to 1 (fully opaque)",
+ default: 1,
+ optional: true,
+ },
+ {
+ name: "scaleX",
+ type: "number",
+ description: "Horizontal scale factor",
+ default: 1,
+ optional: true,
+ },
+ {
+ name: "scaleY",
+ type: "number",
+ description: "Vertical scale factor",
+ default: 1,
+ optional: true,
+ },
+ {
+ name: "skewX",
+ type: "number",
+ default: 0,
+ description: "Horizontal skew in radians, shears along the X axis",
+ optional: true,
+ },
+ {
+ name: "skewY",
+ type: "number",
+ default: 0,
+ description: "Vertical skew in radians, shears along the Y axis",
+ optional: true,
+ },
+ {
+ name: "rotation",
+ type: "number",
+ description:
+ "Rotation angle in degrees applied around the offset point (positive values rotate clockwise)",
+ default: 0,
+ optional: true,
+ },
+ {
+ name: "offsetX",
+ type: "number",
+ description: "Horizontal offset for the transform origin",
+ default: 0,
+ optional: true,
+ },
+ {
+ name: "offsetY",
+ type: "number",
+ description: "Vertical offset for the transform origin",
+ default: 0,
+ optional: true,
+ },
+ {
+ name: "fillColor",
+ type: "string",
+ description:
+ 'Solid fill color as a CSS color string (e.g. "red", "#ff0000", "rgba(255,0,0,0.5)"), or a pre-built CanvasGradient object',
+ default: "red",
+ example: "rgb(255,255,255)",
+ optional: true,
+ },
+ {
+ name: "fillPatternImage",
+ type: "asset",
+ description: "Image element to use as a tiling pattern fill",
+ optional: true,
+ },
+ {
+ name: "fillPatternX",
+ type: "number",
+ description:
+ "Horizontal offset of the pattern origin relative to the shape's top-left corner, in pixels",
+ optional: true,
+ },
+ {
+ name: "fillPatternY",
+ type: "number",
+ description:
+ "Vertical offset of the pattern origin relative to the shape's top-left corner, in pixels",
+ optional: true,
+ },
+ {
+ name: "fillPatternOffsetX",
+ type: "number",
+ description: "Horizontally shifts the pattern image within the tile before repeating",
+ optional: true,
+ },
+ {
+ name: "fillPatternOffsetY",
+ type: "number",
+ description: "Vertically shifts the pattern image within the tile before repeating",
+ optional: true,
+ },
+ {
+ name: "fillPatternScaleX",
+ type: "number",
+ description: "Horizontal scale factor for the pattern tile",
+ optional: true,
+ },
+ {
+ name: "fillPatternScaleY",
+ type: "number",
+ description: "Vertical scale factor for the pattern tile",
+ optional: true,
+ },
+ {
+ name: "fillPatternRotation",
+ type: "number",
+ description: "Rotation of the pattern tile in degrees",
+ optional: true,
+ },
+ {
+ name: "fillPatternRepeat",
+ type: "string",
+ description:
+ 'Repeat mode for the pattern. One of "repeat" (default), "repeat-x", "repeat-y", or "no-repeat"',
+ optional: true,
+ },
+ ],
+};
\ No newline at end of file
diff --git a/libs/schematics/src/libs/project/files/ts/src/components/position-2d.component.ts b/libs/schematics/src/libs/project/files/ts/src/components/position-2d.component.ts
new file mode 100644
index 0000000..149cec5
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/src/components/position-2d.component.ts
@@ -0,0 +1,35 @@
+import type { EditorComponentManifest } from "@nanoforge-dev/ecs";
+
+export class Position2D {
+ name = this.constructor.name;
+
+ constructor(
+ public x: number,
+ public y: number,
+ ) {}
+}
+
+// * Required to generate code
+export default Position2D.name;
+
+// * Required for the editor to display the component and generate code
+export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = {
+ name: "Position2D",
+ description: "Position of an entity in a 2 dimensional space in pixel",
+ params: [
+ {
+ type: "number",
+ name: "x",
+ description: "Horizontal position in pixel",
+ example: 4.2,
+ default: 0,
+ },
+ {
+ type: "number",
+ name: "y",
+ description: "Vertical position in pixel",
+ example: 67,
+ default: 0,
+ },
+ ],
+};
diff --git a/libs/schematics/src/libs/project/files/ts/src/main.ts b/libs/schematics/src/libs/project/files/ts/src/main.ts
new file mode 100644
index 0000000..2402c78
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/src/main.ts
@@ -0,0 +1,39 @@
+import { NanoforgeFactory, type <%= part === "client" ? "ClientRunOptions" : "ServerRunOptions" %> } from "nanoforge";
+import { EcsLibrary } from "@nanoforge-dev/ecs/<%= part %>";<% if (part === "client") { %>
+import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d";
+import { InputLibrary } from "@nanoforge-dev/input";<% } %>
+import { <%= part === "client" ? "NetworkClientLibrary" : "NetworkServerLibrary" %> } from "@nanoforge-dev/network/<%= part %>";
+<% if (part === "client") { %>
+import { DrawableCircle2D } from "./components/drawable-circle-2d.component";<% } %>
+import { Position2D } from "./components/position-2d.component";<% if (part === "client") { %>
+import { draw2D } from "./systems/draw-2d.system";<% if (hasServer) { %>
+import { positionSync } from "./systems/position-sync.system";<% } %><% } else { %>
+import { move2D } from "./systems/move-2d.system";<% } %>
+
+export const main = async (options: <%= part === "client" ? "ClientRunOptions" : "ServerRunOptions" %>): Promise => {
+ const app = NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>({ tickRate: 60 });
+
+ const ecs = new EcsLibrary();
+ app.use(ecs);<% if (part === "client") { %>
+ app.use(new Graphics2DLibrary());
+ app.use(new InputLibrary());<% } %>
+ app.use(new <%= part === "client" ? "NetworkClientLibrary" : "NetworkServerLibrary" %>());
+
+ await app.init(options);
+
+ const registry = ecs.registry;
+<% if (part === "client") { %>
+ const circle = registry.spawnEntity();
+ registry.addComponent(circle, new Position2D(500, 500));
+ registry.addComponent(circle, new DrawableCircle2D(75, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, "red"));
+
+ registry.addSystem(draw2D);<% if (hasServer) { %>
+ registry.addSystem(positionSync);<% } %>
+<% } else { %>
+ const entity = registry.spawnEntity();
+ registry.addComponent(entity, new Position2D(500, 500));
+
+ registry.addSystem(move2D);
+<% } %>
+ await app.run();
+};
diff --git a/libs/schematics/src/libs/project/files/ts/src/systems/draw-2d.system.ts b/libs/schematics/src/libs/project/files/ts/src/systems/draw-2d.system.ts
new file mode 100644
index 0000000..eb075b6
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/src/systems/draw-2d.system.ts
@@ -0,0 +1,25 @@
+import type { EditorSystemManifest, Registry } from "@nanoforge-dev/ecs";
+import { type Context } from "nanoforge";
+
+import { DrawableCircle2D } from "../components/drawable-circle-2d.component";
+import { Position2D } from "../components/position-2d.component";
+
+export function draw2D(registry: Registry, ctx: Context) {
+ const graphic2d = ctx.graphics;
+
+ const entities = registry.getZipper([Position2D, DrawableCircle2D]);
+
+ entities.forEach(({ Position2D, DrawableCircle2D }) => {
+ DrawableCircle2D.addToLayer(graphic2d.baseLayer);
+ DrawableCircle2D.shape.setPosition(Position2D);
+ });
+}
+// * Required to generate code
+export default draw2D.name;
+
+// * Required for the editor to display the system and generate code
+export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = {
+ name: "draw2D",
+ description: "Draw every drawable component",
+ dependencies: ["Position2D", "DrawableCircle2D"],
+};
diff --git a/libs/schematics/src/libs/project/files/ts/src/systems/move-2d.system.ts b/libs/schematics/src/libs/project/files/ts/src/systems/move-2d.system.ts
new file mode 100644
index 0000000..a980282
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/src/systems/move-2d.system.ts
@@ -0,0 +1,26 @@
+import type { EditorSystemManifest, Registry } from "@nanoforge-dev/ecs";
+import { type Context } from "nanoforge";
+
+import { Position2D } from "../components/position-2d.component";
+
+export function move2D(registry: Registry, ctx: Context) {
+ const network = ctx.network;
+ const entities = registry.getZipper([Position2D]);
+
+ entities.forEach(({ Position2D }) => {
+ Position2D.x += 1;
+
+ network.tcp.sendToEverybody(
+ new TextEncoder().encode(JSON.stringify({ x: Position2D.x, y: Position2D.y })),
+ );
+ });
+}
+// * Required to generate code
+export default move2D.name;
+
+// * Required for the editor to display the system and generate code
+export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = {
+ name: "move2D",
+ description: "Moves the entity and broadcasts its position to every connected client",
+ dependencies: ["Position2D"],
+};
diff --git a/libs/schematics/src/libs/project/files/ts/src/systems/position-sync.system.ts b/libs/schematics/src/libs/project/files/ts/src/systems/position-sync.system.ts
new file mode 100644
index 0000000..09fc85f
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/ts/src/systems/position-sync.system.ts
@@ -0,0 +1,27 @@
+import type { EditorSystemManifest, Registry } from "@nanoforge-dev/ecs";
+import { type Context } from "nanoforge";
+
+import { Position2D } from "../components/position-2d.component";
+
+export function positionSync(registry: Registry, ctx: Context) {
+ const network = ctx.network;
+ const packets = network.tcp
+ .getReceivedPackets()
+ .map((packet) => JSON.parse(new TextDecoder().decode(packet)) as { x: number; y: number });
+
+ const [entity] = registry.getZipper([Position2D]);
+
+ packets.forEach((packet) => {
+ entity.Position2D.x = packet.x;
+ entity.Position2D.y = packet.y;
+ });
+}
+// * Required to generate code
+export default positionSync.name;
+
+// * Required for the editor to display the system and generate code
+export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = {
+ name: "positionSync",
+ description: "Overwrites the local entity's position with the latest value received from the server",
+ dependencies: ["Position2D"],
+};
diff --git a/libs/schematics/src/libs/application/files/ts/tsconfig.json b/libs/schematics/src/libs/project/files/ts/tsconfig.json
similarity index 96%
rename from libs/schematics/src/libs/application/files/ts/tsconfig.json
rename to libs/schematics/src/libs/project/files/ts/tsconfig.json
index cdbe237..5aef325 100644
--- a/libs/schematics/src/libs/application/files/ts/tsconfig.json
+++ b/libs/schematics/src/libs/project/files/ts/tsconfig.json
@@ -36,6 +36,6 @@
"target": "ESNext",
"useDefineForClassFields": true,
},
- "include": ["client/**/*.ts", "server/**/*.ts"],
+ "include": ["src"],
"exclude": ["node_modules", ".nanoforge"]
}
diff --git a/libs/schematics/src/libs/project/project.factory.ts b/libs/schematics/src/libs/project/project.factory.ts
new file mode 100644
index 0000000..36df6ba
--- /dev/null
+++ b/libs/schematics/src/libs/project/project.factory.ts
@@ -0,0 +1,135 @@
+import { type Path, join, normalize, strings } from "@angular-devkit/core";
+import {
+ type Rule,
+ type Source,
+ apply,
+ filter,
+ mergeWith,
+ move,
+ renameTemplateFiles,
+ template,
+ url,
+} from "@angular-devkit/schematics";
+
+import { toKebabCase } from "@utils/formatting";
+import { fetchTrustedVersion } from "@utils/registry";
+
+import {
+ DEFAULT_APP_NAME,
+ DEFAULT_LANGUAGE,
+ DEFAULT_NANOFORGE_DEPENDENCY_VERSION,
+ DEFAULT_PACKAGE_MANAGER,
+} from "~/defaults";
+
+import { type ProjectOptions } from "./project.options";
+import { type ProjectSchema } from "./project.schema";
+
+interface DepVersions {
+ ecs: string;
+ graphics2d: string;
+ input: string;
+ network: string;
+ nanoforge: string;
+}
+
+const resolveDepVersions = async (part: ProjectSchema["part"]): Promise => {
+ const isClient = part === "client";
+
+ const [nanoforge, ecs, network, graphics2d, input] = await Promise.all([
+ fetchTrustedVersion("nanoforge", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),
+ fetchTrustedVersion("@nanoforge-dev/ecs", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),
+ fetchTrustedVersion("@nanoforge-dev/network", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),
+ isClient
+ ? fetchTrustedVersion("@nanoforge-dev/graphics-2d", DEFAULT_NANOFORGE_DEPENDENCY_VERSION)
+ : DEFAULT_NANOFORGE_DEPENDENCY_VERSION,
+ isClient
+ ? fetchTrustedVersion("@nanoforge-dev/input", DEFAULT_NANOFORGE_DEPENDENCY_VERSION)
+ : DEFAULT_NANOFORGE_DEPENDENCY_VERSION,
+ ]);
+
+ return {
+ ecs,
+ graphics2d,
+ input,
+ network,
+ nanoforge,
+ };
+};
+
+const transform = async (schema: ProjectSchema): Promise => {
+ const packageManager = schema.packageManager ?? DEFAULT_PACKAGE_MANAGER;
+ const workspace = schema.workspace ?? true;
+ const depVersions = await resolveDepVersions(schema.part);
+
+ const name = schema.name ?? schema.part;
+ const packageName = schema.workspaceName
+ ? `${toKebabCase(schema.workspaceName)}-${toKebabCase(name)}`
+ : toKebabCase(schema.name ?? DEFAULT_APP_NAME);
+
+ return {
+ part: schema.part,
+ appClass: schema.part === "client" ? "NanoforgeClient" : "NanoforgeServer",
+ packageName,
+ language: schema.language ?? DEFAULT_LANGUAGE,
+ strict: schema.strict ?? true,
+ packageManager,
+ workspace,
+ ecsVersion: depVersions.ecs,
+ graphics2dVersion: depVersions.graphics2d,
+ inputVersion: depVersions.input,
+ networkVersion: depVersions.network,
+ nanoforgeVersion: depVersions.nanoforge,
+ initFunctions: schema.initFunctions ?? false,
+ hasServer: schema.hasServer ?? false,
+ docker: schema.docker ?? false,
+ editor: schema.editor ?? false,
+ libs: schema.libs ?? [],
+ allowBuilds: workspace ? [] : (schema.allowBuilds ?? []),
+ };
+};
+
+const generate = (options: ProjectOptions, path: string): Source => {
+ return apply(url(join("./files" as Path, options.language)), [
+ template({
+ dot: ".",
+ ...strings,
+ ...options,
+ }),
+ filter(
+ (path) =>
+ (options.docker && !options.workspace) ||
+ !["Dockerfile", ".dockerignore"].includes(path.split("/").at(-1) ?? ""),
+ ),
+ filter(
+ (path) =>
+ (options.packageManager === "pnpm" && options.allowBuilds.length > 0) ||
+ !(path.split("/").at(-1) ?? "").startsWith("pnpm-workspace.yaml"),
+ ),
+ filter((path) => {
+ const filename = path.split("/").at(-1) ?? "";
+ if (
+ filename.startsWith("drawable-circle-2d.component") ||
+ filename.startsWith("draw-2d.system")
+ ) {
+ return options.part === "client";
+ }
+ if (filename.startsWith("move-2d.system")) {
+ return options.part === "server";
+ }
+ if (filename.startsWith("position-sync.system")) {
+ return options.part === "client" && options.hasServer;
+ }
+ return true;
+ }),
+ renameTemplateFiles(),
+ move(normalize(path)),
+ ]);
+};
+
+export const main = (schema: ProjectSchema): Rule => {
+ return async () => {
+ const options = await transform(schema);
+
+ return mergeWith(generate(options, schema.directory));
+ };
+};
diff --git a/libs/schematics/src/libs/project/project.options.d.ts b/libs/schematics/src/libs/project/project.options.d.ts
new file mode 100644
index 0000000..9722fa2
--- /dev/null
+++ b/libs/schematics/src/libs/project/project.options.d.ts
@@ -0,0 +1,92 @@
+export interface ProjectOptions {
+ /**
+ * The type of project to generate
+ */
+ part: "client" | "server";
+
+ /**
+ * Fully-qualified Nanoforge app class name for the generated part,
+ * used in JSDoc type annotations for JavaScript projects
+ */
+ appClass: string;
+
+ /**
+ * The derived package.json name (from workspaceName and part)
+ */
+ packageName: string;
+
+ /**
+ * NanoForge project language
+ */
+ language: "js" | "ts";
+
+ /**
+ * With strict mode (TypeScript or JavaScript but only on compatible editor)
+ */
+ strict: boolean;
+
+ /**
+ * The used package manager
+ */
+ packageManager: "npm" | "yarn" | "pnpm" | "bun";
+
+ /**
+ * Whether this project is generated inside an existing NanoForge workspace
+ */
+ workspace: boolean;
+
+ /**
+ * Resolved version/protocol for the @nanoforge-dev/ecs dependency (aliased
+ * to @nanoforge-dev/ecs-lib for registry lookups; see project.factory.ts)
+ */
+ ecsVersion: string;
+
+ /** Resolved version/protocol for @nanoforge-dev/graphics-2d (client only) */
+ graphics2dVersion: string;
+
+ /** Resolved version/protocol for @nanoforge-dev/input (client only) */
+ inputVersion: string;
+
+ /**
+ * Resolved version/protocol for the @nanoforge-dev/network dependency
+ * (aliased to @nanoforge-dev/network-client or -server for registry
+ * lookups depending on `part`; see project.factory.ts)
+ */
+ networkVersion: string;
+
+ /** Resolved version/protocol for the nanoforge engine package */
+ nanoforgeVersion: string;
+
+ /**
+ * Add init functions to the project
+ */
+ initFunctions: boolean;
+
+ /**
+ * Only meaningful when `part` is "client": whether the client syncs its
+ * entity position from a companion server instead of owning it locally.
+ */
+ hasServer: boolean;
+
+ /**
+ * Generate a Dockerfile for the project. Ignored when `workspace` is true - a project
+ * inside a workspace is built and run by the workspace's own Dockerfile instead.
+ */
+ docker: boolean;
+
+ /**
+ * Add editor dependencies
+ */
+ editor: boolean;
+
+ /**
+ * Relative paths to shared libs this project depends on
+ */
+ libs: string[];
+
+ /**
+ * Package names allowed to run install/build scripts. Only rendered when
+ * `workspace` is false — see {@link ProjectSchema.allowBuilds}.
+ */
+ allowBuilds: string[];
+}
diff --git a/libs/schematics/src/libs/project/project.schema.d.ts b/libs/schematics/src/libs/project/project.schema.d.ts
new file mode 100644
index 0000000..29e10f4
--- /dev/null
+++ b/libs/schematics/src/libs/project/project.schema.d.ts
@@ -0,0 +1,87 @@
+export interface ProjectSchema {
+ /**
+ * The type of project to generate
+ */
+ part: "client" | "server";
+
+ /**
+ * Name of the parent workspace, used to derive the project package name.
+ * Optional - when omitted, `name` is used as the whole package name
+ * instead of being appended to the workspace name.
+ */
+ workspaceName?: string | null;
+
+ /**
+ * Name used to derive the project package name. When `workspaceName` is
+ * set, the package name is `${workspaceName}-${name}` (replacing the
+ * previous `${workspaceName}-${part}` suffix). When `workspaceName` is
+ * absent, `name` becomes the whole package name. Defaults to `part`.
+ */
+ name?: string | null;
+
+ /**
+ * Full destination path for this project (e.g. "apps/client",
+ * "packages/player-client"). The caller is responsible for including any
+ * part-specific segment - it is not appended automatically.
+ */
+ directory: string;
+
+ /**
+ * NanoForge project language
+ */
+ language: "js" | "ts";
+
+ /**
+ * With strict mode
+ */
+ strict: boolean;
+
+ /**
+ * The used package manager
+ */
+ packageManager: "npm" | "yarn" | "pnpm" | "bun";
+
+ /**
+ * Whether this project is generated inside an existing NanoForge workspace
+ */
+ workspace: boolean;
+
+ /**
+ * Generate a Dockerfile for the project. Ignored when `workspace` is true - a project
+ * inside a workspace is built and run by the workspace's own Dockerfile instead.
+ */
+ docker: boolean;
+
+ /**
+ * Add init functions to the project
+ */
+ initFunctions: boolean;
+
+ /**
+ * Only meaningful when `part` is "client": whether the client syncs its
+ * entity position from a companion server instead of owning it locally.
+ * A server project always simulates and broadcasts its own position,
+ * regardless of this flag.
+ */
+ hasServer: boolean;
+
+ /**
+ * Add editor dependencies
+ */
+ editor: boolean;
+
+ /**
+ * Relative paths to shared libs this project depends on
+ */
+ libs: string[];
+
+ /**
+ * Package names allowed to run install/build scripts. Only applies to a
+ * standalone project (workspace: false) — a project generated inside a
+ * workspace relies on the workspace root's own `allowBuilds` instead.
+ * Written to pnpm-workspace.yaml's `allowBuilds` map (pnpm), package.json's
+ * `allowScripts` map (npm) or `trustedDependencies` array (bun). Ignored
+ * for yarn.
+ */
+ allowBuilds: string[];
+}
diff --git a/libs/schematics/src/libs/project/schema.json b/libs/schematics/src/libs/project/schema.json
new file mode 100644
index 0000000..cc933df
--- /dev/null
+++ b/libs/schematics/src/libs/project/schema.json
@@ -0,0 +1,83 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "SchematicsNanoForgeProject",
+ "title": "NanoForge Project Options Schema",
+ "type": "object",
+ "properties": {
+ "part": {
+ "type": "string",
+ "enum": ["client", "server"],
+ "description": "The type of project to generate"
+ },
+ "workspaceName": {
+ "type": ["string", "null"],
+ "description": "Name of the parent workspace, used to derive the project package name. Optional - when omitted, `name` is used as the whole package name instead of being appended to the workspace name.",
+ "default": null
+ },
+ "name": {
+ "type": ["string", "null"],
+ "description": "Name used to derive the project package name. When `workspaceName` is set, the package name is `${workspaceName}-${name}` (replacing the previous `${workspaceName}-${part}` suffix). When `workspaceName` is absent, `name` becomes the whole package name. Defaults to `part`.",
+ "default": null
+ },
+ "directory": {
+ "type": "string",
+ "description": "Full destination path for this project (e.g. \"apps/client\", \"packages/player-client\"). The caller is responsible for including any part-specific segment - it is not appended automatically.",
+ "default": "apps"
+ },
+ "language": {
+ "type": "string",
+ "enum": ["ts", "js"],
+ "description": "NanoForge project language",
+ "default": "ts"
+ },
+ "strict": {
+ "type": "boolean",
+ "description": "With strict mode",
+ "default": true
+ },
+ "packageManager": {
+ "type": "string",
+ "enum": ["npm", "yarn", "pnpm", "bun"],
+ "description": "The used package manager",
+ "default": "npm"
+ },
+ "workspace": {
+ "type": "boolean",
+ "description": "Whether this project is generated inside an existing NanoForge workspace",
+ "default": true
+ },
+ "docker": {
+ "type": "boolean",
+ "description": "Generate a Dockerfile for the project. Ignored when `workspace` is true - a project inside a workspace is built and run by the workspace's own Dockerfile instead.",
+ "default": false
+ },
+ "initFunctions": {
+ "type": "boolean",
+ "description": "Add init functions to the project",
+ "default": false
+ },
+ "hasServer": {
+ "type": "boolean",
+ "description": "Only meaningful when part is \"client\": whether the client syncs its entity position from a companion server instead of owning it locally. A server project always simulates and broadcasts its own position, regardless of this flag.",
+ "default": false
+ },
+ "editor": {
+ "type": "boolean",
+ "description": "Add editor dependencies",
+ "default": false
+ },
+ "libs": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Relative paths to shared libs this project depends on",
+ "default": []
+ },
+ "allowBuilds": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Package names allowed to run install/build scripts. Only applies to a standalone project (workspace: false); ignored otherwise since the workspace root already handles it.",
+ "default": []
+ }
+ },
+ "required": ["part"]
+}
diff --git a/libs/schematics/src/libs/workspace/files/js/.dockerignore b/libs/schematics/src/libs/workspace/files/js/.dockerignore
new file mode 100644
index 0000000..1c13727
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/js/.dockerignore
@@ -0,0 +1,5 @@
+**/node_modules/
+**/dist/
+**/.nanoforge/
+**/.env
+.git/
diff --git a/libs/schematics/src/libs/workspace/files/js/Dockerfile b/libs/schematics/src/libs/workspace/files/js/Dockerfile
new file mode 100644
index 0000000..df78c19
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/js/Dockerfile
@@ -0,0 +1,39 @@
+<% if (packageManager === "bun") { %>
+FROM oven/bun:1.3 AS base
+<% } else if (packageManager === "pnpm") { %>
+FROM ghcr.io/pnpm/pnpm:12 AS base
+RUN pnpm runtime set node 26 -g
+<% } else { %>
+FROM node:26-slim AS base
+<% } %><% if (packageManager === "yarn") { %>
+RUN npm install -g yarn
+<% } %>
+WORKDIR /app
+
+FROM base AS deps
+<% if (packageManager === "pnpm") { %>
+COPY pnpm-lock.yaml ./
+RUN pnpm fetch
+
+FROM deps AS build
+COPY . .
+RUN pnpm install --frozen-lockfile --offline
+<% } else if (packageManager === "npm") { %>
+FROM deps AS build
+COPY . .
+RUN npm ci
+<% } else { %>
+FROM deps AS build
+COPY . .
+RUN <%= packageManager %> install --frozen-lockfile
+<% } %>
+RUN <%= packageManager %> run build
+
+<% if (packageManager === "bun") { %>FROM oven/bun:1.3-slim<% } else { %>FROM base<% } %>
+WORKDIR /app
+
+COPY --from=build /app .
+<% if (packageManager !== "pnpm") { %>
+USER <% if (packageManager === "bun") { %>bun<% } else { %>node<% } %>
+<% } %>EXPOSE 3000
+CMD ["<%= packageManager %>", "run", "start"]
diff --git a/libs/schematics/src/libs/workspace/files/js/README.md b/libs/schematics/src/libs/workspace/files/js/README.md
new file mode 100644
index 0000000..cf7bc11
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/js/README.md
@@ -0,0 +1,51 @@
+# <%= name %>
+
+A [NanoForge](https://nanoforge.eu) game workspace.
+
+## Prerequisites
+
+- Node.js and <%= packageManager %>
+- The [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `npm install -g @nanoforge-dev/cli`
+
+## Install
+
+```bash
+<%= packageManager %> install
+```
+
+## Structure
+
+This is the root of your NanoForge monorepo. `nanoforge.config.js` lists the app packages, and each
+client or server app lives under `apps/` (generated separately with the `project` schematic).
+
+## Development
+
+```bash
+nf dev -r
+```
+
+`-r` runs the command across every app in the workspace. `nf dev` serves the game and watches your
+files, so changes are picked up as you work. Target a single app instead with `-d apps/`.
+
+## Build & start
+
+```bash
+nf build -r
+nf start -r
+```
+
+`nf build` compiles every app for production, and `nf start` serves the build.
+
+## CLI reference
+
+| Command | Description |
+| ---------- | ------------------------------------- |
+| `nf dev` | Develop with hot reload |
+| `nf build` | Build for production |
+| `nf start` | Run a built app |
+| `-r` | Apply the command to every app |
+
+## More info
+
+- [Documentation](https://docs.nanoforge.eu) - CLI commands, configuration, and guides
+- [nanoforge.eu](https://nanoforge.eu) - learn more about NanoForge
diff --git a/libs/schematics/src/libs/workspace/files/js/__dot__gitignore b/libs/schematics/src/libs/workspace/files/js/__dot__gitignore
new file mode 100644
index 0000000..6857e7d
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/js/__dot__gitignore
@@ -0,0 +1,137 @@
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### JetBrains template
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# File-based project format
+*.iws
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# Turborepo
+.turbo/
+
+### NanoForge template
+# Build/compilation output
+.nanoforge/
+dist/
diff --git a/libs/schematics/src/libs/application/files/js/jsconfig.json b/libs/schematics/src/libs/workspace/files/js/jsconfig.json
similarity index 95%
rename from libs/schematics/src/libs/application/files/js/jsconfig.json
rename to libs/schematics/src/libs/workspace/files/js/jsconfig.json
index 5b67de8..f2e8ecb 100644
--- a/libs/schematics/src/libs/application/files/js/jsconfig.json
+++ b/libs/schematics/src/libs/workspace/files/js/jsconfig.json
@@ -30,6 +30,6 @@
"target": "ESNext",
"useDefineForClassFields": true
},
- "include": ["client/**/*.js", "server/**/*.js"],
+ "include": ["apps/*/src/**/*.js"],
"exclude": ["node_modules", ".nanoforge"]
}
diff --git a/libs/schematics/src/libs/workspace/files/js/nanoforge.config.js b/libs/schematics/src/libs/workspace/files/js/nanoforge.config.js
new file mode 100644
index 0000000..1bbf8d4
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/js/nanoforge.config.js
@@ -0,0 +1,6 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "workspace",
+ packages: ["apps/*"],
+});
diff --git a/libs/schematics/src/libs/workspace/files/js/package.json.template b/libs/schematics/src/libs/workspace/files/js/package.json.template
new file mode 100644
index 0000000..cfd2c9a
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/js/package.json.template
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "<%= name %>",
+ "scripts": {
+ "dev": "nf dev -r",
+ "build": "nf build -r",
+ "start": "nf start -r"
+ },
+ "devDependencies": {
+ "@nanoforge-dev/cli": "<%= cliVersion %>",
+ "nanoforge": "<%= engineVersion %>"
+ },<% if (packageManager !== "pnpm") { %>
+ "workspaces": ["apps/*"],<% } %><% if (packageManager === "npm" && allowBuilds.length > 0) { %>
+ "allowScripts": {<% allowBuilds.forEach(function (pkg, i) { %>
+ "<%= pkg %>": true<% if (i < allowBuilds.length - 1) { %>,<% } %><% }); %>
+ },<% } %><% if (packageManager === "bun" && allowBuilds.length > 0) { %>
+ "trustedDependencies": [<% allowBuilds.forEach(function (pkg, i) { %>"<%= pkg %>"<% if (i < allowBuilds.length - 1) { %>, <% } %><% }); %>],<% } %>
+ "private": true
+}
diff --git a/libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template b/libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template
new file mode 100644
index 0000000..e23904e
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template
@@ -0,0 +1,6 @@
+packages:
+ - "apps/*"
+<% if (allowBuilds.length > 0) { %>
+allowBuilds:<% allowBuilds.forEach(function (pkg) { %>
+ <%= pkg %>: true<% }); %>
+<% } %>
diff --git a/libs/schematics/src/libs/workspace/files/ts/.dockerignore b/libs/schematics/src/libs/workspace/files/ts/.dockerignore
new file mode 100644
index 0000000..1c13727
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/ts/.dockerignore
@@ -0,0 +1,5 @@
+**/node_modules/
+**/dist/
+**/.nanoforge/
+**/.env
+.git/
diff --git a/libs/schematics/src/libs/workspace/files/ts/Dockerfile b/libs/schematics/src/libs/workspace/files/ts/Dockerfile
new file mode 100644
index 0000000..df78c19
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/ts/Dockerfile
@@ -0,0 +1,39 @@
+<% if (packageManager === "bun") { %>
+FROM oven/bun:1.3 AS base
+<% } else if (packageManager === "pnpm") { %>
+FROM ghcr.io/pnpm/pnpm:12 AS base
+RUN pnpm runtime set node 26 -g
+<% } else { %>
+FROM node:26-slim AS base
+<% } %><% if (packageManager === "yarn") { %>
+RUN npm install -g yarn
+<% } %>
+WORKDIR /app
+
+FROM base AS deps
+<% if (packageManager === "pnpm") { %>
+COPY pnpm-lock.yaml ./
+RUN pnpm fetch
+
+FROM deps AS build
+COPY . .
+RUN pnpm install --frozen-lockfile --offline
+<% } else if (packageManager === "npm") { %>
+FROM deps AS build
+COPY . .
+RUN npm ci
+<% } else { %>
+FROM deps AS build
+COPY . .
+RUN <%= packageManager %> install --frozen-lockfile
+<% } %>
+RUN <%= packageManager %> run build
+
+<% if (packageManager === "bun") { %>FROM oven/bun:1.3-slim<% } else { %>FROM base<% } %>
+WORKDIR /app
+
+COPY --from=build /app .
+<% if (packageManager !== "pnpm") { %>
+USER <% if (packageManager === "bun") { %>bun<% } else { %>node<% } %>
+<% } %>EXPOSE 3000
+CMD ["<%= packageManager %>", "run", "start"]
diff --git a/libs/schematics/src/libs/workspace/files/ts/README.md b/libs/schematics/src/libs/workspace/files/ts/README.md
new file mode 100644
index 0000000..7a6c2c6
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/ts/README.md
@@ -0,0 +1,51 @@
+# <%= name %>
+
+A [NanoForge](https://nanoforge.eu) game workspace.
+
+## Prerequisites
+
+- Node.js and <%= packageManager %>
+- The [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `npm install -g @nanoforge-dev/cli`
+
+## Install
+
+```bash
+<%= packageManager %> install
+```
+
+## Structure
+
+This is the root of your NanoForge monorepo. `nanoforge.config.ts` lists the app packages, and each
+client or server app lives under `apps/` (generated separately with the `project` schematic).
+
+## Development
+
+```bash
+nf dev -r
+```
+
+`-r` runs the command across every app in the workspace. `nf dev` serves the game and watches your
+files, so changes are picked up as you work. Target a single app instead with `-d apps/`.
+
+## Build & start
+
+```bash
+nf build -r
+nf start -r
+```
+
+`nf build` compiles every app for production, and `nf start` serves the build.
+
+## CLI reference
+
+| Command | Description |
+| ---------- | ------------------------------------- |
+| `nf dev` | Develop with hot reload |
+| `nf build` | Build for production |
+| `nf start` | Run a built app |
+| `-r` | Apply the command to every app |
+
+## More info
+
+- [Documentation](https://docs.nanoforge.eu) - CLI commands, configuration, and guides
+- [nanoforge.eu](https://nanoforge.eu) - learn more about NanoForge
diff --git a/libs/schematics/src/libs/application/files/ts/__dot__gitignore b/libs/schematics/src/libs/workspace/files/ts/__dot__gitignore
similarity index 97%
rename from libs/schematics/src/libs/application/files/ts/__dot__gitignore
rename to libs/schematics/src/libs/workspace/files/ts/__dot__gitignore
index ba6da87..fc0f862 100644
--- a/libs/schematics/src/libs/application/files/ts/__dot__gitignore
+++ b/libs/schematics/src/libs/workspace/files/ts/__dot__gitignore
@@ -129,6 +129,6 @@ web_modules/
.pnp.*
### NanoForge template
-# Compilation directories
-.nanoforge/client
-.nanoforge/server
+# Build/compilation output
+.nanoforge/
+dist/
diff --git a/libs/schematics/src/libs/workspace/files/ts/nanoforge.config.ts b/libs/schematics/src/libs/workspace/files/ts/nanoforge.config.ts
new file mode 100644
index 0000000..1bbf8d4
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/ts/nanoforge.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "workspace",
+ packages: ["apps/*"],
+});
diff --git a/libs/schematics/src/libs/workspace/files/ts/package.json.template b/libs/schematics/src/libs/workspace/files/ts/package.json.template
new file mode 100644
index 0000000..29b7e4b
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/ts/package.json.template
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "<%= name %>",
+ "scripts": {
+ "dev": "nf dev -r",
+ "build": "nf build -r",
+ "start": "nf start -r"
+ },
+ "devDependencies": {
+ "@nanoforge-dev/cli": "<%= cliVersion %>",
+ "nanoforge": "<%= engineVersion %>",
+ "typescript": "<%= typescriptVersion %>"
+ },<% if (packageManager !== "pnpm") { %>
+ "workspaces": ["apps/*"],<% } %><% if (packageManager === "npm" && allowBuilds.length > 0) { %>
+ "allowScripts": {<% allowBuilds.forEach(function (pkg, i) { %>
+ "<%= pkg %>": true<% if (i < allowBuilds.length - 1) { %>,<% } %><% }); %>
+ },<% } %><% if (packageManager === "bun" && allowBuilds.length > 0) { %>
+ "trustedDependencies": [<% allowBuilds.forEach(function (pkg, i) { %>"<%= pkg %>"<% if (i < allowBuilds.length - 1) { %>, <% } %><% }); %>],<% } %>
+ "private": true
+}
diff --git a/libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template b/libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template
new file mode 100644
index 0000000..e23904e
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template
@@ -0,0 +1,6 @@
+packages:
+ - "apps/*"
+<% if (allowBuilds.length > 0) { %>
+allowBuilds:<% allowBuilds.forEach(function (pkg) { %>
+ <%= pkg %>: true<% }); %>
+<% } %>
diff --git a/libs/schematics/src/libs/workspace/files/ts/tsconfig.json b/libs/schematics/src/libs/workspace/files/ts/tsconfig.json
new file mode 100644
index 0000000..4ccd69c
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/ts/tsconfig.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ // Mapped from https://www.typescriptlang.org/tsconfig
+ "compilerOptions": {
+ // Type Checking
+ "allowUnreachableCode": <%= !strict %>,
+ "allowUnusedLabels": <%= !strict %>,
+ "exactOptionalPropertyTypes": <%= strict %>,
+ "noFallthroughCasesInSwitch": <%= strict %>,
+ "noImplicitOverride": <%= strict %>,
+ "noImplicitReturns": <%= strict %>,
+ "noPropertyAccessFromIndexSignature": <%= !strict %>,
+ "noUncheckedIndexedAccess": <%= strict %>,
+ "noUnusedLocals": <%= strict %>,
+ "noUnusedParameters": <%= strict %>,
+ "strict": <%= strict %>,
+
+ // Modules
+ "allowArbitraryExtensions": false,
+ "allowImportingTsExtensions": false,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "resolvePackageJsonExports": true,
+ "resolvePackageJsonImports": true,
+
+ // Interop Constraints
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": false,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+
+ // Language and Environment
+ "experimentalDecorators": true,
+ "lib": ["ESNext", "esnext.disposable", "DOM"],
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ },
+ "exclude": ["node_modules", ".nanoforge"]
+}
diff --git a/libs/schematics/src/libs/workspace/schema.json b/libs/schematics/src/libs/workspace/schema.json
new file mode 100644
index 0000000..9d843e6
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/schema.json
@@ -0,0 +1,45 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "SchematicsNanoForgeWorkspace",
+ "title": "NanoForge Workspace Options Schema",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the workspace",
+ "default": "nanoforge-app"
+ },
+ "directory": {
+ "type": "string",
+ "description": "NanoForge workspace destination directory (defaults to the workspace name when omitted)"
+ },
+ "language": {
+ "type": "string",
+ "enum": ["ts", "js"],
+ "description": "NanoForge workspace language",
+ "default": "ts"
+ },
+ "strict": {
+ "type": "boolean",
+ "description": "With strict mode (TypeScript or JavaScript but only on compatible editor)",
+ "default": true
+ },
+ "packageManager": {
+ "type": "string",
+ "enum": ["npm", "yarn", "pnpm", "bun"],
+ "description": "NanoForge workspace package manager",
+ "default": "npm"
+ },
+ "allowBuilds": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Package names allowed to run install/build scripts, in addition to `bun` which is always allowed (@nanoforge-dev/cli depends on it and it has its own install script). Written to pnpm-workspace.yaml (pnpm), package.json's allowScripts (npm) or trustedDependencies (bun). Ignored for yarn.",
+ "default": []
+ },
+ "docker": {
+ "type": "boolean",
+ "description": "Generate a Dockerfile building every app in the workspace into one image",
+ "default": false
+ }
+ }
+}
diff --git a/libs/schematics/src/libs/workspace/workspace.factory.ts b/libs/schematics/src/libs/workspace/workspace.factory.ts
new file mode 100644
index 0000000..61cb091
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/workspace.factory.ts
@@ -0,0 +1,84 @@
+import { type Path, join, normalize, strings } from "@angular-devkit/core";
+import {
+ type Rule,
+ type Source,
+ apply,
+ filter,
+ mergeWith,
+ move,
+ renameTemplateFiles,
+ template,
+ url,
+} from "@angular-devkit/schematics";
+
+import { fetchTrustedVersions } from "@utils/registry";
+
+import {
+ DEFAULT_CLI_DEPENDENCY_VERSION,
+ DEFAULT_ENGINE_VERSION,
+ DEFAULT_PACKAGE_MANAGER,
+ DEFAULT_TYPESCRIPT_VERSION,
+} from "~/defaults";
+
+import { type WorkspaceOptions } from "./workspace.options";
+import { type WorkspaceSchema } from "./workspace.schema";
+
+/**
+ * bun ships as a postinstall-driven binary download (its own package.json
+ * declares `"postinstall": "node install.js"`), and @nanoforge-dev/cli
+ * depends on it unconditionally, so every generated workspace needs it
+ * approved to run its build script regardless of what the caller passes.
+ */
+const ALWAYS_ALLOWED_BUILDS = ["bun"];
+
+const transform = async (schema: WorkspaceSchema): Promise => {
+ const packageManager = schema.packageManager ?? DEFAULT_PACKAGE_MANAGER;
+
+ const versions = await fetchTrustedVersions({
+ "@nanoforge-dev/cli": { fallback: DEFAULT_CLI_DEPENDENCY_VERSION },
+ nanoforge: { fallback: DEFAULT_ENGINE_VERSION },
+ typescript: { fallback: DEFAULT_TYPESCRIPT_VERSION, major: 6 },
+ });
+
+ return {
+ name: schema.name,
+ engineVersion: versions.nanoforge,
+ cliVersion: versions["@nanoforge-dev/cli"],
+ typescriptVersion: versions.typescript,
+ language: schema.language,
+ strict: schema.strict,
+ packageManager,
+ allowBuilds: Array.from(new Set([...ALWAYS_ALLOWED_BUILDS, ...(schema.allowBuilds ?? [])])),
+ docker: schema.docker ?? false,
+ };
+};
+
+const generate = (options: WorkspaceOptions, path: string): Source => {
+ return apply(url(join("./files" as Path, options.language)), [
+ template({
+ dot: ".",
+ ...strings,
+ ...options,
+ }),
+ filter(
+ (path) =>
+ options.packageManager === "pnpm" ||
+ !(path.split("/").at(-1) ?? "").startsWith("pnpm-workspace.yaml"),
+ ),
+ filter(
+ (path) =>
+ options.docker || !["Dockerfile", ".dockerignore"].includes(path.split("/").at(-1) ?? ""),
+ ),
+ renameTemplateFiles(),
+ move(normalize(path)),
+ ]);
+};
+
+export const main = (schema: WorkspaceSchema): Rule => {
+ return async () => {
+ const options = await transform(schema);
+ const path = schema.directory ?? options.name;
+
+ return mergeWith(generate(options, path));
+ };
+};
diff --git a/libs/schematics/src/libs/workspace/workspace.options.d.ts b/libs/schematics/src/libs/workspace/workspace.options.d.ts
new file mode 100644
index 0000000..bcc9408
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/workspace.options.d.ts
@@ -0,0 +1,44 @@
+export interface WorkspaceOptions {
+ /**
+ * NanoForge workspace name
+ */
+ name: string;
+
+ /**
+ * Version of engine packages (nanoforge, @nanoforge-dev/cli),
+ * resolved to the newest registry release that respects MIN_RELEASE_AGE_HOURS.
+ */
+ engineVersion: string;
+
+ /** Resolved @nanoforge-dev/cli version/protocol (see {@link engineVersion}). */
+ cliVersion: string;
+
+ /** Resolved typescript version (TypeScript language only). */
+ typescriptVersion: string;
+
+ /**
+ * NanoForge workspace language
+ */
+ language: "js" | "ts";
+
+ /**
+ * With strict mode (TypeScript or JavaScript but only on compatible editor)
+ */
+ strict: boolean;
+
+ /**
+ * The used package manager
+ */
+ packageManager: "npm" | "yarn" | "pnpm" | "bun";
+
+ /**
+ * Package names allowed to run install/build scripts (always includes `bun`).
+ * See {@link WorkspaceSchema.allowBuilds}.
+ */
+ allowBuilds: string[];
+
+ /**
+ * Generate a Dockerfile building every app in the workspace into one image
+ */
+ docker: boolean;
+}
diff --git a/libs/schematics/src/libs/workspace/workspace.schema.d.ts b/libs/schematics/src/libs/workspace/workspace.schema.d.ts
new file mode 100644
index 0000000..fd61e17
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/workspace.schema.d.ts
@@ -0,0 +1,41 @@
+export interface WorkspaceSchema {
+ /**
+ * NanoForge workspace name
+ */
+ name: string;
+
+ /**
+ * NanoForge workspace destination directory
+ */
+ directory: string;
+
+ /**
+ * NanoForge workspace language
+ */
+ language: "js" | "ts";
+
+ /**
+ * With strict mode (TypeScript or JavaScript but only on compatible editor)
+ */
+ strict: boolean;
+
+ /**
+ * The used package manager
+ */
+ packageManager: "npm" | "yarn" | "pnpm" | "bun";
+
+ /**
+ * Package names allowed to run install/build scripts. Written to
+ * pnpm-workspace.yaml's `allowBuilds` map (pnpm), package.json's
+ * `allowScripts` map (npm) or `trustedDependencies` array (bun). Ignored
+ * for yarn. `bun` is always included in addition to this list since
+ * @nanoforge-dev/cli transitively depends on it and it has its own
+ * install script.
+ */
+ allowBuilds: string[];
+
+ /**
+ * Generate a Dockerfile building every app in the workspace into one image
+ */
+ docker: boolean;
+}
diff --git a/libs/schematics/src/utils/config/config.declarator.spec.ts b/libs/schematics/src/utils/config/config.declarator.spec.ts
deleted file mode 100644
index 6d4c34b..0000000
--- a/libs/schematics/src/utils/config/config.declarator.spec.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { ConfigDeclarator } from "./config.declarator";
-
-describe("ConfigDeclarator", () => {
- it("should return a valid JSON string", () => {
- const result = new ConfigDeclarator().declare("{}", {});
- expect(() => JSON.parse(result)).not.toThrow();
- });
-
- it("should include default client config", () => {
- const result = JSON.parse(new ConfigDeclarator().declare("{}", {}));
- expect(result.client).toBeUndefined();
- });
-
- it("should not include server config when server is not enabled", () => {
- const result = JSON.parse(new ConfigDeclarator().declare("{}", {}));
- expect(result.server).toBeUndefined();
- });
-
- it("should format output with 2-space indentation", () => {
- const result = new ConfigDeclarator().declare('{ "server": { "enable": true } }', {});
- expect(result).toContain("\n ");
- });
-
- it("should include server config when baseContent has server.enable true", () => {
- const baseContent = JSON.stringify({ server: { enable: true } });
- const result = JSON.parse(new ConfigDeclarator().declare(baseContent, {}));
- expect(result.server).toBeDefined();
- expect(result.server.enable).toBe(true);
- });
-
- it("should include server config when options has server.enable true", () => {
- const result = JSON.parse(new ConfigDeclarator().declare("{}", { server: { enable: true } }));
- expect(result.server).toBeDefined();
- expect(result.server.enable).toBe(true);
- });
-});
diff --git a/libs/schematics/src/utils/config/config.declarator.ts b/libs/schematics/src/utils/config/config.declarator.ts
deleted file mode 100644
index 1ad2405..0000000
--- a/libs/schematics/src/utils/config/config.declarator.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { type Config } from "@utils/config/config.type";
-import { deepMerge } from "@utils/object";
-import { type DeepPartial } from "@utils/types";
-
-export class ConfigDeclarator {
- public declare(baseContent: string, options: DeepPartial): string {
- const content = JSON.parse(baseContent);
- return JSON.stringify(deepMerge(content, options), null, 2);
- }
-}
diff --git a/libs/schematics/src/utils/config/config.finder.spec.ts b/libs/schematics/src/utils/config/config.finder.spec.ts
deleted file mode 100644
index e37a8e3..0000000
--- a/libs/schematics/src/utils/config/config.finder.spec.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { type Path } from "@angular-devkit/core";
-import { type DirEntry, type Tree } from "@angular-devkit/schematics";
-import { describe, expect, it, vi } from "vitest";
-
-import { ConfigFinder } from "./config.finder";
-
-function createMockDirEntry(
- path: string,
- subfiles: string[],
- parent: DirEntry | null = null,
-): DirEntry {
- return {
- path: path as Path,
- subfiles: subfiles as any,
- subdirs: [] as any,
- parent,
- dir: vi.fn() as any,
- file: vi.fn() as any,
- visit: vi.fn() as any,
- };
-}
-
-function createMockTree(dirEntry: DirEntry): Tree {
- return {
- getDir: vi.fn().mockReturnValue(dirEntry),
- } as unknown as Tree;
-}
-
-describe("ConfigFinder", () => {
- it("should find nanoforge.config.json in the given directory", () => {
- const dir = createMockDirEntry("/project", ["nanoforge.config.json"]);
- const tree = createMockTree(dir);
- const finder = new ConfigFinder(tree);
-
- const result = finder.find("/project" as Path);
-
- expect(result).toBe("/project/nanoforge.config.json");
- });
-
- it("should search parent directories recursively", () => {
- const parent = createMockDirEntry("/", ["nanoforge.config.json"]);
- const child = createMockDirEntry("/project/src", [], parent);
- const tree = createMockTree(child);
- const finder = new ConfigFinder(tree);
-
- const result = finder.find("/project/src" as Path);
-
- expect(result).toBe("/nanoforge.config.json");
- });
-
- it("should return null when config is not found", () => {
- const root = createMockDirEntry("/", [], null);
- const child = createMockDirEntry("/project", [], root);
- const tree = createMockTree(child);
- const finder = new ConfigFinder(tree);
-
- const result = finder.find("/project" as Path);
-
- expect(result).toBeNull();
- });
-
- it("should ignore files that do not match nanoforge.config.json", () => {
- const root = createMockDirEntry("/", [], null);
- const dir = createMockDirEntry("/project", ["package.json", "tsconfig.json"], root);
- const tree = createMockTree(dir);
- const finder = new ConfigFinder(tree);
-
- const result = finder.find("/project" as Path);
-
- expect(result).toBeNull();
- });
-});
diff --git a/libs/schematics/src/utils/config/config.finder.ts b/libs/schematics/src/utils/config/config.finder.ts
deleted file mode 100644
index 04ff4f4..0000000
--- a/libs/schematics/src/utils/config/config.finder.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { type Path, join } from "@angular-devkit/core";
-import { type DirEntry, type Tree } from "@angular-devkit/schematics";
-
-export interface FindOptions {
- path: Path;
-}
-
-export class ConfigFinder {
- constructor(private tree: Tree) {}
-
- public find(path: Path): Path | null {
- const dir = this.tree.getDir(path);
- return this.findIn(dir);
- }
-
- /**
- * Recursively searches for the module file in the given directory.
- *
- * @param directory - The directory to search in.
- * @returns The path to the module file, or null if not found.
- */
- private findIn(directory: DirEntry | null): Path | null {
- if (!directory) {
- return null;
- }
- const moduleFilename = directory.subfiles.find((filename) =>
- /^nanoforge.config.json$/.test(filename),
- );
- return moduleFilename !== undefined
- ? join(directory.path, moduleFilename.valueOf())
- : this.findIn(directory.parent);
- }
-}
diff --git a/libs/schematics/src/utils/config/config.type.ts b/libs/schematics/src/utils/config/config.type.ts
deleted file mode 100644
index a0df082..0000000
--- a/libs/schematics/src/utils/config/config.type.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export interface BuildConfig {
- entry: string;
-}
-
-export interface EditorConfig {
- entry: string;
- save: string;
-}
-
-export interface ClientConfig {
- enable: boolean;
- port: string;
- outDir: string;
- build: BuildConfig;
- editor: EditorConfig;
-}
-
-export interface ServerConfig {
- enable: boolean;
- outDir: string;
- build: BuildConfig;
- editor: EditorConfig;
-}
-
-export interface Config {
- name: string;
- language: "ts" | "js";
- initFunctions: boolean;
- client: ClientConfig;
- server: ServerConfig;
-}
diff --git a/libs/schematics/src/utils/main/const.ts b/libs/schematics/src/utils/main/const.ts
deleted file mode 100644
index e685784..0000000
--- a/libs/schematics/src/utils/main/const.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { SaveLibraryTypeEnum } from "@utils/main/save.type";
-
-export const LIBS_FUNCTIONS_NAME: Record = {
- [SaveLibraryTypeEnum.ASSET_MANAGER]: "useAssetManager",
- [SaveLibraryTypeEnum.COMPONENT_SYSTEM]: "useComponentSystem",
- [SaveLibraryTypeEnum.GRAPHICS]: "useGraphics",
- [SaveLibraryTypeEnum.INPUT]: "useInput",
- [SaveLibraryTypeEnum.NETWORK]: "useNetwork",
- [SaveLibraryTypeEnum.SOUND]: "useSound",
-};
diff --git a/libs/schematics/src/utils/main/enums.ts b/libs/schematics/src/utils/main/enums.ts
deleted file mode 100644
index 9bbeb67..0000000
--- a/libs/schematics/src/utils/main/enums.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export enum InitFunctionEnum {
- BEFORE_INIT = "beforeInit",
- AFTER_INIT = "afterInit",
- BEFORE_REGISTRY_INIT = "beforeRegistryInit",
- AFTER_REGISTRY_INIT = "afterRegistryInit",
- BEFORE_RUN = "beforeRun",
- AFTER_RUN = "afterRun",
-}
diff --git a/libs/schematics/src/utils/main/main-functions.spec.ts b/libs/schematics/src/utils/main/main-functions.spec.ts
deleted file mode 100644
index 0f5874f..0000000
--- a/libs/schematics/src/utils/main/main-functions.spec.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { generateMain } from "./main-functions";
-import { type Save, SaveLibraryTypeEnum } from "./save.type";
-
-const emptySave: Save = {
- libraries: [],
- components: [],
- systems: [],
- entities: [],
-};
-
-describe("generateMain", () => {
- it("should generate a client main with TypeScript types", () => {
- const result = generateMain(
- { part: "client", language: "ts", initFunctions: false, editor: false },
- emptySave,
- );
- expect(result).toContain("NanoforgeFactory.createClient()");
- expect(result).toContain("IRunOptions");
- expect(result).toContain("export async function main(options: IRunOptions)");
- });
-
- it("should generate a server main without types", () => {
- const result = generateMain(
- { part: "server", language: "js", initFunctions: false, editor: false },
- emptySave,
- );
- expect(result).toContain("NanoforgeFactory.createServer()");
- expect(result).not.toContain("IRunOptions");
- expect(result).toContain("export async function main(options)");
- });
-
- it("should include init function imports and calls when enabled", () => {
- const result = generateMain(
- { part: "client", language: "ts", initFunctions: true, editor: false },
- emptySave,
- );
- expect(result).toContain('import { beforeInit } from "./init/before-init";');
- expect(result).toContain("await beforeInit(app);");
- expect(result).toContain("await afterInit(app);");
- expect(result).toContain("await beforeRegistryInit(app, registry);");
- expect(result).toContain("await afterRegistryInit(app, registry);");
- expect(result).toContain("await beforeRun(app);");
- expect(result).toContain("await afterRun(app);");
- });
-
- it("should not include init functions when disabled", () => {
- const result = generateMain(
- { part: "client", language: "ts", initFunctions: false, editor: false },
- emptySave,
- );
- expect(result).not.toContain("beforeInit");
- expect(result).not.toContain("afterRun");
- });
-
- it("should generate library imports, instances, and init calls", () => {
- const save: Save = {
- ...emptySave,
- libraries: [
- {
- id: "gfx",
- type: SaveLibraryTypeEnum.GRAPHICS,
- name: "Graphics",
- path: "@nanoforge/graphics",
- },
- ],
- };
- const result = generateMain(
- { part: "client", language: "ts", initFunctions: false, editor: false },
- save,
- );
- expect(result).toContain('import { Graphics } from "@nanoforge/graphics";');
- expect(result).toContain("const gfx = new Graphics();");
- expect(result).toContain("app.useGraphics(gfx);");
- });
-
- it("should generate entity and system registration", () => {
- const save: Save = {
- libraries: [
- {
- id: "ecs",
- type: SaveLibraryTypeEnum.COMPONENT_SYSTEM,
- name: "ECS",
- path: "@nanoforge/ecs",
- },
- ],
- components: [{ name: "Position", path: "./components/position", paramsNames: ["x", "y"] }],
- systems: [{ name: "PhysicsSystem", path: "./systems/physics" }],
- entities: [
- {
- id: "player",
- components: { Position: { x: 1, y: 2 } },
- },
- ],
- };
- const result = generateMain(
- { part: "client", language: "ts", initFunctions: false, editor: false },
- save,
- );
- expect(result).toContain("const registry = ecs.registry;");
- expect(result).toContain("const player = registry.spawnEntity();");
- expect(result).toContain("registry.addComponent(player, new Position(1, 2));");
- expect(result).toContain("registry.addSystem(PhysicsSystem);");
- });
-
- it("should generate entity even in reverse order", () => {
- const save: Save = {
- libraries: [
- {
- id: "ecs",
- type: SaveLibraryTypeEnum.COMPONENT_SYSTEM,
- name: "ECS",
- path: "@nanoforge/ecs",
- },
- ],
- components: [{ name: "Position", path: "./components/position", paramsNames: ["y", "x"] }],
- systems: [{ name: "PhysicsSystem", path: "./systems/physics" }],
- entities: [
- {
- id: "player",
- components: { Position: { x: 1, y: 2 } },
- },
- ],
- };
- const result = generateMain(
- { part: "client", language: "ts", initFunctions: false, editor: false },
- save,
- );
- expect(result).toContain("const registry = ecs.registry;");
- expect(result).toContain("const player = registry.spawnEntity();");
- expect(result).toContain("registry.addComponent(player, new Position(2, 1));");
- expect(result).toContain("registry.addSystem(PhysicsSystem);");
- });
-});
diff --git a/libs/schematics/src/utils/main/main-functions.ts b/libs/schematics/src/utils/main/main-functions.ts
deleted file mode 100644
index 23c7e46..0000000
--- a/libs/schematics/src/utils/main/main-functions.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { join } from "path";
-
-import { InitFunctionEnum } from "@utils/main/enums";
-
-import { MainGenerator } from "./main.generator";
-import { type Save } from "./save.type";
-
-export interface MainOptions {
- part: "client" | "server";
- language: "js" | "ts";
- initFunctions: boolean;
- editor: boolean;
- outFile?: string;
-}
-
-export const generateMain = (options: MainOptions, save: Save) => {
- const isServer = options.part === "server";
- const hasTypes = options.language === "ts";
- const initFunctions = options.initFunctions;
-
- return (
- !options.editor
- ? new MainGenerator()
- : new MainGenerator(
- true,
- join(
- "../".repeat(options.outFile ? options.outFile.split("/").length - 1 : 3),
- options.part,
- ),
- )
- )
- .generateBaseImports(hasTypes)
- .generateLibsImports(save.libraries)
- .generateInitFunctionsImportsIfNeeded(initFunctions)
- .generateComponentsImports(save.components)
- .generateSystemsImports(save.systems)
-
- .generateMainFunction(hasTypes, (generator) => {
- generator
- .generateApp(isServer)
-
- .generateLibsInstances(save.libraries)
- .generateLibsInit(save.libraries)
-
- .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.BEFORE_INIT)
- .generateAppInit()
- .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.AFTER_INIT)
-
- .generateRegistry(save.libraries)
-
- .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.BEFORE_REGISTRY_INIT)
- .generateEntities(save.libraries, save.components, save.entities)
- .generateSystems(save.systems)
- .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.AFTER_REGISTRY_INIT)
-
- .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.BEFORE_RUN)
- .generateAppRun(initFunctions)
- .generateInitFunctionIfNeeded(initFunctions, InitFunctionEnum.AFTER_RUN);
- })
- .toString();
-};
diff --git a/libs/schematics/src/utils/main/main-utils.ts b/libs/schematics/src/utils/main/main-utils.ts
deleted file mode 100644
index 05c79e0..0000000
--- a/libs/schematics/src/utils/main/main-utils.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export const getStringParam = (value: any): string => {
- if (typeof value === "string") return `"${value}"`;
- if (typeof value === "number") return value.toString();
- if (typeof value === "boolean") return value ? "true" : "false";
- if (typeof value === "object") return JSON.stringify(value);
- return "undefined";
-};
diff --git a/libs/schematics/src/utils/main/main.generator.spec.ts b/libs/schematics/src/utils/main/main.generator.spec.ts
deleted file mode 100644
index 2cd0223..0000000
--- a/libs/schematics/src/utils/main/main.generator.spec.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { MainGenerator } from "./main.generator";
-import { SaveLibraryTypeEnum } from "./save.type";
-
-describe("MainGenerator", () => {
- it("should start with an empty buffer", () => {
- expect(new MainGenerator().toString()).toBe("");
- });
-
- describe("generateBaseImports", () => {
- it("should include type import when hasTypes is true", () => {
- const result = new MainGenerator().generateBaseImports(true).toString();
- expect(result).toContain('import { type IRunOptions } from "@nanoforge-dev/common";');
- expect(result).toContain('import { NanoforgeFactory } from "@nanoforge-dev/core";');
- });
-
- it("should omit type import when hasTypes is false", () => {
- const result = new MainGenerator().generateBaseImports(false).toString();
- expect(result).not.toContain("IRunOptions");
- expect(result).toContain('import { NanoforgeFactory } from "@nanoforge-dev/core";');
- });
- });
-
- describe("generateApp", () => {
- it("should generate createClient call for client", () => {
- const result = new MainGenerator().generateApp(false).toString();
- expect(result).toContain("NanoforgeFactory.createClient()");
- });
-
- it("should generate createServer call for server", () => {
- const result = new MainGenerator().generateApp(true).toString();
- expect(result).toContain("NanoforgeFactory.createServer()");
- });
- });
-
- describe("generateAppInit", () => {
- it("should generate await app.init(options)", () => {
- const result = new MainGenerator().generateAppInit().toString();
- expect(result).toContain("await app.init(options);");
- });
- });
-
- describe("generateAppRun", () => {
- it("should generate await app.run()", () => {
- const result = new MainGenerator().generateAppRun(false).toString();
- expect(result).toContain("await app.run();");
- });
- });
-
- describe("generateMainFunction", () => {
- it("should generate typed function signature when hasTypes is true", () => {
- const result = new MainGenerator().generateMainFunction(true, () => {}).toString();
- expect(result).toContain("export async function main(options: IRunOptions) {");
- expect(result).toContain("}");
- });
-
- it("should generate untyped function signature when hasTypes is false", () => {
- const result = new MainGenerator().generateMainFunction(false, () => {}).toString();
- expect(result).toContain("export async function main(options) {");
- });
-
- it("should indent content inside the function body", () => {
- const result = new MainGenerator()
- .generateMainFunction(false, (gen) => {
- gen.generateAppInit();
- })
- .toString();
- expect(result).toContain(" await app.init(options);");
- });
- });
-
- describe("generateLibsImports", () => {
- it("should generate sorted import statements", () => {
- const libs = [
- {
- id: "gfx",
- type: SaveLibraryTypeEnum.GRAPHICS,
- name: "Graphics",
- path: "@nanoforge/graphics",
- },
- {
- id: "ecs",
- type: SaveLibraryTypeEnum.COMPONENT_SYSTEM,
- name: "ECS",
- path: "@nanoforge/ecs",
- },
- ];
- const result = new MainGenerator().generateLibsImports(libs).toString();
- const lines = result.trim().split("\n");
- expect(lines[0]).toContain("@nanoforge/ecs");
- expect(lines[1]).toContain("@nanoforge/graphics");
- });
- });
-
- describe("generateLibsInstances", () => {
- it("should generate const declarations for each library", () => {
- const libs = [{ id: "gfx", type: SaveLibraryTypeEnum.GRAPHICS, name: "Graphics", path: "" }];
- const result = new MainGenerator().generateLibsInstances(libs).toString();
- expect(result).toContain("const gfx = new Graphics();");
- });
- });
-
- describe("generateLibsInit", () => {
- it("should use known init function for known library types", () => {
- const libs = [{ id: "gfx", type: SaveLibraryTypeEnum.GRAPHICS, name: "Graphics", path: "" }];
- const result = new MainGenerator().generateLibsInit(libs).toString();
- expect(result).toContain("app.useGraphics(gfx);");
- });
-
- it("should use app.use with Symbol for unknown library types", () => {
- const libs = [{ id: "custom", type: "custom-lib", name: "Custom", path: "" }];
- const result = new MainGenerator().generateLibsInit(libs).toString();
- expect(result).toContain('app.use(Symbol("custom-lib"), custom);');
- });
- });
-
- describe("generateRegistry", () => {
- it("should use component-system library id for registry", () => {
- const libs = [
- { id: "myEcs", type: SaveLibraryTypeEnum.COMPONENT_SYSTEM, name: "ECS", path: "" },
- ];
- const result = new MainGenerator().generateRegistry(libs).toString();
- expect(result).toContain("const registry = myEcs.registry;");
- });
-
- it("should fallback to ecsLibrary when no component-system lib is found", () => {
- const result = new MainGenerator().generateRegistry([]).toString();
- expect(result).toContain("const registry = ecsLibrary.registry;");
- });
- });
-
- describe("generateEntities", () => {
- it("should generate entity spawn and component additions", () => {
- const components = [
- {
- name: "Position",
- path: "non",
- paramsNames: ["x", "y"],
- },
- {
- name: "Velocity",
- path: "non",
- paramsNames: ["fast", "__RESERVED_ASSET_asset"],
- },
- ];
- const entities = [
- {
- id: "player",
- components: {
- Position: {
- x: 1,
- y: 2,
- },
- Velocity: {
- fast: 3,
- asset: "test",
- },
- },
- },
- ];
- const result = new MainGenerator().generateEntities([], components, entities).toString();
- expect(result).toContain("const player = registry.spawnEntity();");
- expect(result).toContain("registry.addComponent(player, new Position(1, 2));");
- expect(result).toContain(
- 'registry.addComponent(player, new Velocity(3, assetManagerLibrary.getAsset("test")));',
- );
- });
- });
-
- describe("generateSystems", () => {
- it("should generate addSystem calls", () => {
- const systems = [
- { name: "PhysicsSystem", path: "" },
- { name: "RenderSystem", path: "" },
- ];
- const result = new MainGenerator().generateSystems(systems).toString();
- expect(result).toContain("registry.addSystem(PhysicsSystem);");
- expect(result).toContain("registry.addSystem(RenderSystem);");
- });
- });
-
- describe("generateInitFunctionIfNeeded", () => {
- it("should generate init function call when needed is true", () => {
- const result = new MainGenerator()
- .generateInitFunctionIfNeeded(true, "beforeInit" as any)
- .toString();
- expect(result).toContain("await beforeInit(app);");
- });
-
- it("should not generate anything when needed is false", () => {
- const result = new MainGenerator()
- .generateInitFunctionIfNeeded(false, "beforeInit" as any)
- .toString();
- expect(result).toBe("");
- });
-
- it("should pass registry param for registry init functions", () => {
- const result = new MainGenerator()
- .generateInitFunctionIfNeeded(true, "beforeRegistryInit" as any)
- .toString();
- expect(result).toContain("await beforeRegistryInit(app, registry);");
- });
- });
-
- describe("generateInitFunctionsImportsIfNeeded", () => {
- it("should generate all init function imports when needed", () => {
- const result = new MainGenerator().generateInitFunctionsImportsIfNeeded(true).toString();
- expect(result).toContain("afterInit");
- expect(result).toContain("afterRegistryInit");
- expect(result).toContain("afterRun");
- expect(result).toContain("beforeInit");
- expect(result).toContain("beforeRegistryInit");
- expect(result).toContain("beforeRun");
- });
-
- it("should not generate imports when not needed", () => {
- const result = new MainGenerator().generateInitFunctionsImportsIfNeeded(false).toString();
- expect(result).toBe("");
- });
- });
-});
diff --git a/libs/schematics/src/utils/main/main.generator.ts b/libs/schematics/src/utils/main/main.generator.ts
deleted file mode 100644
index bc20904..0000000
--- a/libs/schematics/src/utils/main/main.generator.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import { joinRelative } from "@utils/path";
-
-import { LIBS_FUNCTIONS_NAME } from "./const";
-import { InitFunctionEnum } from "./enums";
-import { getStringParam } from "./main-utils";
-import {
- type SaveComponent,
- type SaveEntity,
- type SaveLibrary,
- SaveLibraryTypeEnum,
- type SaveSystem,
-} from "./save.type";
-
-export class MainGenerator {
- private buffer = "";
- private indentation = 0;
- private readonly editor: boolean;
- private readonly pathToRoot: string;
-
- constructor(editor: boolean = false, pathToRoot: string = "") {
- this.editor = editor;
- this.pathToRoot = pathToRoot;
- }
-
- toString(): string {
- return this.buffer;
- }
-
- generateBaseImports(hasTypes: boolean): MainGenerator {
- if (this.editor) {
- if (hasTypes)
- this.writeLine(
- `import { type IEditorRunOptions, NanoforgeFactory } from "@nanoforge-dev/core-editor";`,
- );
- else this.writeLine(`import { NanoforgeFactory } from "@nanoforge-dev/core-editor";`);
- } else {
- if (hasTypes) this.writeLine(`import { type IRunOptions } from "@nanoforge-dev/common";`);
- this.writeLine(`import { NanoforgeFactory } from "@nanoforge-dev/core";`);
- }
-
- this.endSection();
- return this;
- }
-
- generateLibsImports(libs: SaveLibrary[]): MainGenerator {
- return this.generateImports(libs, false);
- }
-
- generateComponentsImports(libs: SaveComponent[]): MainGenerator {
- return this.generateImports(libs, true);
- }
-
- generateSystemsImports(libs: SaveSystem[]): MainGenerator {
- return this.generateImports(libs, true);
- }
-
- generateMainFunction(hasTypes: boolean, cb: (generator: MainGenerator) => void): MainGenerator {
- if (this.editor)
- this.writeLine(
- `export async function main(options${hasTypes ? ": IEditorRunOptions" : ""}) {`,
- );
- else this.writeLine(`export async function main(options${hasTypes ? ": IRunOptions" : ""}) {`);
- this.indentation += 1;
- cb(this);
- this.indentation -= 1;
- this.writeLine("}");
- return this;
- }
-
- generateApp(isServer: boolean): MainGenerator {
- this.writeLine(`const app = NanoforgeFactory.create${isServer ? "Server" : "Client"}();`);
- this.endSection();
- return this;
- }
-
- generateAppInit(): MainGenerator {
- this.writeLine(`await app.init(options);`);
- this.endSection();
- return this;
- }
-
- generateAppRun(hasInitFunctions: boolean): MainGenerator {
- this.writeLine(`await app.run();`);
- if (hasInitFunctions) this.endSection();
- return this;
- }
-
- generateLibsInstances(libs: SaveLibrary[]): MainGenerator {
- libs.forEach(({ id, name }) => this.writeLine(`const ${id} = new ${name}();`));
- this.endSection();
- return this;
- }
-
- generateLibsInit(libs: SaveLibrary[]): MainGenerator {
- libs.forEach(({ id, type }) => this.writeLine(this.getInitFunction(id, type)));
- this.endSection();
- return this;
- }
-
- generateRegistry(libs: SaveLibrary[]): MainGenerator {
- const libName =
- libs.find((lib) => lib.type === SaveLibraryTypeEnum.COMPONENT_SYSTEM)?.id ?? "ecsLibrary";
- this.writeLine(`const registry = ${libName}.registry;`);
- this.endSection();
- return this;
- }
-
- generateEntities(
- libs: SaveLibrary[],
- components: SaveComponent[],
- entities: SaveEntity[],
- ): MainGenerator {
- entities.forEach((entity, index) => this.generateEntity(libs, components, entity, index));
- return this;
- }
-
- generateSystems(systems: SaveSystem[]): MainGenerator {
- systems.forEach(({ name }) => this.writeLine(`registry.addSystem(${name});`));
- this.endSection();
- return this;
- }
-
- generateInitFunctionIfNeeded(needed: boolean, func: InitFunctionEnum): MainGenerator {
- if (needed) return this.generateInitFunction(func);
- return this;
- }
-
- generateInitFunctionsImportsIfNeeded(needed: boolean): MainGenerator {
- if (!needed) return this;
- return this.generateImports(
- [
- { name: "afterInit", path: "./init/after-init" },
- { name: "afterRegistryInit", path: "./init/after-registry-init" },
- { name: "afterRun", path: "./init/after-run" },
- { name: "beforeInit", path: "./init/before-init" },
- { name: "beforeRegistryInit", path: "./init/before-registry-init" },
- { name: "beforeRun", path: "./init/before-run" },
- ],
- true,
- );
- }
-
- private generateInitFunction(func: InitFunctionEnum): MainGenerator {
- const params = ["app"];
- if (
- [InitFunctionEnum.BEFORE_REGISTRY_INIT, InitFunctionEnum.AFTER_REGISTRY_INIT].includes(func)
- )
- params.push("registry");
- this.writeLine(`await ${func}(${params.join(", ")});`);
- if (func !== InitFunctionEnum.AFTER_RUN) this.endSection();
- return this;
- }
-
- private generateImports(els: { name: string; path: string }[], relative: boolean): MainGenerator {
- [...els]
- .sort((a, b) => a.path.localeCompare(b.path))
- .forEach(({ name, path }) => {
- path = path.replace(/\.(?:ts|js)$/, "");
- this.writeLine(
- `import { ${name} } from "${relative ? joinRelative(this.pathToRoot, path) : path}";`,
- );
- });
- this.endSection();
- return this;
- }
-
- private generateEntity(
- libs: SaveLibrary[],
- components: SaveComponent[],
- entity: SaveEntity,
- entityIndex: number,
- ): void {
- this.writeLine(`const ${entity.id} = registry.spawnEntity();`);
- Object.entries(entity.components).forEach(([componentName, rawParams]) => {
- const originalsParams = components.find(({ name }) => name === componentName)?.paramsNames;
- if (!originalsParams)
- throw new Error("Missing component in entity or inexistent saved component");
- const params: string[] = originalsParams.map((ogParam) => {
- let isAsset = false;
- if (ogParam.startsWith("__RESERVED_ASSET_")) {
- isAsset = true;
- ogParam = ogParam.replace("__RESERVED_ASSET_", "");
- }
-
- const param = this.editor
- ? `options.editor.save.entities[${entityIndex}].components["${componentName}"]["${ogParam}"]`
- : getStringParam(rawParams[ogParam]);
- if (isAsset) return this.getAssetParam(param, libs);
- return param;
- });
- this.writeLine(
- `registry.addComponent(${entity.id}, new ${componentName}(${params.join(", ")}));`,
- );
- });
- if (this.editor) {
- this.writeLine(
- `registry.addComponent(${entity.id}, { name: "__RESERVED_entityId", entityId: "${entity.id}" });`,
- );
- }
- this.endSection();
- }
-
- private getAssetParam(param: string, libs: SaveLibrary[]): string {
- const libName =
- libs.find((lib) => lib.type === SaveLibraryTypeEnum.ASSET_MANAGER)?.id ??
- "assetManagerLibrary";
- return `${libName}.getAsset(${param})`;
- }
-
- private getInitFunction(name: string, type: SaveLibraryTypeEnum | string): string {
- const baseName = LIBS_FUNCTIONS_NAME[type];
- if (baseName) return `app.${baseName}(${name});`;
- return `app.use(Symbol("${type}"), ${name});`;
- }
-
- private writeLine(text: string, indentation = true): void {
- const line = `${" ".repeat(indentation ? this.indentation * 2 : 0)}${text}\n`;
- this.buffer += line;
- }
-
- private endSection(): void {
- this.writeLine("");
- }
-}
diff --git a/libs/schematics/src/utils/main/save.type.ts b/libs/schematics/src/utils/main/save.type.ts
deleted file mode 100644
index 36372a3..0000000
--- a/libs/schematics/src/utils/main/save.type.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-export enum SaveLibraryTypeEnum {
- COMPONENT_SYSTEM = "component-system",
- GRAPHICS = "graphics",
- ASSET_MANAGER = "asset-manager",
- NETWORK = "network",
- INPUT = "input",
- SOUND = "sound",
-}
-
-export interface SaveLibrary {
- id: string;
- type: SaveLibraryTypeEnum | string;
- name: string;
- path: string;
-}
-
-export interface SaveComponent {
- name: string;
- path: string;
- paramsNames: string[];
-}
-
-export interface SaveSystem {
- name: string;
- path: string;
-}
-
-export interface SaveEntity {
- id: string;
- components: Record>;
-}
-
-export interface Save {
- libraries: SaveLibrary[];
- components: SaveComponent[];
- systems: SaveSystem[];
- entities: SaveEntity[];
-}
diff --git a/libs/schematics/src/utils/registry.spec.ts b/libs/schematics/src/utils/registry.spec.ts
new file mode 100644
index 0000000..6737080
--- /dev/null
+++ b/libs/schematics/src/utils/registry.spec.ts
@@ -0,0 +1,106 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { fetchTrustedVersion, fetchTrustedVersions } from "./registry";
+
+const HOUR = 60 * 60 * 1000;
+
+const mockRegistryResponse = (time: Record, ok = true) => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue({
+ ok,
+ json: () => Promise.resolve({ time }),
+ }),
+ );
+};
+
+describe("fetchTrustedVersion", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2026-09-10T00:00:00Z"));
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ vi.useRealTimers();
+ });
+
+ it("picks the newest version published at least 48h ago", async () => {
+ mockRegistryResponse({
+ created: "2020-01-01T00:00:00Z",
+ modified: "2026-09-09T00:00:00Z",
+ "1.0.0": new Date(Date.now() - 100 * HOUR).toISOString(),
+ "1.1.0": new Date(Date.now() - 72 * HOUR).toISOString(),
+ "1.2.0": new Date(Date.now() - 1 * HOUR).toISOString(), // too fresh
+ });
+
+ const version = await fetchTrustedVersion("some-pkg", "0.0.0");
+ expect(version).toBe("1.1.0");
+ });
+
+ it("excludes prerelease and non-semver tags", async () => {
+ mockRegistryResponse({
+ "1.0.0": new Date(Date.now() - 100 * HOUR).toISOString(),
+ "1.1.0-beta.1": new Date(Date.now() - 100 * HOUR).toISOString(),
+ });
+
+ const version = await fetchTrustedVersion("some-pkg", "0.0.0");
+ expect(version).toBe("1.0.0");
+ });
+
+ it("restricts to a requested major version", async () => {
+ mockRegistryResponse({
+ "6.0.3": new Date(Date.now() - 100 * HOUR).toISOString(),
+ "7.0.2": new Date(Date.now() - 100 * HOUR).toISOString(),
+ });
+
+ const version = await fetchTrustedVersion("typescript", "6.0.0", { major: 6 });
+ expect(version).toBe("6.0.3");
+ });
+
+ it("falls back when every version is too fresh", async () => {
+ mockRegistryResponse({
+ "1.0.0": new Date(Date.now() - 1 * HOUR).toISOString(),
+ });
+
+ const version = await fetchTrustedVersion("some-pkg", "0.0.0");
+ expect(version).toBe("0.0.0");
+ });
+
+ it("falls back when the registry response is not ok", async () => {
+ mockRegistryResponse({}, false);
+ const version = await fetchTrustedVersion("missing-pkg", "0.0.0");
+ expect(version).toBe("0.0.0");
+ });
+
+ it("falls back when the fetch rejects", async () => {
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network down")));
+ const version = await fetchTrustedVersion("some-pkg", "0.0.0");
+ expect(version).toBe("0.0.0");
+ });
+});
+
+describe("fetchTrustedVersions", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2026-09-10T00:00:00Z"));
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ vi.useRealTimers();
+ });
+
+ it("resolves multiple packages and keys the result by package name", async () => {
+ mockRegistryResponse({
+ "2.5.0": new Date(Date.now() - 100 * HOUR).toISOString(),
+ });
+
+ const versions = await fetchTrustedVersions({
+ "pkg-a": { fallback: "0.0.0" },
+ "pkg-b": { fallback: "0.0.1" },
+ });
+
+ expect(versions).toEqual({ "pkg-a": "2.5.0", "pkg-b": "2.5.0" });
+ });
+});
diff --git a/libs/schematics/src/utils/registry.ts b/libs/schematics/src/utils/registry.ts
new file mode 100644
index 0000000..ab784ec
--- /dev/null
+++ b/libs/schematics/src/utils/registry.ts
@@ -0,0 +1,106 @@
+import { MIN_RELEASE_AGE_HOURS } from "~/defaults";
+
+const REGISTRY_URL = "https://registry.npmjs.org";
+const DEFAULT_TIMEOUT_MS = 3000;
+
+const RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
+
+const compareVersions = (a: string, b: string): number => {
+ const partsA = a.split(".").map(Number);
+ const partsB = b.split(".").map(Number);
+ for (let i = 0; i < 3; i++) {
+ const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);
+ if (diff !== 0) {
+ return diff;
+ }
+ }
+ return 0;
+};
+
+export interface FetchTrustedVersionOptions {
+ /**
+ * Only consider versions published at least this many hours ago, mirroring
+ * pnpm's `minimumReleaseAge` supply-chain protection for a freshly
+ * published (and potentially compromised) release.
+ * @default MIN_RELEASE_AGE_HOURS
+ */
+ minAgeHours?: number;
+
+ /**
+ * Restrict candidates to this major version (e.g. `6` only considers
+ * `6.x.x`). Useful to avoid silently jumping a generated project onto a
+ * new major release.
+ */
+ major?: number;
+
+ /** Network timeout in milliseconds. @default 3000 */
+ timeoutMs?: number;
+}
+
+/**
+ * Resolves the newest published version of an npm package that is at least
+ * `minAgeHours` old, so freshly (and potentially maliciously) published
+ * versions aren't picked up immediately. Falls back to `fallback` on any
+ * network error, timeout, missing package, or when no version satisfies the
+ * constraints, so callers never have to handle a rejected promise.
+ */
+export const fetchTrustedVersion = async (
+ packageName: string,
+ fallback: string,
+ options: FetchTrustedVersionOptions = {},
+): Promise => {
+ const { minAgeHours = MIN_RELEASE_AGE_HOURS, major, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
+
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
+
+ try {
+ const response = await fetch(`${REGISTRY_URL}/${encodeURIComponent(packageName)}`, {
+ signal: controller.signal,
+ });
+ if (!response.ok) {
+ return fallback;
+ }
+
+ const data = (await response.json()) as { time?: Record };
+ const time = data.time;
+ if (!time) {
+ return fallback;
+ }
+
+ const cutoff = Date.now() - minAgeHours * 60 * 60 * 1000;
+ const eligible = Object.entries(time)
+ .filter(([version]) => RELEASE_VERSION_PATTERN.test(version))
+ .filter(([version]) => major === undefined || Number(version.split(".")[0]) === major)
+ .filter(([, publishedAt]) => new Date(publishedAt).getTime() <= cutoff)
+ .map(([version]) => version)
+ .sort(compareVersions);
+
+ return eligible.at(-1) ?? fallback;
+ } catch {
+ return fallback;
+ } finally {
+ clearTimeout(timeout);
+ }
+};
+
+/**
+ * Resolves several packages' trusted versions in parallel. Keys of `specs`
+ * are the package name to query the registry for; values are the fallback
+ * to use if that lookup fails. Returns a map from the *same* keys to the
+ * resolved (or fallback) version — typed so callers don't get `| undefined`
+ * back for a key they just passed in.
+ */
+export const fetchTrustedVersions = async <
+ T extends Record,
+>(
+ specs: T,
+): Promise<{ [K in keyof T]: string }> => {
+ const entries = await Promise.all(
+ Object.entries(specs).map(async ([packageName, { fallback, major }]) => {
+ const version = await fetchTrustedVersion(packageName, fallback, { major });
+ return [packageName, version] as const;
+ }),
+ );
+ return Object.fromEntries(entries) as { [K in keyof T]: string };
+};
diff --git a/libs/schematics/tsdown.config.ts b/libs/schematics/tsdown.config.ts
index df494a8..e779764 100644
--- a/libs/schematics/tsdown.config.ts
+++ b/libs/schematics/tsdown.config.ts
@@ -34,11 +34,8 @@ function createLibTsdownConfig(name: string) {
export default [
createTsdownConfig(),
- createLibTsdownConfig("application"),
- createLibTsdownConfig("configuration"),
- createLibTsdownConfig("part-base"),
- createLibTsdownConfig("part-main"),
- createLibTsdownConfig("docker"),
+ createLibTsdownConfig("workspace"),
+ createLibTsdownConfig("project"),
createLibTsdownConfig("component"),
createLibTsdownConfig("system"),
];
From c1076395a022f66927d9f7dc91231c5bb0d280ce Mon Sep 17 00:00:00 2001
From: Exelo
Date: Mon, 14 Sep 2026 03:09:53 +0900
Subject: [PATCH 05/15] fix: remove generate command as it's no longer usefull
(#217)
BREAKING CHANGE: The `generate` command no longer exist
---
.../ISSUE_TEMPLATE/01-command_bug_report.yml | 1 -
.github/ISSUE_TEMPLATE/02-feature_request.yml | 1 -
.github/issue-labeler.yml | 4 -
.github/labeler.yml | 6 -
.github/labels.yml | 4 -
README.md | 10 -
docs/docs/commands/1-overview.mdx | 11 +-
docs/docs/commands/2-commands/2-dev.mdx | 15 +-
.../2-commands/{6-create.mdx => 5-create.mdx} | 0
docs/docs/commands/2-commands/5-generate.mdx | 39 --
.../{7-install.mdx => 6-install.mdx} | 0
.../2-commands/{8-editor.mdx => 7-editor.mdx} | 0
.../{9-registry.mdx => 8-registry.mdx} | 0
.../configuration/1-configuration-file.mdx | 218 ++++++-----
docs/docs/getting-started/1-introduction.mdx | 5 +-
docs/docs/getting-started/3-quickstart.mdx | 2 +-
docs/docs/schematics/1-overview.mdx | 2 +-
docs/guides/cli/3-develop-locally.mdx | 16 -
e2e/cli-generate.test.ts | 355 ------------------
e2e/cli-help.test.ts | 14 -
e2e/helpers/write-project-config.ts | 2 +-
src/action/actions/dev.action.ts | 11 +-
src/action/actions/generate.action.ts | 98 -----
src/action/index.ts | 1 -
src/command/command.loader.ts | 3 -
src/command/commands/dev.command.ts | 3 -
src/command/commands/generate.command.ts | 34 --
.../input/inputs/dev/generate.input.spec.ts | 20 -
src/lib/input/inputs/dev/generate.input.ts | 6 -
src/lib/input/inputs/dev/index.ts | 1 -
src/lib/input/inputs/index.ts | 1 -
src/lib/ui/messages.ts | 6 -
32 files changed, 146 insertions(+), 743 deletions(-)
rename docs/docs/commands/2-commands/{6-create.mdx => 5-create.mdx} (100%)
delete mode 100644 docs/docs/commands/2-commands/5-generate.mdx
rename docs/docs/commands/2-commands/{7-install.mdx => 6-install.mdx} (100%)
rename docs/docs/commands/2-commands/{8-editor.mdx => 7-editor.mdx} (100%)
rename docs/docs/commands/2-commands/{9-registry.mdx => 8-registry.mdx} (100%)
delete mode 100644 e2e/cli-generate.test.ts
delete mode 100644 src/action/actions/generate.action.ts
delete mode 100644 src/command/commands/generate.command.ts
delete mode 100644 src/lib/input/inputs/dev/generate.input.spec.ts
delete mode 100644 src/lib/input/inputs/dev/generate.input.ts
delete mode 100644 src/lib/input/inputs/dev/index.ts
diff --git a/.github/ISSUE_TEMPLATE/01-command_bug_report.yml b/.github/ISSUE_TEMPLATE/01-command_bug_report.yml
index 30d738b..879f998 100644
--- a/.github/ISSUE_TEMPLATE/01-command_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/01-command_bug_report.yml
@@ -16,7 +16,6 @@ body:
- build
- create
- dev
- - generate
- help
- install
- login
diff --git a/.github/ISSUE_TEMPLATE/02-feature_request.yml b/.github/ISSUE_TEMPLATE/02-feature_request.yml
index 9012528..bcf2d51 100644
--- a/.github/ISSUE_TEMPLATE/02-feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/02-feature_request.yml
@@ -14,7 +14,6 @@ body:
- build
- create
- dev
- - generate
- help
- install
- login
diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml
index af0aa6e..a154d38 100644
--- a/.github/issue-labeler.yml
+++ b/.github/issue-labeler.yml
@@ -10,10 +10,6 @@ command:dev:
- "### Which command is this (bug
report|feature request) for\\?\\n\\ndev\\n"
-command:generate:
- - "### Which command is this (bug
- report|feature request) for\\?\\n\\ngenerate\\n"
-
command:help:
- "### Which command is this (bug
report|feature request) for\\?\\n\\nhelp\\n"
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 19c2157..d0086bf 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -16,12 +16,6 @@ command:dev:
- src/command/commands/dev.command.ts
- src/action/actions/dev.action.ts
-command:generate:
- - changed-files:
- - any-glob-to-any-file:
- - src/command/commands/generate.command.ts
- - src/action/actions/generate.action.ts
-
command:install:
- changed-files:
- any-glob-to-any-file:
diff --git a/.github/labels.yml b/.github/labels.yml
index 3b1e5c3..0d245ee 100644
--- a/.github/labels.yml
+++ b/.github/labels.yml
@@ -54,10 +54,6 @@
description: "Related to dev command"
color: "aaa3dc"
-- name: "command:generate"
- description: "Related to generate command"
- color: "aaa3dc"
-
- name: "command:help"
description: "Related to help command"
color: "aaa3dc"
diff --git a/README.md b/README.md
index 231c65d..7cb8d15 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,6 @@ Used to run your nanoforge project in dev mode.
- `-d, --directory ` specify the working directory of the command.
- `-c, --config ` path to the config file.
-- `--generate` generate app files from config, like generate command in dev mode. (default: `false`)
### `editor`
@@ -89,15 +88,6 @@ Used to start the nanoforge editor.
- `--open` open the editor in the default web browser (default: `true` if path is specified, `false` otherwise).
- `--no-open` do not open the editor in the default web browser.
-### `generate`
-
-Used to generate nanoforge project files from config.
-
-- `-d, --directory ` specify the working directory of the command.
-- `-c, --config ` path to the config file.
-- `--editor` generate the editor main file.
-- `--watch` generate app in watching mode. (default: `false`)
-
### `install` or `add`
Used to add nanoforge components, systems or libraries to your project.
diff --git a/docs/docs/commands/1-overview.mdx b/docs/docs/commands/1-overview.mdx
index feff2b4..4d3915f 100644
--- a/docs/docs/commands/1-overview.mdx
+++ b/docs/docs/commands/1-overview.mdx
@@ -36,19 +36,16 @@ nf --help
Serve a built project, optionally over HTTPS.
-
- Generate project files from your configuration.
-
-
+
Create a single component or system.
-
+
Add components, systems, or libraries from the registry.
-
+
Launch the NanoForge visual editor.
-
+
Authenticate, publish, and unpublish packages.
diff --git a/docs/docs/commands/2-commands/2-dev.mdx b/docs/docs/commands/2-commands/2-dev.mdx
index 022d1a6..af69747 100644
--- a/docs/docs/commands/2-commands/2-dev.mdx
+++ b/docs/docs/commands/2-commands/2-dev.mdx
@@ -16,11 +16,10 @@ nf dev
## Options
-| Option | Description |
-| ----------------------------- | ------------------------------------------------------------------------------------------ |
-| `-d, --directory ` | Working directory of the command. |
-| `-c, --config ` | Path to the configuration file. |
-| `--generate` | Generate app files from config in dev mode, like the `generate` command. Default: `false`. |
+| Option | Description |
+| ----------------------------- | --------------------------------- |
+| `-d, --directory ` | Working directory of the command. |
+| `-c, --config ` | Path to the configuration file. |
## Examples
@@ -29,9 +28,3 @@ nf dev
```bash
nf dev
```
-
-**Regenerate files from config while developing:**
-
-```bash
-nf dev --generate
-```
diff --git a/docs/docs/commands/2-commands/6-create.mdx b/docs/docs/commands/2-commands/5-create.mdx
similarity index 100%
rename from docs/docs/commands/2-commands/6-create.mdx
rename to docs/docs/commands/2-commands/5-create.mdx
diff --git a/docs/docs/commands/2-commands/5-generate.mdx b/docs/docs/commands/2-commands/5-generate.mdx
deleted file mode 100644
index 831fc97..0000000
--- a/docs/docs/commands/2-commands/5-generate.mdx
+++ /dev/null
@@ -1,39 +0,0 @@
----
-title: generate
-description: Generate NanoForge project files from configuration
----
-
-## Overview
-
-`nf generate` generates project files from your [configuration](../../configuration/1-configuration-file).
-It reads the config and produces the entry files and wiring your project needs, including the editor
-main file when requested.
-
-## Usage
-
-```bash
-nf generate
-```
-
-## Options
-
-| Option | Description |
-| ----------------------------- | ----------------------------------------- |
-| `-d, --directory ` | Working directory of the command. |
-| `-c, --config ` | Path to the configuration file. |
-| `--editor` | Generate the editor main file. |
-| `--watch` | Generate in watch mode. Default: `false`. |
-
-## Examples
-
-**Generate once:**
-
-```bash
-nf generate
-```
-
-**Regenerate on change, including the editor entry:**
-
-```bash
-nf generate --editor --watch
-```
diff --git a/docs/docs/commands/2-commands/7-install.mdx b/docs/docs/commands/2-commands/6-install.mdx
similarity index 100%
rename from docs/docs/commands/2-commands/7-install.mdx
rename to docs/docs/commands/2-commands/6-install.mdx
diff --git a/docs/docs/commands/2-commands/8-editor.mdx b/docs/docs/commands/2-commands/7-editor.mdx
similarity index 100%
rename from docs/docs/commands/2-commands/8-editor.mdx
rename to docs/docs/commands/2-commands/7-editor.mdx
diff --git a/docs/docs/commands/2-commands/9-registry.mdx b/docs/docs/commands/2-commands/8-registry.mdx
similarity index 100%
rename from docs/docs/commands/2-commands/9-registry.mdx
rename to docs/docs/commands/2-commands/8-registry.mdx
diff --git a/docs/docs/configuration/1-configuration-file.mdx b/docs/docs/configuration/1-configuration-file.mdx
index 4063f2d..51a89b9 100644
--- a/docs/docs/configuration/1-configuration-file.mdx
+++ b/docs/docs/configuration/1-configuration-file.mdx
@@ -1,96 +1,140 @@
---
title: Configuration file
-description: The nanoforge.config.json file consumed by the CLI
+description: The nanoforge.config.ts (or .js) file consumed by the CLI
---
import { Tooltip } from "mintlify/components";
## Overview
-Every NanoForge project has a `nanoforge.config.json` at its root. The CLI reads this file to know
-how to build, generate, and start your game. Commands accept `-c, --config ` to point at a
-different file, and most fields can be overridden per run with command flags.
-
-## Top-level fields
-
-| Field | Type | Default | Description |
-| --------------- | -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
-| `name` | `string` | `nanoforge-app` | The name of the application. |
-| `language` | `"ts" \| "js"` | `ts` | Language used in the project. |
-| `initFunctions` | `boolean` | `true` | Whether init function lifecycle hooks are wired up. |
-| `client` | `object` | - | Client configuration. See below. |
-| `server` | `object` | - | Server configuration. See below. |
-| `ssl` | `object` | - | Ssl configuration for the game. See below. |
-
-## `client`
-
-| Field | Type | Default | Description |
-| ----------------- | --------- | ---------------------------------- | -------------------------------------- |
-| `enable` | `boolean` | `true` | Whether the client is enabled. |
-| `port` | `string` | `3000` | Port the client listens on. |
-| `outDir` | `string` | `.nanoforge/client` | Build output directory for the client. |
-| `build.entry` | `string` | `client/main.ts` | Client entry file. |
-| `build.staticDir` | `string` | `client/static` | Client static assets directory. |
-| `editor.entry` | `string` | `.nanoforge/editor/client/main.ts` | Editor entry file for the client. |
-| `editor.save` | `string` | `.nanoforge/client.save.json` | Editor save file for the client. |
-| `dirs.components` | `string` | `client/components` | Where client components live. |
-| `dirs.systems` | `string` | `client/systems` | Where client systems live. |
-
-## `server`
-
-The `server` block mirrors `client`, minus `port`. It is disabled by default.
-
-| Field | Type | Default | Description |
-| ----------------- | --------- | ---------------------------------- | -------------------------------------- |
-| `enable` | `boolean` | `false` | Whether the server is enabled. |
-| `outDir` | `string` | `.nanoforge/server` | Build output directory for the server. |
-| `build.entry` | `string` | `server/main.ts` | Server entry file. |
-| `build.staticDir` | `string` | `server/static` | Server static assets directory. |
-| `editor.entry` | `string` | `.nanoforge/editor/server/main.ts` | Editor entry file for the server. |
-| `editor.save` | `string` | `.nanoforge/server.save.json` | Editor save file for the server. |
-| `dirs.components` | `string` | `server/components` | Where server components live. |
-| `dirs.systems` | `string` | `server/systems` | Where server systems live. |
-
-## `ssl`
-
-| Field | Type | Default | Description |
-| -------- | -------- | ------- | --------------------------- |
-| `enable` | `boolean | `false` | Whether the ssl is enabled. |
-| `cert` | `string` | - | Certificate file. |
-| `key` | `string` | - | Key file. |
-
-## Example
-
-```json
-{
- "name": "my-game",
- "language": "ts",
- "initFunctions": true,
- "client": {
- "enable": true,
- "port": "3000",
- "outDir": ".nanoforge/client",
- "build": { "entry": "client/main.ts", "staticDir": "client/static" },
- "editor": {
- "entry": ".nanoforge/editor/client/main.ts",
- "save": ".nanoforge/client.save.json"
- },
- "dirs": { "components": "client/components", "systems": "client/systems" }
- },
- "server": {
- "enable": false,
- "outDir": ".nanoforge/server",
- "build": { "entry": "server/main.ts", "staticDir": "server/static" },
- "editor": {
- "entry": ".nanoforge/editor/server/main.ts",
- "save": ".nanoforge/server.save.json"
- },
- "dirs": { "components": "server/components", "systems": "server/systems" }
- },
- "ssl": {
- "enable": true,
- "cert": "path/to/cert",
- "key": "path/to/key"
- }
-}
+Every NanoForge workspace, project, or lib has a `nanoforge.config.ts` (or `.js`) at its root,
+exporting its configuration wrapped in `defineConfig` from `@nanoforge-dev/config`. The CLI reads
+this file to know how to build and start your game, and — for a workspace root — to discover the
+projects and libs it contains.
+
+Commands accept `-c, --config ` to point at a different directory to read the config
+from (default: the current directory). The CLI looks for `nanoforge.config.ts` in that directory
+first, falls back to `nanoforge.config.js` when no `.ts` file exists, and fails if neither is
+present.
+
+## `type`
+
+A config's `type` field discriminates between four shapes:
+
+- `workspace` — a monorepo root listing its `packages`.
+- `lib` — a shared library consumed by clients/servers.
+- `client` — a browser-side NanoForge app.
+- `server` — a server-side NanoForge app.
+
+The config at the root directory the CLI is pointed at must be a `workspace` or a `client`/`server`
+project — a bare `lib` config is not valid there. Inside a `workspace`, none of the packages it
+resolves may themselves be of type `workspace`; nesting workspaces isn't supported.
+
+## Defining a config
+
+Wrap your config in `defineConfig` to get type-checking and editor autocompletion on its fields:
+
+```ts
+import { defineConfig } from "@nanoforge-dev/config";
+
+export default defineConfig({
+ type: "client",
+ entryFile: "src/main.ts",
+ libs: ["../../libs/my-lib"],
+});
+```
+
+`defineConfig` is an identity function — it returns its argument unchanged.
+
+## `workspace`
+
+| Field | Type | Default | Description |
+| ---------- | ---------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `packages` | `string[]` | `[]` | Glob patterns matched against directories relative to the workspace root, each expected to hold its own `client`, `server`, or `lib` config. |
+
+A directory matched by a pattern but missing a `nanoforge.config.ts`/`.js` is skipped without
+error. A pattern that matches no project or lib at all only logs a warning, rather than failing
+the whole resolution. A matched directory whose config is itself of type `workspace` throws.
+
+### Example
+
+```ts
+import { defineConfig } from "@nanoforge-dev/config";
+
+export default defineConfig({
+ type: "workspace",
+ packages: ["apps/*", "libs/*"],
+});
+```
+
+## `lib`
+
+| Field | Type | Default | Description |
+| ---------------- | -------- | ------------------- | ------------------------ |
+| `dir.assets` | `string` | `assets` | Where assets live. |
+| `dir.shared` | `string` | `shared` | Where shared code lives. |
+| `dir.components` | `string` | `shared/components` | Used by the editor only. |
+| `dir.systems` | `string` | `shared/systems` | Used by the editor only. |
+| `dir.scenes` | `string` | `shared/scenes` | Used by the editor only. |
+
+### Example
+
+```ts
+import { defineConfig } from "@nanoforge-dev/config";
+
+export default defineConfig({ type: "lib" });
```
+
+## `client` / `server`
+
+`client` and `server` share the exact same fields and defaults — `type` is the only thing that
+tells them apart.
+
+| Field | Type | Default | Description |
+| ---------------- | ---------- | ---------------- | ------------------------------------------------------ |
+| `entryFile` | `string` | `src/main.ts` | Entry file. |
+| `out.dir` | `string` | `dist` | Build output directory. |
+| `out.mainFile` | `string` | `main.js` | Build output entry file name. |
+| `dir.assets` | `string` | `assets` | Where assets live. |
+| `dir.packages` | `string` | `nf_modules` | Where downloaded package dependencies are installed. |
+| `dir.components` | `string` | `src/components` | Used by the editor only. |
+| `dir.systems` | `string` | `src/systems` | Used by the editor only. |
+| `dir.scenes` | `string` | `src/scenes` | Used by the editor only. |
+| `libs` | `string[]` | `[]` | Relative paths to shared libs this project depends on. |
+
+### Example
+
+```ts
+import { defineConfig } from "@nanoforge-dev/config";
+
+export default defineConfig({
+ type: "server",
+ entryFile: "src/main.ts",
+ dir: { assets: "static" },
+ libs: ["../../libs/shared"],
+});
+```
+
+## Resolving defaults
+
+Missing fields are filled in with the defaults for the config's `type`, deep-merging nested
+objects like `dir` and `out`:
+
+```ts
+import { resolveConfig } from "@nanoforge-dev/config";
+
+const resolved = resolveConfig({ type: "client", dir: { assets: "static" } });
+// resolved.dir.assets === "static"
+// resolved.dir.packages === "nf_modules" (default, not overridden)
+```
+
+Array fields (`packages`, `libs`) are replaced wholesale by an override rather than concatenated
+with the defaults.
+
+## Errors
+
+- `ConfigNotFoundError` — thrown by the CLI when neither `nanoforge.config.ts` nor
+ `nanoforge.config.js` exists in the directory it was pointed at.
+- `ConfigParseError` (from `@nanoforge-dev/config`) — thrown while loading or validating a config
+ file's default export. Its `code` identifies the failure: `not-found`, `load-failed`,
+ `no-default-export`, or `invalid-type`.
diff --git a/docs/docs/getting-started/1-introduction.mdx b/docs/docs/getting-started/1-introduction.mdx
index 57c4ac0..6616b17 100644
--- a/docs/docs/getting-started/1-introduction.mdx
+++ b/docs/docs/getting-started/1-introduction.mdx
@@ -24,10 +24,7 @@ production, and publishing reusable packages to the NanoForge registry.
Run your game locally with hot reloading, then start it as you would in production.
-
- Generate components, systems, and entry files from your configuration.
-
-
+
Publish and consume components, systems, and libraries through the NanoForge registry.
diff --git a/docs/docs/getting-started/3-quickstart.mdx b/docs/docs/getting-started/3-quickstart.mdx
index a6b52ac..be335af 100644
--- a/docs/docs/getting-started/3-quickstart.mdx
+++ b/docs/docs/getting-started/3-quickstart.mdx
@@ -60,4 +60,4 @@ This quickstart takes you from an empty folder to a running game in a few minute
## Where to go next
- Learn each command in detail in the [Commands reference](../commands/1-overview).
-- Follow task-oriented [Guides](../../guides/cli/1-overview) for common workflows.
+- Follow task-oriented [Guides](/guides/cli/1-overview) for common workflows.
diff --git a/docs/docs/schematics/1-overview.mdx b/docs/docs/schematics/1-overview.mdx
index 0f100bb..1b14655 100644
--- a/docs/docs/schematics/1-overview.mdx
+++ b/docs/docs/schematics/1-overview.mdx
@@ -9,7 +9,7 @@ import { Card, CardGroup } from "mintlify/components";
NanoForge schematics are code generators built on [Angular DevKit](https://github.com/angular/angular-cli/tree/main/packages/angular_devkit/schematics). They produce the boilerplate files that every NanoForge project needs — from the initial application scaffold down to individual ECS components and systems.
-The NanoForge CLI (`nf`) calls these schematics internally whenever you run commands like `nf new` or `nf generate`. You can also invoke them directly via the `schematics` CLI for advanced use cases.
+The NanoForge CLI (`nf`) calls these schematics internally whenever you run commands like `nf new` or `nf create`. You can also invoke them directly via the `schematics` CLI for advanced use cases.
## Installation
diff --git a/docs/guides/cli/3-develop-locally.mdx b/docs/guides/cli/3-develop-locally.mdx
index e474f61..3dcda03 100644
--- a/docs/guides/cli/3-develop-locally.mdx
+++ b/docs/guides/cli/3-develop-locally.mdx
@@ -18,22 +18,6 @@ Once a project exists, most of your time is spent in the develop-and-iterate loo
-
- When you change `nanoforge.config.json` - for example adding new component or system directories
- - regenerate the project files. You can do this in a one-off run:
-
- ```bash
- nf generate
- ```
-
- Or have dev mode regenerate as it runs:
-
- ```bash
- nf dev --generate
- ```
-
-
-
Working in a monorepo or with multiple configs? Every command accepts `-d` and `-c`:
diff --git a/e2e/cli-generate.test.ts b/e2e/cli-generate.test.ts
deleted file mode 100644
index 05d74a9..0000000
--- a/e2e/cli-generate.test.ts
+++ /dev/null
@@ -1,355 +0,0 @@
-import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
-import { resolve } from "node:path";
-import { afterAll, beforeAll, describe, expect, it } from "vitest";
-
-import { runCli } from "./helpers/run-cli";
-
-const tmpDir = resolve(__dirname, "../.tmp-e2e-generate");
-
-beforeAll(async () => {
- mkdirSync(tmpDir, { recursive: true });
-});
-
-afterAll(() => {
- rmSync(tmpDir, { recursive: true, force: true });
-});
-
-describe("nf generate (TypeScript, no server)", () => {
- const projectDir = resolve(tmpDir, "gen-ts-no-server");
- const appDir = resolve(projectDir, "gen-app");
-
- beforeAll(async () => {
- mkdirSync(projectDir, { recursive: true });
-
- await runCli([
- "new",
- "--name",
- "gen-app",
- "--language",
- "ts",
- "--package-manager",
- "npm",
- "--strict",
- "--no-server",
- "--init-functions",
- "--skip-install",
- "--no-docker",
- "--no-git",
- "-d",
- projectDir,
- ]);
- });
-
- it("should run the generate command", async () => {
- const { exitCode } = await runCli(["generate", "-d", appDir]);
-
- expect(exitCode).toBe(0);
- });
-
- it("should accept --config option", async () => {
- const { exitCode } = await runCli([
- "generate",
- "-d",
- appDir,
- "--config",
- "nanoforge.config.json",
- ]);
-
- expect(exitCode).toBe(0);
- });
-
- it("should generate client main.ts file", async () => {
- const { exitCode } = await runCli(["generate", "-d", appDir]);
-
- expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, "client/main.ts"))).toBe(true);
- });
-
- it("should generate valid TypeScript client main content", async () => {
- await runCli(["generate", "-d", appDir]);
-
- const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8");
-
- // Core imports
- expect(content).toContain('import { type IRunOptions } from "@nanoforge-dev/common"');
- expect(content).toContain('import { NanoforgeFactory } from "@nanoforge-dev/core"');
-
- // Library imports
- expect(content).toContain('import { AssetManagerLibrary } from "@nanoforge-dev/asset-manager"');
- expect(content).toContain('import { ECSClientLibrary } from "@nanoforge-dev/ecs-client"');
- expect(content).toContain('import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d"');
- expect(content).toContain('import { InputLibrary } from "@nanoforge-dev/input"');
- expect(content).toContain('import { MusicLibrary } from "@nanoforge-dev/music"');
- expect(content).toContain('import { SoundLibrary } from "@nanoforge-dev/sound"');
-
- // Main function signature with TypeScript type
- expect(content).toContain("export async function main(options: IRunOptions)");
-
- // Factory creates client
- expect(content).toContain("NanoforgeFactory.createClient()");
-
- // App lifecycle
- expect(content).toContain("await app.init(options)");
- expect(content).toContain("await app.run()");
- });
-
- it("should generate init function imports and calls", async () => {
- await runCli(["generate", "-d", appDir]);
-
- const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8");
-
- // Init function imports
- expect(content).toContain('import { afterInit } from "./init/after-init"');
- expect(content).toContain('import { beforeInit } from "./init/before-init"');
- expect(content).toContain('import { afterRegistryInit } from "./init/after-registry-init"');
- expect(content).toContain('import { beforeRegistryInit } from "./init/before-registry-init"');
- expect(content).toContain('import { afterRun } from "./init/after-run"');
- expect(content).toContain('import { beforeRun } from "./init/before-run"');
-
- // Init function calls
- expect(content).toContain("await beforeInit(app)");
- expect(content).toContain("await afterInit(app)");
- expect(content).toContain("await beforeRun(app)");
- expect(content).toContain("await afterRun(app)");
- expect(content).toContain("await beforeRegistryInit(app, registry)");
- expect(content).toContain("await afterRegistryInit(app, registry)");
- });
-
- it("should generate library instantiation and registration", async () => {
- await runCli(["generate", "-d", appDir]);
-
- const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8");
-
- // Library instantiation
- expect(content).toContain("const assetManagerLibrary = new AssetManagerLibrary()");
- expect(content).toContain("const ecsLibrary = new ECSClientLibrary()");
- expect(content).toContain("const graphicsLibrary = new Graphics2DLibrary()");
- expect(content).toContain("const inputLibrary = new InputLibrary()");
- expect(content).toContain("const musicLibrary = new MusicLibrary()");
- expect(content).toContain("const soundLibrary = new SoundLibrary()");
-
- // Library registration
- expect(content).toContain("app.useAssetManager(assetManagerLibrary)");
- expect(content).toContain("app.useComponentSystem(ecsLibrary)");
- expect(content).toContain("app.useGraphics(graphicsLibrary)");
- expect(content).toContain("app.useInput(inputLibrary)");
- expect(content).toContain("app.useSound(soundLibrary)");
- });
-
- it("should generate ECS components, systems, and entities", async () => {
- await runCli(["generate", "-d", appDir]);
-
- const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8");
-
- // Component and system imports
- expect(content).toContain('import { ExampleComponent } from "./components/example.component"');
- expect(content).toContain('import { exampleSystem } from "./systems/example.system"');
-
- // Registry access
- expect(content).toContain("const registry = ecsLibrary.registry");
-
- // Entity creation and component attachment
- expect(content).toContain("const exampleEntity = registry.spawnEntity()");
- expect(content).toContain(
- 'registry.addComponent(exampleEntity, new ExampleComponent("example", 10, undefined))',
- );
-
- // System registration
- expect(content).toContain("registry.addSystem(exampleSystem)");
- });
-
- it("should regenerate main.ts with modified save file (add component and entity)", async () => {
- const savePath = resolve(appDir, ".nanoforge/client.save.json");
- const save = JSON.parse(readFileSync(savePath, "utf-8"));
-
- save.components.push({
- name: "HealthComponent",
- path: "./components/health.component.ts",
- paramsNames: ["health"],
- });
-
- save.entities[0].components["HealthComponent"] = { health: 100 };
-
- save.entities.push({
- id: "exampleEntity2",
- components: {
- ExampleComponent: {
- paramA: "example2",
- paramB: 15,
- },
- },
- });
-
- writeFileSync(savePath, JSON.stringify(save, null, 2));
-
- const { exitCode } = await runCli(["generate", "-d", appDir]);
- expect(exitCode).toBe(0);
-
- const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8");
-
- expect(content).toContain('import { HealthComponent } from "./components/health.component"');
- expect(content).toContain("new HealthComponent(100)");
-
- expect(content).toContain("const exampleEntity2 = registry.spawnEntity()");
- expect(content).toContain(
- 'registry.addComponent(exampleEntity2, new ExampleComponent("example2", 15, undefined))',
- );
-
- // Original component should still be present
-
- expect(content).toContain('import { ExampleComponent } from "./components/example.component"');
- expect(content).toContain("const exampleEntity = registry.spawnEntity()");
- expect(content).toContain(
- 'registry.addComponent(exampleEntity, new ExampleComponent("example", 10, undefined))',
- );
- });
-
- it("should not generate server directory", async () => {
- expect(existsSync(resolve(appDir, "server"))).toBe(false);
- });
-
- it("should generate client editor main.ts", async () => {
- await runCli(["generate", "-d", appDir, "--editor"]);
-
- const content = readFileSync(resolve(appDir, ".nanoforge/editor/client/main.ts"), "utf-8");
-
- expect(content).toContain("NanoforgeFactory.createClient()");
- expect(content).toContain('import { ECSClientLibrary } from "@nanoforge-dev/ecs-client"');
- expect(content).toContain('from "@nanoforge-dev/core-editor"');
- });
-});
-
-describe("nf generate (TypeScript, with server)", () => {
- const projectDir = resolve(tmpDir, "gen-ts-with-server");
- const appDir = resolve(projectDir, "gen-server-app");
-
- beforeAll(async () => {
- mkdirSync(projectDir, { recursive: true });
-
- await runCli([
- "new",
- "--name",
- "gen-server-app",
- "--language",
- "ts",
- "--package-manager",
- "npm",
- "--no-strict",
- "--server",
- "--init-functions",
- "--skip-install",
- "--no-docker",
- "--no-git",
- "-d",
- projectDir,
- ]);
- });
-
- it("should run the generate command with server enabled", async () => {
- const { exitCode } = await runCli(["generate", "-d", appDir]);
-
- expect(exitCode).toBe(0);
- });
-
- it("should generate server main.ts file", async () => {
- await runCli(["generate", "-d", appDir]);
-
- expect(existsSync(resolve(appDir, "server/main.ts"))).toBe(true);
- });
-
- it("should generate valid TypeScript server main content", async () => {
- await runCli(["generate", "-d", appDir]);
-
- const content = readFileSync(resolve(appDir, "server/main.ts"), "utf-8");
-
- // Core imports
- expect(content).toContain('import { type IRunOptions } from "@nanoforge-dev/common"');
- expect(content).toContain('import { NanoforgeFactory } from "@nanoforge-dev/core"');
-
- // Server-specific library
- expect(content).toContain('import { ECSServerLibrary } from "@nanoforge-dev/ecs-server"');
-
- // Factory creates server (not client)
- expect(content).toContain("NanoforgeFactory.createServer()");
- expect(content).not.toContain("NanoforgeFactory.createClient()");
-
- // Main function signature
- expect(content).toContain("export async function main(options: IRunOptions)");
-
- // App lifecycle
- expect(content).toContain("await app.init(options)");
- expect(content).toContain("await app.run()");
- });
-
- it("should generate server init function imports and calls", async () => {
- await runCli(["generate", "-d", appDir]);
-
- const content = readFileSync(resolve(appDir, "server/main.ts"), "utf-8");
-
- expect(content).toContain('import { beforeInit } from "./init/before-init"');
- expect(content).toContain('import { afterInit } from "./init/after-init"');
- expect(content).toContain("await beforeInit(app)");
- expect(content).toContain("await afterInit(app)");
- });
-
- it("should generate server ECS setup", async () => {
- await runCli(["generate", "-d", appDir]);
-
- const content = readFileSync(resolve(appDir, "server/main.ts"), "utf-8");
-
- expect(content).toContain("const ecsLibrary = new ECSServerLibrary()");
- expect(content).toContain("app.useComponentSystem(ecsLibrary)");
- expect(content).toContain("const registry = ecsLibrary.registry");
- expect(content).toContain("const exampleEntity = registry.spawnEntity()");
- expect(content).toContain("registry.addSystem(exampleSystem)");
- });
-
- it("should regenerate server main.ts with modified save file (add system)", async () => {
- const savePath = resolve(appDir, ".nanoforge/server.save.json");
- const save = JSON.parse(readFileSync(savePath, "utf-8"));
-
- save.systems.push({
- name: "movementSystem",
- path: "./systems/movement.system.ts",
- });
-
- writeFileSync(savePath, JSON.stringify(save, null, 2));
-
- const { exitCode } = await runCli(["generate", "-d", appDir]);
- expect(exitCode).toBe(0);
-
- const content = readFileSync(resolve(appDir, "server/main.ts"), "utf-8");
-
- expect(content).toContain('import { movementSystem } from "./systems/movement.system"');
- expect(content).toContain("registry.addSystem(movementSystem)");
- // Original system should still be present
- expect(content).toContain('import { exampleSystem } from "./systems/example.system"');
- expect(content).toContain("registry.addSystem(exampleSystem)");
- });
-
- it("should also regenerate client main.ts", async () => {
- await runCli(["generate", "-d", appDir]);
-
- const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8");
-
- expect(content).toContain("NanoforgeFactory.createClient()");
- expect(content).toContain('import { ECSClientLibrary } from "@nanoforge-dev/ecs-client"');
- });
-});
-
-describe("nf generate (with invalid directory)", () => {
- it("should fail when directory does not exist", async () => {
- const { exitCode } = await runCli(["generate", "-d", resolve(tmpDir, "nonexistent")]);
-
- expect(exitCode).not.toBe(0);
- });
-
- it("should fail when no config file is found", async () => {
- const emptyDir = resolve(tmpDir, "empty-dir");
- mkdirSync(emptyDir, { recursive: true });
-
- const { exitCode } = await runCli(["generate", "-d", emptyDir]);
-
- expect(exitCode).not.toBe(0);
- });
-});
diff --git a/e2e/cli-help.test.ts b/e2e/cli-help.test.ts
index d5a1311..6171f70 100644
--- a/e2e/cli-help.test.ts
+++ b/e2e/cli-help.test.ts
@@ -10,7 +10,6 @@ describe("nf --help", () => {
expect(stdout).toContain("Usage: nf [options]");
expect(stdout).toContain("build");
expect(stdout).toContain("dev");
- expect(stdout).toContain("generate");
expect(stdout).toContain("install|add");
expect(stdout).toContain("new");
expect(stdout).toContain("start");
@@ -69,18 +68,6 @@ describe("nf build --help", () => {
});
});
-describe("nf generate --help", () => {
- it("should display generate command options", async () => {
- const { stdout, exitCode } = await runCli(["generate", "--help"]);
-
- expect(exitCode).toBe(0);
- expect(stdout).toContain("generate nanoforge files from config");
- expect(stdout).toContain("--directory");
- expect(stdout).toContain("--config");
- expect(stdout).toContain("--watch");
- });
-});
-
describe("nf install --help", () => {
it("should display install command options", async () => {
const { stdout, exitCode } = await runCli(["install", "--help"]);
@@ -113,7 +100,6 @@ describe("nf dev --help", () => {
expect(exitCode).toBe(0);
expect(stdout).toContain("run your game in dev mode");
expect(stdout).toContain("--directory");
- expect(stdout).toContain("--generate");
expect(stdout).toContain("--editor");
});
});
diff --git a/e2e/helpers/write-project-config.ts b/e2e/helpers/write-project-config.ts
index e7d7953..86df865 100644
--- a/e2e/helpers/write-project-config.ts
+++ b/e2e/helpers/write-project-config.ts
@@ -11,7 +11,7 @@ interface WriteProjectConfigOptions {
/**
* `nf new` still scaffolds a `nanoforge.config.json` (schematics is out of scope for this
* change), but the CLI's config loader now only reads `.ts`/`.js` config modules. Tests that
- * scaffold a project and then run `build`/`generate`/`create` against it need a hand-written
+ * scaffold a project and then run `build`/`create` against it need a hand-written
* `nanoforge.config.ts` alongside the scaffolded JSON — everything not set here falls back to
* the CLI's own defaults, matching what the scaffolded JSON used to rely on too.
*/
diff --git a/src/action/actions/dev.action.ts b/src/action/actions/dev.action.ts
index e7b88fc..2191b04 100644
--- a/src/action/actions/dev.action.ts
+++ b/src/action/actions/dev.action.ts
@@ -1,4 +1,4 @@
-import { type Input, getDevGenerateInput, getDirectoryInput, getEditorInput } from "@lib/input";
+import { type Input, getDirectoryInput, getEditorInput } from "@lib/input";
import { PackageManagerFactory } from "@lib/package-manager";
import { Messages } from "@lib/ui";
@@ -13,23 +13,18 @@ export class DevAction extends AbstractAction {
public async handle(_args: Input, options: Input): Promise {
const directory = getDirectoryInput(options);
- const generate = getDevGenerateInput(options);
const editor = getEditorInput(options);
- const tasks = this.buildTaskList(directory, generate, editor);
+ const tasks = this.buildTaskList(directory, editor);
await Promise.all(tasks);
return { keepAlive: true };
}
- private buildTaskList(directory: string, generate: boolean, editor: boolean): Promise[] {
+ private buildTaskList(directory: string, editor: boolean): Promise[] {
const tasks: Promise[] = [];
const extraFlags = editor ? ["--editor"] : [];
- if (generate) {
- tasks.push(this.runSubCommand("generate", directory, { silent: true, extraFlags }));
- }
-
tasks.push(this.runSubCommand("build", directory, { silent: true, extraFlags }));
tasks.push(this.runSubCommand("start", directory, { silent: false }));
diff --git a/src/action/actions/generate.action.ts b/src/action/actions/generate.action.ts
deleted file mode 100644
index 06389cf..0000000
--- a/src/action/actions/generate.action.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { join } from "node:path";
-
-import { type Config } from "@lib/config";
-import { type Input, getDirectoryInput, getEditorInput, getWatchInput } from "@lib/input";
-import { Collection, CollectionFactory } from "@lib/schematics";
-import { Messages } from "@lib/ui";
-
-import { getCwd } from "@utils/path";
-
-import { AbstractAction, type HandleResult } from "../abstract.action";
-import { getConfig } from "../common/config";
-import { executeSchematic } from "../common/schematics";
-
-interface GenerateValues {
- directory: string;
- language: string;
- initFunctions: boolean;
-}
-
-export class GenerateAction extends AbstractAction {
- protected startMessage = Messages.GENERATE_START;
- protected successMessage = Messages.GENERATE_SUCCESS;
- protected failureMessage = Messages.GENERATE_FAILED;
-
- public async handle(_args: Input, options: Input): Promise {
- const directory = getDirectoryInput(options);
- const config = await getConfig(options, directory);
- const isEditor = getEditorInput(options);
- const isWatch = getWatchInput(options);
-
- await this.generateParts(config, directory, isEditor, isWatch);
-
- if (isWatch) {
- return this.enterWatchMode();
- }
-
- return {};
- }
-
- private extractValues(config: Config): GenerateValues {
- return {
- directory: ".",
- language: config.language,
- initFunctions: config.initFunctions,
- };
- }
-
- private async generateParts(
- config: Config,
- directory: string,
- isEditor: boolean,
- watch: boolean,
- ): Promise {
- const collection = CollectionFactory.create(Collection.NANOFORGE, directory);
- const values = this.extractValues(config);
-
- if (config.client.enable)
- await executeSchematic(
- "Client main file",
- collection,
- "part-main",
- {
- ...values,
- part: "client",
- outFile: !isEditor ? config.client.build.entry : config.client.editor.entry,
- saveFile: config.client.editor.save,
- editor: isEditor,
- },
- watch ? this.watchPath(directory, values.directory, config.client.editor.save) : undefined,
- );
-
- if (config.server.enable)
- await executeSchematic(
- "Server main file",
- collection,
- "part-main",
- {
- ...values,
- part: "server",
- outFile: !isEditor ? config.server.build.entry : config.server.editor.entry,
- saveFile: config.server.editor.save,
- editor: isEditor,
- },
- this.watchPath(directory, values.directory, config.server.editor.save),
- );
- }
-
- private watchPath(directory: string, subDir: string, saveFile: string): string {
- return join(getCwd(directory), subDir, saveFile);
- }
-
- private enterWatchMode(): HandleResult {
- console.info();
- console.info(Messages.GENERATE_WATCH_START);
- console.info();
- return { keepAlive: true };
- }
-}
diff --git a/src/action/index.ts b/src/action/index.ts
index 0d06368..ad55b22 100644
--- a/src/action/index.ts
+++ b/src/action/index.ts
@@ -2,7 +2,6 @@ export * from "./actions/build.action";
export * from "./actions/create.action";
export * from "./actions/dev.action";
export * from "./actions/editor.action";
-export * from "./actions/generate.action";
export * from "./actions/install.action";
export * from "./actions/login.action";
export * from "./actions/logout.action";
diff --git a/src/command/command.loader.ts b/src/command/command.loader.ts
index e06c1d5..51c4192 100644
--- a/src/command/command.loader.ts
+++ b/src/command/command.loader.ts
@@ -8,7 +8,6 @@ import {
CreateAction,
DevAction,
EditorAction,
- GenerateAction,
InstallAction,
LoginAction,
LogoutAction,
@@ -22,7 +21,6 @@ import { BuildCommand } from "./commands/build.command";
import { CreateCommand } from "./commands/create.command";
import { DevCommand } from "./commands/dev.command";
import { EditorCommand } from "./commands/editor.command";
-import { GenerateCommand } from "./commands/generate.command";
import { InstallCommand } from "./commands/install.command";
import { LoginCommand } from "./commands/login.command";
import { LogoutCommand } from "./commands/logout.command";
@@ -39,7 +37,6 @@ export class CommandLoader {
new StartCommand(new StartAction()).load(program);
new DevCommand(new DevAction()).load(program);
new EditorCommand(new EditorAction()).load(program);
- new GenerateCommand(new GenerateAction()).load(program);
new CreateCommand(new CreateAction()).load(program);
new LoginCommand(new LoginAction()).load(program);
new LogoutCommand(new LogoutAction()).load(program);
diff --git a/src/command/commands/dev.command.ts b/src/command/commands/dev.command.ts
index a985e9f..b239d7e 100644
--- a/src/command/commands/dev.command.ts
+++ b/src/command/commands/dev.command.ts
@@ -7,7 +7,6 @@ import { AbstractCommand } from "../abstract.command";
interface DevOptions {
directory?: string;
config?: string;
- generate?: boolean;
editor?: boolean;
}
@@ -18,13 +17,11 @@ export class DevCommand extends AbstractCommand {
.description("run your game in dev mode")
.option("-d, --directory ", "specify the working directory of the command")
.option("-c, --config ", "path to the config file", CONFIG_FILE_NAME)
- .option("--generate", "generate app from config", false)
.option("-e, --editor", "run the editor", false)
.action(async (rawOptions: DevOptions) => {
const options = AbstractCommand.mapToInput({
directory: rawOptions.directory,
config: rawOptions.config,
- generate: rawOptions.generate,
editor: rawOptions.editor,
});
diff --git a/src/command/commands/generate.command.ts b/src/command/commands/generate.command.ts
deleted file mode 100644
index 11203f7..0000000
--- a/src/command/commands/generate.command.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { type Command } from "commander";
-
-import { CONFIG_FILE_NAME } from "@lib/constants";
-
-import { AbstractCommand } from "../abstract.command";
-
-interface GenerateOptions {
- directory?: string;
- config?: string;
- editor?: boolean;
- watch?: boolean;
-}
-
-export class GenerateCommand extends AbstractCommand {
- public load(program: Command) {
- program
- .command("generate")
- .description("generate nanoforge files from config")
- .option("-d, --directory ", "specify the working directory of the command")
- .option("-c, --config ", "path to the config file", CONFIG_FILE_NAME)
- .option("--editor", "specify if the project must generate editor main file")
- .option("--watch", "generate app in watching mode", false)
- .action(async (rawOptions: GenerateOptions) => {
- const options = AbstractCommand.mapToInput({
- directory: rawOptions.directory,
- config: rawOptions.config,
- editor: rawOptions.editor,
- watch: rawOptions.watch,
- });
-
- await this.action.run(new Map(), options);
- });
- }
-}
diff --git a/src/lib/input/inputs/dev/generate.input.spec.ts b/src/lib/input/inputs/dev/generate.input.spec.ts
deleted file mode 100644
index c49fc62..0000000
--- a/src/lib/input/inputs/dev/generate.input.spec.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { type Input } from "../../input.type";
-import { getDevGenerateInput } from "./generate.input";
-
-const createInput = (entries: [string, any][]): Input => {
- return new Map(entries.map(([key, value]) => [key, { value }]));
-};
-
-describe("getDevGenerateInput", () => {
- it("should return the generate value when provided", () => {
- const input = createInput([["generate", true]]);
- expect(getDevGenerateInput(input)).toBe(true);
- });
-
- it("should return false as default when generate is missing", () => {
- const input = createInput([]);
- expect(getDevGenerateInput(input)).toBe(false);
- });
-});
diff --git a/src/lib/input/inputs/dev/generate.input.ts b/src/lib/input/inputs/dev/generate.input.ts
deleted file mode 100644
index be1647f..0000000
--- a/src/lib/input/inputs/dev/generate.input.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { getBooleanInputWithDefault } from "../../base-inputs";
-import { type Input } from "../../input.type";
-
-export const getDevGenerateInput = (inputs: Input): boolean => {
- return getBooleanInputWithDefault(inputs, "generate", false);
-};
diff --git a/src/lib/input/inputs/dev/index.ts b/src/lib/input/inputs/dev/index.ts
deleted file mode 100644
index c0d1098..0000000
--- a/src/lib/input/inputs/dev/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./generate.input";
diff --git a/src/lib/input/inputs/index.ts b/src/lib/input/inputs/index.ts
index 18b6fde..607306f 100644
--- a/src/lib/input/inputs/index.ts
+++ b/src/lib/input/inputs/index.ts
@@ -7,7 +7,6 @@ export * from "./server.input";
export * from "./watch.input";
export * from "./create";
-export * from "./dev";
export * from "./editor";
export * from "./install";
export * from "./login-out";
diff --git a/src/lib/ui/messages.ts b/src/lib/ui/messages.ts
index 8b5d426..a138117 100644
--- a/src/lib/ui/messages.ts
+++ b/src/lib/ui/messages.ts
@@ -57,12 +57,6 @@ export const Messages = {
CREATE_FAILED: failure("Creation failed!"),
CREATE_NAME_QUESTION: "What is the name of your component/system?",
- // --- Generate ---
- GENERATE_START: "NanoForge Generate",
- GENERATE_SUCCESS: success("Generation succeeded!"),
- GENERATE_FAILED: failure("Generation failed!"),
- GENERATE_WATCH_START: "Watching for changes...",
-
// --- Dev ---
DEV_START: "NanoForge Dev Mode",
DEV_SUCCESS: "Dev mode ended",
From 0c905f0ab0d96cfe473162df40145cd2a9a09886 Mon Sep 17 00:00:00 2001
From: Exelo
Date: Tue, 15 Sep 2026 09:18:38 +0900
Subject: [PATCH 06/15] refactor(cli): change new cmd to new archi (#218)
---
.../src/libs/project/files/js/Dockerfile | 9 +-
.../libs/project/files/js/__dot__gitignore | 134 ++++++++++++++++++
.../src/libs/project/files/js/jsconfig.json | 38 ++++-
.../libs/project/files/js/nanoforge.config.js | 3 +-
.../files/js/pnpm-workspace.yaml.template | 2 +-
.../src/libs/project/files/ts/Dockerfile | 9 +-
.../files/ts/pnpm-workspace.yaml.template | 2 +-
.../src/libs/project/files/ts/tsconfig.json | 7 +-
.../src/libs/project/project.factory.ts | 6 +-
.../src/libs/project/project.options.d.ts | 10 --
.../src/libs/project/project.schema.d.ts | 20 ---
.../src/libs/workspace/files/js/Dockerfile | 9 +-
.../ts => workspace/files/js}/__dot__env | 0
.../src/libs/workspace/files/js/jsconfig.json | 3 +-
.../files/js/pnpm-workspace.yaml.template | 2 +-
.../src/libs/workspace/files/ts/Dockerfile | 9 +-
.../src/libs/workspace/files/ts/__dot__env | 6 +
.../files/ts/pnpm-workspace.yaml.template | 2 +-
.../src/libs/workspace/files/ts/tsconfig.json | 2 +-
libs/schematics/src/utils/registry.ts | 2 +-
src/action/actions/new.action.ts | 95 +++++--------
src/lib/input/inputs/new/index.ts | 1 -
.../inputs/new/init-functions.input.spec.ts | 20 ---
.../input/inputs/new/init-functions.input.ts | 6 -
src/lib/schematics/nanoforge.collection.ts | 27 +---
25 files changed, 238 insertions(+), 186 deletions(-)
create mode 100644 libs/schematics/src/libs/project/files/js/__dot__gitignore
rename libs/schematics/src/libs/{project/files/ts => workspace/files/js}/__dot__env (100%)
create mode 100644 libs/schematics/src/libs/workspace/files/ts/__dot__env
delete mode 100644 src/lib/input/inputs/new/init-functions.input.spec.ts
delete mode 100644 src/lib/input/inputs/new/init-functions.input.ts
diff --git a/libs/schematics/src/libs/project/files/js/Dockerfile b/libs/schematics/src/libs/project/files/js/Dockerfile
index 2badd74..7b95dad 100644
--- a/libs/schematics/src/libs/project/files/js/Dockerfile
+++ b/libs/schematics/src/libs/project/files/js/Dockerfile
@@ -1,10 +1,7 @@
-<% if (packageManager === "bun") { %>
-FROM oven/bun:1.3 AS base
-<% } else if (packageManager === "pnpm") { %>
-FROM ghcr.io/pnpm/pnpm:12 AS base
+<% if (packageManager === "bun") { %>FROM oven/bun:1.3 AS base
+<% } else if (packageManager === "pnpm") { %>FROM ghcr.io/pnpm/pnpm:12 AS base
RUN pnpm runtime set node 26 -g
-<% } else { %>
-FROM node:26-slim AS base
+<% } else { %>FROM node:26-slim AS base
<% } %><% if (packageManager === "yarn") { %>
RUN npm install -g yarn
<% } %>
diff --git a/libs/schematics/src/libs/project/files/js/__dot__gitignore b/libs/schematics/src/libs/project/files/js/__dot__gitignore
new file mode 100644
index 0000000..ba6da87
--- /dev/null
+++ b/libs/schematics/src/libs/project/files/js/__dot__gitignore
@@ -0,0 +1,134 @@
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### JetBrains template
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# File-based project format
+*.iws
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+### NanoForge template
+# Compilation directories
+.nanoforge/client
+.nanoforge/server
diff --git a/libs/schematics/src/libs/project/files/js/jsconfig.json b/libs/schematics/src/libs/project/files/js/jsconfig.json
index 3e2e75e..5653881 100644
--- a/libs/schematics/src/libs/project/files/js/jsconfig.json
+++ b/libs/schematics/src/libs/project/files/js/jsconfig.json
@@ -1,6 +1,36 @@
{
- "$schema": "https://json.schemastore.org/jsconfig.json",
- "extends": "../../jsconfig.json",
- "include": ["src/**/*.js"],
- "exclude": ["node_modules", "dist", "../../.nanoforge"]
+ "$schema": "https://json.schemastore.org/jsconfig.json",<% if (workspace) { %>
+ "extends": "../../jsconfig.json",<% } else { %>
+ "compilerOptions": {
+ "allowUnreachableCode": <%= !strict %>,
+ "allowUnusedLabels": <%= !strict %>,
+ "exactOptionalPropertyTypes": <%= strict %>,
+ "noFallthroughCasesInSwitch": <%= strict %>,
+ "noImplicitOverride": <%= strict %>,
+ "noImplicitReturns": <%= strict %>,
+ "noPropertyAccessFromIndexSignature": <%= !strict %>,
+ "noUncheckedIndexedAccess": <%= strict %>,
+ "noUnusedLocals": <%= strict %>,
+ "noUnusedParameters": <%= strict %>,
+
+ "allowArbitraryExtensions": false,
+ "allowImportingTsExtensions": false,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "resolvePackageJsonExports": true,
+ "resolvePackageJsonImports": true,
+
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": false,
+ "forceConsistentCasingInFileNames": true,
+ "isolatedModules": true,
+
+ "experimentalDecorators": true,
+ "lib": ["ESNext", "esnext.disposable", "DOM"],
+ "target": "ESNext",
+ "useDefineForClassFields": true
+ },<% } %>
+ "include": ["src"],
+ "exclude": ["node_modules", ".nanoforge", "dist"]
}
diff --git a/libs/schematics/src/libs/project/files/js/nanoforge.config.js b/libs/schematics/src/libs/project/files/js/nanoforge.config.js
index f9f079f..b05cbcb 100644
--- a/libs/schematics/src/libs/project/files/js/nanoforge.config.js
+++ b/libs/schematics/src/libs/project/files/js/nanoforge.config.js
@@ -1,6 +1,5 @@
import { defineConfig } from "nanoforge/config";
export default defineConfig({
- type: "<%= part %>",<% if (libs.length) { %>
- libs: [<% libs.forEach(function (lib, i) { %>"<%= lib %>"<% if (i < libs.length - 1) { %>, <% } %><% }); %>],<% } %>
+ type: "<%= part %>",
});
diff --git a/libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template b/libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template
index 2312ae9..1e7271b 100644
--- a/libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template
+++ b/libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template
@@ -1,3 +1,3 @@
<% if (allowBuilds.length > 0) { %>allowBuilds:<% allowBuilds.forEach(function (pkg) { %>
<%= pkg %>: true<% }); %>
-<% } %>
+<% } %>
\ No newline at end of file
diff --git a/libs/schematics/src/libs/project/files/ts/Dockerfile b/libs/schematics/src/libs/project/files/ts/Dockerfile
index 70fed63..3f3e585 100644
--- a/libs/schematics/src/libs/project/files/ts/Dockerfile
+++ b/libs/schematics/src/libs/project/files/ts/Dockerfile
@@ -1,10 +1,7 @@
-<% if (packageManager === "bun") { %>
-FROM oven/bun:1.3 AS base
-<% } else if (packageManager === "pnpm") { %>
-FROM ghcr.io/pnpm/pnpm:12 AS base
+<% if (packageManager === "bun") { %>FROM oven/bun:1.3 AS base
+<% } else if (packageManager === "pnpm") { %>FROM ghcr.io/pnpm/pnpm:12 AS base
RUN pnpm runtime set node 26 -g
-<% } else { %>
-FROM node:26-slim AS base
+<% } else { %>FROM node:26-slim AS base
<% } %><% if (packageManager === "yarn") { %>
RUN npm install -g yarn
<% } %>
diff --git a/libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template b/libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template
index 2312ae9..1e7271b 100644
--- a/libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template
+++ b/libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template
@@ -1,3 +1,3 @@
<% if (allowBuilds.length > 0) { %>allowBuilds:<% allowBuilds.forEach(function (pkg) { %>
<%= pkg %>: true<% }); %>
-<% } %>
+<% } %>
\ No newline at end of file
diff --git a/libs/schematics/src/libs/project/files/ts/tsconfig.json b/libs/schematics/src/libs/project/files/ts/tsconfig.json
index 5aef325..70b5f64 100644
--- a/libs/schematics/src/libs/project/files/ts/tsconfig.json
+++ b/libs/schematics/src/libs/project/files/ts/tsconfig.json
@@ -1,5 +1,6 @@
{
- "$schema": "https://json.schemastore.org/tsconfig.json",
+ "$schema": "https://json.schemastore.org/tsconfig.json",<% if (workspace) { %>
+ "extends": "../../tsconfig.json",<% } else { %>
// Mapped from https://www.typescriptlang.org/tsconfig
"compilerOptions": {
// Type Checking
@@ -35,7 +36,7 @@
"lib": ["ESNext", "esnext.disposable", "DOM"],
"target": "ESNext",
"useDefineForClassFields": true,
- },
+ },<% } %>
"include": ["src"],
- "exclude": ["node_modules", ".nanoforge"]
+ "exclude": ["node_modules", ".nanoforge", "dist"]
}
diff --git a/libs/schematics/src/libs/project/project.factory.ts b/libs/schematics/src/libs/project/project.factory.ts
index 36df6ba..dff4730 100644
--- a/libs/schematics/src/libs/project/project.factory.ts
+++ b/libs/schematics/src/libs/project/project.factory.ts
@@ -79,12 +79,10 @@ const transform = async (schema: ProjectSchema): Promise => {
inputVersion: depVersions.input,
networkVersion: depVersions.network,
nanoforgeVersion: depVersions.nanoforge,
- initFunctions: schema.initFunctions ?? false,
- hasServer: schema.hasServer ?? false,
+ hasServer: schema.hasServer,
docker: schema.docker ?? false,
editor: schema.editor ?? false,
- libs: schema.libs ?? [],
- allowBuilds: workspace ? [] : (schema.allowBuilds ?? []),
+ allowBuilds: workspace ? [] : ["bun"],
};
};
diff --git a/libs/schematics/src/libs/project/project.options.d.ts b/libs/schematics/src/libs/project/project.options.d.ts
index 9722fa2..ad4a4ab 100644
--- a/libs/schematics/src/libs/project/project.options.d.ts
+++ b/libs/schematics/src/libs/project/project.options.d.ts
@@ -57,11 +57,6 @@ export interface ProjectOptions {
/** Resolved version/protocol for the nanoforge engine package */
nanoforgeVersion: string;
- /**
- * Add init functions to the project
- */
- initFunctions: boolean;
-
/**
* Only meaningful when `part` is "client": whether the client syncs its
* entity position from a companion server instead of owning it locally.
@@ -79,11 +74,6 @@ export interface ProjectOptions {
*/
editor: boolean;
- /**
- * Relative paths to shared libs this project depends on
- */
- libs: string[];
-
/**
* Package names allowed to run install/build scripts. Only rendered when
* `workspace` is false — see {@link ProjectSchema.allowBuilds}.
diff --git a/libs/schematics/src/libs/project/project.schema.d.ts b/libs/schematics/src/libs/project/project.schema.d.ts
index 29e10f4..5f00a9c 100644
--- a/libs/schematics/src/libs/project/project.schema.d.ts
+++ b/libs/schematics/src/libs/project/project.schema.d.ts
@@ -52,11 +52,6 @@ export interface ProjectSchema {
*/
docker: boolean;
- /**
- * Add init functions to the project
- */
- initFunctions: boolean;
-
/**
* Only meaningful when `part` is "client": whether the client syncs its
* entity position from a companion server instead of owning it locally.
@@ -69,19 +64,4 @@ export interface ProjectSchema {
* Add editor dependencies
*/
editor: boolean;
-
- /**
- * Relative paths to shared libs this project depends on
- */
- libs: string[];
-
- /**
- * Package names allowed to run install/build scripts. Only applies to a
- * standalone project (workspace: false) — a project generated inside a
- * workspace relies on the workspace root's own `allowBuilds` instead.
- * Written to pnpm-workspace.yaml's `allowBuilds` map (pnpm), package.json's
- * `allowScripts` map (npm) or `trustedDependencies` array (bun). Ignored
- * for yarn.
- */
- allowBuilds: string[];
}
diff --git a/libs/schematics/src/libs/workspace/files/js/Dockerfile b/libs/schematics/src/libs/workspace/files/js/Dockerfile
index df78c19..c28346e 100644
--- a/libs/schematics/src/libs/workspace/files/js/Dockerfile
+++ b/libs/schematics/src/libs/workspace/files/js/Dockerfile
@@ -1,10 +1,7 @@
-<% if (packageManager === "bun") { %>
-FROM oven/bun:1.3 AS base
-<% } else if (packageManager === "pnpm") { %>
-FROM ghcr.io/pnpm/pnpm:12 AS base
+<% if (packageManager === "bun") { %>FROM oven/bun:1.3 AS base
+<% } else if (packageManager === "pnpm") { %>FROM ghcr.io/pnpm/pnpm:12 AS base
RUN pnpm runtime set node 26 -g
-<% } else { %>
-FROM node:26-slim AS base
+<% } else { %>FROM node:26-slim AS base
<% } %><% if (packageManager === "yarn") { %>
RUN npm install -g yarn
<% } %>
diff --git a/libs/schematics/src/libs/project/files/ts/__dot__env b/libs/schematics/src/libs/workspace/files/js/__dot__env
similarity index 100%
rename from libs/schematics/src/libs/project/files/ts/__dot__env
rename to libs/schematics/src/libs/workspace/files/js/__dot__env
diff --git a/libs/schematics/src/libs/workspace/files/js/jsconfig.json b/libs/schematics/src/libs/workspace/files/js/jsconfig.json
index f2e8ecb..4b7a35e 100644
--- a/libs/schematics/src/libs/workspace/files/js/jsconfig.json
+++ b/libs/schematics/src/libs/workspace/files/js/jsconfig.json
@@ -30,6 +30,5 @@
"target": "ESNext",
"useDefineForClassFields": true
},
- "include": ["apps/*/src/**/*.js"],
- "exclude": ["node_modules", ".nanoforge"]
+ "exclude": ["node_modules", ".nanoforge", "dist"]
}
diff --git a/libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template b/libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template
index e23904e..34f3b9c 100644
--- a/libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template
+++ b/libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template
@@ -3,4 +3,4 @@ packages:
<% if (allowBuilds.length > 0) { %>
allowBuilds:<% allowBuilds.forEach(function (pkg) { %>
<%= pkg %>: true<% }); %>
-<% } %>
+<% } %>
\ No newline at end of file
diff --git a/libs/schematics/src/libs/workspace/files/ts/Dockerfile b/libs/schematics/src/libs/workspace/files/ts/Dockerfile
index df78c19..c28346e 100644
--- a/libs/schematics/src/libs/workspace/files/ts/Dockerfile
+++ b/libs/schematics/src/libs/workspace/files/ts/Dockerfile
@@ -1,10 +1,7 @@
-<% if (packageManager === "bun") { %>
-FROM oven/bun:1.3 AS base
-<% } else if (packageManager === "pnpm") { %>
-FROM ghcr.io/pnpm/pnpm:12 AS base
+<% if (packageManager === "bun") { %>FROM oven/bun:1.3 AS base
+<% } else if (packageManager === "pnpm") { %>FROM ghcr.io/pnpm/pnpm:12 AS base
RUN pnpm runtime set node 26 -g
-<% } else { %>
-FROM node:26-slim AS base
+<% } else { %>FROM node:26-slim AS base
<% } %><% if (packageManager === "yarn") { %>
RUN npm install -g yarn
<% } %>
diff --git a/libs/schematics/src/libs/workspace/files/ts/__dot__env b/libs/schematics/src/libs/workspace/files/ts/__dot__env
new file mode 100644
index 0000000..75b4595
--- /dev/null
+++ b/libs/schematics/src/libs/workspace/files/ts/__dot__env
@@ -0,0 +1,6 @@
+NANOFORGE_CLIENT_SERVER_TCP_PORT=4444
+NANOFORGE_CLIENT_SERVER_UDP_PORT=4445
+NANOFORGE_CLIENT_SERVER_ADDRESS=127.0.0.1
+
+NANOFORGE_SERVER_LISTENING_TCP_PORT=4444
+NANOFORGE_SERVER_LISTENING_UDP_PORT=4445
diff --git a/libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template b/libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template
index e23904e..34f3b9c 100644
--- a/libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template
+++ b/libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template
@@ -3,4 +3,4 @@ packages:
<% if (allowBuilds.length > 0) { %>
allowBuilds:<% allowBuilds.forEach(function (pkg) { %>
<%= pkg %>: true<% }); %>
-<% } %>
+<% } %>
\ No newline at end of file
diff --git a/libs/schematics/src/libs/workspace/files/ts/tsconfig.json b/libs/schematics/src/libs/workspace/files/ts/tsconfig.json
index 4ccd69c..5156549 100644
--- a/libs/schematics/src/libs/workspace/files/ts/tsconfig.json
+++ b/libs/schematics/src/libs/workspace/files/ts/tsconfig.json
@@ -36,5 +36,5 @@
"target": "ESNext",
"useDefineForClassFields": true,
},
- "exclude": ["node_modules", ".nanoforge"]
+ "exclude": ["node_modules", ".nanoforge", "dist"]
}
diff --git a/libs/schematics/src/utils/registry.ts b/libs/schematics/src/utils/registry.ts
index ab784ec..e3de420 100644
--- a/libs/schematics/src/utils/registry.ts
+++ b/libs/schematics/src/utils/registry.ts
@@ -76,7 +76,7 @@ export const fetchTrustedVersion = async (
.map(([version]) => version)
.sort(compareVersions);
- return eligible.at(-1) ?? fallback;
+ return eligible.at(-1) ? `^${eligible.at(-1)}` : fallback;
} catch {
return fallback;
} finally {
diff --git a/src/action/actions/new.action.ts b/src/action/actions/new.action.ts
index 6220c46..bab6fb8 100644
--- a/src/action/actions/new.action.ts
+++ b/src/action/actions/new.action.ts
@@ -6,7 +6,6 @@ import {
getDirectoryInput,
getEditorInput,
getNewDockerOrAsk,
- getNewInitFunctionsWithDefault,
getNewLanguageInputOrAsk,
getNewLintInput,
getNewNameInputOrAsk,
@@ -31,8 +30,7 @@ interface NewValues {
packageManager: string;
language: string;
strict: boolean;
- server: boolean;
- initFunctions: boolean;
+ multiplayer: boolean;
skipInstall: boolean;
docker: boolean;
lint: boolean;
@@ -71,8 +69,7 @@ export class NewAction extends AbstractAction {
packageManager: await getNewPackageManagerInputOrAsk(inputs),
language: await getNewLanguageInputOrAsk(inputs),
strict: await getNewStrictOrAsk(inputs),
- server: await getNewServerOrAsk(inputs),
- initFunctions: getNewInitFunctionsWithDefault(inputs),
+ multiplayer: await getNewServerOrAsk(inputs),
skipInstall: await getNewSkipInstallOrAsk(inputs),
docker: await getNewDockerOrAsk(inputs),
lint: getNewLintInput(inputs),
@@ -94,91 +91,63 @@ export class NewAction extends AbstractAction {
console.info(Messages.NEW_GENERATION_START);
console.info();
- await this.generateApplication(collection, values);
- await this.generateConfiguration(collection, values);
- await this.generateClientParts(collection, values);
- if (values.docker) await this.generateDocker(collection, values);
-
- if (values.server) {
- await this.generateServerParts(collection, values);
+ if (values.multiplayer) {
+ await this.generateWorkspace(collection, values);
+ await this.generateClientProject(collection, values);
+ await this.generateServerProject(collection, values);
+ } else {
+ await this.generateClientProject(collection, values);
}
}
- private generateApplication(
+ private generateWorkspace(
collection: ReturnType,
values: NewValues,
) {
- return executeSchematic("Application", collection, "application", {
+ return executeSchematic("Workspace", collection, "workspace", {
name: values.name,
directory: values.directory,
- packageManager: values.packageManager,
language: values.language,
strict: values.strict,
- server: values.server,
- lint: values.lint,
- editor: values.editor,
- });
- }
-
- private generateConfiguration(
- collection: ReturnType,
- values: NewValues,
- ) {
- return executeSchematic("Configuration", collection, "configuration", {
- name: values.name,
- directory: values.directory,
- server: values.server,
- language: values.language,
- initFunctions: values.initFunctions,
- });
- }
-
- private async generateClientParts(
- collection: ReturnType,
- values: NewValues,
- ) {
- const partOptions = this.partOptions(values, "client");
-
- await executeSchematic("Client base", collection, "part-base", {
- ...partOptions,
- server: values.server,
- });
- await executeSchematic("Client main file", collection, "part-main", {
- ...partOptions,
+ packageManager: values.packageManager,
+ docker: values.docker,
});
}
- private async generateServerParts(
+ private generateClientProject(
collection: ReturnType,
values: NewValues,
) {
- const partOptions = this.partOptions(values, "server");
-
- await executeSchematic("Server base", collection, "part-base", {
- ...partOptions,
- server: values.server,
- });
- await executeSchematic("Server main file", collection, "part-main", {
- ...partOptions,
+ return executeSchematic("Client project", collection, "project", {
+ ...this.projectOptions(values, "client"),
+ hasServer: values.multiplayer,
});
}
- private async generateDocker(
+ private generateServerProject(
collection: ReturnType,
values: NewValues,
) {
- await executeSchematic("Docker", collection, "docker", {
- directory: values.directory,
- packageManager: values.packageManager,
- });
+ return executeSchematic(
+ "Server project",
+ collection,
+ "project",
+ this.projectOptions(values, "server"),
+ );
}
- private partOptions(values: NewValues, part: "client" | "server") {
+ private projectOptions(values: NewValues, part: "client" | "server") {
return {
part,
- directory: values.directory,
+ workspaceName: values.multiplayer ? values.name : undefined,
+ name: values.multiplayer ? part : values.name,
+ directory: values.multiplayer ? join(values.directory, "apps", part) : values.directory,
language: values.language,
- initFunctions: values.initFunctions,
+ strict: values.strict,
+ packageManager: values.packageManager,
+ workspace: values.multiplayer,
+ docker: values.docker,
+ editor: values.editor,
};
}
diff --git a/src/lib/input/inputs/new/index.ts b/src/lib/input/inputs/new/index.ts
index 9532ed4..af35a3a 100644
--- a/src/lib/input/inputs/new/index.ts
+++ b/src/lib/input/inputs/new/index.ts
@@ -1,5 +1,4 @@
export * from "./docker.input";
-export * from "./init-functions.input";
export * from "./language.input";
export * from "./lint.input";
export * from "./package-manager.input";
diff --git a/src/lib/input/inputs/new/init-functions.input.spec.ts b/src/lib/input/inputs/new/init-functions.input.spec.ts
deleted file mode 100644
index 15c9d53..0000000
--- a/src/lib/input/inputs/new/init-functions.input.spec.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { type Input } from "../../input.type";
-import { getNewInitFunctionsWithDefault } from "./init-functions.input";
-
-const createInput = (entries: [string, any][]): Input => {
- return new Map(entries.map(([key, value]) => [key, { value }]));
-};
-
-describe("getNewInitFunctionsWithDefault", () => {
- it("should return the value when provided", () => {
- const input = createInput([["initFunctions", true]]);
- expect(getNewInitFunctionsWithDefault(input)).toBe(true);
- });
-
- it("should return false as default when missing", () => {
- const input = createInput([]);
- expect(getNewInitFunctionsWithDefault(input)).toBe(false);
- });
-});
diff --git a/src/lib/input/inputs/new/init-functions.input.ts b/src/lib/input/inputs/new/init-functions.input.ts
deleted file mode 100644
index 5c20e74..0000000
--- a/src/lib/input/inputs/new/init-functions.input.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { getBooleanInputWithDefault } from "../../base-inputs";
-import { type Input } from "../../input.type";
-
-export const getNewInitFunctionsWithDefault = (inputs: Input) => {
- return getBooleanInputWithDefault(inputs, "initFunctions", false);
-};
diff --git a/src/lib/schematics/nanoforge.collection.ts b/src/lib/schematics/nanoforge.collection.ts
index 4989cec..16731ff 100644
--- a/src/lib/schematics/nanoforge.collection.ts
+++ b/src/lib/schematics/nanoforge.collection.ts
@@ -14,29 +14,14 @@ export interface Schematic {
export class NanoforgeCollection extends AbstractCollection {
private static schematics: Schematic[] = [
{
- name: "application",
- alias: "application",
- description: "Generate a new application",
+ name: "project",
+ alias: "project",
+ description: "Generate a new project",
},
{
- name: "configuration",
- alias: "config",
- description: "Generate a CLI configuration file",
- },
- {
- name: "part-base",
- alias: "base",
- description: "Generate a NanoForge Part Base",
- },
- {
- name: "part-main",
- alias: "main",
- description: "Generate a NanoForge Part Main file",
- },
- {
- name: "docker",
- alias: "docker",
- description: "Generate a Dockerfile for the application",
+ name: "workspace",
+ alias: "workspace",
+ description: "Generate a new workspace",
},
{
name: "component",
From 388c258cd14eca35a26e3bc7f519aa78ac069563 Mon Sep 17 00:00:00 2001
From: Exelo
Date: Fri, 18 Sep 2026 17:57:13 +0900
Subject: [PATCH 07/15] refactor: change build cmd to new archi (#219)
* refactor(config): change the config handling to feat with the new archi
* refactor(cli): change the build command to fit the new architecture and improve
* fix: add good editor entry file
---
e2e/cli-build.test.ts | 5 +
e2e/cli-create.test.ts | 7 +
e2e/cli-help.test.ts | 1 -
libs/config/src/defaults/default-configs.ts | 22 ++-
libs/config/src/index.ts | 2 +
libs/config/src/merge/deep-merge.ts | 4 +-
libs/config/src/merge/resolve-config.ts | 11 +-
libs/config/src/parse/parse-config.ts | 3 +-
libs/config/src/parse/validate-config.ts | 21 +++
libs/config/src/types/client-config.type.ts | 19 +-
libs/config/src/types/index.ts | 8 +-
libs/config/src/types/mixins.type.ts | 19 ++
libs/config/src/types/server-config.type.ts | 9 +-
libs/config/src/types/utils.type.ts | 3 +
.../libs/project/files/js/nanoforge.config.js | 4 +
package.json | 5 +-
pnpm-lock.yaml | 169 +++---------------
src/action/actions/build.action.ts | 130 +++++++-------
src/command/commands/build.command.ts | 5 -
src/lib/config/config-defaults.ts | 45 -----
src/lib/config/config-loader.spec.ts | 106 +++--------
src/lib/config/config-loader.ts | 64 ++-----
src/lib/config/config.type.ts | 138 --------------
src/lib/config/index.ts | 6 +-
.../config/workspace-config-parser.spec.ts | 133 ++++++++++++++
src/lib/config/workspace-config-parser.ts | 87 +++++++++
src/lib/config/workspace-config.types.ts | 24 +++
src/lib/config/workspace-loader.ts | 28 +++
28 files changed, 535 insertions(+), 543 deletions(-)
create mode 100644 libs/config/src/types/utils.type.ts
delete mode 100644 src/lib/config/config-defaults.ts
delete mode 100644 src/lib/config/config.type.ts
create mode 100644 src/lib/config/workspace-config-parser.spec.ts
create mode 100644 src/lib/config/workspace-config-parser.ts
create mode 100644 src/lib/config/workspace-config.types.ts
create mode 100644 src/lib/config/workspace-loader.ts
diff --git a/e2e/cli-build.test.ts b/e2e/cli-build.test.ts
index 82da5d5..95ba92d 100644
--- a/e2e/cli-build.test.ts
+++ b/e2e/cli-build.test.ts
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { runCli } from "./helpers/run-cli";
+import { writeProjectConfig } from "./helpers/write-project-config";
const tmpDir = resolve(__dirname, "../.tmp-e2e-build");
@@ -38,6 +39,8 @@ describe("nf build (TypeScript, no server)", () => {
"-d",
projectDir,
]);
+
+ writeProjectConfig(appDir, { name: "build-app", language: "ts", server: false });
});
it("should run the build command", async () => {
@@ -145,6 +148,8 @@ describe("nf build (TypeScript, with server)", () => {
"-d",
projectDir,
]);
+
+ writeProjectConfig(appDir, { name: "build-server-app", language: "ts", server: true });
});
it("should run the build command with server enabled", async () => {
diff --git a/e2e/cli-create.test.ts b/e2e/cli-create.test.ts
index 4e09265..b9f9cdb 100644
--- a/e2e/cli-create.test.ts
+++ b/e2e/cli-create.test.ts
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { runCli } from "./helpers/run-cli";
+import { writeProjectConfig } from "./helpers/write-project-config";
const tmpDir = resolve(__dirname, "../.tmp-e2e-create");
@@ -38,6 +39,8 @@ describe("nf create (TypeScript, client component)", () => {
"-d",
projectDir,
]);
+
+ writeProjectConfig(appDir, { name: "create-app", language: "ts", server: false });
});
it("should create a component successfully", async () => {
@@ -133,6 +136,8 @@ describe("nf create (TypeScript, server component)", () => {
"-d",
projectDir,
]);
+
+ writeProjectConfig(appDir, { name: "create-server-app", language: "ts", server: true });
});
it("should create a server component successfully", async () => {
@@ -213,6 +218,8 @@ describe("nf create (JavaScript)", () => {
"-d",
projectDir,
]);
+
+ writeProjectConfig(appDir, { name: "create-js-app", language: "js", server: false });
});
it("should create a JavaScript component successfully", async () => {
diff --git a/e2e/cli-help.test.ts b/e2e/cli-help.test.ts
index 6171f70..f2b8da0 100644
--- a/e2e/cli-help.test.ts
+++ b/e2e/cli-help.test.ts
@@ -87,7 +87,6 @@ describe("nf start --help", () => {
expect(exitCode).toBe(0);
expect(stdout).toContain("start your game");
expect(stdout).toContain("--directory");
- expect(stdout).toContain("--config");
expect(stdout).toContain("--port");
expect(stdout).toContain("--watch");
});
diff --git a/libs/config/src/defaults/default-configs.ts b/libs/config/src/defaults/default-configs.ts
index 26d216c..2b5c59d 100644
--- a/libs/config/src/defaults/default-configs.ts
+++ b/libs/config/src/defaults/default-configs.ts
@@ -1,10 +1,11 @@
import type { ClientConfig, LibConfig, ServerConfig, WorkspaceConfig } from "../types";
+import type { DeepRequired } from "../types/utils.type";
-export const defaultWorkspaceConfig: Omit = {
+export const defaultWorkspaceConfig: DeepRequired> = {
packages: [],
};
-export const defaultLibConfig: Omit = {
+export const defaultLibConfig: DeepRequired> = {
dir: {
assets: "assets",
shared: "shared",
@@ -15,7 +16,7 @@ export const defaultLibConfig: Omit = {
},
};
-export const defaultClientConfig: Omit = {
+export const defaultClientConfig: DeepRequired> = {
entryFile: "src/main.ts",
out: {
dir: "dist",
@@ -31,10 +32,19 @@ export const defaultClientConfig: Omit = {
scenes: "src/scenes",
},
+ editor: {
+ entryFile: "src/main.ts",
+ },
+
+ port: "3000",
+ ssl: {
+ enable: false,
+ },
+
libs: [],
};
-export const defaultServerConfig: Omit = {
+export const defaultServerConfig: DeepRequired> = {
entryFile: "src/main.ts",
out: {
dir: "dist",
@@ -50,5 +60,9 @@ export const defaultServerConfig: Omit = {
scenes: "src/scenes",
},
+ editor: {
+ entryFile: "src/main.ts",
+ },
+
libs: [],
};
diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts
index c4d0991..30c1b37 100644
--- a/libs/config/src/index.ts
+++ b/libs/config/src/index.ts
@@ -20,9 +20,11 @@ export type {
BuildableConfig,
ClientConfig,
ContainLibConfig,
+ EditorConfig,
LibConfig,
NanoforgeConfig,
ServerConfig,
SourceableConfig,
+ SslConfig,
WorkspaceConfig,
} from "./types";
diff --git a/libs/config/src/merge/deep-merge.ts b/libs/config/src/merge/deep-merge.ts
index 2d20d88..0d8d912 100644
--- a/libs/config/src/merge/deep-merge.ts
+++ b/libs/config/src/merge/deep-merge.ts
@@ -1,3 +1,5 @@
+import type { DeepPartial } from "../types/utils.type";
+
const isPlainObject = (value: unknown): value is Record =>
typeof value === "object" && value !== null && !Array.isArray(value);
@@ -31,5 +33,5 @@ const mergeUnknown = (
*/
export const deepMerge = >(
defaults: T,
- override: Partial,
+ override: DeepPartial,
): T => mergeUnknown(defaults, override as Record) as T;
diff --git a/libs/config/src/merge/resolve-config.ts b/libs/config/src/merge/resolve-config.ts
index 69ebf7b..6440b3b 100644
--- a/libs/config/src/merge/resolve-config.ts
+++ b/libs/config/src/merge/resolve-config.ts
@@ -11,24 +11,25 @@ import type {
ServerConfig,
WorkspaceConfig,
} from "../types";
+import type { DeepRequired } from "../types/utils.type";
import { deepMerge } from "./deep-merge";
-export const resolveWorkspaceConfig = (config: WorkspaceConfig): WorkspaceConfig => ({
+export const resolveWorkspaceConfig = (config: WorkspaceConfig): DeepRequired => ({
...deepMerge(defaultWorkspaceConfig, config),
type: "workspace",
});
-export const resolveLibConfig = (config: LibConfig): LibConfig => ({
+export const resolveLibConfig = (config: LibConfig): DeepRequired => ({
...deepMerge(defaultLibConfig, config),
type: "lib",
});
-export const resolveClientConfig = (config: ClientConfig): ClientConfig => ({
+export const resolveClientConfig = (config: ClientConfig): DeepRequired => ({
...deepMerge(defaultClientConfig, config),
type: "client",
});
-export const resolveServerConfig = (config: ServerConfig): ServerConfig => ({
+export const resolveServerConfig = (config: ServerConfig): DeepRequired => ({
...deepMerge(defaultServerConfig, config),
type: "server",
});
@@ -37,7 +38,7 @@ export const resolveServerConfig = (config: ServerConfig): ServerConfig => ({
* Merges a `nanoforge.config.ts` config against the defaults matching its
* `type`.
*/
-export const resolveConfig = (config: NanoforgeConfig): NanoforgeConfig => {
+export const resolveConfig = (config: NanoforgeConfig): DeepRequired => {
switch (config.type) {
case "workspace":
return resolveWorkspaceConfig(config);
diff --git a/libs/config/src/parse/parse-config.ts b/libs/config/src/parse/parse-config.ts
index d3ca6b2..466490e 100644
--- a/libs/config/src/parse/parse-config.ts
+++ b/libs/config/src/parse/parse-config.ts
@@ -1,9 +1,10 @@
import { resolveConfig } from "../merge/resolve-config";
import type { NanoforgeConfig } from "../types";
+import type { DeepRequired } from "../types/utils.type";
import { loadConfigModule } from "./load-config-module";
import { assertNanoforgeConfig } from "./validate-config";
-export const parseConfig = async (path: string | URL): Promise => {
+export const parseConfig = async (path: string | URL): Promise> => {
const raw = await loadConfigModule(path);
assertNanoforgeConfig(raw, String(path));
return resolveConfig(raw);
diff --git a/libs/config/src/parse/validate-config.ts b/libs/config/src/parse/validate-config.ts
index c504586..5132f25 100644
--- a/libs/config/src/parse/validate-config.ts
+++ b/libs/config/src/parse/validate-config.ts
@@ -17,6 +17,23 @@ const containLibConfigShape = {
libs: z.array(z.string()).optional(),
};
+const editorConfigShape = {
+ editor: z
+ .object({
+ entryFile: z.string().optional(),
+ })
+ .optional(),
+};
+
+const sslConfigShape = {
+ ssl: z
+ .union([
+ z.object({ enable: z.literal(false).optional() }),
+ z.object({ enable: z.literal(true), cert: z.string(), key: z.string() }),
+ ])
+ .optional(),
+};
+
const sourceableConfigShape = {
dir: z
.object({
@@ -52,6 +69,9 @@ const clientConfigSchema = z.object({
...sourceableConfigShape,
...buildableConfigShape,
...containLibConfigShape,
+ ...editorConfigShape,
+ ...sslConfigShape,
+ port: z.string().optional(),
});
const serverConfigSchema = z.object({
@@ -59,6 +79,7 @@ const serverConfigSchema = z.object({
...sourceableConfigShape,
...buildableConfigShape,
...containLibConfigShape,
+ ...editorConfigShape,
});
const nanoforgeConfigSchema = z.discriminatedUnion("type", [
diff --git a/libs/config/src/types/client-config.type.ts b/libs/config/src/types/client-config.type.ts
index bcacd00..070423c 100644
--- a/libs/config/src/types/client-config.type.ts
+++ b/libs/config/src/types/client-config.type.ts
@@ -1,5 +1,20 @@
import type { BaseConfig } from "./base-config.type";
-import type { BuildableConfig, ContainLibConfig, SourceableConfig } from "./mixins.type";
+import type {
+ BuildableConfig,
+ ContainLibConfig,
+ EditorConfig,
+ SourceableConfig,
+ SslConfig,
+} from "./mixins.type";
export interface ClientConfig
- extends BaseConfig<"client">, SourceableConfig, BuildableConfig, ContainLibConfig {}
+ extends
+ BaseConfig<"client">,
+ SourceableConfig,
+ BuildableConfig,
+ ContainLibConfig,
+ EditorConfig,
+ SslConfig {
+ /** Port the client dev-server loader listens on. */
+ port?: string;
+}
diff --git a/libs/config/src/types/index.ts b/libs/config/src/types/index.ts
index fabef4c..bdc627f 100644
--- a/libs/config/src/types/index.ts
+++ b/libs/config/src/types/index.ts
@@ -6,7 +6,13 @@ import type { WorkspaceConfig } from "./workspace-config.type";
export type { BaseConfig } from "./base-config.type";
export type { ClientConfig } from "./client-config.type";
export type { LibConfig } from "./lib-config.type";
-export type { BuildableConfig, ContainLibConfig, SourceableConfig } from "./mixins.type";
+export type {
+ BuildableConfig,
+ ContainLibConfig,
+ EditorConfig,
+ SourceableConfig,
+ SslConfig,
+} from "./mixins.type";
export type { ServerConfig } from "./server-config.type";
export type { WorkspaceConfig } from "./workspace-config.type";
diff --git a/libs/config/src/types/mixins.type.ts b/libs/config/src/types/mixins.type.ts
index 9c2f32b..e780b97 100644
--- a/libs/config/src/types/mixins.type.ts
+++ b/libs/config/src/types/mixins.type.ts
@@ -10,6 +10,25 @@ export interface BuildableConfig {
};
}
+export interface EditorConfig {
+ editor?: {
+ /** Entry point used to build/run this project in editor mode (`--editor`). */
+ entryFile?: string;
+ };
+}
+
+export interface SslConfig {
+ ssl?:
+ | {
+ enable?: false;
+ }
+ | {
+ enable: true;
+ cert: string;
+ key: string;
+ };
+}
+
export interface SourceableConfig {
dir?: {
assets?: string;
diff --git a/libs/config/src/types/server-config.type.ts b/libs/config/src/types/server-config.type.ts
index 769e898..46ba676 100644
--- a/libs/config/src/types/server-config.type.ts
+++ b/libs/config/src/types/server-config.type.ts
@@ -1,5 +1,10 @@
import type { BaseConfig } from "./base-config.type";
-import type { BuildableConfig, ContainLibConfig, SourceableConfig } from "./mixins.type";
+import type {
+ BuildableConfig,
+ ContainLibConfig,
+ EditorConfig,
+ SourceableConfig,
+} from "./mixins.type";
export interface ServerConfig
- extends BaseConfig<"server">, SourceableConfig, BuildableConfig, ContainLibConfig {}
+ extends BaseConfig<"server">, SourceableConfig, BuildableConfig, ContainLibConfig, EditorConfig {}
diff --git a/libs/config/src/types/utils.type.ts b/libs/config/src/types/utils.type.ts
new file mode 100644
index 0000000..bc71828
--- /dev/null
+++ b/libs/config/src/types/utils.type.ts
@@ -0,0 +1,3 @@
+export type DeepRequired = T extends object ? { [P in keyof T]-?: DeepRequired } : T;
+
+export type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial } : T;
diff --git a/libs/schematics/src/libs/project/files/js/nanoforge.config.js b/libs/schematics/src/libs/project/files/js/nanoforge.config.js
index b05cbcb..fd98e6b 100644
--- a/libs/schematics/src/libs/project/files/js/nanoforge.config.js
+++ b/libs/schematics/src/libs/project/files/js/nanoforge.config.js
@@ -2,4 +2,8 @@ import { defineConfig } from "nanoforge/config";
export default defineConfig({
type: "<%= part %>",
+ entryFile: "src/main.js",<% if (libs.length) { %>
+ editor: {
+ entryFile: "src/main.js",
+ },
});
diff --git a/package.json b/package.json
index 834fb62..5e251ca 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,8 @@
"@angular-devkit/schematics": "catalog:schematics",
"@angular-devkit/schematics-cli": "catalog:schematics",
"@inquirer/prompts": "catalog:cli",
+ "@nanoforge-dev/config": "workspace:*",
+ "@nanoforge-dev/editor": "catalog:core",
"@nanoforge-dev/loader-client": "catalog:loader",
"@nanoforge-dev/loader-server": "catalog:loader",
"@nanoforge-dev/schematics": "workspace:*",
@@ -97,9 +99,6 @@
"typescript": "catalog:build",
"vitest": "catalog:test"
},
- "optionalDependencies": {
- "@nanoforge-dev/editor": "catalog:core"
- },
"packageManager": "pnpm@12.0.0",
"engines": {
"node": "26"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 420850e..a92edc3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -119,9 +119,6 @@ catalogs:
typescript:
specifier: ^6.0.3
version: 6.0.3
- unrun:
- specifier: ^0.3.0
- version: 0.3.1
ci:
'@commitlint/cli':
specifier: ^21.2.2
@@ -164,9 +161,6 @@ catalogs:
'@nanoforge-dev/editor':
specifier: ^0.1.4
version: 0.1.4
- '@types/bun':
- specifier: ^1.3.14
- version: 1.4.0
'@types/node':
specifier: ^26.4.0
version: 26.4.0
@@ -195,6 +189,12 @@ catalogs:
reflect-metadata:
specifier: ^0.2.2
version: 0.2.2
+ unrun:
+ specifier: ^0.3.0
+ version: 0.3.1
+ zod:
+ specifier: ^4.5.4
+ version: 4.5.4
lint:
'@nanoforge-dev/utils-eslint-config':
specifier: ^1.0.2
@@ -252,6 +252,12 @@ importers:
'@inquirer/prompts':
specifier: catalog:cli
version: 8.7.0(@types/node@26.4.0)
+ '@nanoforge-dev/config':
+ specifier: workspace:*
+ version: link:libs/config
+ '@nanoforge-dev/editor':
+ specifier: catalog:core
+ version: 0.1.4
'@nanoforge-dev/loader-client':
specifier: catalog:loader
version: 1.5.0
@@ -352,12 +358,15 @@ importers:
vitest:
specifier: catalog:test
version: 4.1.11(@types/node@26.4.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0))
- optionalDependencies:
- '@nanoforge-dev/editor':
- specifier: catalog:core
- version: 0.1.4
libs/config:
+ dependencies:
+ unrun:
+ specifier: catalog:libs
+ version: 0.3.1(synckit@0.11.13)
+ zod:
+ specifier: catalog:libs
+ version: 4.5.4
devDependencies:
'@favware/cliff-jumper':
specifier: catalog:ci
@@ -383,111 +392,10 @@ importers:
typescript:
specifier: catalog:build
version: 6.0.3
- unrun:
- specifier: catalog:build
- version: 0.3.1(synckit@0.11.13)
vitest:
specifier: catalog:test
version: 4.1.11(@types/node@26.4.0)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.0)(jiti@2.6.1)(yaml@2.9.0))
- libs/loader/client:
- dependencies:
- '@nanoforge-dev/loader-website':
- specifier: workspace:*
- version: link:../website
- bun:
- specifier: catalog:core
- version: 1.4.0
- commander:
- specifier: catalog:cli
- version: 15.0.0
- devDependencies:
- '@favware/cliff-jumper':
- specifier: catalog:ci
- version: 6.1.0
- '@nanoforge-dev/utils-eslint-config':
- specifier: catalog:lint
- version: 1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)
- '@nanoforge-dev/utils-prettier-config':
- specifier: catalog:lint
- version: 1.0.2
- '@trivago/prettier-plugin-sort-imports':
- specifier: catalog:lint
- version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2)
- '@types/bun':
- specifier: catalog:core
- version: 1.4.0
- eslint:
- specifier: catalog:lint
- version: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
- prettier:
- specifier: catalog:lint
- version: 3.9.6
- typescript:
- specifier: catalog:build
- version: 6.0.3
-
- libs/loader/server:
- dependencies:
- commander:
- specifier: catalog:cli
- version: 15.0.0
- devDependencies:
- '@favware/cliff-jumper':
- specifier: catalog:ci
- version: 6.1.0
- '@nanoforge-dev/utils-eslint-config':
- specifier: catalog:lint
- version: 1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)
- '@nanoforge-dev/utils-prettier-config':
- specifier: catalog:lint
- version: 1.0.2
- '@trivago/prettier-plugin-sort-imports':
- specifier: catalog:lint
- version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2)
- bun:
- specifier: catalog:core
- version: 1.4.0
- eslint:
- specifier: catalog:lint
- version: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
- prettier:
- specifier: catalog:lint
- version: 3.9.6
- typescript:
- specifier: catalog:build
- version: 6.0.3
-
- libs/loader/website:
- devDependencies:
- '@favware/cliff-jumper':
- specifier: catalog:ci
- version: 6.1.0
- '@nanoforge-dev/utils-eslint-config':
- specifier: catalog:lint
- version: 1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)
- '@nanoforge-dev/utils-prettier-config':
- specifier: catalog:lint
- version: 1.0.2
- '@trivago/prettier-plugin-sort-imports':
- specifier: catalog:lint
- version: 6.0.2(prettier@3.9.6)(supports-color@10.2.2)
- '@types/bun':
- specifier: catalog:core
- version: 1.4.0
- bun:
- specifier: catalog:core
- version: 1.4.0
- eslint:
- specifier: catalog:lint
- version: 10.9.1(jiti@2.6.1)(supports-color@10.2.2)
- prettier:
- specifier: catalog:lint
- version: 3.9.6
- typescript:
- specifier: catalog:build
- version: 6.0.3
-
libs/schematics:
dependencies:
'@angular-devkit/core':
@@ -1433,9 +1341,6 @@ packages:
cpu: [arm64]
os: [win32]
- '@types/bun@1.4.0':
- resolution: {integrity: sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ==}
-
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -1776,9 +1681,6 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
- bun-types@1.4.0:
- resolution: {integrity: sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q==}
-
bun@1.4.0:
resolution: {integrity: sha512-iRiFkc2W7UVpCyZXO9tod45TP9QCyN19fWqbpeN/jaM/K7uzeHYx/OSPsahMJazGKBgPsnxRt+4Jc43d8BcHZw==}
cpu: [arm64, x64]
@@ -3135,6 +3037,9 @@ packages:
yuku-parser@0.8.7:
resolution: {integrity: sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==}
+ zod@4.5.4:
+ resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==}
+
snapshots:
'@actions/core@3.0.1':
@@ -3623,7 +3528,6 @@ snapshots:
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.3
- optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
@@ -3655,7 +3559,6 @@ snapshots:
micromatch: 4.0.8
tar: 7.5.22
typescript: 6.0.3
- optional: true
'@nanoforge-dev/loader-client@1.5.0':
dependencies:
@@ -3963,10 +3866,6 @@ snapshots:
'@turbo/windows-arm64@2.10.12':
optional: true
- '@types/bun@1.4.0':
- dependencies:
- bun-types: 1.4.0
-
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -4285,11 +4184,6 @@ snapshots:
braces@3.0.3:
dependencies:
fill-range: 7.1.1
- optional: true
-
- bun-types@1.4.0:
- dependencies:
- '@types/node': 26.4.0
bun@1.4.0:
optionalDependencies:
@@ -4326,8 +4220,7 @@ snapshots:
dependencies:
readdirp: 5.1.1
- chownr@3.0.0:
- optional: true
+ chownr@3.0.0: {}
class-transformer@0.5.1: {}
@@ -4644,7 +4537,6 @@ snapshots:
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
- optional: true
find-index@0.1.1: {}
@@ -4776,8 +4668,7 @@ snapshots:
is-interactive@2.0.0: {}
- is-number@7.0.0:
- optional: true
+ is-number@7.0.0: {}
is-plain-obj@4.1.0: {}
@@ -4939,7 +4830,6 @@ snapshots:
dependencies:
braces: 3.0.3
picomatch: 2.3.2
- optional: true
mimic-function@5.0.1: {}
@@ -4958,7 +4848,6 @@ snapshots:
minizlib@3.1.0:
dependencies:
minipass: 7.1.3
- optional: true
ms@2.1.3: {}
@@ -5085,8 +4974,7 @@ snapshots:
picocolors@1.1.1: {}
- picomatch@2.3.2:
- optional: true
+ picomatch@2.3.2: {}
picomatch@4.0.5: {}
@@ -5262,7 +5150,6 @@ snapshots:
minipass: 7.1.3
minizlib: 3.1.0
yallist: 5.0.0
- optional: true
tinybench@2.9.0: {}
@@ -5280,7 +5167,6 @@ snapshots:
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
- optional: true
tree-kill@1.2.2: {}
@@ -5438,8 +5324,7 @@ snapshots:
y18n@5.0.8: {}
- yallist@5.0.0:
- optional: true
+ yallist@5.0.0: {}
yaml@2.9.0:
optional: true
@@ -5497,3 +5382,5 @@ snapshots:
'@yuku-parser/binding-linux-x64-musl': 0.8.7
'@yuku-parser/binding-win32-arm64': 0.8.7
'@yuku-parser/binding-win32-x64': 0.8.7
+
+ zod@4.5.4: {}
diff --git a/src/action/actions/build.action.ts b/src/action/actions/build.action.ts
index df8ae2f..f3c118f 100644
--- a/src/action/actions/build.action.ts
+++ b/src/action/actions/build.action.ts
@@ -1,7 +1,13 @@
+import { defaultClientConfig, defaultServerConfig } from "@nanoforge-dev/config";
import { watch } from "chokidar";
-import { dirname, join } from "node:path";
+import { dirname, join, relative } from "node:path";
-import { type Config } from "@lib/config";
+import {
+ type ResolvedProject,
+ type WorkspaceConfig,
+ parseWorkspaceConfig,
+ resolveProjects,
+} from "@lib/config";
import {
type Input,
getDirectoryInput,
@@ -16,14 +22,13 @@ import { copyFiles, resetFolder } from "@utils/files";
import { getCwd } from "@utils/path";
import { runSafe } from "@utils/run-safe";
-import { getConfig } from "~/action/common/config";
-
import { AbstractAction, type HandleResult } from "../abstract.action";
interface BuildTarget {
name: string;
+ directory: string;
entry: string;
- static: string;
+ static?: string;
output: string;
platform: "browser" | "node";
}
@@ -35,12 +40,12 @@ export class BuildAction extends AbstractAction {
public async handle(_args: Input, options: Input): Promise {
const directory = getDirectoryInput(options);
- const config = await getConfig(options, directory);
+ const workspaceConfig = await parseWorkspaceConfig(directory);
const isEditor = getEditorInput(options);
const isWatch = getWatchInput(options);
- const targets = this.resolveTargets(config, options, isEditor);
- const results = await this.buildAll(targets, directory, isWatch);
+ const targets = this.resolveTargets(workspaceConfig, directory, options, isEditor);
+ const results = await this.buildAll(targets, isWatch);
if (isWatch) {
return this.enterWatchMode();
@@ -49,85 +54,77 @@ export class BuildAction extends AbstractAction {
return { success: results.every(Boolean) };
}
- private resolveTargets(config: Config, options: Input, isEditor: boolean): BuildTarget[] {
- const targets: BuildTarget[] = [];
-
- if (config.client.enable)
- targets.push(
- this.createTarget(
- "Client",
-
- "browser",
- getStringInputWithDefault(
- options,
- "clientEntry",
- !isEditor ? config.client.build.entry : config.client.editor.entry,
- ),
- getStringInputWithDefault(options, "clientStaticDir", config.client.build.staticDir),
- getStringInputWithDefault(options, "clientOutDir", config.client.outDir),
- ),
- );
- if (config.server.enable)
- targets.push(
- this.createTarget(
- "Server",
- "node",
- getStringInputWithDefault(
- options,
- "serverEntry",
- !isEditor ? config.server.build.entry : config.server.editor.entry,
- ),
- getStringInputWithDefault(options, "serverStaticDir", config.server.build.staticDir),
- getStringInputWithDefault(options, "serverOutDir", config.server.outDir),
- ),
- );
-
- return targets;
+ private resolveTargets(
+ workspaceConfig: WorkspaceConfig,
+ baseDirectory: string,
+ options: Input,
+ isEditor: boolean,
+ ): BuildTarget[] {
+ const projects = resolveProjects(workspaceConfig, baseDirectory);
+
+ return projects.map((project) =>
+ this.createTarget(project, baseDirectory, options, isEditor, projects.length > 1),
+ );
}
private createTarget(
- name: string,
- platform: "browser" | "node",
- entryFile: string,
- staticDir: string,
- outDir: string,
+ project: ResolvedProject,
+ baseDirectory: string,
+ options: Input,
+ isEditor: boolean,
+ disambiguate: boolean,
): BuildTarget {
+ const isClient = project.config.type === "client";
+ const label = isClient ? "Client" : "Server";
+ const relativePath = relative(baseDirectory, project.directory);
+ const name = disambiguate && relativePath !== "" ? `${label} (${relativePath})` : label;
+ const defaults = isClient ? defaultClientConfig : defaultServerConfig;
+
+ const defaultEntry = isEditor
+ ? (project.config.editor?.entryFile ?? defaults.editor.entryFile)
+ : (project.config.entryFile ?? defaults.entryFile);
+
return {
name,
- entry: entryFile,
- static: staticDir,
- output: outDir,
- platform,
+ directory: project.directory,
+ entry: getStringInputWithDefault(
+ options,
+ isClient ? "clientEntry" : "serverEntry",
+ defaultEntry,
+ ),
+ static: getStringInputWithDefault(
+ options,
+ isClient ? "clientStaticDir" : "serverStaticDir",
+ project.config.dir?.assets ?? defaults.dir.assets,
+ ),
+ output: getStringInputWithDefault(
+ options,
+ isClient ? "clientOutDir" : "serverOutDir",
+ project.config.out?.dir ?? defaults.out.dir,
+ ),
+ platform: isClient ? "browser" : "node",
};
}
- private async buildAll(
- targets: BuildTarget[],
- directory: string,
- isWatch: boolean,
- ): Promise {
+ private async buildAll(targets: BuildTarget[], isWatch: boolean): Promise {
const results: boolean[] = [];
for (const target of targets) {
- const result = await this.buildTarget(target, directory, isWatch);
+ const result = await this.buildTarget(target, isWatch);
results.push(result);
}
return results;
}
- private async buildTarget(
- target: BuildTarget,
- directory: string,
- isWatch: boolean,
- ): Promise {
+ private async buildTarget(target: BuildTarget, isWatch: boolean): Promise {
const packageManager = PackageManagerFactory.create(PackageManagerName.LOCAL_BUN);
const executeBuild = (rebuild = false) =>
runSafe(() => {
- this.resetOut(target.output, directory);
- this.copyFiles(target, directory);
+ this.resetOut(target.output, target.directory);
+ this.copyFiles(target, target.directory);
return packageManager.build(
target.name,
- directory,
+ target.directory,
target.entry,
target.output,
["--asset-naming", "[name].[ext]", "--target", target.platform],
@@ -136,7 +133,7 @@ export class BuildAction extends AbstractAction {
}, false);
if (isWatch) {
- this.watchDirectory(directory, target.entry, () => executeBuild(true));
+ this.watchDirectory(target.directory, target.entry, () => executeBuild(true));
}
const result = await executeBuild();
@@ -160,6 +157,7 @@ export class BuildAction extends AbstractAction {
}
private copyFiles(target: BuildTarget, directory: string): void {
+ if (!target.static) return;
const from = getCwd(join(directory, target.static));
const to = getCwd(join(directory, target.output));
copyFiles(from, to);
diff --git a/src/command/commands/build.command.ts b/src/command/commands/build.command.ts
index 61e6472..8619ef1 100644
--- a/src/command/commands/build.command.ts
+++ b/src/command/commands/build.command.ts
@@ -1,12 +1,9 @@
import { type Command } from "commander";
-import { CONFIG_FILE_NAME } from "@lib/constants";
-
import { AbstractCommand } from "../abstract.command";
interface BuildOptions {
directory?: string;
- config?: string;
clientEntry?: string;
serverEntry?: string;
clientStaticDir?: string;
@@ -23,7 +20,6 @@ export class BuildCommand extends AbstractCommand {
.command("build")
.description("build your game")
.option("-d, --directory ", "specify the working directory of the command")
- .option("-c, --config ", "path to the config file", CONFIG_FILE_NAME)
.option("--client-entry ", "specify the entry file of the client")
.option("--server-entry ", "specify the entry file of the server")
.option("--client-static-dir ", "specify the static directory of the client")
@@ -35,7 +31,6 @@ export class BuildCommand extends AbstractCommand {
.action(async (rawOptions: BuildOptions) => {
const options = AbstractCommand.mapToInput({
directory: rawOptions.directory,
- config: rawOptions.config,
clientEntry: rawOptions.clientEntry,
serverEntry: rawOptions.serverEntry,
clientStaticDir: rawOptions.clientStaticDir,
diff --git a/src/lib/config/config-defaults.ts b/src/lib/config/config-defaults.ts
deleted file mode 100644
index fe84202..0000000
--- a/src/lib/config/config-defaults.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { type Config } from "./config.type";
-
-export const CONFIG_DEFAULTS: Config = {
- name: "nanoforge-app",
- language: "ts",
- initFunctions: false,
- ssl: {
- enable: false,
- cert: "",
- key: "",
- },
- client: {
- enable: true,
- port: "3000",
- outDir: ".nanoforge/client",
- build: {
- entry: "client/main.ts",
- staticDir: "client/static",
- },
- editor: {
- entry: ".nanoforge/editor/client/main.ts",
- save: ".nanoforge/client.save.json",
- },
- dirs: {
- components: "client/components",
- systems: "client/systems",
- },
- },
- server: {
- enable: false,
- outDir: ".nanoforge/server",
- build: {
- entry: "server/main.ts",
- staticDir: "server/static",
- },
- editor: {
- entry: ".nanoforge/editor/server/main.ts",
- save: ".nanoforge/server.save.json",
- },
- dirs: {
- components: "server/components",
- systems: "server/systems",
- },
- },
-};
diff --git a/src/lib/config/config-loader.spec.ts b/src/lib/config/config-loader.spec.ts
index d61c13b..d6da267 100644
--- a/src/lib/config/config-loader.spec.ts
+++ b/src/lib/config/config-loader.spec.ts
@@ -1,105 +1,49 @@
-import { plainToInstance } from "class-transformer";
-import { validate } from "class-validator";
-import { existsSync, readFileSync } from "node:fs";
+import { parseConfig } from "@nanoforge-dev/config";
+import { existsSync } from "node:fs";
+import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
+import { getConfig } from "./config-loader";
+
vi.mock("node:fs", () => ({
existsSync: vi.fn(),
- readFileSync: vi.fn(),
-}));
-
-vi.mock("class-transformer", () => ({
- plainToInstance: vi.fn(),
- Expose: () => () => {},
- Type: () => () => {},
}));
-vi.mock("class-validator", () => ({
- validate: vi.fn(),
- IsBoolean: () => () => {},
- IsEnum: () => () => {},
- IsNotEmpty: () => () => {},
- IsPort: () => () => {},
- IsString: () => () => {},
- ValidateNested: () => () => {},
+vi.mock("@nanoforge-dev/config", () => ({
+ parseConfig: vi.fn(),
}));
-describe("loadConfig", () => {
+describe("getConfig", () => {
beforeEach(() => {
vi.resetAllMocks();
- vi.resetModules();
});
- it("should load config from a named file", async () => {
- const rawConfig = { name: "test-app", language: "ts" };
- vi.mocked(readFileSync).mockReturnValue(JSON.stringify(rawConfig));
- vi.mocked(plainToInstance).mockReturnValue(rawConfig as any);
- vi.mocked(validate).mockResolvedValue([]);
+ it("loads nanoforge.config.ts when it exists", async () => {
+ vi.mocked(existsSync).mockImplementation((path) => String(path).endsWith(".ts"));
+ const resolved = { type: "client" as const };
+ vi.mocked(parseConfig).mockResolvedValue(resolved);
- const { loadConfig: freshLoad } = await import("./config-loader");
- const result = await freshLoad("/project", "custom.json");
+ const result = await getConfig("/project");
- expect(readFileSync).toHaveBeenCalledWith("/project/custom.json", "utf-8");
- expect(result).toEqual(rawConfig);
+ expect(parseConfig).toHaveBeenCalledWith(join("/project", "nanoforge.config.ts"));
+ expect(result).toBe(resolved);
});
- it("should search for nanoforge.config.json when no name provided", async () => {
- const rawConfig = { name: "my-app" };
- vi.mocked(existsSync).mockReturnValue(true);
- vi.mocked(readFileSync).mockReturnValue(JSON.stringify(rawConfig));
- vi.mocked(plainToInstance).mockReturnValue(rawConfig as any);
- vi.mocked(validate).mockResolvedValue([]);
+ it("falls back to nanoforge.config.js when no .ts file exists", async () => {
+ vi.mocked(existsSync).mockImplementation((path) => String(path).endsWith(".js"));
+ const resolved = { type: "server" as const };
+ vi.mocked(parseConfig).mockResolvedValue(resolved);
- const { loadConfig: freshLoad } = await import("./config-loader");
- const result = await freshLoad("/project");
+ const result = await getConfig("/project");
- expect(existsSync).toHaveBeenCalled();
- expect(result).toEqual(rawConfig);
+ expect(parseConfig).toHaveBeenCalledWith(join("/project", "nanoforge.config.js"));
+ expect(result).toBe(resolved);
});
- it("should throw when no config file is found", async () => {
+ it("throws when neither nanoforge.config.ts nor .js exists", async () => {
vi.mocked(existsSync).mockReturnValue(false);
- const { loadConfig: freshLoad } = await import("./config-loader");
-
- await expect(freshLoad("/project")).rejects.toThrow("Configuration file not found at path:");
- });
-
- it("should throw when config file cannot be parsed", async () => {
- vi.mocked(readFileSync).mockReturnValue("invalid json");
- vi.mocked(existsSync).mockReturnValue(true);
-
- const { loadConfig: freshLoad } = await import("./config-loader");
-
- await expect(freshLoad("/project")).rejects.toThrow("File System Error [read config file]");
- });
-
- it("should throw on validation errors", async () => {
- const rawConfig = { name: "" };
- vi.mocked(existsSync).mockReturnValue(true);
- vi.mocked(readFileSync).mockReturnValue(JSON.stringify(rawConfig));
- vi.mocked(plainToInstance).mockReturnValue(rawConfig as any);
- vi.mocked(validate).mockResolvedValue([
- { property: "name", toString: () => "name must not be empty" } as any,
- ]);
-
- const { loadConfig: freshLoad } = await import("./config-loader");
-
- await expect(freshLoad("/project")).rejects.toThrow("Invalid config");
- });
-
- it("should cache config after first load", async () => {
- const rawConfig = { name: "cached-app" };
- vi.mocked(existsSync).mockReturnValue(true);
- vi.mocked(readFileSync).mockReturnValue(JSON.stringify(rawConfig));
- vi.mocked(plainToInstance).mockReturnValue(rawConfig as any);
- vi.mocked(validate).mockResolvedValue([]);
-
- const { loadConfig: freshLoad } = await import("./config-loader");
-
- await freshLoad("/project");
- await freshLoad("/project");
-
- expect(readFileSync).toHaveBeenCalledTimes(1);
+ await expect(getConfig("/project")).rejects.toThrow("Configuration file not found at path:");
+ expect(parseConfig).not.toHaveBeenCalled();
});
});
diff --git a/src/lib/config/config-loader.ts b/src/lib/config/config-loader.ts
index 86ae6d3..e7697f0 100644
--- a/src/lib/config/config-loader.ts
+++ b/src/lib/config/config-loader.ts
@@ -1,54 +1,26 @@
-import { plainToInstance } from "class-transformer";
-import { validate } from "class-validator";
-import { existsSync, readFileSync } from "node:fs";
+import { type NanoforgeConfig, parseConfig } from "@nanoforge-dev/config";
+import { existsSync } from "node:fs";
import { join } from "node:path";
-import { CONFIG_FILE_NAME } from "@lib/constants";
+import { ConfigNotFoundError } from "@utils/errors";
-import { CLIError, ConfigNotFoundError, FileSystemError } from "@utils/errors";
-import { deepMerge } from "@utils/object";
+const CONFIG_BASE_NAME = "nanoforge.config";
+const CONFIG_EXTENSIONS = ["ts", "js"];
-import { CONFIG_DEFAULTS } from "./config-defaults";
-import { Config } from "./config.type";
-
-let config: Config | null;
-
-const getConfigPath = (directory: string, name?: string) => {
- if (name) {
- return join(directory, name);
- } else {
- for (const n of [CONFIG_FILE_NAME]) {
- const path = join(directory, n);
- if (existsSync(path)) return path;
- }
- throw new ConfigNotFoundError(join(directory, CONFIG_FILE_NAME));
+const getConfigPath = (directory: string): string => {
+ for (const ext of CONFIG_EXTENSIONS) {
+ const path = join(directory, `${CONFIG_BASE_NAME}.${ext}`);
+ if (existsSync(path)) return path;
}
+ throw new ConfigNotFoundError(join(directory, `${CONFIG_BASE_NAME}.ts`));
};
-export const loadConfig = async (
- directory: string,
- name?: string,
- noThrow: boolean = false,
-): Promise => {
- if (config) return config;
-
- let rawData;
-
- const path = getConfigPath(directory, name);
- try {
- rawData = deepMerge(CONFIG_DEFAULTS, JSON.parse(readFileSync(path, "utf-8")));
- } catch {
- rawData = noThrow ? CONFIG_DEFAULTS : null;
- }
- if (!rawData) throw new FileSystemError("read config file", path);
-
- const data = plainToInstance(Config, rawData, {
- excludeExtraneousValues: true,
- });
- const errors = await validate(data);
- if (errors.length > 0)
- throw new CLIError(`Invalid config:\n${errors.toString().replace(/,/g, "\n")}`);
-
- config = data;
- return config;
+/**
+ * Looks up a `nanoforge.config.ts` file in `directory`, falling back to
+ * `nanoforge.config.js` when no `.ts` file exists. Throws a
+ * `ConfigNotFoundError` when neither is present.
+ */
+export const loadConfig = async (directory: string): Promise => {
+ const path = getConfigPath(directory);
+ return parseConfig(path);
};
diff --git a/src/lib/config/config.type.ts b/src/lib/config/config.type.ts
deleted file mode 100644
index b50435c..0000000
--- a/src/lib/config/config.type.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { Expose, Type } from "class-transformer";
-import { IsBoolean, IsEnum, IsNotEmpty, IsPort, IsString, ValidateNested } from "class-validator";
-
-export class BuildConfig {
- @Expose()
- @IsString()
- @IsNotEmpty()
- entry!: string;
-
- @Expose()
- @IsString()
- @IsNotEmpty()
- staticDir!: string;
-}
-
-export class EditorConfig {
- @Expose()
- @IsString()
- @IsNotEmpty()
- entry!: string;
-
- @Expose()
- @IsString()
- @IsNotEmpty()
- save!: string;
-}
-
-export class DirsConfig {
- @Expose()
- @IsString()
- @IsNotEmpty()
- components!: string;
-
- @Expose()
- @IsString()
- @IsNotEmpty()
- systems!: string;
-}
-
-export class ClientConfig {
- @Expose()
- @IsBoolean()
- enable!: boolean;
-
- @Expose()
- @IsPort()
- port!: string;
-
- @Expose()
- @IsString()
- @IsNotEmpty()
- outDir!: string;
-
- @Expose()
- @Type(() => BuildConfig)
- @ValidateNested()
- build!: BuildConfig;
-
- @Expose()
- @Type(() => EditorConfig)
- @ValidateNested()
- editor!: EditorConfig;
-
- @Expose()
- @Type(() => DirsConfig)
- @ValidateNested()
- dirs!: DirsConfig;
-}
-
-export class ServerConfig {
- @Expose()
- @IsBoolean()
- enable!: boolean;
-
- @Expose()
- @IsString()
- @IsNotEmpty()
- outDir!: string;
-
- @Expose()
- @Type(() => BuildConfig)
- @ValidateNested()
- build!: BuildConfig;
-
- @Expose()
- @Type(() => EditorConfig)
- @ValidateNested()
- editor!: EditorConfig;
-
- @Expose()
- @Type(() => DirsConfig)
- @ValidateNested()
- dirs!: DirsConfig;
-}
-
-export class SslConfig {
- @Expose()
- @IsBoolean()
- enable!: boolean;
-
- @Expose()
- @IsString()
- cert!: string;
-
- @Expose()
- @IsString()
- key!: string;
-}
-
-export class Config {
- @Expose()
- @IsString()
- @IsNotEmpty()
- name!: string;
-
- @Expose()
- @IsEnum(["ts", "js"])
- language!: "ts" | "js";
-
- @Expose()
- @IsBoolean()
- initFunctions!: boolean;
-
- @Expose()
- @Type(() => ClientConfig)
- @ValidateNested()
- client!: ClientConfig;
-
- @Expose()
- @Type(() => ServerConfig)
- @ValidateNested()
- server!: ServerConfig;
-
- @Expose()
- @Type(() => SslConfig)
- @ValidateNested()
- ssl!: SslConfig;
-}
diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts
index aacfbbc..a8646a7 100644
--- a/src/lib/config/index.ts
+++ b/src/lib/config/index.ts
@@ -1,2 +1,6 @@
-export * from "./config.type";
export * from "./config-loader";
+export * from "./workspace-config-parser";
+export * from "./workspace-config.types";
+export * from "./workspace-loader";
+export { ConfigParseError } from "@nanoforge-dev/config";
+export type { NanoforgeConfig as Config } from "@nanoforge-dev/config";
diff --git a/src/lib/config/workspace-config-parser.spec.ts b/src/lib/config/workspace-config-parser.spec.ts
new file mode 100644
index 0000000..9ff924f
--- /dev/null
+++ b/src/lib/config/workspace-config-parser.spec.ts
@@ -0,0 +1,133 @@
+import { existsSync, statSync } from "node:fs";
+import { glob } from "node:fs/promises";
+import { join } from "node:path";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { ConfigNotFoundError } from "@utils/errors";
+
+import { getConfig } from "./config-loader";
+import { parseWorkspaceConfig } from "./workspace-config-parser";
+
+vi.mock("node:fs", () => ({
+ existsSync: vi.fn(),
+ statSync: vi.fn(),
+}));
+
+vi.mock("node:fs/promises", () => ({
+ glob: vi.fn(),
+}));
+
+vi.mock("./config-loader", () => ({
+ getConfig: vi.fn(),
+}));
+
+const asDirectory = () => ({ isDirectory: () => true });
+
+const asyncIterableOf = (values: T[]): AsyncIterable => ({
+ [Symbol.asyncIterator]: async function* () {
+ for (const value of values) yield value;
+ },
+});
+
+describe("parseWorkspaceConfig", () => {
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.mocked(existsSync).mockReturnValue(true);
+ vi.mocked(statSync).mockReturnValue(asDirectory() as ReturnType);
+ });
+
+ it("resolves a root project config", async () => {
+ const config = { type: "client" as const };
+ vi.mocked(getConfig).mockResolvedValue(config);
+
+ const result = await parseWorkspaceConfig("/app");
+
+ expect(result).toEqual({ type: "project", config });
+ });
+
+ it("throws when the root config is a lib", async () => {
+ vi.mocked(getConfig).mockResolvedValue({ type: "lib" as const });
+
+ await expect(parseWorkspaceConfig("/app")).rejects.toThrow(
+ "the root config cannot be of type 'lib'",
+ );
+ });
+
+ it("resolves every directory a package pattern matches", async () => {
+ const workspaceConfig = { type: "workspace" as const, packages: ["apps/*"] };
+ const clientConfig = { type: "client" as const };
+ const libConfig = { type: "lib" as const };
+
+ vi.mocked(glob).mockReturnValue(
+ asyncIterableOf(["apps/client", "apps/foo-lib"]) as ReturnType,
+ );
+ vi.mocked(getConfig).mockImplementation(async (directory: string) => {
+ if (directory === "/app") return workspaceConfig;
+ if (directory === join("/app", "apps/client")) return clientConfig;
+ if (directory === join("/app", "apps/foo-lib")) return libConfig;
+ throw new Error(`unexpected directory: ${directory}`);
+ });
+
+ const result = await parseWorkspaceConfig("/app");
+
+ expect(result).toEqual({
+ type: "workspace",
+ config: workspaceConfig,
+ projects: {
+ "apps/client": { type: "project", config: clientConfig },
+ "apps/foo-lib": { type: "lib", config: libConfig },
+ },
+ });
+ });
+
+ it("skips a matched directory with no config instead of throwing", async () => {
+ const workspaceConfig = { type: "workspace" as const, packages: ["apps/*"] };
+ const clientConfig = { type: "client" as const };
+
+ vi.mocked(glob).mockReturnValue(
+ asyncIterableOf(["apps/client", "apps/empty"]) as ReturnType,
+ );
+ vi.mocked(getConfig).mockImplementation(async (directory: string) => {
+ if (directory === "/app") return workspaceConfig;
+ if (directory === join("/app", "apps/client")) return clientConfig;
+ throw new ConfigNotFoundError(join(directory, "nanoforge.config.ts"));
+ });
+
+ const result = await parseWorkspaceConfig("/app");
+
+ expect(result).toEqual({
+ type: "workspace",
+ config: workspaceConfig,
+ projects: {
+ "apps/client": { type: "project", config: clientConfig },
+ },
+ });
+ });
+
+ it("warns instead of throwing when a pattern matches no project or lib", async () => {
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+ const workspaceConfig = { type: "workspace" as const, packages: ["apps/*"] };
+
+ vi.mocked(glob).mockReturnValue(asyncIterableOf([]) as ReturnType);
+ vi.mocked(getConfig).mockResolvedValue(workspaceConfig);
+
+ const result = await parseWorkspaceConfig("/app");
+
+ expect(result).toEqual({ type: "workspace", config: workspaceConfig, projects: {} });
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("apps/*"));
+ });
+
+ it("throws when a matched package is itself a workspace", async () => {
+ const workspaceConfig = { type: "workspace" as const, packages: ["apps/*"] };
+
+ vi.mocked(glob).mockReturnValue(asyncIterableOf(["apps/nested"]) as ReturnType);
+ vi.mocked(getConfig).mockImplementation(async (directory: string) => {
+ if (directory === "/app") return workspaceConfig;
+ return { type: "workspace" as const };
+ });
+
+ await expect(parseWorkspaceConfig("/app")).rejects.toThrow(
+ "a workspace package cannot itself be of type 'workspace'",
+ );
+ });
+});
diff --git a/src/lib/config/workspace-config-parser.ts b/src/lib/config/workspace-config-parser.ts
new file mode 100644
index 0000000..eaa62d5
--- /dev/null
+++ b/src/lib/config/workspace-config-parser.ts
@@ -0,0 +1,87 @@
+import type { NanoforgeConfig } from "@nanoforge-dev/config";
+import { existsSync, statSync } from "node:fs";
+import { glob } from "node:fs/promises";
+import { join } from "node:path";
+
+import { CLIError, ConfigNotFoundError } from "@utils/errors";
+
+import { loadConfig } from "./config-loader";
+import type {
+ BaseWorkspaceConfig,
+ LibConfig,
+ ProjectConfig,
+ WorkspaceConfig,
+} from "./workspace-config.types";
+
+const resolvePackageConfig = async (
+ packageDir: string,
+): Promise => {
+ let config: NanoforgeConfig;
+ try {
+ config = await loadConfig(packageDir);
+ } catch (error) {
+ if (error instanceof ConfigNotFoundError) return undefined;
+ throw error;
+ }
+
+ if (config.type === "workspace") {
+ throw new CLIError(
+ `Invalid config at '${packageDir}': a workspace package cannot itself be of type 'workspace'.`,
+ "Nest projects and libs under the root workspace instead of another workspace.",
+ );
+ }
+
+ return config.type === "lib" ? { type: "lib", config } : { type: "project", config };
+};
+
+const resolvePackagePattern = async (
+ directory: string,
+ pattern: string,
+): Promise> => {
+ const projects: Record = {};
+
+ for await (const relativePath of glob(pattern, { cwd: directory })) {
+ const packageDir = join(directory, relativePath);
+ if (!existsSync(packageDir) || !statSync(packageDir).isDirectory()) continue;
+
+ const entry = await resolvePackageConfig(packageDir);
+ if (entry) projects[relativePath] = entry;
+ }
+
+ if (Object.keys(projects).length === 0) {
+ console.warn(
+ `[nanoforge] No project or lib config found for workspace package pattern '${pattern}' in '${directory}'.`,
+ );
+ }
+
+ return projects;
+};
+
+const parseBaseWorkspaceConfig = async (
+ directory: string,
+ config: Extract,
+): Promise => {
+ const patterns = config.packages ?? [];
+
+ const projects: Record = {};
+ for (const pattern of patterns) {
+ Object.assign(projects, await resolvePackagePattern(directory, pattern));
+ }
+
+ return { type: "workspace", config, projects };
+};
+
+export const parseWorkspaceConfig = async (directory: string): Promise => {
+ const config = await loadConfig(directory);
+
+ if (config.type === "lib") {
+ throw new CLIError(
+ `Invalid config at '${directory}': the root config cannot be of type 'lib'.`,
+ "Use a 'workspace' or a 'client'/'server' project config at the root.",
+ );
+ }
+
+ if (config.type === "workspace") return parseBaseWorkspaceConfig(directory, config);
+
+ return { type: "project", config };
+};
diff --git a/src/lib/config/workspace-config.types.ts b/src/lib/config/workspace-config.types.ts
new file mode 100644
index 0000000..1fe329e
--- /dev/null
+++ b/src/lib/config/workspace-config.types.ts
@@ -0,0 +1,24 @@
+import type {
+ ClientConfig,
+ LibConfig as NfLibConfig,
+ WorkspaceConfig as NfWorkspaceConfig,
+ ServerConfig,
+} from "@nanoforge-dev/config";
+
+export interface ProjectConfig {
+ type: "project";
+ config: ClientConfig | ServerConfig;
+}
+
+export interface LibConfig {
+ type: "lib";
+ config: NfLibConfig;
+}
+
+export interface BaseWorkspaceConfig {
+ type: "workspace";
+ config: NfWorkspaceConfig;
+ projects: Record;
+}
+
+export type WorkspaceConfig = ProjectConfig | BaseWorkspaceConfig;
diff --git a/src/lib/config/workspace-loader.ts b/src/lib/config/workspace-loader.ts
new file mode 100644
index 0000000..df7d376
--- /dev/null
+++ b/src/lib/config/workspace-loader.ts
@@ -0,0 +1,28 @@
+import type { ClientConfig, ServerConfig } from "@nanoforge-dev/config";
+import { join } from "node:path";
+
+import type { WorkspaceConfig } from "./workspace-config.types";
+
+export interface ResolvedProject {
+ directory: string;
+ config: ClientConfig | ServerConfig;
+}
+
+export const resolveProjects = (
+ workspace: WorkspaceConfig,
+ baseDirectory: string,
+): ResolvedProject[] => {
+ if (workspace.type === "project") {
+ return [{ directory: baseDirectory, config: workspace.config }];
+ }
+
+ return Object.entries(workspace.projects)
+ .filter(
+ (entry): entry is [string, Extract<(typeof entry)[1], { type: "project" }>] =>
+ entry[1].type === "project",
+ )
+ .map(([relativePath, project]) => ({
+ directory: join(baseDirectory, relativePath),
+ config: project.config,
+ }));
+};
From a29592a5f1fc14d22b9772d8e8bf42ced7fffa24 Mon Sep 17 00:00:00 2001
From: Exelo
Date: Fri, 18 Sep 2026 17:57:13 +0900
Subject: [PATCH 08/15] refactor(cli): change start command to fit the new
architecture (#220)
* fix: add good editor entry file
* refactor(cli): change start command to fit the new architecture
* fix(start): add extension on TLS errors and rename SSL to TLS
---
README.md | 4 +-
docs/docs/commands/2-commands/4-start.mdx | 4 +-
e2e/helpers/write-project-config.ts | 2 +-
libs/config/src/defaults/default-configs.ts | 2 +-
libs/config/src/index.ts | 2 +-
libs/config/src/parse/validate-config.ts | 6 +-
libs/config/src/types/client-config.type.ts | 4 +-
libs/config/src/types/index.ts | 2 +-
libs/config/src/types/mixins.type.ts | 4 +-
.../libs/project/files/js/nanoforge.config.js | 2 +-
src/action/actions/start.action.ts | 208 ++++++++++--------
src/command/commands/start.command.ts | 13 +-
12 files changed, 142 insertions(+), 111 deletions(-)
diff --git a/README.md b/README.md
index 7cb8d15..1bb959b 100644
--- a/README.md
+++ b/README.md
@@ -154,8 +154,8 @@ Used to start your nanoforge project.
- `--client-dir ` specify the directory of the client.
- `--server-dir ` specify the directory of the server.
- `--watch` run app in watching mode. (default: `false`)
-- `--cert ` path to the SSL certificate for HTTPS.
-- `--key ` path to the SSL key for HTTPS.
+- `--cert ` path to the TLS certificate for HTTPS.
+- `--key ` path to the TLS key for HTTPS.
### `unpublish`
diff --git a/docs/docs/commands/2-commands/4-start.mdx b/docs/docs/commands/2-commands/4-start.mdx
index 1b9e8b2..cf1f49e 100644
--- a/docs/docs/commands/2-commands/4-start.mdx
+++ b/docs/docs/commands/2-commands/4-start.mdx
@@ -27,8 +27,8 @@ nf start
| `--client-dir ` | Directory of the built client. |
| `--server-dir ` | Directory of the built server. |
| `--watch` | Run in watch mode. Default: `false`. |
-| `--cert ` | Path to the SSL certificate for HTTPS. |
-| `--key ` | Path to the SSL key for HTTPS. |
+| `--cert ` | Path to the TLS certificate for HTTPS. |
+| `--key ` | Path to the TLS key for HTTPS. |
## Examples
diff --git a/e2e/helpers/write-project-config.ts b/e2e/helpers/write-project-config.ts
index 86df865..9db4030 100644
--- a/e2e/helpers/write-project-config.ts
+++ b/e2e/helpers/write-project-config.ts
@@ -26,7 +26,7 @@ export const writeProjectConfig = (
initFunctions,
client: { type: "client", enable: true },
server: { type: "server", enable: server },
- ssl: { enable: false },
+ tls: { enable: false },
},
null,
2,
diff --git a/libs/config/src/defaults/default-configs.ts b/libs/config/src/defaults/default-configs.ts
index 2b5c59d..e164859 100644
--- a/libs/config/src/defaults/default-configs.ts
+++ b/libs/config/src/defaults/default-configs.ts
@@ -37,7 +37,7 @@ export const defaultClientConfig: DeepRequired> = {
},
port: "3000",
- ssl: {
+ tls: {
enable: false,
},
diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts
index 30c1b37..3f73ba0 100644
--- a/libs/config/src/index.ts
+++ b/libs/config/src/index.ts
@@ -25,6 +25,6 @@ export type {
NanoforgeConfig,
ServerConfig,
SourceableConfig,
- SslConfig,
+ TlsConfig,
WorkspaceConfig,
} from "./types";
diff --git a/libs/config/src/parse/validate-config.ts b/libs/config/src/parse/validate-config.ts
index 5132f25..debaa39 100644
--- a/libs/config/src/parse/validate-config.ts
+++ b/libs/config/src/parse/validate-config.ts
@@ -25,8 +25,8 @@ const editorConfigShape = {
.optional(),
};
-const sslConfigShape = {
- ssl: z
+const tlsConfigShape = {
+ tls: z
.union([
z.object({ enable: z.literal(false).optional() }),
z.object({ enable: z.literal(true), cert: z.string(), key: z.string() }),
@@ -70,7 +70,7 @@ const clientConfigSchema = z.object({
...buildableConfigShape,
...containLibConfigShape,
...editorConfigShape,
- ...sslConfigShape,
+ ...tlsConfigShape,
port: z.string().optional(),
});
diff --git a/libs/config/src/types/client-config.type.ts b/libs/config/src/types/client-config.type.ts
index 070423c..0394e69 100644
--- a/libs/config/src/types/client-config.type.ts
+++ b/libs/config/src/types/client-config.type.ts
@@ -4,7 +4,7 @@ import type {
ContainLibConfig,
EditorConfig,
SourceableConfig,
- SslConfig,
+ TlsConfig,
} from "./mixins.type";
export interface ClientConfig
@@ -14,7 +14,7 @@ export interface ClientConfig
BuildableConfig,
ContainLibConfig,
EditorConfig,
- SslConfig {
+ TlsConfig {
/** Port the client dev-server loader listens on. */
port?: string;
}
diff --git a/libs/config/src/types/index.ts b/libs/config/src/types/index.ts
index bdc627f..bcb5ca5 100644
--- a/libs/config/src/types/index.ts
+++ b/libs/config/src/types/index.ts
@@ -11,7 +11,7 @@ export type {
ContainLibConfig,
EditorConfig,
SourceableConfig,
- SslConfig,
+ TlsConfig,
} from "./mixins.type";
export type { ServerConfig } from "./server-config.type";
export type { WorkspaceConfig } from "./workspace-config.type";
diff --git a/libs/config/src/types/mixins.type.ts b/libs/config/src/types/mixins.type.ts
index e780b97..5bbad3d 100644
--- a/libs/config/src/types/mixins.type.ts
+++ b/libs/config/src/types/mixins.type.ts
@@ -17,8 +17,8 @@ export interface EditorConfig {
};
}
-export interface SslConfig {
- ssl?:
+export interface TlsConfig {
+ tls?:
| {
enable?: false;
}
diff --git a/libs/schematics/src/libs/project/files/js/nanoforge.config.js b/libs/schematics/src/libs/project/files/js/nanoforge.config.js
index fd98e6b..d04d057 100644
--- a/libs/schematics/src/libs/project/files/js/nanoforge.config.js
+++ b/libs/schematics/src/libs/project/files/js/nanoforge.config.js
@@ -2,7 +2,7 @@ import { defineConfig } from "nanoforge/config";
export default defineConfig({
type: "<%= part %>",
- entryFile: "src/main.js",<% if (libs.length) { %>
+ entryFile: "src/main.js",
editor: {
entryFile: "src/main.js",
},
diff --git a/src/action/actions/start.action.ts b/src/action/actions/start.action.ts
index 2198e37..e131e5d 100644
--- a/src/action/actions/start.action.ts
+++ b/src/action/actions/start.action.ts
@@ -1,7 +1,13 @@
+import { type ClientConfig, defaultClientConfig, defaultServerConfig } from "@nanoforge-dev/config";
import dotenv from "dotenv";
import { join, resolve } from "path";
-import { type Config } from "@lib/config";
+import {
+ type ResolvedProject,
+ type WorkspaceConfig,
+ parseWorkspaceConfig,
+ resolveProjects,
+} from "@lib/config";
import {
type Input,
getDirectoryInput,
@@ -16,11 +22,9 @@ import { CLIError } from "@utils/errors";
import { getCwd, getModulePath } from "@utils/path";
import { runSafe } from "@utils/run-safe";
-import { getConfig } from "~/action/common/config";
-
import { AbstractAction, type HandleResult } from "../abstract.action";
-interface SSLOptions {
+interface TLSOptions {
cert: string;
key: string;
}
@@ -30,6 +34,27 @@ interface FullEnv {
server: Record;
}
+interface ClientStartTarget {
+ directory: string;
+ outDir: string;
+ platform: "client";
+ port: string;
+ tls?: TLSOptions;
+}
+
+interface ServerStartTarget {
+ directory: string;
+ outDir: string;
+ platform: "server";
+}
+
+type StartTarget = ClientStartTarget | ServerStartTarget;
+
+const isClientTarget = (target: StartTarget): target is ClientStartTarget =>
+ target.platform === "client";
+const isServerTarget = (target: StartTarget): target is ServerStartTarget =>
+ target.platform === "server";
+
export class StartAction extends AbstractAction {
protected startMessage = Messages.START_START;
protected successMessage = Messages.START_SUCCESS;
@@ -37,155 +62,166 @@ export class StartAction extends AbstractAction {
public async handle(_args: Input, options: Input): Promise {
const directory = getDirectoryInput(options);
- const config = await getConfig(options, directory);
- const clientDir = getStringInputWithDefault(options, "clientDir", config.client.outDir);
- const serverDir = getStringInputWithDefault(options, "serverDir", config.server.outDir);
+ const workspaceConfig = await parseWorkspaceConfig(directory);
const watch = getWatchInput(options);
- const port = getStringInputWithDefault(options, "port", config.client.port);
- const ssl = this.resolveSSL(options, config);
-
- const tasks = this.buildStartTasks(config, directory, {
- clientDir,
- serverDir,
- watch,
- port,
- ssl,
- });
+
+ const targets = this.resolveTargets(workspaceConfig, directory, options);
+ const serverTargets = targets.filter(isServerTarget);
+ const clientTargets = targets.filter(isClientTarget);
+
+ if (targets.length === 0) {
+ throw new CLIError(
+ `No client or server project found at '${directory}'.`,
+ "Check your nanoforge.config's 'packages' patterns, or run 'nf new' to scaffold one.",
+ );
+ }
+
+ const env = this.parseEnv(directory);
+ const tasks = this.buildStartTasks(serverTargets, clientTargets, watch, env);
await Promise.all(tasks);
return { keepAlive: true };
}
- private resolveSSL(options: Input, config: Config): SSLOptions | undefined {
+ private resolveTLS(options: Input, config: ClientConfig): TLSOptions | undefined {
const cliCert = getStringInput(options, "cert");
const cliKey = getStringInput(options, "key");
- const isSslRequested = Boolean(cliCert || cliKey || config.ssl?.enable);
+ const tls = config.tls;
+ const configTls = tls && tls.enable ? tls : undefined;
+ const isTlsRequested = Boolean(cliCert || cliKey || configTls);
- if (!isSslRequested) return undefined;
+ if (!isTlsRequested) return undefined;
- const cert = cliCert ? cliCert : config.ssl?.cert;
- const key = cliKey ? cliKey : config.ssl?.key;
+ const cert = cliCert ?? configTls?.cert;
+ const key = cliKey ?? configTls?.key;
if (!cert) {
throw new CLIError(
- "No certificate found for SSL.",
- "Please provide a certificate path with --cert or configure 'ssl.cert' in your nanoforge.config.json.",
+ "No certificate found for TLS.",
+ "Please provide a certificate path with --cert or configure 'tls.cert' in your nanoforge.config.ts",
);
}
if (!key) {
throw new CLIError(
- "No key found for SSL.",
- "Please provide a key path with --key or configure 'ssl.key' in your nanoforge.config.json.",
+ "No key found for TLS.",
+ "Please provide a key path with --key or configure 'tls.key' in your nanoforge.config.ts",
);
}
+ return { cert, key };
+ }
+
+ private resolveTargets(
+ workspaceConfig: WorkspaceConfig,
+ baseDirectory: string,
+ options: Input,
+ ): StartTarget[] {
+ const projects = resolveProjects(workspaceConfig, baseDirectory);
+ return projects.map((project) => this.createTarget(project, options));
+ }
+
+ private createTarget(project: ResolvedProject, options: Input): StartTarget {
+ if (project.config.type === "server") {
+ return {
+ directory: project.directory,
+ outDir: getStringInputWithDefault(
+ options,
+ "serverDir",
+ project.config.out?.dir ?? defaultServerConfig.out.dir,
+ ),
+ platform: "server",
+ };
+ }
+
return {
- cert,
- key,
+ directory: project.directory,
+ outDir: getStringInputWithDefault(
+ options,
+ "clientDir",
+ project.config.out?.dir ?? defaultClientConfig.out.dir,
+ ),
+ platform: "client",
+ port: getStringInputWithDefault(
+ options,
+ "port",
+ project.config.port ?? defaultClientConfig.port,
+ ),
+ tls: this.resolveTLS(options, project.config),
};
}
private buildStartTasks(
- config: Config,
- directory: string,
- options: {
- clientDir: string;
- serverDir: string;
- watch: boolean;
- port: string;
- ssl?: SSLOptions;
- },
+ serverTargets: ServerStartTarget[],
+ clientTargets: ClientStartTarget[],
+ watch: boolean,
+ env: FullEnv,
): Promise[] {
- const env = this.parseEnv(directory);
const tasks: Promise[] = [];
- const { clientDir, serverDir, watch, port, ssl } = options;
- if (config.server.enable)
- tasks.push(this.startServer(directory, config, { serverDir, watch }, env));
+ for (const target of serverTargets) {
+ tasks.push(this.startServer(target, watch, env));
+ }
- if (config.client.enable)
- tasks.push(
- this.startClient(directory, config, { clientDir, serverDir, watch, port, ssl }, env),
- );
+ for (const target of clientTargets) {
+ tasks.push(this.startClient(target, { watch, serverTargets }, env));
+ }
return tasks;
}
private async startClient(
- directory: string,
- config: Config,
- options: {
- clientDir: string;
- serverDir: string;
- watch: boolean;
- port: string;
- ssl?: SSLOptions;
- },
+ target: ClientStartTarget,
+ options: { watch: boolean; serverTargets: ServerStartTarget[] },
env: FullEnv,
): Promise {
const loaderPath = getModulePath("@nanoforge-dev/loader-client/package.json", true);
- const params = this.buildClientParams(directory, config, options);
+ const params = this.buildClientParams(target, options);
await this.runLoader("Client", loaderPath, params, env.client);
}
private async startServer(
- directory: string,
- config: Config,
- options: { serverDir: string; watch: boolean },
+ target: ServerStartTarget,
+ watch: boolean,
env: FullEnv,
): Promise {
const loaderPath = getModulePath("@nanoforge-dev/loader-server/package.json", true);
- const params = this.buildServerParams(directory, config, options);
+ const params = this.buildServerParams(target, watch);
await this.runLoader("Server", loaderPath, params, env.server);
}
private buildClientParams(
- directory: string,
- config: Config,
- options: {
- clientDir: string;
- serverDir: string;
- watch: boolean;
- port: string;
- ssl?: SSLOptions;
- },
+ target: ClientStartTarget,
+ options: { watch: boolean; serverTargets: ServerStartTarget[] },
): string[] {
const params: Record = {
- "-d": getCwd(join(directory, options.clientDir)),
- "-p": options.port,
+ "-d": getCwd(join(target.directory, target.outDir)),
+ "-p": target.port,
};
- if (options.watch) params["--watch"] = true;
if (options.watch) {
params["--watch"] = true;
- if (config.server.enable) {
- params["--watch-server-dir"] = getCwd(join(directory, options.serverDir));
+ const [server] = options.serverTargets;
+ if (server && options.serverTargets.length === 1) {
+ params["--watch-server-dir"] = getCwd(join(server.directory, server.outDir));
}
}
- if (options.ssl) {
- params["--cert"] = options.ssl.cert;
- params["--key"] = options.ssl.key;
+ if (target.tls) {
+ params["--cert"] = target.tls.cert;
+ params["--key"] = target.tls.key;
}
return this.buildParams(params);
}
- private buildServerParams(
- directory: string,
- _config: Config,
- options: {
- serverDir: string;
- watch: boolean;
- },
- ): string[] {
+ private buildServerParams(target: ServerStartTarget, watch: boolean): string[] {
const params: Record = {
- "-d": getCwd(join(directory, options.serverDir)),
+ "-d": getCwd(join(target.directory, target.outDir)),
};
- if (options.watch) params["--watch"] = true;
+ if (watch) params["--watch"] = true;
return this.buildParams(params);
}
diff --git a/src/command/commands/start.command.ts b/src/command/commands/start.command.ts
index 4299acc..036afda 100644
--- a/src/command/commands/start.command.ts
+++ b/src/command/commands/start.command.ts
@@ -1,12 +1,9 @@
import { type Command } from "commander";
-import { CONFIG_FILE_NAME } from "@lib/constants";
-
import { AbstractCommand } from "../abstract.command";
interface StartOptions {
directory?: string;
- config?: string;
port?: string;
clientDir?: string;
serverDir?: string;
@@ -21,17 +18,15 @@ export class StartCommand extends AbstractCommand {
.command("start")
.description("start your game")
.option("-d, --directory ", "specify the working directory of the command")
- .option("-c, --config ", "path to the config file", CONFIG_FILE_NAME)
.option("-p, --port ", "specify the port of the loader (the website to load the game)")
- .option("--client-dir ", "specify the directory of the client")
- .option("--server-dir ", "specify the directory of the server")
+ .option("--client-dir ", "specify the output directory of the client")
+ .option("--server-dir ", "specify the output directory of the server")
.option("--watch", "run app in watching mode", false)
- .option("--cert ", "path to the SSL certificate for HTTPS")
- .option("--key ", "path to the SSL key for HTTPS")
+ .option("--cert ", "path to the TLS certificate for HTTPS")
+ .option("--key ", "path to the TLS key for HTTPS")
.action(async (rawOptions: StartOptions) => {
const options = AbstractCommand.mapToInput({
directory: rawOptions.directory,
- config: rawOptions.config,
port: rawOptions.port,
clientDir: rawOptions.clientDir,
serverDir: rawOptions.serverDir,
From 32613ab23471c8ffda273b7c29cf91c872ba63ea Mon Sep 17 00:00:00 2001
From: Exelo
Date: Fri, 18 Sep 2026 17:57:14 +0900
Subject: [PATCH 09/15] refactor(cli): change create command to fit the new
architecture (#222)
* feat(config): add language config
* refactor(cli): change create command to fit the new architecture
* test: fix tests
---
e2e/cli-help.test.ts | 1 -
libs/config/src/defaults/default-configs.ts | 4 ++
libs/config/src/index.ts | 1 +
libs/config/src/parse/config-error.ts | 2 +-
libs/config/src/parse/validate-config.ts | 6 +++
libs/config/src/types/client-config.type.ts | 2 +
libs/config/src/types/index.ts | 1 +
libs/config/src/types/mixins.type.ts | 5 ++
libs/config/src/types/server-config.type.ts | 9 +++-
libs/config/test/parse-config.spec.ts | 2 +-
libs/schematics/e2e/project.test.ts | 49 ++-----------------
libs/schematics/e2e/workspace.test.ts | 3 +-
.../libs/project/files/js/nanoforge.config.js | 1 +
src/action/actions/create.action.ts | 27 +++++++---
src/action/common/config.ts | 6 ---
src/command/commands/create.command.ts | 12 -----
src/lib/config/config-loader.spec.ts | 10 ++--
.../config/workspace-config-parser.spec.ts | 16 +++---
18 files changed, 67 insertions(+), 90 deletions(-)
delete mode 100644 src/action/common/config.ts
diff --git a/e2e/cli-help.test.ts b/e2e/cli-help.test.ts
index f2b8da0..662025d 100644
--- a/e2e/cli-help.test.ts
+++ b/e2e/cli-help.test.ts
@@ -56,7 +56,6 @@ describe("nf build --help", () => {
expect(exitCode).toBe(0);
expect(stdout).toContain("build your game");
expect(stdout).toContain("--directory");
- expect(stdout).toContain("--config");
expect(stdout).toContain("--client-entry");
expect(stdout).toContain("--server-entry");
expect(stdout).toContain("--client-static-dir");
diff --git a/libs/config/src/defaults/default-configs.ts b/libs/config/src/defaults/default-configs.ts
index e164859..2b06faf 100644
--- a/libs/config/src/defaults/default-configs.ts
+++ b/libs/config/src/defaults/default-configs.ts
@@ -36,6 +36,8 @@ export const defaultClientConfig: DeepRequired> = {
entryFile: "src/main.ts",
},
+ language: "ts",
+
port: "3000",
tls: {
enable: false,
@@ -64,5 +66,7 @@ export const defaultServerConfig: DeepRequired> = {
entryFile: "src/main.ts",
},
+ language: "ts",
+
libs: [],
};
diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts
index 3f73ba0..efce8b3 100644
--- a/libs/config/src/index.ts
+++ b/libs/config/src/index.ts
@@ -21,6 +21,7 @@ export type {
ClientConfig,
ContainLibConfig,
EditorConfig,
+ LanguageConfig,
LibConfig,
NanoforgeConfig,
ServerConfig,
diff --git a/libs/config/src/parse/config-error.ts b/libs/config/src/parse/config-error.ts
index d97e854..54736ca 100644
--- a/libs/config/src/parse/config-error.ts
+++ b/libs/config/src/parse/config-error.ts
@@ -8,7 +8,7 @@ export class ConfigParseError extends Error {
public readonly path: string,
options?: { cause?: unknown },
) {
- super(message, options);
+ super(`${message}${options?.cause ? `\n${options?.cause}` : ""}`);
this.name = "ConfigParseError";
}
}
diff --git a/libs/config/src/parse/validate-config.ts b/libs/config/src/parse/validate-config.ts
index debaa39..8a07e97 100644
--- a/libs/config/src/parse/validate-config.ts
+++ b/libs/config/src/parse/validate-config.ts
@@ -25,6 +25,10 @@ const editorConfigShape = {
.optional(),
};
+const languageConfigShape = {
+ language: z.enum(["ts", "js"]).optional(),
+};
+
const tlsConfigShape = {
tls: z
.union([
@@ -70,6 +74,7 @@ const clientConfigSchema = z.object({
...buildableConfigShape,
...containLibConfigShape,
...editorConfigShape,
+ ...languageConfigShape,
...tlsConfigShape,
port: z.string().optional(),
});
@@ -80,6 +85,7 @@ const serverConfigSchema = z.object({
...buildableConfigShape,
...containLibConfigShape,
...editorConfigShape,
+ ...languageConfigShape,
});
const nanoforgeConfigSchema = z.discriminatedUnion("type", [
diff --git a/libs/config/src/types/client-config.type.ts b/libs/config/src/types/client-config.type.ts
index 0394e69..b5b1fca 100644
--- a/libs/config/src/types/client-config.type.ts
+++ b/libs/config/src/types/client-config.type.ts
@@ -3,6 +3,7 @@ import type {
BuildableConfig,
ContainLibConfig,
EditorConfig,
+ LanguageConfig,
SourceableConfig,
TlsConfig,
} from "./mixins.type";
@@ -14,6 +15,7 @@ export interface ClientConfig
BuildableConfig,
ContainLibConfig,
EditorConfig,
+ LanguageConfig,
TlsConfig {
/** Port the client dev-server loader listens on. */
port?: string;
diff --git a/libs/config/src/types/index.ts b/libs/config/src/types/index.ts
index bcb5ca5..3a0523a 100644
--- a/libs/config/src/types/index.ts
+++ b/libs/config/src/types/index.ts
@@ -10,6 +10,7 @@ export type {
BuildableConfig,
ContainLibConfig,
EditorConfig,
+ LanguageConfig,
SourceableConfig,
TlsConfig,
} from "./mixins.type";
diff --git a/libs/config/src/types/mixins.type.ts b/libs/config/src/types/mixins.type.ts
index 5bbad3d..50785f9 100644
--- a/libs/config/src/types/mixins.type.ts
+++ b/libs/config/src/types/mixins.type.ts
@@ -17,6 +17,11 @@ export interface EditorConfig {
};
}
+export interface LanguageConfig {
+ /** Source language, used to pick `.ts`/`.js` component and system schematic templates. */
+ language?: "ts" | "js";
+}
+
export interface TlsConfig {
tls?:
| {
diff --git a/libs/config/src/types/server-config.type.ts b/libs/config/src/types/server-config.type.ts
index 46ba676..c7f3abc 100644
--- a/libs/config/src/types/server-config.type.ts
+++ b/libs/config/src/types/server-config.type.ts
@@ -3,8 +3,15 @@ import type {
BuildableConfig,
ContainLibConfig,
EditorConfig,
+ LanguageConfig,
SourceableConfig,
} from "./mixins.type";
export interface ServerConfig
- extends BaseConfig<"server">, SourceableConfig, BuildableConfig, ContainLibConfig, EditorConfig {}
+ extends
+ BaseConfig<"server">,
+ SourceableConfig,
+ BuildableConfig,
+ ContainLibConfig,
+ EditorConfig,
+ LanguageConfig {}
diff --git a/libs/config/test/parse-config.spec.ts b/libs/config/test/parse-config.spec.ts
index fd5fb6e..2d98c72 100644
--- a/libs/config/test/parse-config.spec.ts
+++ b/libs/config/test/parse-config.spec.ts
@@ -30,7 +30,7 @@ describe("parseConfig", () => {
it("rejects with code 'load-failed' when the file throws while loading", async () => {
const error = await parseConfig(fixture("throwing-config.ts")).catch((e) => e);
expect(error).toMatchObject({ constructor: ConfigParseError, code: "load-failed" });
- expect(error.cause).toBeInstanceOf(Error);
+ expect(error.message).toContain("boom");
});
it("rejects with code 'no-default-export' when there's no default export", async () => {
diff --git a/libs/schematics/e2e/project.test.ts b/libs/schematics/e2e/project.test.ts
index 340fa1f..a489c4f 100644
--- a/libs/schematics/e2e/project.test.ts
+++ b/libs/schematics/e2e/project.test.ts
@@ -243,7 +243,7 @@ describe("project schematic", () => {
describe.each(["npm", "yarn", "pnpm", "bun"] as const)(
"with %s package manager",
(packageManager) => {
- it("uses the workspace-linking protocol by default", async () => {
+ it("resolves a real registry version by default", async () => {
const tree = await runner.runSchematic("project", {
part: "client",
workspaceName: `${packageManager}-dep-workspace`,
@@ -253,9 +253,8 @@ describe("project schematic", () => {
const packageJson = JSON.parse(
tree.readContent(`/${packageManager}-dep-apps/client/package.json`),
);
- const expected = packageManager === "npm" ? "*" : "workspace:*";
- expect(packageJson.devDependencies["@nanoforge-dev/ecs"]).toBe(expected);
- expect(packageJson.devDependencies["nanoforge"]).toBe(expected);
+ expect(packageJson.devDependencies["@nanoforge-dev/ecs"]).toMatch(RESOLVED_VERSION);
+ expect(packageJson.devDependencies["nanoforge"]).toMatch(RESOLVED_VERSION);
});
it("resolves a real registry version when workspace is false", async () => {
@@ -287,35 +286,6 @@ describe("project schematic", () => {
});
});
- describe("with init functions enabled", () => {
- let tree: UnitTestTree;
-
- beforeAll(async () => {
- tree = await runner.runSchematic("project", {
- part: "client",
- workspaceName: "init-workspace",
- directory: "init-apps/client",
- initFunctions: true,
- });
- });
-
- it("should generate init function files", () => {
- expect(tree.files).toContain("/init-apps/client/src/init/before-init.ts");
- expect(tree.files).toContain("/init-apps/client/src/init/after-init.ts");
- expect(tree.files).toContain("/init-apps/client/src/init/before-registry-init.ts");
- expect(tree.files).toContain("/init-apps/client/src/init/after-registry-init.ts");
- expect(tree.files).toContain("/init-apps/client/src/init/before-run.ts");
- expect(tree.files).toContain("/init-apps/client/src/init/after-run.ts");
- });
-
- it("should wire the init functions into main.ts", () => {
- const content = tree.readContent("/init-apps/client/src/main.ts");
- expect(content).toContain('import { beforeInit } from "./init/before-init"');
- expect(content).toContain("await beforeInit(app);");
- expect(content).toContain("await afterRun(app);");
- });
- });
-
describe("with JavaScript", () => {
let tree: UnitTestTree;
@@ -338,19 +308,6 @@ describe("project schematic", () => {
});
});
- describe("with libs", () => {
- it("should list libs in nanoforge.config.ts", async () => {
- const tree = await runner.runSchematic("project", {
- part: "client",
- workspaceName: "libs-workspace",
- directory: "libs-apps/client",
- libs: ["../../libs/shared"],
- });
- const content = tree.readContent("/libs-apps/client/nanoforge.config.ts");
- expect(content).toContain('libs: ["../../libs/shared"]');
- });
- });
-
describe("@nanoforge-dev/ecs and @nanoforge-dev/network version resolution", () => {
it("still writes the ecs/network keys even though they're resolved via their real per-part packages", async () => {
// @nanoforge-dev/ecs and @nanoforge-dev/network aren't published under
diff --git a/libs/schematics/e2e/workspace.test.ts b/libs/schematics/e2e/workspace.test.ts
index 6356e6b..f09dda0 100644
--- a/libs/schematics/e2e/workspace.test.ts
+++ b/libs/schematics/e2e/workspace.test.ts
@@ -26,7 +26,6 @@ describe("workspace schematic", () => {
it("should generate workspace files", () => {
expect(tree.files).toContain("/my-workspace/package.json");
expect(tree.files).toContain("/my-workspace/tsconfig.json");
- expect(tree.files).toContain("/my-workspace/tsconfig.spec.json");
expect(tree.files).toContain("/my-workspace/.gitignore");
expect(tree.files).toContain("/my-workspace/nanoforge.config.ts");
expect(tree.files).toContain("/my-workspace/README.md");
@@ -146,7 +145,7 @@ describe("workspace schematic", () => {
// `allowBuilds` map instead. See https://pnpm.io/settings.
expect(packageJson).not.toHaveProperty("pnpm");
expect(packageJson.workspaces).toBeUndefined();
- expect(packageJson.devDependencies["@nanoforge-dev/cli"]).toBe("workspace:*");
+ expect(packageJson.devDependencies["@nanoforge-dev/cli"]).toMatch(RESOLVED_VERSION);
});
it("should generate a pnpm-workspace.yaml with apps only (no libs)", async () => {
diff --git a/libs/schematics/src/libs/project/files/js/nanoforge.config.js b/libs/schematics/src/libs/project/files/js/nanoforge.config.js
index d04d057..b42a8a5 100644
--- a/libs/schematics/src/libs/project/files/js/nanoforge.config.js
+++ b/libs/schematics/src/libs/project/files/js/nanoforge.config.js
@@ -6,4 +6,5 @@ export default defineConfig({
editor: {
entryFile: "src/main.js",
},
+ language: "js",
});
diff --git a/src/action/actions/create.action.ts b/src/action/actions/create.action.ts
index a2b6605..9c2a05f 100644
--- a/src/action/actions/create.action.ts
+++ b/src/action/actions/create.action.ts
@@ -1,18 +1,20 @@
+import { defaultClientConfig, defaultServerConfig } from "@nanoforge-dev/config";
+
+import { parseWorkspaceConfig } from "@lib/config";
import {
type Input,
getCreateNameInputOrAsk,
getCreateTypeInput,
getDirectoryInput,
getPathInputWithDefault,
- getServerInput,
} from "@lib/input";
import { Collection, CollectionFactory } from "@lib/schematics";
import { Messages } from "@lib/ui";
+import { CLIError } from "@utils/errors";
import { capitalize } from "@utils/formatting";
import { AbstractAction, type HandleResult } from "../abstract.action";
-import { getConfig } from "../common/config";
import { executeSchematic } from "../common/schematics";
interface CreateValues {
@@ -29,22 +31,33 @@ export class CreateAction extends AbstractAction {
public async handle(args: Input, options: Input): Promise {
const directory = getDirectoryInput(options);
- const config = await getConfig(options, directory, true);
+ const workspaceConfig = await parseWorkspaceConfig(directory);
+
+ if (workspaceConfig.type === "workspace") {
+ throw new CLIError(
+ `Cannot run 'create' at a workspace root ('${directory}').`,
+ "Run it from within a specific client/server project directory instead (-d ).",
+ );
+ }
+
+ const { config } = workspaceConfig;
+ const isClient = config.type === "client";
+ const defaults = isClient ? defaultClientConfig : defaultServerConfig;
const type = getCreateTypeInput(args);
const name = await getCreateNameInputOrAsk(options);
- const isServer = getServerInput(options);
const path = getPathInputWithDefault(
options,
- config[isServer ? "server" : "client"].dirs[type === "component" ? "components" : "systems"],
+ config.dir?.[type === "component" ? "components" : "systems"] ??
+ defaults.dir[type === "component" ? "components" : "systems"],
);
await this.generateElement(directory, type, {
name,
directory: path,
- part: isServer ? "server" : "client",
- language: config.language,
+ part: config.type,
+ language: config.language ?? defaults.language,
});
return {};
diff --git a/src/action/common/config.ts b/src/action/common/config.ts
deleted file mode 100644
index f7e879e..0000000
--- a/src/action/common/config.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { type Config, loadConfig } from "@lib/config";
-import { type Input, getConfigInput } from "@lib/input";
-
-export const getConfig = (inputs: Input, dir: string, noThrow?: boolean): Promise => {
- return loadConfig(dir, getConfigInput(inputs), noThrow);
-};
diff --git a/src/command/commands/create.command.ts b/src/command/commands/create.command.ts
index 7f91988..30d0950 100644
--- a/src/command/commands/create.command.ts
+++ b/src/command/commands/create.command.ts
@@ -1,14 +1,10 @@
import { type Command } from "commander";
-import { CONFIG_FILE_NAME } from "@lib/constants";
-
import { AbstractCommand } from "../abstract.command";
interface CreateOptions {
directory?: string;
- config: string;
name?: string;
- server: boolean;
path?: string;
}
@@ -18,13 +14,7 @@ export class CreateCommand extends AbstractCommand {
.command("create ")
.description("create nanoforge components or systems")
.option("-d, --directory ", "specify the working directory of the command")
- .option("-c, --config ", "path to the config file", CONFIG_FILE_NAME)
.option("-n, --name ", "name of the component/system")
- .option(
- "-s, --server",
- "install components/systems on server (default install on client)",
- false,
- )
.option(
"-p, --path ",
"path to the component/system folder (default: /)",
@@ -36,9 +26,7 @@ export class CreateCommand extends AbstractCommand {
const options = AbstractCommand.mapToInput({
directory: rawOptions.directory,
- config: rawOptions.config,
name: rawOptions.name,
- server: rawOptions.server,
path: rawOptions.path,
});
diff --git a/src/lib/config/config-loader.spec.ts b/src/lib/config/config-loader.spec.ts
index d6da267..b39997a 100644
--- a/src/lib/config/config-loader.spec.ts
+++ b/src/lib/config/config-loader.spec.ts
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
-import { getConfig } from "./config-loader";
+import { loadConfig } from "./config-loader";
vi.mock("node:fs", () => ({
existsSync: vi.fn(),
@@ -13,7 +13,7 @@ vi.mock("@nanoforge-dev/config", () => ({
parseConfig: vi.fn(),
}));
-describe("getConfig", () => {
+describe("loadConfig", () => {
beforeEach(() => {
vi.resetAllMocks();
});
@@ -23,7 +23,7 @@ describe("getConfig", () => {
const resolved = { type: "client" as const };
vi.mocked(parseConfig).mockResolvedValue(resolved);
- const result = await getConfig("/project");
+ const result = await loadConfig("/project");
expect(parseConfig).toHaveBeenCalledWith(join("/project", "nanoforge.config.ts"));
expect(result).toBe(resolved);
@@ -34,7 +34,7 @@ describe("getConfig", () => {
const resolved = { type: "server" as const };
vi.mocked(parseConfig).mockResolvedValue(resolved);
- const result = await getConfig("/project");
+ const result = await loadConfig("/project");
expect(parseConfig).toHaveBeenCalledWith(join("/project", "nanoforge.config.js"));
expect(result).toBe(resolved);
@@ -43,7 +43,7 @@ describe("getConfig", () => {
it("throws when neither nanoforge.config.ts nor .js exists", async () => {
vi.mocked(existsSync).mockReturnValue(false);
- await expect(getConfig("/project")).rejects.toThrow("Configuration file not found at path:");
+ await expect(loadConfig("/project")).rejects.toThrow("Configuration file not found at path:");
expect(parseConfig).not.toHaveBeenCalled();
});
});
diff --git a/src/lib/config/workspace-config-parser.spec.ts b/src/lib/config/workspace-config-parser.spec.ts
index 9ff924f..5617a67 100644
--- a/src/lib/config/workspace-config-parser.spec.ts
+++ b/src/lib/config/workspace-config-parser.spec.ts
@@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { ConfigNotFoundError } from "@utils/errors";
-import { getConfig } from "./config-loader";
+import { loadConfig } from "./config-loader";
import { parseWorkspaceConfig } from "./workspace-config-parser";
vi.mock("node:fs", () => ({
@@ -18,7 +18,7 @@ vi.mock("node:fs/promises", () => ({
}));
vi.mock("./config-loader", () => ({
- getConfig: vi.fn(),
+ loadConfig: vi.fn(),
}));
const asDirectory = () => ({ isDirectory: () => true });
@@ -38,7 +38,7 @@ describe("parseWorkspaceConfig", () => {
it("resolves a root project config", async () => {
const config = { type: "client" as const };
- vi.mocked(getConfig).mockResolvedValue(config);
+ vi.mocked(loadConfig).mockResolvedValue(config);
const result = await parseWorkspaceConfig("/app");
@@ -46,7 +46,7 @@ describe("parseWorkspaceConfig", () => {
});
it("throws when the root config is a lib", async () => {
- vi.mocked(getConfig).mockResolvedValue({ type: "lib" as const });
+ vi.mocked(loadConfig).mockResolvedValue({ type: "lib" as const });
await expect(parseWorkspaceConfig("/app")).rejects.toThrow(
"the root config cannot be of type 'lib'",
@@ -61,7 +61,7 @@ describe("parseWorkspaceConfig", () => {
vi.mocked(glob).mockReturnValue(
asyncIterableOf(["apps/client", "apps/foo-lib"]) as ReturnType,
);
- vi.mocked(getConfig).mockImplementation(async (directory: string) => {
+ vi.mocked(loadConfig).mockImplementation(async (directory: string) => {
if (directory === "/app") return workspaceConfig;
if (directory === join("/app", "apps/client")) return clientConfig;
if (directory === join("/app", "apps/foo-lib")) return libConfig;
@@ -87,7 +87,7 @@ describe("parseWorkspaceConfig", () => {
vi.mocked(glob).mockReturnValue(
asyncIterableOf(["apps/client", "apps/empty"]) as ReturnType,
);
- vi.mocked(getConfig).mockImplementation(async (directory: string) => {
+ vi.mocked(loadConfig).mockImplementation(async (directory: string) => {
if (directory === "/app") return workspaceConfig;
if (directory === join("/app", "apps/client")) return clientConfig;
throw new ConfigNotFoundError(join(directory, "nanoforge.config.ts"));
@@ -109,7 +109,7 @@ describe("parseWorkspaceConfig", () => {
const workspaceConfig = { type: "workspace" as const, packages: ["apps/*"] };
vi.mocked(glob).mockReturnValue(asyncIterableOf([]) as ReturnType);
- vi.mocked(getConfig).mockResolvedValue(workspaceConfig);
+ vi.mocked(loadConfig).mockResolvedValue(workspaceConfig);
const result = await parseWorkspaceConfig("/app");
@@ -121,7 +121,7 @@ describe("parseWorkspaceConfig", () => {
const workspaceConfig = { type: "workspace" as const, packages: ["apps/*"] };
vi.mocked(glob).mockReturnValue(asyncIterableOf(["apps/nested"]) as ReturnType);
- vi.mocked(getConfig).mockImplementation(async (directory: string) => {
+ vi.mocked(loadConfig).mockImplementation(async (directory: string) => {
if (directory === "/app") return workspaceConfig;
return { type: "workspace" as const };
});
From 0c7d818438681aa9148840b21d7299720de8011c Mon Sep 17 00:00:00 2001
From: Exelo
Date: Thu, 24 Sep 2026 19:03:25 +0900
Subject: [PATCH 10/15] fix(start): change path for detecting package manager
(#224)
---
src/action/actions/start.action.ts | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/src/action/actions/start.action.ts b/src/action/actions/start.action.ts
index e131e5d..8aca366 100644
--- a/src/action/actions/start.action.ts
+++ b/src/action/actions/start.action.ts
@@ -15,7 +15,7 @@ import {
getStringInputWithDefault,
getWatchInput,
} from "@lib/input";
-import { PackageManagerFactory } from "@lib/package-manager";
+import { type PackageManager, PackageManagerFactory } from "@lib/package-manager";
import { Messages } from "@lib/ui";
import { CLIError } from "@utils/errors";
@@ -77,7 +77,8 @@ export class StartAction extends AbstractAction {
}
const env = this.parseEnv(directory);
- const tasks = this.buildStartTasks(serverTargets, clientTargets, watch, env);
+ const packageManager = await PackageManagerFactory.find(directory);
+ const tasks = this.buildStartTasks(packageManager, serverTargets, clientTargets, watch, env);
await Promise.all(tasks);
return { keepAlive: true };
@@ -152,6 +153,7 @@ export class StartAction extends AbstractAction {
}
private buildStartTasks(
+ packageManager: PackageManager,
serverTargets: ServerStartTarget[],
clientTargets: ClientStartTarget[],
watch: boolean,
@@ -160,17 +162,18 @@ export class StartAction extends AbstractAction {
const tasks: Promise[] = [];
for (const target of serverTargets) {
- tasks.push(this.startServer(target, watch, env));
+ tasks.push(this.startServer(packageManager, target, watch, env));
}
for (const target of clientTargets) {
- tasks.push(this.startClient(target, { watch, serverTargets }, env));
+ tasks.push(this.startClient(packageManager, target, { watch, serverTargets }, env));
}
return tasks;
}
private async startClient(
+ packageManager: PackageManager,
target: ClientStartTarget,
options: { watch: boolean; serverTargets: ServerStartTarget[] },
env: FullEnv,
@@ -178,10 +181,11 @@ export class StartAction extends AbstractAction {
const loaderPath = getModulePath("@nanoforge-dev/loader-client/package.json", true);
const params = this.buildClientParams(target, options);
- await this.runLoader("Client", loaderPath, params, env.client);
+ await this.runLoader(packageManager, "Client", loaderPath, params, env.client);
}
private async startServer(
+ packageManager: PackageManager,
target: ServerStartTarget,
watch: boolean,
env: FullEnv,
@@ -189,7 +193,7 @@ export class StartAction extends AbstractAction {
const loaderPath = getModulePath("@nanoforge-dev/loader-server/package.json", true);
const params = this.buildServerParams(target, watch);
- await this.runLoader("Server", loaderPath, params, env.server);
+ await this.runLoader(packageManager, "Server", loaderPath, params, env.server);
}
private buildClientParams(
@@ -263,13 +267,13 @@ export class StartAction extends AbstractAction {
}
private async runLoader(
+ packageManager: PackageManager,
name: string,
directory: string,
params: string[],
env: Record,
): Promise {
await runSafe(async () => {
- const packageManager = await PackageManagerFactory.find(directory);
await packageManager.run(name, directory, "start", params, env, [], true);
});
}
From a98dfe26a0e3f703f2dad62a96e9ac9ee149705f Mon Sep 17 00:00:00 2001
From: Exelo
Date: Thu, 24 Sep 2026 19:18:04 +0900
Subject: [PATCH 11/15] ci: add release handling on monorepo (#225)
---
.github/CONTRIBUTING.md | 60 +++++++++++++++++--
.../ISSUE_TEMPLATE/01-command_bug_report.yml | 13 +++-
.github/ISSUE_TEMPLATE/02-feature_request.yml | 11 ++++
.github/actions/prepare/action.yml | 6 +-
.github/issue-labeler.yml | 12 ++++
.github/labeler.yml | 18 ++++++
.github/labels.yml | 12 ++++
.github/workflows/alpha-release.yml | 12 +++-
.github/workflows/pre-release.yml | 4 +-
.github/workflows/release.yml | 24 +-------
.github/workflows/tests.yml | 4 +-
README.md | 2 +-
libs/schematics/.cliff-jumperrc.json | 2 +-
libs/schematics/README.md | 2 +-
libs/schematics/cliff.toml | 2 +-
libs/schematics/package.json | 13 ++--
.../schematics/nanoforge.collection.spec.ts | 20 +++----
17 files changed, 162 insertions(+), 55 deletions(-)
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 4775d01..ce8939f 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -2,12 +2,64 @@
If you wish to contribute to the NanoForge project, fork the repository and submit a pull request. Please mind following the pre-commit hooks to keep the codebase as clean as possible.
+## Repository structure
+
+This repository is a [pnpm workspace](https://pnpm.io/workspaces) monorepo orchestrated with [Turborepo](https://turborepo.com). It contains the following packages:
+
+| Path | Package | Description |
+| ----------------- | --------------------------- | ----------------------------------------------------- |
+| `.` | `@nanoforge-dev/cli` | The `nf` command line interface |
+| `libs/config` | `@nanoforge-dev/config` | Config types and helpers for `nanoforge.config.ts` |
+| `libs/schematics` | `@nanoforge-dev/schematics` | Schematics used by the CLI to generate projects files |
+
+The CLI depends on the libraries through `workspace:*`, so the libraries must be built before the CLI. The `repo:*` scripts take care of this for you.
+
## Setup
To get ready to work on the codebase, please do the following:
1. Fork & clone the repository, and make sure you're on the **main** branch
-2. Run `pnpm install --frozen-lockfile` ([install](https://pnpm.io/installation))
-3. Make your changes
-4. Run `pnpm format && pnpm build && pnpm test:unit` to run ESLint/Prettier, build and tests
-5. [Submit a pull request](https://github.com/NanoForge-dev/CLI/compare) (Make sure you follow the [conventional commit format](https://github.com/NanoForge-dev/CLI/blob/main/.github/COMMIT_CONVENTION.md))
+2. Use the Node.js version defined in [`.nvmrc`](../.nvmrc)
+3. Run `pnpm install --frozen-lockfile` ([install](https://pnpm.io/installation))
+4. Run `pnpm repo:build` to build every package
+5. Make your changes
+6. Run `pnpm repo:format && pnpm repo:build && pnpm repo:test` to run ESLint/Prettier, build and tests on every package
+7. [Submit a pull request](https://github.com/NanoForge-dev/CLI/compare) (Make sure you follow the [conventional commit format](https://github.com/NanoForge-dev/CLI/blob/main/.github/COMMIT_CONVENTION.md))
+
+## Useful commands
+
+Scripts prefixed with `repo:` run on every package of the workspace through Turborepo:
+
+| Command | Description |
+| --------------------- | --------------------------------------------- |
+| `pnpm repo:build` | Build all packages |
+| `pnpm repo:lint` | Check formatting and lint all packages |
+| `pnpm repo:format` | Format and fix lint errors in all packages |
+| `pnpm repo:test` | Run unit and end-to-end tests of all packages |
+| `pnpm repo:test:unit` | Run unit tests of all packages |
+| `pnpm repo:test:e2e` | Run end-to-end tests of all packages |
+
+Scripts without prefix (`build`, `lint`, `test`, ...) only run on the CLI package. To run a script on a single library, use the pnpm `--filter` flag:
+
+```sh
+pnpm --filter @nanoforge-dev/config test:unit
+pnpm --filter @nanoforge-dev/schematics build
+```
+
+## Commits
+
+Commits and pull request titles must follow the [conventional commit format](https://github.com/NanoForge-dev/CLI/blob/main/.github/COMMIT_CONVENTION.md). Use the package as scope when your change targets a single one: `cli`, `config` or `schematics` (e.g. `feat(config): add server port validation`).
+
+Git hooks are installed automatically by `pnpm install`:
+
+- `commit-msg` checks the commit message with commitlint
+- `pre-commit` formats staged files with lint-staged
+- `pre-push` runs `repo:build`, `repo:lint` and `repo:test`
+
+## Releases
+
+All the packages of this repository share the same version and are released together. Releases are handled by maintainers through GitHub Actions:
+
+1. The **Pre-Release** workflow opens a `releases/cli@` pull request that bumps every package to `` and updates their changelogs. The version must be higher than the latest published version of **every** package (e.g. `@nanoforge-dev/schematics` is already at `2.2.1` on npm)
+2. Merging this pull request triggers the **Release** workflow, which publishes the packages to npm (libraries first), creates the `` tag and GitHub release, and synchronizes the documentation
+3. The **Alpha Release** workflow can be triggered manually to publish an `alpha` pre-release of a single package from any branch
diff --git a/.github/ISSUE_TEMPLATE/01-command_bug_report.yml b/.github/ISSUE_TEMPLATE/01-command_bug_report.yml
index 879f998..cf6c03b 100644
--- a/.github/ISSUE_TEMPLATE/01-command_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/01-command_bug_report.yml
@@ -7,7 +7,17 @@ body:
value: |
Thank you for filing an issue! If you are here to ask a question, don't hesitate to open a discussion instead: https://github.com/NanoForge-dev/CLI/discussions.
- This issue form is for Nanoforge CLI.
+ This issue form is for the Nanoforge CLI and its libraries (`@nanoforge-dev/config`, `@nanoforge-dev/schematics`). If the issue is not related to a CLI command, pick `other` as the command.
+ - type: dropdown
+ id: package
+ attributes:
+ label: Which package is this bug report for?
+ options:
+ - "@nanoforge-dev/cli"
+ - "@nanoforge-dev/config"
+ - "@nanoforge-dev/schematics"
+ validations:
+ required: true
- type: dropdown
id: command
attributes:
@@ -16,6 +26,7 @@ body:
- build
- create
- dev
+ - editor
- help
- install
- login
diff --git a/.github/ISSUE_TEMPLATE/02-feature_request.yml b/.github/ISSUE_TEMPLATE/02-feature_request.yml
index bcf2d51..a4f7706 100644
--- a/.github/ISSUE_TEMPLATE/02-feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/02-feature_request.yml
@@ -6,6 +6,16 @@ body:
attributes:
value: |
Thank you for submitting an idea !
+ - type: dropdown
+ id: package
+ attributes:
+ label: Which package is this feature request for?
+ options:
+ - "@nanoforge-dev/cli"
+ - "@nanoforge-dev/config"
+ - "@nanoforge-dev/schematics"
+ validations:
+ required: true
- type: dropdown
id: command
attributes:
@@ -14,6 +24,7 @@ body:
- build
- create
- dev
+ - editor
- help
- install
- login
diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml
index 61fe548..facd38a 100644
--- a/.github/actions/prepare/action.yml
+++ b/.github/actions/prepare/action.yml
@@ -8,7 +8,7 @@ inputs:
description: Install pnpm depedencies
default: "true"
build:
- description: Build packages
+ description: Build all workspace packages
default: "true"
runs:
using: composite
@@ -17,7 +17,7 @@ runs:
name: Install Node.js
uses: actions/setup-node@v6
with:
- node-version: 25
+ node-version-file: .nvmrc
package-manager-cache: false
registry-url: https://registry.npmjs.org
@@ -28,4 +28,4 @@ runs:
- if: inputs.build == 'true'
name: Build packages
shell: bash
- run: pnpm run build
+ run: pnpm run repo:build
diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml
index a154d38..397fb14 100644
--- a/.github/issue-labeler.yml
+++ b/.github/issue-labeler.yml
@@ -10,6 +10,10 @@ command:dev:
- "### Which command is this (bug
report|feature request) for\\?\\n\\ndev\\n"
+command:editor:
+ - "### Which command is this (bug
+ report|feature request) for\\?\\n\\neditor\\n"
+
command:help:
- "### Which command is this (bug
report|feature request) for\\?\\n\\nhelp\\n"
@@ -41,3 +45,11 @@ command:start:
command:unpublish:
- "### Which command is this (bug
report|feature request) for\\?\\n\\nunpublish\\n"
+
+lib:config:
+ - "### Which package is this (bug
+ report|feature request) for\\?\\n\\n@nanoforge-dev/config\\n"
+
+lib:schematics:
+ - "### Which package is this (bug
+ report|feature request) for\\?\\n\\n@nanoforge-dev/schematics\\n"
diff --git a/.github/labeler.yml b/.github/labeler.yml
index d0086bf..89e7091 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -16,6 +16,12 @@ command:dev:
- src/command/commands/dev.command.ts
- src/action/actions/dev.action.ts
+command:editor:
+ - changed-files:
+ - any-glob-to-any-file:
+ - src/command/commands/editor.command.ts
+ - src/action/actions/editor.action.ts
+
command:install:
- changed-files:
- any-glob-to-any-file:
@@ -63,3 +69,15 @@ documentation:
- any-glob-to-any-file:
- docs/*
- docs/**/*
+
+lib:config:
+ - changed-files:
+ - any-glob-to-any-file:
+ - libs/config/*
+ - libs/config/**/*
+
+lib:schematics:
+ - changed-files:
+ - any-glob-to-any-file:
+ - libs/schematics/*
+ - libs/schematics/**/*
diff --git a/.github/labels.yml b/.github/labels.yml
index 0d245ee..30ae617 100644
--- a/.github/labels.yml
+++ b/.github/labels.yml
@@ -54,6 +54,10 @@
description: "Related to dev command"
color: "aaa3dc"
+- name: "command:editor"
+ description: "Related to editor command"
+ color: "aaa3dc"
+
- name: "command:help"
description: "Related to help command"
color: "aaa3dc"
@@ -86,6 +90,14 @@
description: "Related to unpublish command"
color: "aaa3dc"
+- name: "lib:config"
+ description: "Related to the @nanoforge-dev/config library"
+ color: "c5def5"
+
+- name: "lib:schematics"
+ description: "Related to the @nanoforge-dev/schematics library"
+ color: "c5def5"
+
- name: "question"
description: "Further information is requested"
color: "d876e3"
diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml
index 587dcfb..2e2de91 100644
--- a/.github/workflows/alpha-release.yml
+++ b/.github/workflows/alpha-release.yml
@@ -3,6 +3,14 @@ name: Alpha Release
on:
workflow_dispatch:
inputs:
+ package:
+ description: "The published name of a single package to release"
+ type: choice
+ required: true
+ options:
+ - "@nanoforge-dev/cli"
+ - "@nanoforge-dev/config"
+ - "@nanoforge-dev/schematics"
dry_run:
description: Perform a dry run?
type: boolean
@@ -16,7 +24,7 @@ jobs:
alpha-release:
name: Alpha release
runs-on: ubuntu-latest
- if: github.repository_owner == 'NanoForge-dev' && github.ref_name != 'main' && github.ref_name != 'master'
+ if: github.repository_owner == 'NanoForge-dev'
steps:
- name: Checkout repository
uses: actions/checkout@v7
@@ -27,7 +35,7 @@ jobs:
- name: Release alpha
uses: ./node_modules/@nanoforge-dev/actions/actions/release-dev
with:
- package: "@nanoforge-dev/cli"
+ package: ${{ inputs.package }}
tag: alpha
dry: ${{ inputs.dry_run }}
env:
diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml
index ce4d8e4..3977f21 100644
--- a/.github/workflows/pre-release.yml
+++ b/.github/workflows/pre-release.yml
@@ -34,8 +34,10 @@ jobs:
- name: Create release PR
uses: ./node_modules/@nanoforge-dev/actions/actions/create-packages-release-pr
with:
- packages: "@nanoforge-dev/cli"
+ packages: "@nanoforge-dev/config,@nanoforge-dev/schematics,@nanoforge-dev/cli"
version: ${{ inputs.version }}
+ branch-format: "releases/cli@{version}"
+ commit-format: "chore: release @nanoforge-dev/cli@{version}"
dry: ${{ inputs.dry_run }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c4a89b7..bee025f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,7 +15,7 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
- if: github.repository_owner == 'NanoForge-dev' && github.event.pull_request.merged == true && startsWith(github.head_ref, 'releases/')
+ if: github.repository_owner == 'NanoForge-dev' && github.event.pull_request.merged == true && startsWith(github.head_ref, 'releases/cli@')
steps:
- name: Checkout repository
uses: actions/checkout@v7
@@ -26,33 +26,13 @@ jobs:
- name: Release packages
uses: ./node_modules/@nanoforge-dev/actions/actions/release-packages
with:
- packages: "@nanoforge-dev/cli"
+ packages: "@nanoforge-dev/config,@nanoforge-dev/schematics,@nanoforge-dev/cli"
tag-format: "{version}"
latest: true
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- beta-release:
- name: Beta release
- runs-on: ubuntu-latest
- if: github.repository_owner == 'NanoForge-dev' && github.event.pull_request.merged == true && !startsWith(github.head_ref, 'releases/')
- steps:
- - name: Checkout repository
- uses: actions/checkout@v7
-
- - name: Prepare
- uses: ./.github/actions/prepare
-
- - name: Release beta
- uses: ./node_modules/@nanoforge-dev/actions/actions/release-dev
- with:
- package: "@nanoforge-dev/cli"
- tag: beta
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
publish-docs:
name: Publish docs
runs-on: ubuntu-latest
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a7cdc53..cf20107 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -20,7 +20,7 @@ jobs:
uses: ./.github/actions/prepare
- name: Run linter
- run: pnpm run lint
+ run: pnpm run repo:lint
- name: Run tests
- run: pnpm run test
+ run: pnpm run repo:test
diff --git a/README.md b/README.md
index 1bb959b..6343fec 100644
--- a/README.md
+++ b/README.md
@@ -183,7 +183,7 @@ Variables are scoped by prefix:
> **Note:** Prefixes are stripped before the variable is exposed to libraries.
-For full documentation on how libraries consume these variables, see [@nanoforge-dev/config](https://github.com/NanoForge-dev/Engine/tree/main/packages/config).
+For full documentation on how libraries consume these variables, see [@nanoforge-dev/config](https://github.com/NanoForge-dev/CLI/tree/main/libs/config).
## Contributing
diff --git a/libs/schematics/.cliff-jumperrc.json b/libs/schematics/.cliff-jumperrc.json
index 2caa355..39ace0a 100644
--- a/libs/schematics/.cliff-jumperrc.json
+++ b/libs/schematics/.cliff-jumperrc.json
@@ -2,7 +2,7 @@
"$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
"name": "schematics",
"org": "nanoforge-dev",
- "packagePath": ".",
+ "packagePath": "libs/schematics",
"tagTemplate": "{{new-version}}",
"identifierBase": false
}
diff --git a/libs/schematics/README.md b/libs/schematics/README.md
index f7ec6de..90ab999 100644
--- a/libs/schematics/README.md
+++ b/libs/schematics/README.md
@@ -16,7 +16,7 @@
## About
-This repository contains the Schematics of NanoForge. Check [releases][github-releases] to see versions of the Schematics. Nanoforge is a powerful game engine for web browser.
+This package contains the Schematics of NanoForge, used by the [CLI][cli-source] to generate project files. Check [releases][github-releases] to see versions of the Schematics. Nanoforge is a powerful game engine for web browser.
## Usage
diff --git a/libs/schematics/cliff.toml b/libs/schematics/cliff.toml
index 9c2cd71..4bb6a65 100644
--- a/libs/schematics/cliff.toml
+++ b/libs/schematics/cliff.toml
@@ -76,4 +76,4 @@ sort_commits = "newest"
[remote.github]
owner = "NanoForge-dev"
-repo = "schematics"
+repo = "CLI"
diff --git a/libs/schematics/package.json b/libs/schematics/package.json
index 80641fd..70ff2da 100644
--- a/libs/schematics/package.json
+++ b/libs/schematics/package.json
@@ -8,8 +8,8 @@
"game",
"schematics"
],
- "homepage": "https://github.com/NanoForge-dev/schematics#readme",
- "bugs": "https://github.com/NanoForge-dev/schematics/issues",
+ "homepage": "https://github.com/NanoForge-dev/CLI#readme",
+ "bugs": "https://github.com/NanoForge-dev/CLI/issues",
"license": "MIT",
"contributors": [
"Bill ",
@@ -44,9 +44,10 @@
},
"repository": {
"type": "git",
- "url": "git+https://github.com/NanoForge-dev/schematics.git"
+ "url": "git+https://github.com/NanoForge-dev/CLI.git",
+ "directory": "libs/schematics"
},
- "funding": "https://github.com/NanoForge-dev/schematics?sponsor",
+ "funding": "https://github.com/NanoForge-dev/CLI?sponsor",
"scripts": {
"build": "tsc --noEmit && tsdown",
"postbuild": "pnpm run copy:collection && pnpm run copy:lib",
@@ -55,7 +56,7 @@
"lint": "prettier --check . && eslint --format=pretty src",
"format": "prettier --write . && eslint --fix --format=pretty src",
"prepack": "pnpm run build && pnpm run lint",
- "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r . --include-path 'libs/schematics/*'",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'libs/schematics/*'",
"release": "cliff-jumper",
"test": "pnpm run test:unit && pnpm run test:e2e",
"test:unit": "vitest run -c vitest.config.ts",
@@ -89,7 +90,7 @@
"typescript": "catalog:build",
"vitest": "catalog:test"
},
- "packageManager": "pnpm@11.9.0",
+ "packageManager": "pnpm@12.0.0",
"engines": {
"node": "26"
},
diff --git a/src/lib/schematics/nanoforge.collection.spec.ts b/src/lib/schematics/nanoforge.collection.spec.ts
index a3dadeb..545e30e 100644
--- a/src/lib/schematics/nanoforge.collection.spec.ts
+++ b/src/lib/schematics/nanoforge.collection.spec.ts
@@ -11,29 +11,29 @@ describe("NanoforgeCollection", () => {
it("should return all available schematics", () => {
const schematics = collection.getSchematics();
const names = schematics.map((s) => s.name);
- expect(names).toContain("application");
- expect(names).toContain("configuration");
- expect(names).toContain("part-base");
- expect(names).toContain("part-main");
+ expect(names).toContain("project");
+ expect(names).toContain("workspace");
+ expect(names).toContain("component");
+ expect(names).toContain("system");
});
it("should have aliases for each schematic", () => {
const schematics = collection.getSchematics();
const aliases = schematics.map((s) => s.alias);
- expect(aliases).toContain("application");
- expect(aliases).toContain("config");
- expect(aliases).toContain("base");
- expect(aliases).toContain("main");
+ expect(aliases).toContain("project");
+ expect(aliases).toContain("workspace");
+ expect(aliases).toContain("component");
+ expect(aliases).toContain("system");
});
});
describe("execute", () => {
it("should accept a valid schematic name", async () => {
- await expect(collection.execute("application", [])).resolves.not.toThrow();
+ await expect(collection.execute("project", [])).resolves.not.toThrow();
});
it("should accept a valid schematic alias", async () => {
- await expect(collection.execute("config", [])).resolves.not.toThrow();
+ await expect(collection.execute("workspace", [])).resolves.not.toThrow();
});
it("should reject an invalid schematic name", async () => {
From d53432a2344c716f69e6733f48d08a3fea2cf592 Mon Sep 17 00:00:00 2001
From: Exelo
Date: Thu, 24 Sep 2026 23:05:52 +0900
Subject: [PATCH 12/15] chore: upgrade loader (#227)
* chore: upgrade loader to 1.5.1
* test: fix tests
---
e2e/cli-build.test.ts | 136 ++++++++--------
e2e/cli-create.test.ts | 171 ++++++---------------
e2e/cli-install.test.ts | 31 ++--
e2e/cli-new-config.test.ts | 120 ++++++---------
e2e/cli-new.test.ts | 165 +++++++++-----------
e2e/helpers/project-fixtures.ts | 36 +++++
e2e/helpers/write-project-config.ts | 36 -----
libs/schematics/e2e/project.test.ts | 35 ++---
libs/schematics/e2e/workspace.test.ts | 6 +-
libs/schematics/src/utils/registry.spec.ts | 8 +-
pnpm-lock.yaml | 39 ++---
pnpm-workspace.yaml | 9 +-
12 files changed, 318 insertions(+), 474 deletions(-)
create mode 100644 e2e/helpers/project-fixtures.ts
delete mode 100644 e2e/helpers/write-project-config.ts
diff --git a/e2e/cli-build.test.ts b/e2e/cli-build.test.ts
index 95ba92d..7a917ab 100644
--- a/e2e/cli-build.test.ts
+++ b/e2e/cli-build.test.ts
@@ -2,11 +2,29 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
+import { stripDefineConfig, writeStandaloneEntry } from "./helpers/project-fixtures";
import { runCli } from "./helpers/run-cli";
-import { writeProjectConfig } from "./helpers/write-project-config";
const tmpDir = resolve(__dirname, "../.tmp-e2e-build");
+const newProject = (name: string, directory: string, server: boolean) =>
+ runCli([
+ "new",
+ "--name",
+ name,
+ "--language",
+ "ts",
+ "--package-manager",
+ "npm",
+ "--strict",
+ server ? "--server" : "--no-server",
+ "--skip-install",
+ "--no-docker",
+ "--no-git",
+ "-d",
+ directory,
+ ]);
+
beforeAll(async () => {
mkdirSync(tmpDir, { recursive: true });
});
@@ -18,67 +36,48 @@ afterAll(() => {
describe("nf build (TypeScript, no server)", () => {
const projectDir = resolve(tmpDir, "build-ts-no-server");
const appDir = resolve(projectDir, "build-app");
+ const outDir = resolve(appDir, "dist");
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
-
- await runCli([
- "new",
- "--name",
- "build-app",
- "--language",
- "ts",
- "--package-manager",
- "npm",
- "--strict",
- "--no-server",
- "--no-init-functions",
- "--no-skip-install",
- "--no-docker",
- "--no-git",
- "-d",
- projectDir,
- ]);
-
- writeProjectConfig(appDir, { name: "build-app", language: "ts", server: false });
+ await newProject("build-app", projectDir, false);
+ stripDefineConfig(appDir);
+ writeStandaloneEntry(appDir);
});
it("should run the build command", async () => {
const { exitCode } = await runCli(["build", "-d", appDir]);
expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, ".nanoforge", "client"))).toBe(true);
- expect(existsSync(resolve(appDir, ".nanoforge", "client", "main.js"))).toBe(true);
+ expect(existsSync(resolve(outDir, "main.js"))).toBe(true);
});
- it("should accept --config option", async () => {
- const { exitCode } = await runCli(["build", "-d", appDir, "--config", "nanoforge.config.json"]);
+ it("should accept --client-out-dir option", async () => {
+ const { exitCode } = await runCli(["build", "-d", appDir, "--client-out-dir", "custom-out"]);
expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, ".nanoforge", "client", "main.js"))).toBe(true);
+ expect(existsSync(resolve(appDir, "custom-out", "main.js"))).toBe(true);
});
- it("should accept --client-out-dir option", async () => {
- const { exitCode } = await runCli(["build", "-d", appDir, "--client-out-dir", "custom-out"]);
+ it("should accept --client-entry option", async () => {
+ writeFileSync(resolve(appDir, "src", "other.ts"), 'console.log("other");\n');
+
+ const { exitCode } = await runCli(["build", "-d", appDir, "--client-entry", "src/other.ts"]);
expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, "custom-out"))).toBe(true);
- expect(existsSync(resolve(appDir, "custom-out", "main.js"))).toBe(true);
+ expect(existsSync(resolve(outDir, "other.js"))).toBe(true);
});
- it("should copy static files to output dir", async () => {
- const staticDir = resolve(appDir, "client", "static");
- mkdirSync(staticDir, { recursive: true });
- writeFileSync(resolve(staticDir, "asset.txt"), "hello");
+ it("should copy assets to output dir", async () => {
+ writeFileSync(resolve(appDir, "assets", "asset.txt"), "hello");
const { exitCode } = await runCli(["build", "-d", appDir]);
expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, ".nanoforge", "client", "asset.txt"))).toBe(true);
+ expect(existsSync(resolve(outDir, "asset.txt"))).toBe(true);
});
it("should reset output dir before rebuild", async () => {
- const outDir = resolve(appDir, ".nanoforge", "client");
mkdirSync(outDir, { recursive: true });
writeFileSync(resolve(outDir, "stale.js"), "// stale");
@@ -103,99 +102,89 @@ describe("nf build (TypeScript, no server)", () => {
]);
expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, ".nanoforge", "client", "custom-asset.txt"))).toBe(true);
+ expect(existsSync(resolve(outDir, "custom-asset.txt"))).toBe(true);
});
it("should keep process alive with --watch flag", async () => {
const { killed } = await runCli(["build", "-d", appDir, "--watch"], { timeout: 3000 });
-
expect(killed).toBe(true);
});
it("should accept --editor flag and use editor entry", async () => {
- const editorEntryDir = resolve(appDir, ".nanoforge", "editor", "client");
- mkdirSync(editorEntryDir, { recursive: true });
- writeFileSync(resolve(editorEntryDir, "main.ts"), 'console.log("editor");');
-
const { exitCode } = await runCli(["build", "-d", appDir, "--editor"]);
expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, ".nanoforge", "client", "main.js"))).toBe(true);
+ expect(existsSync(resolve(outDir, "main.js"))).toBe(true);
});
});
describe("nf build (TypeScript, with server)", () => {
const projectDir = resolve(tmpDir, "build-ts-with-server");
- const appDir = resolve(projectDir, "build-server-app");
+ const workspaceDir = resolve(projectDir, "build-server-app");
+ const clientDir = resolve(workspaceDir, "apps/client");
+ const serverDir = resolve(workspaceDir, "apps/server");
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
+ await newProject("build-server-app", projectDir, true);
+ for (const dir of [workspaceDir, clientDir, serverDir]) stripDefineConfig(dir);
+ writeStandaloneEntry(clientDir);
+ writeStandaloneEntry(serverDir);
+ });
- await runCli([
- "new",
- "--name",
- "build-server-app",
- "--language",
- "ts",
- "--package-manager",
- "npm",
- "--no-strict",
- "--server",
- "--no-init-functions",
- "--no-skip-install",
- "--no-docker",
- "--no-git",
- "-d",
- projectDir,
- ]);
+ it("should build every project of the workspace", async () => {
+ const { exitCode } = await runCli(["build", "-d", workspaceDir]);
- writeProjectConfig(appDir, { name: "build-server-app", language: "ts", server: true });
+ expect(exitCode).toBe(0);
+ expect(existsSync(resolve(clientDir, "dist", "main.js"))).toBe(true);
+ expect(existsSync(resolve(serverDir, "dist", "main.js"))).toBe(true);
});
- it("should run the build command with server enabled", async () => {
- const { exitCode } = await runCli(["build", "-d", appDir]);
+ it("should build a single project of the workspace", async () => {
+ rmSync(resolve(clientDir, "dist"), { recursive: true, force: true });
+
+ const { exitCode } = await runCli(["build", "-d", serverDir]);
expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, ".nanoforge", "client", "main.js"))).toBe(true);
- expect(existsSync(resolve(appDir, ".nanoforge", "server", "main.js"))).toBe(true);
+ expect(existsSync(resolve(serverDir, "dist", "main.js"))).toBe(true);
+ expect(existsSync(resolve(clientDir, "dist"))).toBe(false);
});
it("should accept --server-out-dir option", async () => {
const { exitCode } = await runCli([
"build",
"-d",
- appDir,
+ workspaceDir,
"--server-out-dir",
"custom-server-out",
]);
expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, "custom-server-out"))).toBe(true);
- expect(existsSync(resolve(appDir, "custom-server-out", "main.js"))).toBe(true);
+ expect(existsSync(resolve(serverDir, "custom-server-out", "main.js"))).toBe(true);
+ expect(existsSync(resolve(clientDir, "dist", "main.js"))).toBe(true);
});
it("should accept --server-static-dir option and copy files from it", async () => {
- const customStaticDir = resolve(appDir, "custom-server-static");
+ const customStaticDir = resolve(serverDir, "custom-server-static");
mkdirSync(customStaticDir, { recursive: true });
writeFileSync(resolve(customStaticDir, "server-asset.txt"), "server");
const { exitCode } = await runCli([
"build",
"-d",
- appDir,
+ workspaceDir,
"--server-static-dir",
"custom-server-static",
]);
expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, ".nanoforge", "server", "server-asset.txt"))).toBe(true);
+ expect(existsSync(resolve(serverDir, "dist", "server-asset.txt"))).toBe(true);
});
});
describe("nf build (with invalid directory)", () => {
it("should fail when directory does not exist", async () => {
const { exitCode } = await runCli(["build", "-d", resolve(tmpDir, "nonexistent")]);
-
expect(exitCode).not.toBe(0);
});
@@ -204,7 +193,6 @@ describe("nf build (with invalid directory)", () => {
mkdirSync(emptyDir, { recursive: true });
const { exitCode } = await runCli(["build", "-d", emptyDir]);
-
expect(exitCode).not.toBe(0);
});
});
diff --git a/e2e/cli-create.test.ts b/e2e/cli-create.test.ts
index b9f9cdb..cb88640 100644
--- a/e2e/cli-create.test.ts
+++ b/e2e/cli-create.test.ts
@@ -2,11 +2,29 @@ import { existsSync, mkdirSync, rmSync } from "node:fs";
import { resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
+import { stripDefineConfig } from "./helpers/project-fixtures";
import { runCli } from "./helpers/run-cli";
-import { writeProjectConfig } from "./helpers/write-project-config";
const tmpDir = resolve(__dirname, "../.tmp-e2e-create");
+const newProject = (name: string, directory: string, language: "ts" | "js", server: boolean) =>
+ runCli([
+ "new",
+ "--name",
+ name,
+ "--language",
+ language,
+ "--package-manager",
+ "npm",
+ "--no-strict",
+ server ? "--server" : "--no-server",
+ "--skip-install",
+ "--no-docker",
+ "--no-git",
+ "-d",
+ directory,
+ ]);
+
beforeAll(() => {
mkdirSync(tmpDir, { recursive: true });
});
@@ -15,32 +33,14 @@ afterAll(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
-describe("nf create (TypeScript, client component)", () => {
+describe("nf create (TypeScript, client project)", () => {
const projectDir = resolve(tmpDir, "create-ts-client");
const appDir = resolve(projectDir, "create-app");
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
-
- await runCli([
- "new",
- "--name",
- "create-app",
- "--language",
- "ts",
- "--package-manager",
- "npm",
- "--strict",
- "--no-server",
- "--no-init-functions",
- "--skip-install",
- "--no-docker",
- "--no-git",
- "-d",
- projectDir,
- ]);
-
- writeProjectConfig(appDir, { name: "create-app", language: "ts", server: false });
+ await newProject("create-app", projectDir, "ts", false);
+ stripDefineConfig(appDir);
});
it("should create a component successfully", async () => {
@@ -58,8 +58,8 @@ describe("nf create (TypeScript, client component)", () => {
expect(stdout).toContain("Element successfully created");
});
- it("should generate a component file in client/components", () => {
- expect(existsSync(resolve(appDir, "client/components/player.component.ts"))).toBe(true);
+ it("should generate a component file in src/components", () => {
+ expect(existsSync(resolve(appDir, "src/components/player.component.ts"))).toBe(true);
});
it("should create a system successfully", async () => {
@@ -76,23 +76,8 @@ describe("nf create (TypeScript, client component)", () => {
expect(stdout).toContain("Element successfully created");
});
- it("should generate a system file in client/systems", () => {
- expect(existsSync(resolve(appDir, "client/systems/movement.system.ts"))).toBe(true);
- });
-
- it("should accept --config option", async () => {
- const { exitCode } = await runCli([
- "create",
- "component",
- "--name",
- "health",
- "--config",
- "nanoforge.config.json",
- "-d",
- appDir,
- ]);
-
- expect(exitCode).toBe(0);
+ it("should generate a system file in src/systems", () => {
+ expect(existsSync(resolve(appDir, "src/systems/movement.system.ts"))).toBe(true);
});
it("should create a component with a custom path via --path", async () => {
@@ -102,42 +87,26 @@ describe("nf create (TypeScript, client component)", () => {
"--name",
"custom",
"--path",
- "client/custom-components",
+ "src/custom-components",
"-d",
appDir,
]);
expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, "client/custom-components/custom.component.ts"))).toBe(true);
+ expect(existsSync(resolve(appDir, "src/custom-components/custom.component.ts"))).toBe(true);
});
});
-describe("nf create (TypeScript, server component)", () => {
+describe("nf create (TypeScript, server project in a workspace)", () => {
const projectDir = resolve(tmpDir, "create-ts-server");
- const appDir = resolve(projectDir, "create-server-app");
+ const workspaceDir = resolve(projectDir, "create-server-app");
+ const serverDir = resolve(workspaceDir, "apps/server");
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
-
- await runCli([
- "new",
- "--name",
- "create-server-app",
- "--language",
- "ts",
- "--package-manager",
- "npm",
- "--no-strict",
- "--server",
- "--no-init-functions",
- "--skip-install",
- "--no-docker",
- "--no-git",
- "-d",
- projectDir,
- ]);
-
- writeProjectConfig(appDir, { name: "create-server-app", language: "ts", server: true });
+ await newProject("create-server-app", projectDir, "ts", true);
+ stripDefineConfig(workspaceDir);
+ stripDefineConfig(serverDir);
});
it("should create a server component successfully", async () => {
@@ -145,52 +114,34 @@ describe("nf create (TypeScript, server component)", () => {
"create",
"component",
"--name",
- "enemy",
- "--server",
+ "network",
"-d",
- appDir,
+ serverDir,
]);
expect(exitCode).toBe(0);
- });
-
- it("should generate a component file in server/components", () => {
- expect(existsSync(resolve(appDir, "server/components/enemy.component.ts"))).toBe(true);
+ expect(existsSync(resolve(serverDir, "src/components/network.component.ts"))).toBe(true);
});
it("should create a server system successfully", async () => {
- const { exitCode } = await runCli([
- "create",
- "system",
- "--name",
- "physics",
- "--server",
- "-d",
- appDir,
- ]);
+ const { exitCode } = await runCli(["create", "system", "--name", "sync", "-d", serverDir]);
expect(exitCode).toBe(0);
+ expect(existsSync(resolve(serverDir, "src/systems/sync.system.ts"))).toBe(true);
});
- it("should generate a system file in server/systems", () => {
- expect(existsSync(resolve(appDir, "server/systems/physics.system.ts"))).toBe(true);
- });
-
- it("should create a server component with a custom path via --path", async () => {
+ it("should fail when run at the workspace root", async () => {
const { exitCode } = await runCli([
"create",
"component",
"--name",
- "network",
- "--server",
- "--path",
- "server/custom-components",
+ "root",
"-d",
- appDir,
+ workspaceDir,
]);
- expect(exitCode).toBe(0);
- expect(existsSync(resolve(appDir, "server/custom-components/network.component.ts"))).toBe(true);
+ expect(exitCode).not.toBe(0);
+ expect(existsSync(resolve(workspaceDir, "src/components/root.component.ts"))).toBe(false);
});
});
@@ -200,46 +151,26 @@ describe("nf create (JavaScript)", () => {
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
-
- await runCli([
- "new",
- "--name",
- "create-js-app",
- "--language",
- "js",
- "--package-manager",
- "npm",
- "--no-strict",
- "--no-server",
- "--no-init-functions",
- "--skip-install",
- "--no-docker",
- "--no-git",
- "-d",
- projectDir,
- ]);
-
- writeProjectConfig(appDir, { name: "create-js-app", language: "js", server: false });
+ await newProject("create-js-app", projectDir, "js", false);
+ stripDefineConfig(appDir);
});
it("should create a JavaScript component successfully", async () => {
const { exitCode } = await runCli(["create", "component", "--name", "sprite", "-d", appDir]);
-
expect(exitCode).toBe(0);
});
it("should generate a .js component file", () => {
- expect(existsSync(resolve(appDir, "client/components/sprite.component.js"))).toBe(true);
+ expect(existsSync(resolve(appDir, "src/components/sprite.component.js"))).toBe(true);
});
it("should create a JavaScript system successfully", async () => {
const { exitCode } = await runCli(["create", "system", "--name", "render", "-d", appDir]);
-
expect(exitCode).toBe(0);
});
it("should generate a .js system file", () => {
- expect(existsSync(resolve(appDir, "client/systems/render.system.js"))).toBe(true);
+ expect(existsSync(resolve(appDir, "src/systems/render.system.js"))).toBe(true);
});
});
@@ -253,16 +184,12 @@ describe("nf create (error cases)", () => {
"-d",
resolve(tmpDir, "nonexistent"),
]);
-
expect(exitCode).not.toBe(0);
});
it("should fail with an invalid type", async () => {
- const projectDir = resolve(tmpDir, "create-ts-client");
- const appDir = resolve(projectDir, "create-app");
-
+ const appDir = resolve(tmpDir, "create-ts-client/create-app");
const { exitCode } = await runCli(["create", "invalid-type", "--name", "test", "-d", appDir]);
-
expect(exitCode).not.toBe(0);
});
});
diff --git a/e2e/cli-install.test.ts b/e2e/cli-install.test.ts
index 4d4149a..9bcdeed 100644
--- a/e2e/cli-install.test.ts
+++ b/e2e/cli-install.test.ts
@@ -1,4 +1,4 @@
-import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
+import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
@@ -18,26 +18,14 @@ describe("nf install (with existing project)", () => {
const projectDir = resolve(tmpDir, "install-project");
const appDir = resolve(projectDir, "install-app");
- beforeAll(async () => {
- mkdirSync(projectDir, { recursive: true });
-
- await runCli([
- "new",
- "--name",
- "install-app",
- "--language",
- "ts",
- "--package-manager",
- "npm",
- "--strict",
- "--no-server",
- "--no-init-functions",
- "--no-skip-install",
- "--no-docker",
- "--no-git",
- "-d",
- projectDir,
- ]);
+ beforeAll(() => {
+ // `install -l` only needs a package.json: a generated project cannot be used until the
+ // engine v2 packages it depends on are published on npm.
+ mkdirSync(appDir, { recursive: true });
+ writeFileSync(
+ resolve(appDir, "package.json"),
+ JSON.stringify({ name: "install-app", private: true }, null, 2),
+ );
});
it("should run the install command with a library name", async () => {
@@ -124,7 +112,6 @@ describe("nf install (without library name)", () => {
"npm",
"--strict",
"--no-server",
- "--no-init-functions",
"--skip-install",
"--no-docker",
"--no-git",
diff --git a/e2e/cli-new-config.test.ts b/e2e/cli-new-config.test.ts
index 80ef8eb..72f61e4 100644
--- a/e2e/cli-new-config.test.ts
+++ b/e2e/cli-new-config.test.ts
@@ -6,6 +6,24 @@ import { runCli } from "./helpers/run-cli";
const tmpDir = resolve(__dirname, "../.tmp-e2e-new-config");
+const newProject = (name: string, directory: string, extraArgs: string[]) =>
+ runCli([
+ "new",
+ "--name",
+ name,
+ "--language",
+ "ts",
+ "--package-manager",
+ "npm",
+ "--strict",
+ "--skip-install",
+ "--no-docker",
+ "--no-git",
+ ...extraArgs,
+ "-d",
+ directory,
+ ]);
+
beforeAll(() => {
mkdirSync(tmpDir, { recursive: true });
});
@@ -16,79 +34,46 @@ afterAll(() => {
describe("nf new config output (no server)", () => {
const projectDir = resolve(tmpDir, "config-no-server");
- let config: any;
+ let config: string;
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
-
- await runCli([
- "new",
- "--name",
- "config-test",
- "--language",
- "ts",
- "--package-manager",
- "npm",
- "--strict",
- "--no-server",
- "--no-init-functions",
- "--skip-install",
- "--no-docker",
- "--no-git",
- "-d",
- projectDir,
- ]);
-
- config = JSON.parse(
- readFileSync(resolve(projectDir, "config-test/nanoforge.config.json"), "utf-8"),
- );
+ await newProject("config-test", projectDir, ["--no-server"]);
+ config = readFileSync(resolve(projectDir, "config-test/nanoforge.config.ts"), "utf-8");
});
- it("should have client config", () => {
- expect(config.client.enable).toBe(true);
+ it("should generate a client project config", () => {
+ expect(config).toContain('type: "client"');
});
- it("should not have server enabled", () => {
- expect(config.server?.enable).not.toBe(true);
+ it("should use defineConfig from nanoforge", () => {
+ expect(config).toContain('import { defineConfig } from "nanoforge/config";');
});
});
describe("nf new config output (with server)", () => {
const projectDir = resolve(tmpDir, "config-with-server");
- let config: any;
+ const workspaceDir = resolve(projectDir, "server-test");
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
+ await newProject("server-test", projectDir, ["--server"]);
+ });
- await runCli([
- "new",
- "--name",
- "server-test",
- "--language",
- "ts",
- "--package-manager",
- "npm",
- "--no-strict",
- "--server",
- "--no-init-functions",
- "--skip-install",
- "--no-docker",
- "--no-git",
- "-d",
- projectDir,
- ]);
-
- config = JSON.parse(
- readFileSync(resolve(projectDir, "server-test/nanoforge.config.json"), "utf-8"),
- );
+ it("should generate a workspace config including the apps", () => {
+ const config = readFileSync(resolve(workspaceDir, "nanoforge.config.ts"), "utf-8");
+ expect(config).toContain('type: "workspace"');
+ expect(config).toContain('packages: ["apps/*"]');
});
- it("should have server enabled", () => {
- expect(config.server.enable).toBe(true);
+ it("should generate a client project config", () => {
+ const config = readFileSync(resolve(workspaceDir, "apps/client/nanoforge.config.ts"), "utf-8");
+ expect(config).toContain('type: "client"');
});
- it("should have client build config", () => {
- expect(config.client.enable).toBe(true);
+ it("should generate a server project config", () => {
+ const config = readFileSync(resolve(workspaceDir, "apps/server/nanoforge.config.ts"), "utf-8");
+ expect(config).toContain('type: "server"');
});
});
@@ -98,26 +83,7 @@ describe("nf new package.json output", () => {
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
-
- await runCli([
- "new",
- "--name",
- "pkg-app",
- "--language",
- "ts",
- "--package-manager",
- "npm",
- "--strict",
- "--no-server",
- "--no-init-functions",
- "--skip-install",
- "--no-docker",
- "--editor",
- "--no-git",
- "-d",
- projectDir,
- ]);
-
+ await newProject("pkg-app", projectDir, ["--no-server", "--editor"]);
pkg = JSON.parse(readFileSync(resolve(projectDir, "pkg-app/package.json"), "utf-8"));
});
@@ -127,7 +93,11 @@ describe("nf new package.json output", () => {
it("should have nanoforge dependencies", () => {
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
- expect(allDeps).toHaveProperty("@nanoforge-dev/core");
- expect(allDeps).toHaveProperty("@nanoforge-dev/core-editor");
+ expect(allDeps).toHaveProperty("nanoforge");
+ expect(allDeps).toHaveProperty("@nanoforge-dev/ecs");
+ expect(allDeps).toHaveProperty("@nanoforge-dev/graphics-2d");
});
+
+ // TODO: `--editor` does not add any editor dependency to the generated project yet.
+ it.todo("should have editor dependencies with --editor");
});
diff --git a/e2e/cli-new.test.ts b/e2e/cli-new.test.ts
index 143ac7a..2eacf09 100644
--- a/e2e/cli-new.test.ts
+++ b/e2e/cli-new.test.ts
@@ -16,6 +16,7 @@ afterAll(() => {
describe("nf new (TypeScript, no server)", () => {
const projectDir = resolve(tmpDir, "ts-no-server");
+ const appDir = resolve(projectDir, "ts-app");
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
@@ -32,8 +33,7 @@ describe("nf new (TypeScript, no server)", () => {
"npm",
"--strict",
"--no-server",
- "--no-init-functions",
- "--no-skip-install",
+ "--skip-install",
"--no-docker",
"--no-git",
"-d",
@@ -46,47 +46,74 @@ describe("nf new (TypeScript, no server)", () => {
});
it("should create the project directory", () => {
- expect(existsSync(resolve(projectDir, "ts-app"))).toBe(true);
+ expect(existsSync(appDir)).toBe(true);
});
- it("should generate nanoforge.config.json", () => {
- const configPath = resolve(projectDir, "ts-app/nanoforge.config.json");
+ it("should generate a client nanoforge.config.ts", () => {
+ const configPath = resolve(appDir, "nanoforge.config.ts");
expect(existsSync(configPath)).toBe(true);
-
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
- expect(config.client).toBeDefined();
- expect(config.client.enable).toBe(true);
+ expect(readFileSync(configPath, "utf-8")).toContain('type: "client"');
});
it("should generate package.json", () => {
- expect(existsSync(resolve(projectDir, "ts-app/package.json"))).toBe(true);
+ const pkg = JSON.parse(readFileSync(resolve(appDir, "package.json"), "utf-8"));
+ expect(pkg.name).toBe("ts-app");
});
it("should generate tsconfig.json for TypeScript", () => {
- expect(existsSync(resolve(projectDir, "ts-app/tsconfig.json"))).toBe(true);
+ expect(existsSync(resolve(appDir, "tsconfig.json"))).toBe(true);
});
- it("should generate client directory", () => {
- expect(existsSync(resolve(projectDir, "ts-app/client"))).toBe(true);
+ it("should generate the client entry file", () => {
+ expect(existsSync(resolve(appDir, "src/main.ts"))).toBe(true);
});
- it("should generate client main file", () => {
- expect(existsSync(resolve(projectDir, "ts-app/client/main.ts"))).toBe(true);
+ it("should generate components and systems", () => {
+ expect(existsSync(resolve(appDir, "src/components"))).toBe(true);
+ expect(existsSync(resolve(appDir, "src/systems"))).toBe(true);
});
- it("should not generate server directory", () => {
- expect(existsSync(resolve(projectDir, "ts-app/server"))).toBe(false);
+ it("should not generate a workspace", () => {
+ expect(existsSync(resolve(appDir, "apps"))).toBe(false);
});
- it("should not have server enabled in config", () => {
- const configPath = resolve(projectDir, "ts-app/nanoforge.config.json");
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
- expect(config.server?.enable).not.toBe(true);
+ it("should not generate docker files", () => {
+ expect(existsSync(resolve(appDir, "Dockerfile"))).toBe(false);
+ });
+});
+
+describe("nf new (with dependencies installation)", () => {
+ // TODO: enable once the engine v2 packages (`@nanoforge-dev/ecs`, `@nanoforge-dev/network`
+ // and `nanoforge`) are published on npm, installing a generated project fails until then.
+ it.skip("should install the project dependencies", async () => {
+ const projectDir = resolve(tmpDir, "with-install");
+ mkdirSync(projectDir, { recursive: true });
+
+ const { exitCode } = await runCli([
+ "new",
+ "--name",
+ "install-app",
+ "--language",
+ "ts",
+ "--package-manager",
+ "npm",
+ "--strict",
+ "--no-server",
+ "--no-skip-install",
+ "--no-docker",
+ "--no-git",
+ "-d",
+ projectDir,
+ ]);
+
+ expect(exitCode).toBe(0);
+ expect(existsSync(resolve(projectDir, "install-app/node_modules"))).toBe(true);
});
});
describe("nf new (JavaScript, with server)", () => {
const projectDir = resolve(tmpDir, "js-with-server");
+ const workspaceDir = resolve(projectDir, "js-game");
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
@@ -103,7 +130,6 @@ describe("nf new (JavaScript, with server)", () => {
"pnpm",
"--no-strict",
"--server",
- "--init-functions",
"--skip-install",
"--no-docker",
"--no-git",
@@ -115,42 +141,33 @@ describe("nf new (JavaScript, with server)", () => {
expect(stdout).toContain("Project successfully created");
});
- it("should generate server directory", () => {
- expect(existsSync(resolve(projectDir, "js-game/server"))).toBe(true);
+ it("should generate a workspace nanoforge.config.js", () => {
+ const content = readFileSync(resolve(workspaceDir, "nanoforge.config.js"), "utf-8");
+ expect(content).toContain('type: "workspace"');
+ expect(content).toContain('packages: ["apps/*"]');
});
- it("should generate server main file as .js", () => {
- expect(existsSync(resolve(projectDir, "js-game/server/main.js"))).toBe(true);
+ it("should generate a pnpm workspace", () => {
+ expect(existsSync(resolve(workspaceDir, "pnpm-workspace.yaml"))).toBe(true);
});
- it("should generate client main file as .js", () => {
- expect(existsSync(resolve(projectDir, "js-game/client/main.js"))).toBe(true);
+ it("should generate the client project", () => {
+ const content = readFileSync(resolve(workspaceDir, "apps/client/nanoforge.config.js"), "utf-8");
+ expect(content).toContain('type: "client"');
+ expect(existsSync(resolve(workspaceDir, "apps/client/src/main.js"))).toBe(true);
});
- it("should have server enabled in config", () => {
- const configPath = resolve(projectDir, "js-game/nanoforge.config.json");
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
- expect(config.server.enable).toBe(true);
- });
-
- it("should generate init function files for client", () => {
- expect(existsSync(resolve(projectDir, "js-game/client/init/before-init.js"))).toBe(true);
- expect(existsSync(resolve(projectDir, "js-game/client/init/after-init.js"))).toBe(true);
- });
-
- it("should generate init function files for server", () => {
- expect(existsSync(resolve(projectDir, "js-game/server/init/before-init.js"))).toBe(true);
- expect(existsSync(resolve(projectDir, "js-game/server/init/after-init.js"))).toBe(true);
+ it("should generate the server project", () => {
+ const content = readFileSync(resolve(workspaceDir, "apps/server/nanoforge.config.js"), "utf-8");
+ expect(content).toContain('type: "server"');
+ expect(existsSync(resolve(workspaceDir, "apps/server/src/main.js"))).toBe(true);
});
it("should generate jsconfig.json instead of tsconfig.json", () => {
- expect(existsSync(resolve(projectDir, "js-game/jsconfig.json"))).toBe(true);
- expect(existsSync(resolve(projectDir, "js-game/tsconfig.json"))).toBe(false);
- });
-
- it("should generate save files in .nanoforge", () => {
- expect(existsSync(resolve(projectDir, "js-game/.nanoforge/client.save.json"))).toBe(true);
- expect(existsSync(resolve(projectDir, "js-game/.nanoforge/server.save.json"))).toBe(true);
+ expect(existsSync(resolve(workspaceDir, "jsconfig.json"))).toBe(true);
+ expect(existsSync(resolve(workspaceDir, "tsconfig.json"))).toBe(false);
+ expect(existsSync(resolve(workspaceDir, "apps/client/jsconfig.json"))).toBe(true);
+ expect(existsSync(resolve(workspaceDir, "apps/server/jsconfig.json"))).toBe(true);
});
});
@@ -174,7 +191,6 @@ describe("nf new (with --path option)", () => {
"npm",
"--strict",
"--no-server",
- "--no-init-functions",
"--skip-install",
"--no-docker",
"--no-git",
@@ -187,12 +203,14 @@ describe("nf new (with --path option)", () => {
});
it("should create the project in the custom path", () => {
- expect(existsSync(resolve(projectDir, "custom/subdir"))).toBe(true);
+ expect(existsSync(resolve(projectDir, "custom/subdir/nanoforge.config.ts"))).toBe(true);
+ expect(existsSync(resolve(projectDir, "path-app"))).toBe(false);
});
});
describe("nf new (with typescript with docker option)", () => {
const projectDir = resolve(tmpDir, "ts-with-docker");
+ const appDir = resolve(projectDir, "ts-app");
beforeAll(async () => {
mkdirSync(projectDir, { recursive: true });
@@ -209,7 +227,6 @@ describe("nf new (with typescript with docker option)", () => {
"npm",
"--strict",
"--no-server",
- "--no-init-functions",
"--skip-install",
"--docker",
"--no-git",
@@ -222,50 +239,16 @@ describe("nf new (with typescript with docker option)", () => {
expect(stdout).toContain("Project successfully created");
});
- it("should create the project directory", () => {
- expect(existsSync(resolve(projectDir, "ts-app"))).toBe(true);
- });
-
- it("should generate nanoforge.config.json", () => {
- const configPath = resolve(projectDir, "ts-app/nanoforge.config.json");
- expect(existsSync(configPath)).toBe(true);
-
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
- expect(config.client).toBeDefined();
- expect(config.client.enable).toBe(true);
- });
-
- it("should generate package.json", () => {
- expect(existsSync(resolve(projectDir, "ts-app/package.json"))).toBe(true);
- });
-
- it("should generate tsconfig.json for TypeScript", () => {
- expect(existsSync(resolve(projectDir, "ts-app/tsconfig.json"))).toBe(true);
- });
-
- it("should generate client directory", () => {
- expect(existsSync(resolve(projectDir, "ts-app/client"))).toBe(true);
- });
-
- it("should generate client main file", () => {
- expect(existsSync(resolve(projectDir, "ts-app/client/main.ts"))).toBe(true);
- });
-
- it("should not generate server directory", () => {
- expect(existsSync(resolve(projectDir, "ts-app/server"))).toBe(false);
- });
-
- it("should not have server enabled in config", () => {
- const configPath = resolve(projectDir, "ts-app/nanoforge.config.json");
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
- expect(config.server?.enable).not.toBe(true);
+ it("should generate the project", () => {
+ expect(existsSync(resolve(appDir, "nanoforge.config.ts"))).toBe(true);
+ expect(existsSync(resolve(appDir, "src/main.ts"))).toBe(true);
});
it("should generate Dockerfile", () => {
- expect(existsSync(resolve(projectDir, "ts-app/Dockerfile"))).toBe(true);
+ expect(existsSync(resolve(appDir, "Dockerfile"))).toBe(true);
});
it("should generate .dockerignore", () => {
- expect(existsSync(resolve(projectDir, "ts-app/.dockerignore"))).toBe(true);
+ expect(existsSync(resolve(appDir, ".dockerignore"))).toBe(true);
});
});
diff --git a/e2e/helpers/project-fixtures.ts b/e2e/helpers/project-fixtures.ts
new file mode 100644
index 0000000..d44656d
--- /dev/null
+++ b/e2e/helpers/project-fixtures.ts
@@ -0,0 +1,36 @@
+import { existsSync, readFileSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+
+const CONFIG_IMPORT = 'import { defineConfig } from "nanoforge/config";\n';
+const DEFINE_CONFIG_CALL = /defineConfig\(([\s\S]*)\);/;
+
+/**
+ * Configs generated by `nf new` import `defineConfig` from `nanoforge/config`, which is not
+ * published yet, so the CLI cannot load them in a project scaffolded with `--skip-install`.
+ * `defineConfig` is an identity function: stripping the import and the call keeps the exact
+ * generated config while making it loadable without dependencies.
+ *
+ * TODO: remove once `nanoforge` is published with its `./config` export.
+ */
+export const stripDefineConfig = (projectDir: string): void => {
+ const path = ["nanoforge.config.ts", "nanoforge.config.js"]
+ .map((file) => join(projectDir, file))
+ .find((file) => existsSync(file));
+ if (!path) throw new Error(`No nanoforge config found in '${projectDir}'`);
+
+ const content = readFileSync(path, "utf-8");
+ if (!content.includes(CONFIG_IMPORT) || !DEFINE_CONFIG_CALL.test(content))
+ throw new Error(`Unexpected config format in '${path}', update stripDefineConfig`);
+
+ writeFileSync(path, content.replace(CONFIG_IMPORT, "").replace(DEFINE_CONFIG_CALL, "$1;"));
+};
+
+/**
+ * Engine packages imported by the generated entry file are not published yet, so builds run
+ * against an entry file without any dependency.
+ *
+ * TODO: remove once the engine v2 packages are published.
+ */
+export const writeStandaloneEntry = (projectDir: string, entry = "src/main.ts"): void => {
+ writeFileSync(join(projectDir, entry), 'console.log("nanoforge");\n');
+};
diff --git a/e2e/helpers/write-project-config.ts b/e2e/helpers/write-project-config.ts
deleted file mode 100644
index 9db4030..0000000
--- a/e2e/helpers/write-project-config.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { writeFileSync } from "node:fs";
-import { join } from "node:path";
-
-interface WriteProjectConfigOptions {
- name: string;
- language: "ts" | "js";
- initFunctions?: boolean;
- server?: boolean;
-}
-
-/**
- * `nf new` still scaffolds a `nanoforge.config.json` (schematics is out of scope for this
- * change), but the CLI's config loader now only reads `.ts`/`.js` config modules. Tests that
- * scaffold a project and then run `build`/`create` against it need a hand-written
- * `nanoforge.config.ts` alongside the scaffolded JSON — everything not set here falls back to
- * the CLI's own defaults, matching what the scaffolded JSON used to rely on too.
- */
-export const writeProjectConfig = (
- appDir: string,
- { name, language, initFunctions = false, server = false }: WriteProjectConfigOptions,
-) => {
- const content = `export default ${JSON.stringify(
- {
- name,
- language,
- initFunctions,
- client: { type: "client", enable: true },
- server: { type: "server", enable: server },
- tls: { enable: false },
- },
- null,
- 2,
- )};\n`;
-
- writeFileSync(join(appDir, "nanoforge.config.ts"), content);
-};
diff --git a/libs/schematics/e2e/project.test.ts b/libs/schematics/e2e/project.test.ts
index a489c4f..1c2363e 100644
--- a/libs/schematics/e2e/project.test.ts
+++ b/libs/schematics/e2e/project.test.ts
@@ -356,13 +356,12 @@ describe("project schematic", () => {
});
describe("allowBuilds (standalone projects are their own root)", () => {
- it("is ignored when generated inside a workspace, even if passed", async () => {
+ it("is empty when generated inside a workspace", async () => {
const tree = await runner.runSchematic("project", {
part: "server",
workspaceName: "in-workspace-allow-builds",
directory: "in-workspace-allow-builds-apps/server",
packageManager: "pnpm",
- allowBuilds: ["wrtc"],
});
expect(tree.files).not.toContain(
"/in-workspace-allow-builds-apps/server/pnpm-workspace.yaml",
@@ -374,48 +373,33 @@ describe("project schematic", () => {
expect(packageJson).not.toHaveProperty("trustedDependencies");
});
- it("writes a pnpm-workspace.yaml allowBuilds map for a standalone pnpm project", async () => {
+ it("always allows bun in a pnpm-workspace.yaml for a standalone pnpm project", async () => {
const tree = await runner.runSchematic("project", {
- part: "server",
+ part: "client",
workspaceName: "standalone-allow-builds",
- directory: "standalone-allow-builds-apps/server",
+ directory: "standalone-allow-builds-apps/client",
workspace: false,
packageManager: "pnpm",
- allowBuilds: ["wrtc"],
});
const pnpmWorkspaceYaml = tree.readContent(
- "/standalone-allow-builds-apps/server/pnpm-workspace.yaml",
+ "/standalone-allow-builds-apps/client/pnpm-workspace.yaml",
);
expect(pnpmWorkspaceYaml).toContain("allowBuilds:");
- expect(pnpmWorkspaceYaml).toContain("wrtc: true");
- });
-
- it("does not generate pnpm-workspace.yaml for a standalone pnpm project with no allowBuilds", async () => {
- const tree = await runner.runSchematic("project", {
- part: "client",
- workspaceName: "standalone-no-allow-builds",
- directory: "standalone-no-allow-builds-apps/client",
- workspace: false,
- packageManager: "pnpm",
- });
- expect(tree.files).not.toContain(
- "/standalone-no-allow-builds-apps/client/pnpm-workspace.yaml",
- );
+ expect(pnpmWorkspaceYaml).toContain("bun: true");
});
- it("writes allowScripts/trustedDependencies for a standalone npm/bun project", async () => {
+ it("always allows bun in allowScripts/trustedDependencies for a standalone npm/bun project", async () => {
const npmTree = await runner.runSchematic("project", {
part: "server",
workspaceName: "standalone-npm-allow-builds",
directory: "standalone-npm-allow-builds-apps/server",
workspace: false,
packageManager: "npm",
- allowBuilds: ["wrtc"],
});
const npmPackageJson = JSON.parse(
npmTree.readContent("/standalone-npm-allow-builds-apps/server/package.json"),
);
- expect(npmPackageJson.allowScripts).toEqual({ wrtc: true });
+ expect(npmPackageJson.allowScripts).toEqual({ bun: true });
const bunTree = await runner.runSchematic("project", {
part: "server",
@@ -423,12 +407,11 @@ describe("project schematic", () => {
directory: "standalone-bun-allow-builds-apps/server",
workspace: false,
packageManager: "bun",
- allowBuilds: ["wrtc"],
});
const bunPackageJson = JSON.parse(
bunTree.readContent("/standalone-bun-allow-builds-apps/server/package.json"),
);
- expect(bunPackageJson.trustedDependencies).toEqual(["wrtc"]);
+ expect(bunPackageJson.trustedDependencies).toEqual(["bun"]);
});
});
});
diff --git a/libs/schematics/e2e/workspace.test.ts b/libs/schematics/e2e/workspace.test.ts
index f09dda0..10445b1 100644
--- a/libs/schematics/e2e/workspace.test.ts
+++ b/libs/schematics/e2e/workspace.test.ts
@@ -64,13 +64,13 @@ describe("workspace schematic", () => {
expect(packageJson.devDependencies["typescript"]).toMatch(RESOLVED_VERSION);
});
- it("pins typescript to a specific version, not a range", () => {
- expect(packageJson.devDependencies["typescript"]).toMatch(/^\d+\.\d+\.\d+$/);
+ it("resolves typescript to a caret range (or the exact fallback version)", () => {
+ expect(packageJson.devDependencies["typescript"]).toMatch(/^\^?\d+\.\d+\.\d+$/);
});
it("does not jump typescript to its major-7 native rewrite", () => {
const version = packageJson.devDependencies["typescript"] as string;
- expect(version.split(".")[0]).toBe("6");
+ expect(version.replace(/^\^/, "").split(".")[0]).toBe("6");
});
it("should declare a workspace-typed nanoforge.config.ts with apps only (no libs)", () => {
diff --git a/libs/schematics/src/utils/registry.spec.ts b/libs/schematics/src/utils/registry.spec.ts
index 6737080..a90034f 100644
--- a/libs/schematics/src/utils/registry.spec.ts
+++ b/libs/schematics/src/utils/registry.spec.ts
@@ -35,7 +35,7 @@ describe("fetchTrustedVersion", () => {
});
const version = await fetchTrustedVersion("some-pkg", "0.0.0");
- expect(version).toBe("1.1.0");
+ expect(version).toBe("^1.1.0");
});
it("excludes prerelease and non-semver tags", async () => {
@@ -45,7 +45,7 @@ describe("fetchTrustedVersion", () => {
});
const version = await fetchTrustedVersion("some-pkg", "0.0.0");
- expect(version).toBe("1.0.0");
+ expect(version).toBe("^1.0.0");
});
it("restricts to a requested major version", async () => {
@@ -55,7 +55,7 @@ describe("fetchTrustedVersion", () => {
});
const version = await fetchTrustedVersion("typescript", "6.0.0", { major: 6 });
- expect(version).toBe("6.0.3");
+ expect(version).toBe("^6.0.3");
});
it("falls back when every version is too fresh", async () => {
@@ -101,6 +101,6 @@ describe("fetchTrustedVersions", () => {
"pkg-b": { fallback: "0.0.1" },
});
- expect(versions).toEqual({ "pkg-a": "2.5.0", "pkg-b": "2.5.0" });
+ expect(versions).toEqual({ "pkg-a": "^2.5.0", "pkg-b": "^2.5.0" });
});
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a92edc3..18eb710 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -213,11 +213,11 @@ catalogs:
version: 3.9.6
loader:
'@nanoforge-dev/loader-client':
- specifier: ^1.5.0
- version: 1.5.0
+ specifier: ^1.5.1
+ version: 1.5.1
'@nanoforge-dev/loader-server':
- specifier: ^1.5.0
- version: 1.5.0
+ specifier: ^1.5.1
+ version: 1.5.1
schematics:
'@angular-devkit/core':
specifier: ^22.0.4
@@ -260,10 +260,10 @@ importers:
version: 0.1.4
'@nanoforge-dev/loader-client':
specifier: catalog:loader
- version: 1.5.0
+ version: 1.5.1
'@nanoforge-dev/loader-server':
specifier: catalog:loader
- version: 1.5.0
+ version: 1.5.1
'@nanoforge-dev/schematics':
specifier: workspace:*
version: link:libs/schematics
@@ -879,17 +879,17 @@ packages:
resolution: {integrity: sha512-bqbrH8ulFSiZz1iisJHtHeEzj1RV/70uErCYHeW4DIhVPsWbBfWKFvg/ghAbmPk5vsj0YnhDX6DSpik4S1QoiQ==}
engines: {node: '25'}
- '@nanoforge-dev/loader-client@1.5.0':
- resolution: {integrity: sha512-FUcrfXlcaG4dsjya7fUUFkr5zwKB0nCihYeEOC1K54gl4XV6S1lgNnAfdkl4jZfzyyw4jcZUB+73t4ymBhz08g==}
- engines: {node: '25'}
+ '@nanoforge-dev/loader-client@1.5.1':
+ resolution: {integrity: sha512-1PueGpj8DbjRcZTVVCKAkKi4A8gyBaFTkfp78fFrW47J0HZcfS8pq+AaNaBjEUOozn0E7L+U8zGf/5laxTRe1A==}
+ engines: {node: '26'}
- '@nanoforge-dev/loader-server@1.5.0':
- resolution: {integrity: sha512-1rly9IgunGz8Ux6lGDugUXgmNbnNjWGM8ABy7EsdrKWV74gyj7cuU6X69UolO28XUPCdbeISNcBEZV5hLK8Rlg==}
- engines: {node: '25'}
+ '@nanoforge-dev/loader-server@1.5.1':
+ resolution: {integrity: sha512-RNqA0zpEhUXENKu5chJ+vQOhr8qK+a7z6XePmiCJygpSScJ8p1dlWquFYNwu9dBLHBP8BhK9dSFvXjPvHAYBxQ==}
+ engines: {node: '26'}
- '@nanoforge-dev/loader-website@1.5.0':
- resolution: {integrity: sha512-X37wlYyoL/q8ZPk5I8W3z01novfDSopizVQXbk/sw0dRXoONtIb6xnZLlN3z4jkDkPOy6IVes0bLGr8h4aFOcA==}
- engines: {node: '25'}
+ '@nanoforge-dev/loader-website@1.5.1':
+ resolution: {integrity: sha512-Ugu5CvfbsCK2wG8RXBHkkVlNHkSlXz2xXxfqglID7Cb4E8PHO0p1hbE6e7+1fWXAdFhEivGQHSI1POIvgyP1Qg==}
+ engines: {node: '26'}
'@nanoforge-dev/utils-eslint-config@1.0.2':
resolution: {integrity: sha512-hIsVGGpVIW3RvPZocWEf15Y5eqmIxTtLfWI66wqx+AO6PHcatUBhEi4sk1i54Qb2yGFueRBS/dSXj0kxG4Rbcw==}
@@ -3560,17 +3560,18 @@ snapshots:
tar: 7.5.22
typescript: 6.0.3
- '@nanoforge-dev/loader-client@1.5.0':
+ '@nanoforge-dev/loader-client@1.5.1':
dependencies:
- '@nanoforge-dev/loader-website': 1.5.0
+ '@nanoforge-dev/loader-website': 1.5.1
bun: 1.4.0
commander: 15.0.0
- '@nanoforge-dev/loader-server@1.5.0':
+ '@nanoforge-dev/loader-server@1.5.1':
dependencies:
+ bun: 1.4.0
commander: 15.0.0
- '@nanoforge-dev/loader-website@1.5.0': {}
+ '@nanoforge-dev/loader-website@1.5.1': {}
'@nanoforge-dev/utils-eslint-config@1.0.2(@types/eslint@9.6.1)(eslint@10.9.1(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.6)(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 10feea7..5c5c96a 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -47,8 +47,8 @@ catalogs:
eslint: ^10.9.1
prettier: ^3.9.6
loader:
- '@nanoforge-dev/loader-client': ^1.5.0
- '@nanoforge-dev/loader-server': ^1.5.0
+ '@nanoforge-dev/loader-client': ^1.5.1
+ '@nanoforge-dev/loader-server': ^1.5.1
schematics:
'@angular-devkit/core': ^22.0.4
'@angular-devkit/schematics': ^22.1.6
@@ -57,3 +57,8 @@ catalogs:
test:
'@vitest/coverage-v8': ^4.1.11
vitest: ^4.1.11
+
+minimumReleaseAgeExclude:
+ - '@nanoforge-dev/loader-client@1.5.1'
+ - '@nanoforge-dev/loader-server@1.5.1'
+ - '@nanoforge-dev/loader-website@1.5.1'
From 78da77e12b7d8c1761c74547395b1b9872e0ba65 Mon Sep 17 00:00:00 2001
From: Exelo
Date: Thu, 24 Sep 2026 23:22:44 +0900
Subject: [PATCH 13/15] chore: update contributors list (#229)
---
libs/config/package.json | 4 ++--
libs/schematics/package.json | 4 ++--
package.json | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/libs/config/package.json b/libs/config/package.json
index 25a6b79..66a0346 100644
--- a/libs/config/package.json
+++ b/libs/config/package.json
@@ -12,11 +12,11 @@
"bugs": "https://github.com/NanoForge-dev/CLI/issues",
"license": "MIT",
"contributors": [
- "Bill ",
"Exelo ",
"Fexkoser ",
+ "Harper ",
"Josephine ",
- "Tchips "
+ "Leo "
],
"files": [
"dist"
diff --git a/libs/schematics/package.json b/libs/schematics/package.json
index 70ff2da..4be5c5f 100644
--- a/libs/schematics/package.json
+++ b/libs/schematics/package.json
@@ -12,11 +12,11 @@
"bugs": "https://github.com/NanoForge-dev/CLI/issues",
"license": "MIT",
"contributors": [
- "Bill ",
"Exelo ",
"Fexkoser ",
+ "Harper ",
"Josephine ",
- "Tchips "
+ "Leo