@vttforge/core 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,296 @@
1
+ # @vttforge/core
2
+
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c24b2e9: Fix two things `InferSchema` got wrong about a field's runtime type.
8
+
9
+ `ColorField` inferred as `string`. It stores a CSS string but initializes
10
+ into a `Color` instance, so `system.tint` is an object with `.css`, `.rgb`
11
+ and friends — and the old typing made every property access on it a lie the
12
+ compiler accepted. It is also nullable by default, unlike the other
13
+ string-backed fields: the field's own defaults are `nullable: true,
14
+ initial: null`, so reading `.css` off a fresh document was a real crash the
15
+ types allowed. It now infers as `Color | null`, and drops the null when
16
+ `nullable: false` is set.
17
+
18
+ Presence was half-implemented. Only `nullable: true` widened the type;
19
+ `required: false` did not. A field that resolves to `undefined` when absent
20
+ was typed as always present. The rule now follows how a field actually
21
+ resolves a missing value: an explicit `initial` always fills, so it never
22
+ widens; otherwise `required: false` admits `undefined` and `nullable: true`
23
+ admits `null`, and the two compose.
24
+
25
+ `Color` is described structurally rather than imported, so the inference
26
+ surface still carries no dependency on a Foundry type package.
27
+ - 3f09683: Add `registerModule()` for modules that contribute document sub-types.
28
+
29
+ Foundry files a module's sub-type under `<module-id>.<type>`. Register the bare name and there is no error — the type just never appears. `registerModule()` adds the prefix, and `moduleSubType(id, type)` gives you the same string wherever else you need it (registering the sheet, checking `actor.type`, writing `documentTypes` in the manifest).
30
+
31
+ ```ts
32
+ registerModule({
33
+ id: 'pdf-character-sheet',
34
+ itemDataModels: { pdf: PdfItemData }, // → CONFIG.Item.dataModels['pdf-character-sheet.pdf']
35
+ });
36
+ ```
37
+
38
+ `registerSystem()` was the only option before, and it is the wrong shape for a module: it writes bare keys and also replaces the document classes, the initiative formula, and the status-effect array — all of which belong to the system. There is no option to replace them here, and `statusEffects` appends instead of assigning.
39
+ - 98b742c: Give each field its own defaults when inferring a schema.
40
+
41
+ Every field class picks its own defaults, and they disagree. The inference treated them as if they agreed, so three fields were typed as shapes they cannot hold:
42
+
43
+ - `NumberField` is optional and nullable out of the box. `new fields.NumberField()` is `number | null | undefined`, not `number`.
44
+ - `StringField` is optional. A bare one is `string | undefined`.
45
+ - `FilePathField` starts at `null`, the way `ColorField` does. A bare one is `string | null`.
46
+
47
+ The rest were already right, for reasons worth naming: booleans and HTML fields are required and supply their own initial; arrays, sets and schemas are required and build their own empty value; document references are required but nullable.
48
+
49
+ This will surface errors in schemas that leave the options off. The fix is to declare what you meant — `{ required: true, nullable: false, initial: 0 }` — which is what the field needed all along.
50
+ - 8657721: Let `BaseTypeDataModel` learn your schema.
51
+
52
+ Hand it the function that returns your fields and it implements `static defineSchema()` for you. The schema is written once, and `this` inside `prepareDerivedData()` knows its own fields:
53
+
54
+ ```ts
55
+ class CharacterData extends BaseTypeDataModel(defineCharacterSchema) {
56
+ declare armorClass: number;
57
+ prepareDerivedData() {
58
+ this.armorClass = 10 + this.level; // this.level is number
59
+ }
60
+ }
61
+
62
+ type CharacterSystem = CharacterData['$inferData'];
63
+ ```
64
+
65
+ Derived values are not in the schema, so declare them on the subclass.
66
+
67
+ Calling `BaseTypeDataModel()` with no arguments works exactly as before.
68
+ - f11b1f4: Type `SetField` and `ForeignDocumentField` in `InferSchema`.
69
+
70
+ A `SetField` holds a `Set`, not an array. Inferring it as an array handed you `push` and index access on a value that has neither, and the compiler agreed.
71
+
72
+ A `ForeignDocumentField` reads back as the document itself — the data model installs the field as a getter, so the property gives you the instance, not the function that fetched it. Under `idOnly` it stays the id string. Both admit `null` unless the schema sets `nullable: false`.
73
+
74
+ Also exported: `SetFieldInstance`, `SetFieldCtor`, `SetFieldOptions`, `ForeignDocumentFieldInstance`, `ForeignDocumentFieldCtor`, `ForeignDocumentFieldOptions`, and `DocumentClass`.
75
+ - 74c4126: Type the statics on `BaseActorSheet()` and `BaseItemSheet()`.
76
+
77
+ Both returned a bare constructor, so a subclass writing `super.DEFAULT_OPTIONS` — the pattern the docs show and every sheet needs — failed to compile. TypeScript cannot see a static through an untyped constructor. The example system is JavaScript, so nothing caught it.
78
+
79
+ They now return `SheetBaseCtor`, which carries `DEFAULT_OPTIONS` and `DRAG_DROP`. A subclass declaring either needs the `override` modifier, which is TypeScript correctly seeing the inherited static.
80
+
81
+ ## 0.5.0
82
+
83
+ ### Minor Changes
84
+
85
+ - 9462144: Require Node 26.
86
+
87
+ The floor moves from `>=22.14.0` to `>=26.0.0` across every package and the
88
+ four scaffolding templates, and the bundler target for the Node-side
89
+ packages moves from `node22` to `node26`.
90
+
91
+ Node 22 entered maintenance in October 2025 and receives security fixes
92
+ only. Node 26 becomes the active LTS line on 2026-10-28.
93
+
94
+ This is breaking for anyone on Node 22 or 24. It is marked `minor` rather
95
+ than `major` on purpose: these packages are still on 0.x, where a minor
96
+ signals the break, and a major would push every package to 1.0.0 — a claim
97
+ of API stability that has not been audited, on packages two of which are
98
+ still stubs.
99
+
100
+ The templates move to the versions this release publishes. On 0.x a caret
101
+ pins the minor, so their old ranges would not have matched.
102
+
103
+ CI now pins Node through `actions/setup-node` instead of inheriting whatever
104
+ the runner image ships, so the version the packages declare is the version
105
+ they are tested on. It was not before: the workflow took the image's Node,
106
+ and nothing enforced the declared floor because `engine-strict` is not set.
107
+
108
+ ## 0.4.0
109
+
110
+ ### Minor Changes
111
+
112
+ - 50721f9: fix: align with Foundry v13 manifest schema and add `prepareBaseData` hook
113
+
114
+ Three coordinated changes:
115
+
116
+ **`@vttforge/core`** — `BaseTypeDataModel()` now ships a `prepareBaseData()`
117
+ no-op stub alongside `prepareDerivedData()`. Use `prepareBaseData()` to
118
+ initialize fields that Active Effects need to mutate (base max HP, base AC),
119
+ since AEs apply between `prepareBaseData()` and `prepareDerivedData()`.
120
+ `prepareDerivedData()` stays the place for values that depend on the
121
+ AE-mutated state.
122
+
123
+ The misleading `_addDataFieldMigrations()` static stub is removed. The real
124
+ field-rename API is `_addDataFieldMigration(source, oldKey, newKey, apply?)`
125
+ called inside a `static migrateData(source)` override — consumers who need
126
+ it can call it directly on the Foundry-provided base via `super`.
127
+
128
+ **`@vttforge/vite-plugin`** — emits the canonical v13 `styles` form
129
+ (`[{ src: "styles/foo.css" }]`) in the built manifest. Still accepts the
130
+ legacy string form (`["styles/foo.css"]`) and the v13 object form as input,
131
+ so existing consumers don't need to change their source manifest. Additional
132
+ metadata declared on object entries (e.g. `layer` for cascade layer
133
+ placement) is preserved through the rewrite — only `src` is rewritten to
134
+ point at the bundled output.
135
+
136
+ **`@vttforge-examples/simple-system`** — manifest aligned with v13 schema:
137
+ `gridDistance` / `gridUnits` collapsed into the `grid` object; `styles`
138
+ declared in object form; `flags.hotReload` declared at the root of `flags`
139
+ (not under the package namespace) and switched to the object form
140
+ (`{ extensions, paths }`) that Foundry's runtime hot-reload watcher
141
+ actually reads. The previous shape was a double no-op — wrong location AND
142
+ wrong form, so the watcher silently exited without registering any
143
+ extensions.
144
+
145
+ Hot-reload enablement on the Foundry server side is deferred — runtime
146
+ configuration changes interact with felddy's `CONTAINER_PRESERVE_CONFIG`
147
+ flag in ways that require a dedicated design pass. That work is tracked
148
+ separately and will land alongside the developer-facing hot-reload bridge.
149
+
150
+ ## 0.3.0
151
+
152
+ ### Minor Changes
153
+
154
+ - 234a4b2: Extend `BaseActorSheet` and add `BaseItemSheet` — the boilerplate every shipping
155
+ system copy-pastes is now hoisted into the SDK.
156
+ - `static DRAG_DROP` — declare drag sources / drop targets as data; the base
157
+ wires real `foundry.applications.ux.DragDrop` instances in `_onRender` with
158
+ `isEditable`-gated permissions and a default `_onDragStart` that serialises
159
+ `data-item-id` elements as `{ type: "Item", uuid }`.
160
+ - `_prepareContext` auto-fills `context.tabs[group]` for every group declared
161
+ in ApplicationV2's `static TABS`, eliminating manual `_prepareTabs(group)`
162
+ calls in subclass `_prepareContext`.
163
+ - Typed drop dispatch: override `onDropItem(item, event)` / `onDropActor(...)` /
164
+ `onDropFolder(...)` / `onDropActiveEffect(...)` and skip the `fromUuid()`
165
+ ceremony. Returning `undefined` falls through to Foundry's default
166
+ `_onDropX`; return anything else to take ownership.
167
+ - New `BaseItemSheet()` mirror with the same `static DRAG_DROP` + tab
168
+ auto-population, minus the drop dispatch (items rarely receive drops).
169
+ - Exports new `DragDropConfig` type for typed `static DRAG_DROP` declarations.
170
+
171
+ `editImage` is intentionally not reinvented — it already ships on
172
+ `DocumentSheetV2` (inherited by both `ActorSheetV2` and `ItemSheetV2`).
173
+ Templates wire `<img data-edit="img">` and Foundry's built-in action handles
174
+ the `FilePicker` flow.
175
+
176
+ - 4fd5a07: Error registry codegen — `docsUrl` now resolves to a real page.
177
+
178
+ `postbuild` hook (`packages/core/scripts/codegen-errors.mjs`) reads the
179
+ just-built `dist/index.mjs`, calls `listErrorEntries()`, and emits:
180
+ - `dist/errors-manifest.json` — versioned JSON catalogue shipped in the
181
+ tarball alongside the bundled JS/types. Stable shape (`version`,
182
+ `package`, `packageVersion`, `entries[]`) so external tooling (the v0.3
183
+ docs site, IDE extensions, lint rules) has a single source of truth.
184
+ - `docs/errors/VTTF-NNNN.md` at the repo root — one Markdown stub per code,
185
+ committed so the `docsUrl` already resolves while the full VitePress site
186
+ is being built in v0.3.
187
+
188
+ New runtime helper: `getErrorManifest()` returns the same data as
189
+ `listErrorEntries()`, wrapped in a typed `ErrorManifest` envelope with a
190
+ stable `version: 1` field for future format migrations.
191
+
192
+ Plan deviation: the codegen runs as `postbuild` (not `prebuild`) so the
193
+ script imports the just-built ESM directly instead of needing
194
+ `tsx`/`unrun` to load the TS source. Documented inline in the codegen
195
+ script.
196
+
197
+ - 0896bb0: Add `createMigrationRunner()` for declarative schema migrations, plus
198
+ `onReady` lifecycle on `registerSystem()`.
199
+
200
+ `createMigrationRunner({ systemId, migrations, ... })` returns `{ register(),
201
+ run(), targetVersion }`. Call `register()` from `init` to register the
202
+ `schemaVersion` setting; call `run()` from `ready` (gated by
203
+ `game.user.isGM`) to execute every pending migration in order. Migrations use
204
+ semver versions and `foundry.utils.isNewerVersion` for comparison — the same
205
+ contract `system.json`'s `flags.<systemId>.needsMigrationVersion` /
206
+ `compatibleMigrationVersion` use, matching the dnd5e production pattern.
207
+
208
+ Failure semantics: `schemaVersion` is committed per-migration, so a
209
+ mid-sequence throw leaves the world at the last successful version and the
210
+ retry on the next world load picks up exactly where it failed. Migration
211
+ errors are wrapped in `VttfError VTTF-0004` with the original error on
212
+ `.cause`; calling `run()` against a world older than `compatibleVersion`
213
+ throws `VttfError VTTF-0005`.
214
+
215
+ `registerSystem()` gains `onReady?: () => void | Promise<void>` — the natural
216
+ place to wire `migrationRunner.run()`. Not GM-gated; consumer guards inside
217
+ their callback.
218
+
219
+ New error codes (append-only): `VTTF-0004 MigrationFailed`,
220
+ `VTTF-0005 WorldTooOldForMigration`.
221
+
222
+ - 49a8718: Fix `BaseActorSheet` / `BaseItemSheet` tab handling so sheets work without
223
+ per-consumer workarounds.
224
+
225
+ Two issues surfaced when running the example sheet inside a live Foundry v13:
226
+ - **`context.tabs` double-wrap on single-group sheets.** The previous
227
+ `_prepareContext` override unconditionally set `context.tabs[group]`,
228
+ even when ApplicationV2 already populated a flat
229
+ `context.tabs[tabId]` for single-group sheets. The collision forced
230
+ consumers to either unwrap manually or write `context.tabs.<group>.<tabId>`
231
+ in every template. Fixed: BaseActorSheet/BaseItemSheet now only fill
232
+ `context.tabs[group]` for **multi-group** sheets (single-group sheets
233
+ see ApplicationV2's flat shape untouched).
234
+
235
+ - **No default `tab`-style action handler.** ApplicationV2 doesn't ship a
236
+ built-in handler for `data-action="…"` tab navigation buttons, and the
237
+ bare name `tab` is reserved by the framework (custom handlers under that
238
+ name never fire). Fixed: both base sheets now ship a `vttforgeTab`
239
+ action that toggles `.active` on the matching nav button
240
+ (`[data-action="vttforgeTab"][data-group=…][data-tab=…]`) and content
241
+ section (`section.tab[data-group=…][data-tab=…]`) and updates
242
+ `sheet.tabGroups[group]`. Templates that already used the old per-sheet
243
+ workaround need to rename `data-action="tab"` → `data-action="vttforgeTab"`.
244
+
245
+ Discovered during development testing — not derived from any external
246
+ source.
247
+
248
+ Patch bump for the example: drops the `_prepareContext` unwrap workaround
249
+ and the per-sheet `_onTab` static handlers added in the previous PR,
250
+ since both now live in the SDK.
251
+
252
+ ## 0.2.0
253
+
254
+ ### Minor Changes
255
+
256
+ - 5dd98c1: Add `fields()` factory and `InferSchema<T>` for typed `defineSchema()` outputs.
257
+
258
+ Covers the v0.1 partial scope from PRD §7: `NumberField`, `StringField`,
259
+ `BooleanField`, `HTMLField`, `ArrayField`, `SchemaField`, `ColorField`,
260
+ `FilePathField`. Calling `fields()` lazy-resolves `globalThis.foundry.data.fields`
261
+ and throws `VttfError VTTF-0002` outside the Foundry runtime — same pattern as
262
+ `BaseTypeDataModel()` / `BaseActorSheet()`.
263
+
264
+ `InferSchema<S>` derives the `system` shape from a `defineSchema()` return value,
265
+ recursing through `ArrayField` and `SchemaField` and honouring the single
266
+ nullability rule `nullable: true` → `T | null`. Full class-level inference
267
+ (`BaseTypeDataModel<typeof Schema>`), `$inferData`, `EmbeddedDataField`,
268
+ `EmbeddedDocumentField`, `TypedSchemaField`, and the full required×initial
269
+ nullability matrix remain v1.0 scope and will ship from `@vttforge/types`.
270
+
271
+ ## 0.1.0
272
+
273
+ ### Minor Changes
274
+
275
+ - 4900e83: Foundation MVP (PR 4 of 4) — `@vttforge/core` ships its first runtime surface (registerSystem, SystemConfig, BaseTypeDataModel, BaseActorSheet, VttfError + VTTF-NNNN registry) and `@vttforge/styles` ships its first `--vttf-*` token set wrapped in the `vttforge.tokens` cascade layer.
276
+
277
+ Both packages have working consumer entrypoints (verified by an external smoke test loading the built `.mjs` from a throwaway dir) and the SDK contracts match the canonical Foundry v13 patterns (TypeDataModel migration, ActorSheetV2 + HandlebarsApplicationMixin, staged init hooks, marker classes).
278
+
279
+ Status remains pre-1.0 and APIs are explicitly unstable — these are the first releases that have real code instead of placeholder `export {}`.
280
+
281
+ ## 0.0.1
282
+
283
+ Initial functional release (v0.1 MVP slice). Foundry v13+ system runtime helpers.
284
+
285
+ ### Added
286
+
287
+ - `registerSystem({ id, actorDataModels, itemDataModels, actorDocumentClass, itemDocumentClass, combat, statusEffects, onBeforeInit, onAfterInit })` — one-call boot that schedules CONFIG mutations via `Hooks.once("init", ...)`. Idempotent per `id` (throws `VTTF-0001` on duplicate). Sets `CONFIG.ActiveEffect.legacyTransferral = false` by default.
288
+ - `SystemConfig` — typed wrapper around `game.settings.register/get/set`. Tracks registered keys locally; reads/writes against an unregistered key throw `VTTF-0003` instead of returning `undefined`.
289
+ - `BaseTypeDataModel()` — mixin over `foundry.abstract.TypeDataModel` providing a safe default `migrateData` that delegates to `super` (chained-migration guard), a stub `_addDataFieldMigrations`, and a no-op `prepareDerivedData`.
290
+ - `BaseActorSheet()` — mixin over `HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2)` with `DEFAULT_OPTIONS` that ship the `vttforge` marker class so consumers can scope CSS without specificity wars.
291
+ - `VttfError` + `VTTF-NNNN` registry — central, append-only error codes (`VTTF-0001` SystemAlreadyRegistered, `VTTF-0002` MissingFoundryGlobals, `VTTF-0003` UnknownSetting), each with a `name`, `summary`, and `docsUrl` pointing at `https://vttforge.dev/errors/VTTF-NNNN`. Supports native ES2022 `cause` and `AggregateError`.
292
+
293
+ ### Verified
294
+
295
+ - 32 Vitest unit tests across 5 files, all passing on Node 22.14 and Node 24.
296
+ - External consumer smoke test (importing the published `.mjs` from a throwaway dir) confirms every exported symbol behaves as designed when Foundry globals are present or absent.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fabricio Cavalcante de Souza and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # @vttforge/core
2
2
 
3
- Runtime utilities for building FoundryVTT systems and modules.
3
+ Core runtime utilities for VTTForge — the modern SDK for building [FoundryVTT](https://foundryvtt.com) v13+ systems and modules.
4
4
 
5
- > 🚧 **Placeholder release.** This is a reserved name on the npm registry. The first usable version is coming soon.
5
+ > **Status:** v0.0.1 placeholder. Real APIs land in v0.1.0.
6
6
 
7
- Track progress and contribute at **https://github.com/vttforge/vttforge**.
7
+ ## Planned API surface (v0.1.0)
8
8
 
9
- ---
10
-
11
- VTTForge is an independent, community-developed project. It is not affiliated with, endorsed by, or sponsored by Foundry Gaming LLC. "Foundry Virtual Tabletop", "Foundry VTT", and "FVTT" are trademarks of Foundry Gaming LLC.
9
+ - `BaseTypeDataModel` — eliminates stub `migrateData()` and centralises `InferSchema<T>` inference.
10
+ - `BaseActorSheet` / `BaseItemSheet` — declarative replacements for `_getTabs`, DragDrop wiring, `_onEditImage`, `_onDrop`.
11
+ - `SystemConfig` typed wrapper around `game.settings`, eliminates hardcoded system-ID strings.
12
+ - `registerSystem()` — one-call init that replaces the manual `Hooks.once("init", …)` block.
13
+ - `createMigrationRunner` — declarative data migrations with version gates.
14
+ - Error registry — `VttfError` + central `VTTF-NNNN` codes with `docsUrl` pointing at `vttforge.dev/errors/`.
@@ -0,0 +1,32 @@
1
+ {
2
+ "version": 1,
3
+ "package": "@vttforge/core",
4
+ "packageVersion": "0.6.0",
5
+ "entries": [
6
+ {
7
+ "code": "VTTF-0001",
8
+ "name": "SystemAlreadyRegistered",
9
+ "summary": "registerSystem() was called more than once for the same system id. This is almost always a hot-reload artefact or a duplicate import."
10
+ },
11
+ {
12
+ "code": "VTTF-0002",
13
+ "name": "MissingFoundryGlobals",
14
+ "summary": "VTTForge code ran in an environment without Foundry globals (game, Hooks, CONFIG). Initialise inside the Foundry runtime, not in a Node test without mocks."
15
+ },
16
+ {
17
+ "code": "VTTF-0003",
18
+ "name": "UnknownSetting",
19
+ "summary": "SystemConfig.get() / set() was called with a key that was never passed to SystemConfig.register(). Register the setting in your init hook before reading it."
20
+ },
21
+ {
22
+ "code": "VTTF-0004",
23
+ "name": "MigrationFailed",
24
+ "summary": "A migration function passed to createMigrationRunner() threw. The original error is available on .cause. The schemaVersion setting is not advanced past the failed migration so retrying on the next world load picks up where the failure left off."
25
+ },
26
+ {
27
+ "code": "VTTF-0005",
28
+ "name": "WorldTooOldForMigration",
29
+ "summary": "createMigrationRunner() was called on a world whose stored schemaVersion is older than the configured compatibleVersion floor. Upgrade the world to a supported intermediate version before continuing — running migrations across the gap would corrupt data."
30
+ }
31
+ ]
32
+ }