diff --git a/packages/core-editor/.cliff-jumperrc.json b/packages/core-editor/.cliff-jumperrc.json
new file mode 100644
index 00000000..c760745b
--- /dev/null
+++ b/packages/core-editor/.cliff-jumperrc.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
+ "name": "core-editor",
+ "org": "nanoforge-dev",
+ "packagePath": "packages/core-editor",
+ "identifierBase": false
+}
diff --git a/packages/core-editor/.gitignore b/packages/core-editor/.gitignore
new file mode 100644
index 00000000..f7652f89
--- /dev/null
+++ b/packages/core-editor/.gitignore
@@ -0,0 +1,231 @@
+### 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
diff --git a/packages/core-editor/.idea/.name b/packages/core-editor/.idea/.name
new file mode 100644
index 00000000..b5da13ff
--- /dev/null
+++ b/packages/core-editor/.idea/.name
@@ -0,0 +1 @@
+[NanoForge] Engine Core Editor
\ No newline at end of file
diff --git a/packages/core-editor/.idea/[NanoForge] Engine Core Editor.iml b/packages/core-editor/.idea/[NanoForge] Engine Core Editor.iml
new file mode 100644
index 00000000..24643cc3
--- /dev/null
+++ b/packages/core-editor/.idea/[NanoForge] Engine Core Editor.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core-editor/.idea/codeStyles/Project.xml b/packages/core-editor/.idea/codeStyles/Project.xml
new file mode 100644
index 00000000..b70d7533
--- /dev/null
+++ b/packages/core-editor/.idea/codeStyles/Project.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core-editor/.idea/codeStyles/codeStyleConfig.xml b/packages/core-editor/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 00000000..79ee123c
--- /dev/null
+++ b/packages/core-editor/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core-editor/.idea/git_toolbox_blame.xml b/packages/core-editor/.idea/git_toolbox_blame.xml
new file mode 100644
index 00000000..7dc12496
--- /dev/null
+++ b/packages/core-editor/.idea/git_toolbox_blame.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core-editor/.idea/git_toolbox_prj.xml b/packages/core-editor/.idea/git_toolbox_prj.xml
new file mode 100644
index 00000000..02b915b8
--- /dev/null
+++ b/packages/core-editor/.idea/git_toolbox_prj.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core-editor/.idea/inspectionProfiles/Project_Default.xml b/packages/core-editor/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..03d9549e
--- /dev/null
+++ b/packages/core-editor/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core-editor/.idea/jsLinters/eslint.xml b/packages/core-editor/.idea/jsLinters/eslint.xml
new file mode 100644
index 00000000..541945bb
--- /dev/null
+++ b/packages/core-editor/.idea/jsLinters/eslint.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core-editor/.idea/modules.xml b/packages/core-editor/.idea/modules.xml
new file mode 100644
index 00000000..529602fb
--- /dev/null
+++ b/packages/core-editor/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core-editor/.idea/prettier.xml b/packages/core-editor/.idea/prettier.xml
new file mode 100644
index 00000000..0c83ac4e
--- /dev/null
+++ b/packages/core-editor/.idea/prettier.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core-editor/.idea/vcs.xml b/packages/core-editor/.idea/vcs.xml
new file mode 100644
index 00000000..b2bdec2d
--- /dev/null
+++ b/packages/core-editor/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/core-editor/.nvmrc b/packages/core-editor/.nvmrc
new file mode 100644
index 00000000..c519bf5b
--- /dev/null
+++ b/packages/core-editor/.nvmrc
@@ -0,0 +1 @@
+v24.11.0
diff --git a/packages/core-editor/.prettierignore b/packages/core-editor/.prettierignore
new file mode 100644
index 00000000..64b127c9
--- /dev/null
+++ b/packages/core-editor/.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/packages/core-editor/CHANGELOG.md b/packages/core-editor/CHANGELOG.md
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/core-editor/LICENSE b/packages/core-editor/LICENSE
new file mode 100644
index 00000000..62c6400b
--- /dev/null
+++ b/packages/core-editor/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright © 2025 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/packages/core-editor/README.md b/packages/core-editor/README.md
new file mode 100644
index 00000000..7daa7a82
--- /dev/null
+++ b/packages/core-editor/README.md
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## About
+
+`@nanoforge-dev/core-editor` is a package made to allow the editor to make change to a game even if it's already compiled and running.
+
+If you want a core to run your game use the `@nanoforge-dev/core`.
+
+## Installation
+
+**Node.js 24.11.0 or newer is required.**
+
+```sh
+npm install @nanoforge-dev/core-editor
+yarn add @nanoforge-dev/core-editor
+pnpm add @nanoforge-dev/core-editor
+bun add @nanoforge-dev/core-editor
+```
+
+## 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/Engine
+[discussions]: https://github.com/NanoForge-dev/Engine/discussions
+[source]: https://github.com/NanoForge-dev/Engine/tree/main/packages/core-editor
+[npm]: https://www.npmjs.com/package/@nanoforge-dev/core-editor
+[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md
diff --git a/packages/core-editor/cliff.toml b/packages/core-editor/cliff.toml
new file mode 100644
index 00000000..a9abf0b7
--- /dev/null
+++ b/packages/core-editor/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 = "@nanoforge-dev/core-editor@[0-9]*"
+ignore_tags = ""
+topo_order = false
+sort_commits = "newest"
+
+[remote.github]
+owner = "NanoForge-dev"
+repo = "Engine"
diff --git a/packages/core-editor/eslint.config.js b/packages/core-editor/eslint.config.js
new file mode 100644
index 00000000..62ec06dc
--- /dev/null
+++ b/packages/core-editor/eslint.config.js
@@ -0,0 +1,3 @@
+import config from "@nanoforge-dev/utils-eslint-config";
+
+export default config;
diff --git a/packages/core-editor/package.json b/packages/core-editor/package.json
new file mode 100644
index 00000000..74bd0ece
--- /dev/null
+++ b/packages/core-editor/package.json
@@ -0,0 +1,92 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "@nanoforge-dev/core-editor",
+ "version": "0.0.0",
+ "description": "NanoForge Engine - Core Editor",
+ "keywords": [
+ "nanoforge",
+ "game",
+ "engine"
+ ],
+ "homepage": "https://github.com/NanoForge-dev/Engine#readme",
+ "bugs": "https://github.com/NanoForge-dev/Engine/issues",
+ "license": "MIT",
+ "contributors": [
+ "Bill ",
+ "Exelo ",
+ "Fexkoser ",
+ "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/Engine.git",
+ "directory": "packages/core-editor"
+ },
+ "funding": "https://github.com/NanoForge-dev/Engine?sponsor",
+ "scripts": {
+ "build": "tsc --noEmit && tsup",
+ "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 'packages/core-editor/*'",
+ "release": "cliff-jumper"
+ },
+ "dependencies": {
+ "@nanoforge-dev/asset-manager": "workspace:*",
+ "@nanoforge-dev/common": "workspace:*",
+ "@nanoforge-dev/ecs-client": "workspace:*",
+ "@nanoforge-dev/input": "workspace:*",
+ "class-transformer": "catalog:config",
+ "class-validator": "catalog:config"
+ },
+ "devDependencies": {
+ "@favware/cliff-jumper": "catalog:ci",
+ "@nanoforge-dev/utils-eslint-config": "workspace:*",
+ "@nanoforge-dev/utils-prettier-config": "workspace:*",
+ "@trivago/prettier-plugin-sort-imports": "catalog:lint",
+ "eslint": "catalog:lint",
+ "prettier": "catalog:lint",
+ "tsup": "catalog:build",
+ "typescript": "catalog:core",
+ "vitest": "catalog:test"
+ },
+ "packageManager": "pnpm@10.29.3",
+ "engines": {
+ "node": "25"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "lint-staged": {
+ "**": [
+ "prettier --ignore-unknown --write"
+ ],
+ "src/**/*.ts": [
+ "eslint --fix"
+ ]
+ }
+}
diff --git a/packages/core-editor/prettier.config.js b/packages/core-editor/prettier.config.js
new file mode 100644
index 00000000..27d0e269
--- /dev/null
+++ b/packages/core-editor/prettier.config.js
@@ -0,0 +1,3 @@
+import config from "@nanoforge-dev/utils-prettier-config";
+
+export default config;
diff --git a/packages/core-editor/src/application/nanoforge-application.ts b/packages/core-editor/src/application/nanoforge-application.ts
new file mode 100644
index 00000000..66bb18eb
--- /dev/null
+++ b/packages/core-editor/src/application/nanoforge-application.ts
@@ -0,0 +1,57 @@
+import {
+ type IAssetManagerLibrary,
+ type IComponentSystemLibrary,
+ type ILibrary,
+ type INetworkLibrary,
+ NfNotInitializedException,
+} from "@nanoforge-dev/common";
+
+import { ApplicationConfig } from "../../../core/src/application/application-config";
+import type { IApplicationOptions } from "../../../core/src/application/application-options.type";
+import { EditableApplicationContext } from "../../../core/src/common/context/contexts/application.editable-context";
+import { type IEditorRunOptions } from "../common/context/options.type";
+import { Core } from "../core/core";
+
+export abstract class NanoforgeApplication {
+ protected applicationConfig: ApplicationConfig;
+ private _core?: Core;
+ private readonly _options: IApplicationOptions;
+
+ constructor(options?: Partial) {
+ this.applicationConfig = new ApplicationConfig();
+
+ this._options = {
+ tickRate: 60,
+ ...(options ?? {}),
+ };
+ }
+
+ public use(sym: symbol, library: ILibrary): void {
+ this.applicationConfig.useLibrary(sym, library);
+ }
+
+ public useComponentSystem(library: IComponentSystemLibrary) {
+ this.applicationConfig.useComponentSystemLibrary(library);
+ }
+
+ public useNetwork(library: INetworkLibrary) {
+ this.applicationConfig.useNetworkLibrary(library);
+ }
+
+ public useAssetManager(library: IAssetManagerLibrary) {
+ this.applicationConfig.useAssetManagerLibrary(library);
+ }
+
+ public init(options: IEditorRunOptions): Promise {
+ this._core = new Core(
+ this.applicationConfig,
+ new EditableApplicationContext(this.applicationConfig.libraryManager),
+ );
+ return this._core.init(options, this._options);
+ }
+
+ public run() {
+ if (!this._core) throw new NfNotInitializedException("Core");
+ return this._core?.run();
+ }
+}
diff --git a/packages/core-editor/src/application/nanoforge-client.ts b/packages/core-editor/src/application/nanoforge-client.ts
new file mode 100644
index 00000000..1ddfe4ac
--- /dev/null
+++ b/packages/core-editor/src/application/nanoforge-client.ts
@@ -0,0 +1,21 @@
+import {
+ type IGraphicsLibrary,
+ type IInputLibrary,
+ type ISoundLibrary,
+} from "@nanoforge-dev/common";
+
+import { NanoforgeApplication } from "./nanoforge-application";
+
+export class NanoforgeClient extends NanoforgeApplication {
+ public useGraphics(library: IGraphicsLibrary) {
+ this.applicationConfig.useGraphicsLibrary(library);
+ }
+
+ public useInput(library: IInputLibrary) {
+ this.applicationConfig.useInputLibrary(library);
+ }
+
+ public useSound(library: ISoundLibrary) {
+ this.applicationConfig.useSoundLibrary(library);
+ }
+}
diff --git a/packages/core-editor/src/application/nanoforge-factory.ts b/packages/core-editor/src/application/nanoforge-factory.ts
new file mode 100644
index 00000000..98ec685e
--- /dev/null
+++ b/packages/core-editor/src/application/nanoforge-factory.ts
@@ -0,0 +1,15 @@
+import { type IApplicationOptions } from "../../../core/src/application/application-options.type";
+import { NanoforgeClient } from "./nanoforge-client";
+import { NanoforgeServer } from "./nanoforge-server";
+
+class NanoforgeFactoryStatic {
+ createClient(options?: Partial): NanoforgeClient {
+ return new NanoforgeClient(options);
+ }
+
+ createServer(options?: Partial): NanoforgeServer {
+ return new NanoforgeServer(options);
+ }
+}
+
+export const NanoforgeFactory = new NanoforgeFactoryStatic();
diff --git a/packages/core-editor/src/application/nanoforge-server.ts b/packages/core-editor/src/application/nanoforge-server.ts
new file mode 100644
index 00000000..74cca8a8
--- /dev/null
+++ b/packages/core-editor/src/application/nanoforge-server.ts
@@ -0,0 +1,3 @@
+import { NanoforgeApplication } from "./nanoforge-application";
+
+export class NanoforgeServer extends NanoforgeApplication {}
diff --git a/packages/core-editor/src/common/context/event-emitter.type.ts b/packages/core-editor/src/common/context/event-emitter.type.ts
new file mode 100644
index 00000000..c90caba3
--- /dev/null
+++ b/packages/core-editor/src/common/context/event-emitter.type.ts
@@ -0,0 +1,24 @@
+export enum EventTypeEnum {
+ HOT_RELOAD = "hot-reload",
+ HARD_RELOAD = "hard-reload",
+}
+
+export type ListenerType = (...args: any[]) => void;
+
+export interface IEventEmitter {
+ listeners: Record;
+ eventQueue: { event: EventTypeEnum | string; args: any[] }[];
+
+ runEvents: () => void;
+
+ emitEvent: (event: EventTypeEnum, ...args: any) => void;
+
+ addListener: (event: EventTypeEnum | string, listener: ListenerType) => void;
+ on: (event: EventTypeEnum | string, listener: ListenerType) => void;
+
+ removeListener: (event: EventTypeEnum | string, listener: ListenerType) => void;
+ off: (event: EventTypeEnum | string, listener: ListenerType) => void;
+
+ removeListenersForEvent: (event: EventTypeEnum | string) => void;
+ removeAllListeners: () => void;
+}
diff --git a/packages/core-editor/src/common/context/options.type.ts b/packages/core-editor/src/common/context/options.type.ts
new file mode 100644
index 00000000..eabf7e62
--- /dev/null
+++ b/packages/core-editor/src/common/context/options.type.ts
@@ -0,0 +1,24 @@
+import { type IEventEmitter } from "./event-emitter.type";
+import { type Save } from "./save.type";
+
+export type IEditorRunOptions = IEditorRunClientOptions | IEditorRunServerOptions;
+
+export interface IEditorRunClientOptions {
+ canvas: HTMLCanvasElement;
+ files: Map;
+ env: Record;
+ editor: {
+ save: Save;
+ coreEvents: IEventEmitter;
+ editorEvents: IEventEmitter;
+ };
+}
+export interface IEditorRunServerOptions {
+ files: Map;
+ env: Record;
+ editor: {
+ save: Save;
+ coreEvents: IEventEmitter;
+ editorEvents: IEventEmitter;
+ };
+}
diff --git a/packages/core-editor/src/common/context/save.type.ts b/packages/core-editor/src/common/context/save.type.ts
new file mode 100644
index 00000000..36372a33
--- /dev/null
+++ b/packages/core-editor/src/common/context/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/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts
new file mode 100644
index 00000000..b917ce2e
--- /dev/null
+++ b/packages/core-editor/src/core/core.ts
@@ -0,0 +1,112 @@
+import {
+ ClearContext,
+ ClientLibraryManager,
+ Context,
+ type IRunnerLibrary,
+ InitContext,
+ type LibraryHandle,
+ LibraryStatusEnum,
+ NfNotInitializedException,
+} from "@nanoforge-dev/common";
+import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client";
+
+import { type ApplicationConfig } from "../../../core/src/application/application-config";
+import type { IApplicationOptions } from "../../../core/src/application/application-options.type";
+import { type EditableApplicationContext } from "../../../core/src/common/context/contexts/application.editable-context";
+import { EditableExecutionContext } from "../../../core/src/common/context/contexts/executions/execution.editable-context";
+import { type EditableLibraryContext } from "../../../core/src/common/context/contexts/library.editable-context";
+import { ConfigRegistry } from "../../../core/src/config/config-registry";
+import { type IEditorRunOptions } from "../common/context/options.type";
+import { CoreEditor } from "../editor/core-editor";
+
+export class Core {
+ private readonly config: ApplicationConfig;
+ private readonly context: EditableApplicationContext;
+ private options?: IApplicationOptions;
+ public editor?: CoreEditor;
+ private _configRegistry?: ConfigRegistry;
+
+ constructor(config: ApplicationConfig, context: EditableApplicationContext) {
+ this.config = config;
+ this.context = context;
+ }
+
+ public async init(options: IEditorRunOptions, appOptions: IApplicationOptions): Promise {
+ this.options = appOptions;
+ this._configRegistry = new ConfigRegistry(options.env);
+ await this.runInit(this.getInitContext(options));
+ this.editor = new CoreEditor(
+ options.editor,
+ this.config.getComponentSystemLibrary().library,
+ );
+ }
+
+ public async run(): Promise {
+ if (!this.options) throw new NfNotInitializedException("Core");
+
+ const context = this.getExecutionContext();
+ const clientContext = this.getClientContext();
+ const libraries = this.config.libraryManager.getExecutionLibraries();
+
+ const runner = async (delta: number) => {
+ this.context.setDelta(delta);
+ this.editor?.runEvents();
+ await this.runExecute(clientContext, libraries);
+ };
+
+ const tickLengthMs = 1000 / this.options.tickRate;
+ let previousTick = Date.now();
+
+ const render = async () => {
+ if (!context.application.isRunning) {
+ await this.runClear(this.getClearContext());
+ return;
+ }
+ const tickStart = Date.now();
+ await runner(tickStart - previousTick);
+ previousTick = tickStart;
+ setTimeout(render, tickLengthMs + tickStart - Date.now());
+ };
+
+ context.application.setIsRunning(true);
+ setTimeout(render);
+ }
+
+ private getInitContext(options: IEditorRunOptions): InitContext {
+ if (!this._configRegistry) throw new NfNotInitializedException("Core");
+
+ return new InitContext(this.context, this.config.libraryManager, this._configRegistry, options);
+ }
+
+ private getExecutionContext(): EditableExecutionContext {
+ return new EditableExecutionContext(this.context, this.config.libraryManager);
+ }
+
+ private getClearContext(): ClearContext {
+ return new ClearContext(this.context, this.config.libraryManager);
+ }
+
+ private getClientContext(): Context {
+ return new Context(this.context, new ClientLibraryManager(this.config.libraryManager));
+ }
+
+ private async runInit(context: InitContext): Promise {
+ for (const handle of this.config.libraryManager.getInitLibraries()) {
+ await handle.library.__init(context);
+ (handle.context as EditableLibraryContext).setStatus(LibraryStatusEnum.LOADED);
+ }
+ }
+
+ private async runExecute(context: Context, libraries: LibraryHandle[]) {
+ for (const handle of libraries) {
+ await handle.library.__run(context);
+ }
+ }
+
+ private async runClear(context: ClearContext) {
+ for (const handle of this.config.libraryManager.getClearLibraries()) {
+ await handle.library.__clear(context);
+ (handle.context as EditableLibraryContext).setStatus(LibraryStatusEnum.CLEAR);
+ }
+ }
+}
diff --git a/packages/core-editor/src/editor/core-editor.ts b/packages/core-editor/src/editor/core-editor.ts
new file mode 100644
index 00000000..553638d5
--- /dev/null
+++ b/packages/core-editor/src/editor/core-editor.ts
@@ -0,0 +1,51 @@
+import { NfNotFound } from "@nanoforge-dev/common";
+import { type ECSClientLibrary, type Entity } from "@nanoforge-dev/ecs-client";
+
+import { EventTypeEnum } from "../common/context/event-emitter.type";
+import { type IEditorRunOptions } from "../common/context/options.type";
+
+export class CoreEditor {
+ private editor: IEditorRunOptions["editor"];
+ private ecsLibrary: ECSClientLibrary;
+ constructor(editor: IEditorRunOptions["editor"], ecsLibrary: ECSClientLibrary) {
+ this.editor = editor;
+ this.ecsLibrary = ecsLibrary;
+ this.editor.coreEvents?.addListener(EventTypeEnum.HOT_RELOAD, this.askEntitiesHotReload);
+ }
+
+ public runEvents() {
+ this.editor.coreEvents?.runEvents();
+ }
+
+ public askEntitiesHotReload(): void {
+ const reg = this.ecsLibrary.registry;
+ const save = this.editor.save;
+ save.entities.forEach(({ id, components }) => {
+ Object.entries(components).forEach(([componentName, params]) => {
+ const ogComponent = save.components.find(({ name: paramName }) => {
+ return paramName == componentName;
+ });
+ if (!ogComponent) {
+ throw new NfNotFound("Component: " + componentName + " not found in saved components");
+ }
+ const ecsEntity: Entity = this.getEntityFromEntityId(id);
+ const ecsComponent = reg.getEntityComponent(ecsEntity, {
+ name: componentName,
+ });
+ Object.entries(params).forEach(([paramName, paramValue]) => {
+ ecsComponent[paramName] = paramValue;
+ });
+ reg.addComponent(ecsEntity, ecsComponent);
+ });
+ });
+ }
+
+ private getEntityFromEntityId(entityId: string): Entity {
+ const reg = this.ecsLibrary.registry;
+ return reg.entityFromIndex(
+ reg
+ .getComponentsConst({ name: "__RESERVED_ENTITY_ID" })
+ .getIndex({ name: "__RESERVED_ENTITY_ID", entityId: entityId }),
+ );
+ }
+}
diff --git a/packages/core-editor/src/index.ts b/packages/core-editor/src/index.ts
new file mode 100644
index 00000000..4273d62e
--- /dev/null
+++ b/packages/core-editor/src/index.ts
@@ -0,0 +1,4 @@
+export * from "./application/nanoforge-factory";
+
+export type { NanoforgeClient } from "./application/nanoforge-client";
+export type { NanoforgeServer } from "./application/nanoforge-server";
diff --git a/packages/core-editor/test/editor-feature.spec.ts b/packages/core-editor/test/editor-feature.spec.ts
new file mode 100644
index 00000000..c02f1a03
--- /dev/null
+++ b/packages/core-editor/test/editor-feature.spec.ts
@@ -0,0 +1,149 @@
+import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { EventTypeEnum } from "../src/common/context/event-emitter.type";
+import type { IEditorRunOptions } from "../src/common/context/options.type";
+import { type Save, type SaveComponent, type SaveEntity } from "../src/common/context/save.type";
+import { CoreEditor } from "../src/editor/core-editor";
+import { EventEmitter } from "./helpers/event-emitter";
+
+describe("EditorFeatures", () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe("eventEmitter", () => {
+ it("should execute eventQueue once", async () => {
+ const events = new EventEmitter();
+ events.emitEvent(EventTypeEnum.HOT_RELOAD);
+ events.emitEvent(EventTypeEnum.HOT_RELOAD);
+ const spyHotReload = vi
+ .spyOn(CoreEditor.prototype, "askEntitiesHotReload")
+ .mockImplementation(() => {});
+ new CoreEditor(
+ { coreEvents: events } as IEditorRunOptions["editor"],
+ {} as ECSClientLibrary,
+ ).runEvents();
+ expect(spyHotReload).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe("askEntitiesHotReload", () => {
+ it("should reload entities with new save variables", async () => {
+ const getIndex = vi.fn((component) => {
+ return Number(component.entityId.slice(-1));
+ });
+
+ const FakeRegistry = vi.fn(
+ class {
+ addComponent = vi.fn();
+ getComponentsConst = vi.fn(() => ({ getIndex }));
+ getEntityComponent = vi.fn((entity: number, component) => {
+ return (
+ {
+ 2: {
+ Position: {
+ name: "Position",
+ x: 3,
+ y: 4,
+ },
+ Bullets: {
+ name: "Bullets",
+ number: 4,
+ bulletTypes: ["9mm"],
+ },
+ __RESERVED_ENTITY_ID: {
+ entityId: "ent2",
+ },
+ },
+ 3: {
+ Position: {
+ name: "Position",
+ x: 7,
+ y: 8,
+ },
+ __RESERVED_ENTITY_ID: {
+ entityId: "ent3",
+ },
+ },
+ } as Record>
+ )[entity]?.[component.name];
+ });
+ entityFromIndex = vi.fn((index) => {
+ return index;
+ });
+ },
+ );
+
+ const components: SaveComponent[] = [
+ {
+ name: "Position",
+ path: "/tmp/pos",
+ paramsNames: ["x", "y"],
+ },
+ {
+ name: "Bullets",
+ path: "/tmp/pos",
+ paramsNames: ["bulletTypes", "number"],
+ },
+ ];
+ const entities: SaveEntity[] = [
+ {
+ id: "ent2",
+ components: {
+ Position: {
+ x: 1,
+ y: 2,
+ },
+ Bullets: {
+ bulletTypes: ["fire", "water", "rocket"],
+ number: 1000,
+ },
+ },
+ },
+ {
+ id: "ent3",
+ components: {
+ Position: {
+ x: 5,
+ y: 6,
+ },
+ },
+ },
+ ];
+ const fakeReg = new FakeRegistry();
+ new CoreEditor(
+ {
+ save: {
+ components,
+ entities,
+ } as any as Save,
+ } as any as IEditorRunOptions["editor"],
+ { registry: fakeReg } as any as ECSClientLibrary,
+ ).askEntitiesHotReload();
+ expect(fakeReg.getComponentsConst).toHaveBeenCalledWith({ name: "__RESERVED_ENTITY_ID" });
+ expect(getIndex).toHaveBeenNthCalledWith(1, {
+ entityId: "ent2",
+ name: "__RESERVED_ENTITY_ID",
+ });
+ expect(getIndex).toHaveBeenNthCalledWith(2, {
+ entityId: "ent2",
+ name: "__RESERVED_ENTITY_ID",
+ });
+ expect(getIndex).toHaveBeenNthCalledWith(3, {
+ entityId: "ent3",
+ name: "__RESERVED_ENTITY_ID",
+ });
+ expect(fakeReg.getEntityComponent).toHaveBeenNthCalledWith(1, 2, { name: "Position" });
+ expect(fakeReg.getEntityComponent).toHaveBeenNthCalledWith(2, 2, { name: "Bullets" });
+ expect(fakeReg.getEntityComponent).toHaveBeenNthCalledWith(3, 3, { name: "Position" });
+ expect(fakeReg.addComponent).toHaveBeenNthCalledWith(1, 2, { name: "Position", x: 1, y: 2 });
+ expect(fakeReg.addComponent).toHaveBeenNthCalledWith(2, 2, {
+ name: "Bullets",
+ bulletTypes: ["fire", "water", "rocket"],
+ number: 1000,
+ });
+ expect(fakeReg.addComponent).toHaveBeenNthCalledWith(3, 3, { name: "Position", x: 5, y: 6 });
+ });
+ });
+});
diff --git a/packages/core-editor/test/helpers/event-emitter.ts b/packages/core-editor/test/helpers/event-emitter.ts
new file mode 100644
index 00000000..95c6eb62
--- /dev/null
+++ b/packages/core-editor/test/helpers/event-emitter.ts
@@ -0,0 +1,50 @@
+import {
+ type EventTypeEnum,
+ type IEventEmitter,
+ type ListenerType,
+} from "../../src/common/context/event-emitter.type";
+
+export class EventEmitter implements IEventEmitter {
+ public listeners: Record = {};
+ public eventQueue: { event: EventTypeEnum | string; args: any[] }[] = [];
+
+ public runEvents = () => {
+ this.eventQueue.forEach(({ event, args }) => {
+ this.listeners[event]?.forEach((listener) => {
+ listener(...args);
+ });
+ });
+ this.eventQueue = [];
+ };
+
+ public emitEvent(event: EventTypeEnum | string, ...args: any[]) {
+ this.eventQueue.push({ event, args });
+ }
+
+ public addListener(event: EventTypeEnum | string, listener: ListenerType): void {
+ if (!this.listeners[event]) this.listeners[event] = [];
+ this.listeners[event].push(listener);
+ }
+ public on(event: EventTypeEnum | string, listener: ListenerType): void {
+ this.addListener(event, listener);
+ }
+
+ public removeListener(event: EventTypeEnum | string, listener: ListenerType): void {
+ if (!this.listeners[event]) return;
+ const index = this.listeners[event].indexOf(listener);
+ if (index >= 0) {
+ this.listeners[event].splice(index, 1);
+ }
+ }
+ public off(event: EventTypeEnum | string, listener: ListenerType): void {
+ this.removeListener(event, listener);
+ }
+
+ public removeListenersForEvent(event: EventTypeEnum | string): void {
+ if (!this.listeners[event]) return;
+ this.listeners[event] = [];
+ }
+ public removeAllListeners(): void {
+ this.listeners = {};
+ }
+}
diff --git a/packages/core-editor/tsconfig.json b/packages/core-editor/tsconfig.json
new file mode 100644
index 00000000..9e6d724b
--- /dev/null
+++ b/packages/core-editor/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ "extends": "../../tsconfig.json",
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/core-editor/tsconfig.spec.json b/packages/core-editor/tsconfig.spec.json
new file mode 100644
index 00000000..8270caba
--- /dev/null
+++ b/packages/core-editor/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/packages/core-editor/tsup.config.ts b/packages/core-editor/tsup.config.ts
new file mode 100644
index 00000000..f3b6e6ce
--- /dev/null
+++ b/packages/core-editor/tsup.config.ts
@@ -0,0 +1,3 @@
+import { createTsupConfig } from "../../tsup.config.js";
+
+export default [createTsupConfig()];
diff --git a/packages/core-editor/typedoc.json b/packages/core-editor/typedoc.json
new file mode 100644
index 00000000..7e2ec1a3
--- /dev/null
+++ b/packages/core-editor/typedoc.json
@@ -0,0 +1,5 @@
+{
+ "extends": ["../../typedoc.base.json"],
+ "entryPoints": ["src/index.ts"],
+ "skipErrorChecking": true
+}
diff --git a/packages/core/src/application/application-config.ts b/packages/core/src/application/application-config.ts
index 82d28bed..7eeede0c 100644
--- a/packages/core/src/application/application-config.ts
+++ b/packages/core/src/application/application-config.ts
@@ -31,8 +31,8 @@ export class ApplicationConfig {
this._libraryManager.set(sym, library);
}
- public getComponentSystemLibrary() {
- return this._libraryManager.getComponentSystem();
+ public getComponentSystemLibrary() {
+ return this._libraryManager.getComponentSystem();
}
public useComponentSystemLibrary(library: IComponentSystemLibrary) {
diff --git a/packages/ecs-client/src/index.ts b/packages/ecs-client/src/index.ts
index 5e93dd98..8759c383 100644
--- a/packages/ecs-client/src/index.ts
+++ b/packages/ecs-client/src/index.ts
@@ -6,6 +6,8 @@ export type {
EditorSystemManifest,
System,
Registry,
+ Entity,
+ SparseArray,
} from "@nanoforge-dev/ecs-lib";
export { ECSClientLibrary } from "./ecs-client-library";
diff --git a/packages/ecs-lib/lib/libecs.d.ts b/packages/ecs-lib/lib/libecs.d.ts
index 6ff945f1..d4d8cf0b 100644
--- a/packages/ecs-lib/lib/libecs.d.ts
+++ b/packages/ecs-lib/lib/libecs.d.ts
@@ -16,7 +16,7 @@ export interface ClassHandle {
[Symbol.dispose](): void;
clone(): this;
}
-export interface container extends ClassHandle {
+export interface container extends ClassHandle, Iterable {
size(): number;
get(_0: number): any | undefined | undefined;
push_back(_0?: any): void;
diff --git a/packages/ecs-lib/src/index.ts b/packages/ecs-lib/src/index.ts
index ab84137a..a0d20643 100644
--- a/packages/ecs-lib/src/index.ts
+++ b/packages/ecs-lib/src/index.ts
@@ -1,3 +1,3 @@
export { AbstractECSLibrary } from "./ecs-library.abstract";
-export type { Component, System, Registry } from "../lib/libecs";
+export type { Component, System, Registry, SparseArray, Entity } from "../lib/libecs";
export type * from "./editor-manifest.type";
diff --git a/packages/ecs-server/src/index.ts b/packages/ecs-server/src/index.ts
index 0c14ef42..045ba79b 100644
--- a/packages/ecs-server/src/index.ts
+++ b/packages/ecs-server/src/index.ts
@@ -6,6 +6,8 @@ export type {
EditorSystemManifest,
System,
Registry,
+ Entity,
+ SparseArray,
} from "@nanoforge-dev/ecs-lib";
export { ECSServerLibrary } from "./ecs-server-library";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4b78210a..35a1e681 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -420,6 +420,55 @@ importers:
specifier: catalog:test
version: 4.0.18(@types/node@25.3.5)(jiti@2.6.1)(yaml@2.8.2)
+ packages/core-editor:
+ dependencies:
+ '@nanoforge-dev/asset-manager':
+ specifier: workspace:*
+ version: link:../asset-manager
+ '@nanoforge-dev/common':
+ specifier: workspace:*
+ version: link:../common
+ '@nanoforge-dev/ecs-client':
+ specifier: workspace:*
+ version: link:../ecs-client
+ '@nanoforge-dev/input':
+ specifier: workspace:*
+ version: link:../input
+ class-transformer:
+ specifier: catalog:config
+ version: 0.5.1
+ class-validator:
+ specifier: catalog:config
+ version: 0.14.4
+ devDependencies:
+ '@favware/cliff-jumper':
+ specifier: catalog:ci
+ version: 6.0.0
+ '@nanoforge-dev/utils-eslint-config':
+ specifier: workspace:*
+ version: link:../../utils/eslint-config
+ '@nanoforge-dev/utils-prettier-config':
+ specifier: workspace:*
+ version: link:../../utils/prettier-config
+ '@trivago/prettier-plugin-sort-imports':
+ specifier: catalog:lint
+ version: 6.0.2(prettier@3.8.1)
+ eslint:
+ specifier: catalog:lint
+ version: 10.0.3(jiti@2.6.1)
+ prettier:
+ specifier: catalog:lint
+ version: 3.8.1
+ tsup:
+ specifier: catalog:build
+ version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.2)
+ typescript:
+ specifier: catalog:core
+ version: 5.9.3
+ vitest:
+ specifier: catalog:test
+ version: 4.0.18(@types/node@25.3.5)(jiti@2.6.1)(yaml@2.8.2)
+
packages/ecs-client:
dependencies:
'@nanoforge-dev/common':