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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/brave-pandas-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
'@dynamic-field-kit/vue': patch
---

Stop `MultiFieldInput` from pinning the whole package into every consumer
bundle.

`MultiFieldInput` renders itself recursively for repeatable groups, and reached
itself through a module-scope `multiFieldInputSelfRef = MultiFieldInput`
assignment. A bare top-level assignment is a side effect no bundler is allowed
to drop, so it anchored `MultiFieldInput` → `FieldInput` → `DynamicInput` →
`getDefaultRenderer` → every default renderer, even in an app that imported none
of them.

The self-reference is now a hoisted function declaration returning
`MultiFieldInput`. Its body is not evaluated until a group actually renders, so
nothing is retained until something uses it. The explicit return type keeps
TypeScript from having to infer `MultiFieldInput` from inside its own
initializer, which is what the forward-declared `let` was working around.

Measured with esbuild, minified, `vue` external:

| App imports | Before | After |
| ---------------------- | -------- | ---------------- |
| one core helper | 13,934 B | 2,479 B (−82%) |
| `DynamicInput` only | 13,934 B | 9,196 B (−34%) |
| `MultiFieldInput` only | 13,944 B | 13,958 B (+14 B) |
| everything | 20,530 B | 20,543 B (+13 B) |

Apps that pull in the whole surface pay 13–14 bytes for the wrapper function,
which is the honest cost of the change.

No behaviour or type change: `dist/index.d.ts` is byte-identical, and the vue
suite (115 tests, including the repeatable-group recursion) passes unchanged.
32 changes: 32 additions & 0 deletions .changeset/clever-donkeys-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@dynamic-field-kit/angular': patch
'@dynamic-field-kit/core': patch
'@dynamic-field-kit/react': patch
'@dynamic-field-kit/vue': patch
---

Close the remaining documentation gaps in each package README, so every public
export is described somewhere. README ships in the npm tarball, so this reaches
the package pages only through a release.

- **Sync vs async validation** is now spelled out in core, with the consequence
that was previously implicit: `validateField` / `validateFields` cannot await,
so a `validate` hook returning a Promise is treated as valid on the sync path.
`useDynamicForm` and `createDynamicFormStore` validate synchronously
(including on submit), so async rules have to run through
`validateFieldsAsync` explicitly. Each adapter README repeats the caveat and
links to the core section.
- Document `validateFieldAsync`, `validateFieldsAsync`, `resolveOptions` and
`validators` in the react, vue and angular export lists, separated from each
adapter's own exports so it is clear they are core re-exports.
- Document the core types that appear in every signature but had no definition
in the README: `Properties`, `ValidatorFn`, `FieldValidatorResult`,
`FieldValidatorFunction`, and `FormStep` alongside `WizardState`.
- Angular: document `layoutRegistry` / `LayoutRegistry`, the `ColumnLayout` /
`RowLayout` / `GridLayout` components and `BaseInputComponent`, with a custom
layout example. Its layout registry holds standalone components rather than
render functions, which is the one place the three adapters genuinely differ,
and it was the only adapter not documenting its registry at all.
- Rename angular's `## What it exports` to `## Exports` to match react and vue.
- Link the demo sub-routes: react's enterprise-features and wizard pages, and
from core the per-framework demos plus the wizard it documents.
35 changes: 35 additions & 0 deletions .changeset/olive-pumas-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
'@dynamic-field-kit/core': patch
'@dynamic-field-kit/react': patch
'@dynamic-field-kit/vue': patch
---

Let consumer bundlers drop the parts of the adapters an app does not use.

Components were declared as bare top-level calls — `defineComponent({...})` in
vue, `React.memo(...)` in react. A bundler cannot prove such a call is
side-effect free, so it has to evaluate it even when the result is unused, which
kept every default renderer and every component in an app's bundle no matter how
little of the package it imported. Marking those calls `/* @__PURE__ */` makes
them droppable. Measured with esbuild, minified, framework external:

| App imports | Before | After |
| -------------------------- | -------- | --------------- |
| react: one core helper | 11,149 B | 3,258 B (−71%) |
| react: `DynamicInput` only | 11,150 B | 6,830 B (−39%) |
| react: everything | 17,601 B | unchanged |
| vue: one core helper | 17,379 B | 13,934 B (−20%) |
| vue: `DynamicInput` only | 17,379 B | 13,934 B (−20%) |
| vue: everything | 20,530 B | unchanged |

Apps that use the whole surface are unchanged, which is the expected result —
there is nothing to drop. The shipped `dist` grows slightly (react +0.03 KB, vue
+0.29 KB) because the annotations are comments in the bundle; the trade is a
bigger published file for a smaller consumer bundle.

`@dynamic-field-kit/core` now declares `"sideEffects": false`. It has no
top-level execution at all — the only module-scope work is `new FieldRegistry()`
assigned to an export — so the claim is accurate, and it lets bundlers that rely
on the flag rather than their own analysis skip core entirely when it is unused.
The adapters deliberately do not set it: their entry side-effect-imports the
default layouts in order to register them.
30 changes: 30 additions & 0 deletions .changeset/shiny-otters-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
'@dynamic-field-kit/core': patch
'@dynamic-field-kit/react': patch
'@dynamic-field-kit/vue': patch
---

Stop publishing sourcemaps, roughly halving what each package installs.

tsup was emitting sourcemaps with `sourcesContent`, so every `.map` carried a
full copy of the TypeScript source. That is what made them work at all — `files`
only publishes `dist`, so a map referencing `../src/*.ts` would otherwise
resolve to nothing — but it also made them about half of each tarball, shipped
to every consumer on every install.

| Package | Unpacked | Tarball | Files |
| ------- | ----------------------- | --------------------- | ----- |
| core | 157.0 → 83.0 KB (−47%) | 33.3 → 17.5 KB (−47%) | 8 → 6 |
| react | 207.6 → 90.1 KB (−57%) | 46.8 → 19.2 KB (−59%) | 8 → 6 |
| vue | 250.4 → 109.4 KB (−56%) | 47.6 → 20.5 KB (−57%) | 8 → 6 |

Nothing that ends up in an application bundle changes — sourcemaps never do.
What changes is install size, and the ability to step into the library's
TypeScript source while debugging a consuming app.

This is a deliberate trade, not a free win: the maps worked. Each package's
`tsup.config.ts` carries the reasoning next to a `sourcemap: false` that is one
word away from restoring them.

`@dynamic-field-kit/angular` is unaffected; ng-packagr's published output does
not carry them.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ jobs:
github.event_name == 'pull_request' &&
!startsWith(github.head_ref, 'changeset-release/')
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
# changeset status diffs against the base branch, so it needs history.
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version-file: '.nvmrc'
cache: 'npm'
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ jobs:
# prefixed or they 404.
SITE_BASE: /${{ github.event.repository.name }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version-file: '.nvmrc'
cache: 'npm'
Expand Down Expand Up @@ -86,7 +86,7 @@ jobs:
find _site -maxdepth 2 -name index.html

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v5
with:
path: _site

Expand All @@ -99,4 +99,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
29 changes: 17 additions & 12 deletions .github/workflows/quality-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ jobs:
lint-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version-file: '.nvmrc'
cache: 'npm'
Expand All @@ -38,7 +38,7 @@ jobs:
# Common ancestor of the four paths is `packages`, so the artifact stores
# `<pkg>/dist/...`.
- name: Upload built dist
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: package-dist
path: |
Expand All @@ -55,6 +55,11 @@ jobs:
- name: Type tests
run: npm run test:types --workspace=@dynamic-field-kit/core

# Runs after the build so the suite's "every package reports a size"
# assertion has a dist to look at.
- name: Test build scripts
run: npm run test:scripts

- name: Show bundle sizes
run: node scripts/show-sizes.js

Expand Down Expand Up @@ -85,10 +90,10 @@ jobs:
coverage-file: packages/angular/coverage/lcov.info
coverage-name: angular
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version-file: '.nvmrc'
cache: 'npm'
Expand All @@ -105,7 +110,7 @@ jobs:

- name: Upload coverage
if: matrix.coverage-file != '' && success()
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v7
with:
files: ${{ matrix.coverage-file }}
flags: unittests
Expand All @@ -121,18 +126,18 @@ jobs:
matrix:
app: [react-app, vue-app, angular-app]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version-file: '.nvmrc'
cache: 'npm'

# The example apps depend on the packages via `file:` paths, so the dist
# has to exist before their install resolves them.
- name: Download built dist
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: package-dist
path: packages
Expand Down Expand Up @@ -160,10 +165,10 @@ jobs:
runs-on: ubuntu-latest
needs: [lint-and-build, test]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version-file: '.nvmrc'
cache: 'npm'
Expand All @@ -174,7 +179,7 @@ jobs:
# Reuse the dist built by lint-and-build instead of rebuilding all four
# packages. Extract into `packages/` so paths become packages/<pkg>/dist.
- name: Download built dist
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: package-dist
path: packages
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ jobs:
needs: [gates]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
ref: ${{ github.ref_name }}
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version-file: '.nvmrc'
cache: 'npm'
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ node_modules/
packages/*/dist
dist/

# vite's transient config bundle, left behind when a vitest run is interrupted
*.timestamp-*.mjs

# test coverage
packages/*/coverage
coverage/
Expand Down
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ build/
out/
coverage/

# vite's transient config bundle, left behind when a vitest run is interrupted.
# Must stay ignored here as well as in .gitignore: `prettier --check .` walks
# untracked files too, so an orphaned one fails format-check after a local
# `npm run test --workspace=@dynamic-field-kit/angular`.
*.timestamp-*.mjs

# Framework caches
.angular/
.next/
Expand Down
5 changes: 3 additions & 2 deletions example/react-app/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ const geistMono = Geist_Mono({
});

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
title: 'Dynamic Field Kit - React Example',
description:
'Schema-driven forms rendered by @dynamic-field-kit/react: renderer registry, layouts, conditional fields, repeatable groups, form state and a multi-step wizard.',
};

export default function RootLayout({
Expand Down
6 changes: 5 additions & 1 deletion example/vue-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vue-app</title>
<title>Dynamic Field Kit - Vue Example</title>
<meta
name="description"
content="Schema-driven forms rendered by @dynamic-field-kit/vue: renderer registry, layouts, conditional fields, repeatable groups and form state."
/>
</head>
<body>
<div id="app"></div>
Expand Down
Loading