Showing preview only (350K chars total). Download the full file or copy to clipboard to get everything.
Repository: maxatwork/form2js
Branch: master
Commit: f9fe5aed9e47
Files: 131
Total size: 316.8 KB
Directory structure:
gitextract_xzm58w39/
├── .changeset/
│ ├── README.md
│ └── config.json
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── pages.yml
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── apps/
│ └── docs/
│ ├── astro.config.mjs
│ ├── package.json
│ ├── playwright.config.ts
│ ├── src/
│ │ ├── components/
│ │ │ ├── api/
│ │ │ │ ├── ApiPackageNav.tsx
│ │ │ │ ├── ApiPackageSummaryList.tsx
│ │ │ │ └── ApiToc.tsx
│ │ │ ├── landing/
│ │ │ │ ├── ApiDocsCta.astro
│ │ │ │ ├── Hero.astro
│ │ │ │ └── InstallSection.astro
│ │ │ └── playground/
│ │ │ ├── PlaygroundShell.tsx
│ │ │ ├── ReactInspectorPanel.tsx
│ │ │ ├── ResultPanel.tsx
│ │ │ ├── StandardResultPanel.tsx
│ │ │ ├── VariantHeader.tsx
│ │ │ ├── bootstrap/
│ │ │ │ └── jquery-bootstrap.ts
│ │ │ ├── types.ts
│ │ │ ├── variant-registry.ts
│ │ │ └── variants/
│ │ │ ├── core-variant.tsx
│ │ │ ├── form-data-variant.tsx
│ │ │ ├── form-variant.tsx
│ │ │ ├── jquery-variant.tsx
│ │ │ ├── js2form-variant.tsx
│ │ │ └── react-variant.tsx
│ │ ├── env.d.ts
│ │ ├── layouts/
│ │ │ ├── ApiDocsLayout.astro
│ │ │ ├── DesignShell.astro
│ │ │ └── DocsShell.astro
│ │ ├── lib/
│ │ │ ├── api-docs-source.ts
│ │ │ ├── api-packages.ts
│ │ │ └── site-routes.ts
│ │ ├── pages/
│ │ │ ├── api/
│ │ │ │ ├── [package].astro
│ │ │ │ └── index.astro
│ │ │ ├── index.astro
│ │ │ └── migrate.astro
│ │ └── styles/
│ │ ├── global.css
│ │ ├── landing.css
│ │ ├── playground.css
│ │ └── themes/
│ │ ├── dark-brutalism.css
│ │ ├── editorial.css
│ │ ├── linear-dark.css
│ │ ├── outrun-sunset.css
│ │ └── terminal-noir.css
│ ├── test/
│ │ ├── api-docs-page.test.tsx
│ │ ├── api-docs-source.test.ts
│ │ ├── data-variants.test.tsx
│ │ ├── docs-pipeline.test.ts
│ │ ├── docs-root-scripts.test.ts
│ │ ├── homepage-shell.test.ts
│ │ ├── playground-shell.test.tsx
│ │ ├── playground-styles.test.ts
│ │ ├── react-variant.test.tsx
│ │ ├── site-routes.test.ts
│ │ ├── standard-variants.test.tsx
│ │ └── variant-registry.test.ts
│ ├── test-e2e/
│ │ ├── api-docs.spec.ts
│ │ └── homepage.spec.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── changeset/
│ └── config.json
├── docs/
│ ├── api-core.md
│ ├── api-dom.md
│ ├── api-form-data.md
│ ├── api-index.md
│ ├── api-jquery.md
│ ├── api-js2form.md
│ ├── api-react.md
│ └── migrate.md
├── eslint.config.js
├── package.json
├── packages/
│ ├── core/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ ├── test/
│ │ │ └── core.test.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── dom/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── standalone.ts
│ │ ├── test/
│ │ │ └── dom.test.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── form-data/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── test/
│ │ │ └── form-data.test.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── jquery/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── standalone.ts
│ │ ├── test/
│ │ │ └── jquery.test.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── js2form/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── test/
│ │ │ └── js2form.test.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ └── react/
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ └── index.ts
│ ├── test/
│ │ └── react.test.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
├── scripts/
│ ├── bump-version.mjs
│ └── rewrite-scope.mjs
├── test/
│ └── integration/
│ ├── bump-version.test.ts
│ ├── dependency-security.test.ts
│ ├── form-flow.test.ts
│ ├── package-metadata.test.ts
│ └── workflow-node-version.test.ts
├── tsconfig.base.json
└── turbo.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .changeset/README.md
================================================
# Changesets
Run `npm run changeset` to add release notes and version bumps.
================================================
FILE: .changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [
[
"@form2js/core",
"@form2js/dom",
"@form2js/form-data",
"@form2js/js2form",
"@form2js/jquery",
"@form2js/react"
]
],
"linked": [],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": []
}
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- master
pull_request:
permissions:
contents: read
jobs:
checks:
name: checks (node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [22.14.0]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- name: Install
run: npm ci
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm run test:packages && npm run test:integration
- name: Build
run: npm run build
- name: Package Dry Run
run: npm run pack:dry-run
docs-e2e:
name: docs-e2e (node 22.14.0)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.14.0
cache: npm
- name: Install
run: npm ci
- name: Install Playwright Chromium
run: npm -w @form2js/docs exec playwright install --with-deps chromium
- name: Build workspace packages
run: npm run build:packages
- name: Test Docs E2E
run: npm run test:docs
================================================
FILE: .github/workflows/pages.yml
================================================
name: Deploy Docs Site
on:
push:
branches:
- master
paths:
- "apps/docs/**"
- "docs/**"
- "packages/**"
- ".github/workflows/pages.yml"
- "package.json"
- "package-lock.json"
- "turbo.json"
- "tsconfig.base.json"
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.14.0
cache: npm
- name: Install
run: npm ci
- name: Configure Pages
id: pages
uses: actions/configure-pages@v5
- name: Build workspace packages
run: npm run build:packages
- name: Build docs app
run: npm -w @form2js/docs run build
env:
DOCS_BASE_PATH: ${{ steps.pages.outputs.base_path }}
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: apps/docs/dist
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
branches:
- master
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.14.0
registry-url: https://registry.npmjs.org
cache: npm
- name: Use npm 11
run: npm install --global npm@11.6.2
- name: Verify toolchain
run: node --version && npm --version
- name: Install
run: npm ci
- name: Build workspace packages
run: npm run build:packages
- name: Create release PR or publish
uses: changesets/action@v1
with:
version: npx changeset version
publish: npm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: true
================================================
FILE: .gitignore
================================================
node_modules
.npm
.turbo
dist
coverage
*.tsbuildinfo
.idea
.hg
.hgignore
.astro
**/.astro/
docs/triage-*
.env
.env.local
.superpowers/brainstorm
.worktrees/
.worktrees
apps/docs/test-results/
apps/docs/playwright-report/
docs/superpowers/
================================================
FILE: LICENSE
================================================
Copyright (c) 2010 Maxim Vasiliev
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.
================================================
FILE: README.md
================================================
# form2js
🚀 **form2js is back — modernized and actively maintained.**
Originally created in 2010, now rewritten for modern JavaScript, TypeScript, ESM, React, and modular usage.
Legacy version is available in the [legacy branch](https://github.com/maxatwork/form2js/tree/legacy).
Migrating from legacy form2js? Start with the [migration guide](https://maxatwork.github.io/form2js/migrate/).
## Description
A small family of packages for turning form-shaped data into objects, and objects back into forms.
It is not a serializer, not an ORM, and not a new religion. It just does this one job, does it reliably, and leaves before anyone starts a committee about it.
## Documentation
- [Docs Site](https://maxatwork.github.io/form2js/) - overview, installation, unified playground, and published API reference.
- [Migration Guide](https://maxatwork.github.io/form2js/migrate/) - map old `form2js` and `jquery.toObject` usage to the current package family.
- [API Reference Source](docs/api-index.md) - markdown source for the published API docs page.
## Migration from Legacy
If you are moving from the archived single-package version, start with the [migration guide](https://maxatwork.github.io/form2js/migrate/).
Quick package map:
- Legacy browser `form2js(...)` usage -> `@form2js/dom`
- Legacy jQuery `$("#form").toObject()` usage -> `@form2js/jquery`
- Server or pipeline `FormData` parsing -> `@form2js/form-data`
- React submit handling -> `@form2js/react`
- Object back into fields -> `@form2js/js2form`
The current project keeps the naming rules and core parsing model, but splits the old browser-era API into environment-specific packages.
## Packages
| Package | npm | Purpose | Module | Standalone | Node.js |
| ------- | --- | ------- | ------ | ---------- | ------- |
| [`@form2js/react`](https://www.npmjs.com/package/@form2js/react) | [](https://www.npmjs.com/package/@form2js/react) | React submit hook with parsing/validation state | Yes | No | Browser-focused |
| [`@form2js/dom`](https://www.npmjs.com/package/@form2js/dom) | [](https://www.npmjs.com/package/@form2js/dom) | Extract DOM fields to object (`formToObject`, `form2js`) | Yes | Yes | With DOM shim (`jsdom`) |
| [`@form2js/form-data`](https://www.npmjs.com/package/@form2js/form-data) | [](https://www.npmjs.com/package/@form2js/form-data) | Convert `FormData`/entries to object | Yes | No | Yes |
| [`@form2js/js2form`](https://www.npmjs.com/package/@form2js/js2form) | [](https://www.npmjs.com/package/@form2js/js2form) | Populate DOM fields from object (`objectToForm`, `js2form`) | Yes | No | With DOM shim (`jsdom`) |
| [`@form2js/core`](https://www.npmjs.com/package/@form2js/core) | [](https://www.npmjs.com/package/@form2js/core) | Path parsing and object transformation engine | Yes | No | Yes |
| [`@form2js/jquery`](https://www.npmjs.com/package/@form2js/jquery) | [](https://www.npmjs.com/package/@form2js/jquery) | jQuery plugin adapter (`$.fn.toObject`) | Yes | Yes | Browser-focused |
## Installation
Install only what you need:
```bash
npm install @form2js/react react
npm install @form2js/dom
npm install @form2js/form-data
npm install @form2js/js2form
npm install @form2js/core
npm install @form2js/jquery jquery
```
For browser standalone usage, use script builds where available:
- `@form2js/dom`: `dist/standalone.global.js`
- `@form2js/jquery`: `dist/standalone.global.js`
## Usage
### `@form2js/dom`
HTML used in examples:
```html
<form id="profileForm">
<input name="person.name.first" value="Esme" />
<input name="person.name.last" value="Weatherwax" />
<label
><input type="checkbox" name="person.tags[]" value="witch" checked />
witch</label
>
</form>
```
Module:
```ts
import { formToObject } from "@form2js/dom";
const result = formToObject(document.getElementById("profileForm"));
// => { person: { name: { first: "Esme", last: "Weatherwax" }, tags: ["witch"] } }
```
Standalone:
```html
<script src="https://unpkg.com/@form2js/dom/dist/standalone.global.js"></script>
<script>
const result = formToObject(document.getElementById("profileForm"));
// or form2js(...)
</script>
```
### `@form2js/form-data`
Module (browser or Node 18+):
```ts
import { formDataToObject } from "@form2js/form-data";
const fd = new FormData(formElement);
const result = formDataToObject(fd);
```
Node.js note:
- Node 18+ has global `FormData`.
- You can also pass iterable entries directly, which is handy in server pipelines:
```ts
import { entriesToObject } from "@form2js/form-data";
const result = entriesToObject([
["person.name.first", "Sam"],
["person.roles[]", "captain"],
]);
// => { person: { name: { first: "Sam" }, roles: ["captain"] } }
```
With schema validation (works with Zod or any `{ parse(unknown) }` schema):
```ts
import { z } from "zod";
import { formDataToObject } from "@form2js/form-data";
const PersonSchema = z.object({
person: z.object({
age: z.coerce.number().int().min(0)
})
});
const result = formDataToObject([["person.age", "42"]], {
schema: PersonSchema
});
// => { person: { age: 42 } }
```
Standalone:
- Not shipped for this package. Use module imports.
### `@form2js/react`
Module:
```ts
import { z } from "zod";
import { useForm2js } from "@form2js/react";
const FormDataSchema = z.object({
person: z.object({
name: z.object({
first: z.string().min(1)
})
})
});
export function ProfileForm(): React.JSX.Element {
const { onSubmit, isSubmitting, isError, error, isSuccess, reset } = useForm2js(
async (data) => {
// data is inferred from schema when schema is provided
await sendFormData(data);
},
{
schema: FormDataSchema
}
);
return (
<form
onSubmit={(event) => {
void onSubmit(event);
}}
>
<input name="person.name.first" defaultValue="Sam" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</button>
{isError ? <p>{String(error)}</p> : null}
{isSuccess ? <p>Saved</p> : null}
<button type="button" onClick={reset}>
Reset state
</button>
</form>
);
}
```
### `@form2js/jquery`
HTML used in examples:
```html
<form id="profileForm">
<input name="person.name.first" value="Sam" />
<input name="person.name.last" value="Vimes" />
</form>
```
Module:
```ts
import $ from "jquery";
import { installToObjectPlugin } from "@form2js/jquery";
installToObjectPlugin($);
const data = $("#profileForm").toObject({ mode: "first" });
// => { person: { name: { first: "Sam", last: "Vimes" } } }
```
Standalone:
```html
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://unpkg.com/@form2js/jquery/dist/standalone.global.js"></script>
<script>
const data = $("#profileForm").toObject({ mode: "combine" });
</script>
```
### `@form2js/js2form`
HTML used in examples (before calling `objectToForm`):
```html
<form id="profileForm">
<input name="person.name.first" />
<input name="person.name.last" />
</form>
```
Module:
```ts
import { objectToForm } from "@form2js/js2form";
objectToForm(document.getElementById("profileForm"), {
person: { name: { first: "Tiffany", last: "Aching" } },
});
// fields are now populated in the form
```
Standalone:
- Not shipped as a dedicated global bundle. Use module imports.
### `@form2js/core`
Module:
```ts
import { entriesToObject, objectToEntries } from "@form2js/core";
const data = entriesToObject([
{ key: "person.name.first", value: "Vimes" },
{ key: "person.tags[]", value: "watch" },
]);
const pairs = objectToEntries(data);
```
Node.js:
- Fully supported (no DOM dependency).
Standalone:
- Not shipped for this package. Use module imports.
## Legacy behavior notes
Compatibility with the old project is intentional.
- Name paths define output shape (`person.name.first`).
- Array and indexed syntax is preserved (`items[]`, `items[5].name`).
- Rails-style names are supported (`rails[field][value]`).
- DOM extraction follows native browser form submission semantics for checkbox and radio values.
- Unsafe key path segments (`__proto__`, `prototype`, `constructor`) are rejected by default.
- This library does data shaping, not JSON/XML serialization.
## Design boundaries and non-goals
These boundaries are intentional and are used for issue triage.
- Sparse indexes are compacted in first-seen order (`items[5]`, `items[8]` -> `items[0]`, `items[1]`).
- Type inference is minimal by design; DOM extraction keeps native string values instead of coercing checkbox/radio fields.
- Unchecked indexed controls are omitted and therefore do not reserve compacted array slots; include another submitted field when row identity matters.
- `formToObject` reads successful form control values, not option labels. Disabled controls (including disabled fieldset descendants) and button-like inputs are excluded unless you explicitly opt in to disabled values.
- `extractPairs`/`formToObject` support `nodeCallback`; return `SKIP_NODE` to exclude a node entirely, or `{ name|key, value }` to inject a custom entry.
- Parser inputs reject unsafe path segments by default. Use `allowUnsafePathSegments: true` only with trusted inputs.
- `objectToForm` supports `nodeCallback`; returning `false` skips the default assignment for that node.
- `objectToForm` sets form control state and values; it does not dispatch synthetic `change` or `input` events.
- Empty collections are not synthesized when no matching fields are present (for example, unchecked checkbox groups).
- Dynamic key/value remapping (for example, converting `key`/`val` fields into arbitrary object keys) is application logic.
- For file payloads and richer multipart semantics, use `FormData` and `@form2js/form-data`.
## Contributing
### Setup
```bash
npm ci
```
### Local checks
```bash
npm run lint
npm run typecheck
npm run test
npm run build
npm run pack:dry-run
```
### Local docs site
```bash
npm run docs
npm run docs:build
```
The homepage includes the unified playground for `@form2js/react`, `@form2js/dom`, `@form2js/jquery`, `@form2js/js2form`, `@form2js/core`, and `@form2js/form-data`.
### GitHub Pages docs site
The published docs site is deployed by `.github/workflows/pages.yml`.
- Trigger: pushes to `master` that touch `apps/docs/**`, `docs/**`, `packages/**`, `.github/workflows/pages.yml`, `package.json`, `package-lock.json`, `turbo.json`, or `tsconfig.base.json`, plus manual `workflow_dispatch`.
- Output: `apps/docs/dist`.
- URL: `https://maxatwork.github.io/form2js/`.
In repository settings, set Pages source to `GitHub Actions` once, and then the workflow handles updates.
### Before opening a PR
1. Keep changes focused to one problem area where possible.
2. Add or update tests for behavior changes.
3. Add a changeset (`npm run changeset`) for user-visible changes.
4. Include migration notes in README if behavior or API changes.
### Filing PRs and issues
Please include:
- Clear expected vs actual behavior.
- Minimal reproduction (HTML snippet or input entries).
- Package name and version.
- Environment (`node -v`, browser/version if relevant).
## Release workflow
- CI runs lint, typecheck, test, build, and package dry-run.
- Releases are managed with Changesets and the published `@form2js/*` packages are versioned in lockstep.
## Scope rewrite helper
Default scope is `@form2js/*`.
If you need to publish under another scope:
```bash
npm run scope:rewrite -- --scope @your-scope --dry-run
npm run scope:rewrite -- --scope @your-scope
```
This rewrites package names, internal dependencies, and import references.
## License
MIT, see `LICENSE`.
================================================
FILE: apps/docs/astro.config.mjs
================================================
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
const base = process.env.DOCS_BASE_PATH ?? "/";
export default defineConfig({
base,
integrations: [react()],
vite: {
server: {
fs: {
allow: ["../.."]
}
}
}
});
================================================
FILE: apps/docs/package.json
================================================
{
"name": "@form2js/docs",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"test": "vitest run",
"test:e2e": "playwright test",
"lint": "eslint \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\" \"test-e2e/**/*.ts\"",
"typecheck": "astro sync && tsc --noEmit",
"clean": "rimraf dist .astro"
},
"dependencies": {
"@astrojs/react": "^5.0.2",
"@form2js/core": "3.4.0",
"@form2js/dom": "3.4.0",
"@form2js/form-data": "3.4.0",
"@form2js/jquery": "3.4.0",
"@form2js/js2form": "3.4.0",
"@form2js/react": "3.4.0",
"astro": "^6.1.1",
"jquery": "^3.7.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"unified": "^11.0.5",
"zod": "^4.1.5"
},
"devDependencies": {
"@playwright/test": "^1.54.2",
"@types/jquery": "^3.5.33",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9"
}
}
================================================
FILE: apps/docs/playwright.config.ts
================================================
import { defineConfig, devices } from "@playwright/test";
const docsE2eHost = process.env.DOCS_E2E_HOST ?? "127.0.0.1";
const docsE2ePort = Number(process.env.DOCS_E2E_PORT ?? "4321");
const docsE2eUrl = `http://${docsE2eHost}:${docsE2ePort}/`;
export default defineConfig({
testDir: "./test-e2e",
use: {
baseURL: docsE2eUrl,
trace: "retain-on-failure"
},
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"]
}
}
],
webServer: {
command:
`PUBLIC_DOCS_E2E_FAULTS=1 npm -w @form2js/docs run build && npm -w @form2js/docs run preview -- --host ${docsE2eHost} --port ${docsE2ePort}`,
url: docsE2eUrl,
reuseExistingServer: !process.env.CI
}
});
================================================
FILE: apps/docs/src/components/api/ApiPackageNav.tsx
================================================
import React from "react";
import type { ApiPackageEntry } from "../../lib/api-packages";
import { apiPackageDocsPath } from "../../lib/site-routes";
type ApiPackageNavEntry = Pick<ApiPackageEntry, "slug" | "packageName">;
interface ApiPackageNavProps {
activeSlug?: ApiPackageNavEntry["slug"];
basePath: string;
packages: ApiPackageNavEntry[];
}
export function ApiPackageNav({
activeSlug,
basePath,
packages
}: ApiPackageNavProps): React.JSX.Element {
return (
<nav aria-label="API packages" className="api-package-nav">
<p className="api-package-nav__eyebrow">Packages</p>
<ul className="api-package-nav__list">
{packages.map((entry) => (
<li key={entry.slug}>
<a
aria-current={entry.slug === activeSlug ? "page" : undefined}
className="api-package-nav__link"
href={apiPackageDocsPath(basePath, entry.slug)}
>
{entry.packageName}
</a>
</li>
))}
</ul>
</nav>
);
}
================================================
FILE: apps/docs/src/components/api/ApiPackageSummaryList.tsx
================================================
import React from "react";
import type { ApiPackageEntry } from "../../lib/api-packages";
import { apiPackageDocsPath } from "../../lib/site-routes";
type ApiPackageSummaryEntry = Pick<
ApiPackageEntry,
"slug" | "packageName" | "summary"
>;
interface ApiPackageSummaryListProps {
basePath: string;
packages: ApiPackageSummaryEntry[];
}
export function ApiPackageSummaryList({
basePath,
packages
}: ApiPackageSummaryListProps): React.JSX.Element {
return (
<section aria-labelledby="api-package-list" className="api-package-summary-list">
<h2 id="api-package-list">Packages</h2>
<div className="api-package-summary-list__items">
{packages.map((entry) => (
<article className="api-package-summary" key={entry.slug}>
<h3>
<a href={apiPackageDocsPath(basePath, entry.slug)}>{entry.packageName}</a>
</h3>
<p>{entry.summary}</p>
</article>
))}
</div>
</section>
);
}
================================================
FILE: apps/docs/src/components/api/ApiToc.tsx
================================================
import React, { useEffect, useMemo, useState } from "react";
import type { ApiHeading } from "../../lib/api-docs-source";
interface ApiTocProps {
headings: ApiHeading[];
initialActiveSlug?: string;
}
interface TocGroup {
heading: ApiHeading;
children: ApiHeading[];
}
function groupHeadings(headings: ApiHeading[]): TocGroup[] {
const groups: TocGroup[] = [];
for (const heading of headings) {
if (heading.depth === 2 || groups.length === 0) {
groups.push({
heading,
children: []
});
continue;
}
groups[groups.length - 1]?.children.push(heading);
}
return groups;
}
export function ApiToc({ headings, initialActiveSlug }: ApiTocProps): React.JSX.Element {
const groups = useMemo(() => groupHeadings(headings), [headings]);
const [activeSlug, setActiveSlug] = useState(initialActiveSlug ?? headings[0]?.slug ?? "");
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const hashSlug = window.location.hash.replace(/^#/, "");
if (hashSlug) {
setActiveSlug(hashSlug);
}
const observedHeadings = headings
.map((heading) => document.getElementById(heading.slug))
.filter((heading): heading is HTMLElement => Boolean(heading));
if (observedHeadings.length === 0 || typeof IntersectionObserver === "undefined") {
return;
}
const observer = new IntersectionObserver(
(entries) => {
const visibleEntry = entries
.filter((entry) => entry.isIntersecting)
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
if (visibleEntry?.target.id) {
setActiveSlug(visibleEntry.target.id);
}
},
{
rootMargin: "-20% 0px -60% 0px",
threshold: [0.2, 0.6, 1]
}
);
for (const heading of observedHeadings) {
observer.observe(heading);
}
const handleHashChange = (): void => {
const nextHashSlug = window.location.hash.replace(/^#/, "");
if (nextHashSlug) {
setActiveSlug(nextHashSlug);
}
};
window.addEventListener("hashchange", handleHashChange);
return () => {
observer.disconnect();
window.removeEventListener("hashchange", handleHashChange);
};
}, [headings]);
return (
<nav aria-label="On this page" className="api-toc">
<p className="api-toc__eyebrow">On this page</p>
<ul className="api-toc__list">
{groups.map((group) => (
<li key={group.heading.slug}>
<a
aria-current={activeSlug === group.heading.slug ? "true" : undefined}
className="api-toc__link"
href={`#${group.heading.slug}`}
onClick={() => {
setActiveSlug(group.heading.slug);
}}
>
{group.heading.text}
</a>
{group.children.length > 0 ? (
<ul className="api-toc__sublist">
{group.children.map((child) => (
<li key={child.slug}>
<a
aria-current={activeSlug === child.slug ? "true" : undefined}
className="api-toc__sublink"
href={`#${child.slug}`}
onClick={() => {
setActiveSlug(child.slug);
}}
>
{child.text}
</a>
</li>
))}
</ul>
) : null}
</li>
))}
</ul>
</nav>
);
}
================================================
FILE: apps/docs/src/components/landing/ApiDocsCta.astro
================================================
---
// apps/docs/src/components/landing/ApiDocsCta.astro
import { apiDocsPath } from "../../lib/site-routes";
const basePath = import.meta.env.BASE_URL;
---
<section class="section content-wrap">
<p class="section-eyebrow">Reference</p>
<h2 class="section-title">API Documentation</h2>
<p class="section-desc">
Exact signatures, option defaults, TypeScript types, and compatibility
notes for every package — all generated from the same source of truth.
</p>
<a class="btn-primary" href={apiDocsPath(basePath)}>Open API Docs →</a>
</section>
================================================
FILE: apps/docs/src/components/landing/Hero.astro
================================================
---
// apps/docs/src/components/landing/Hero.astro
import { apiDocsPath } from "../../lib/site-routes";
const basePath = import.meta.env.BASE_URL;
---
<section class="hero">
<p class="hero__eyebrow">form serialization library</p>
<h1 class="hero__title">Turn forms<br />into objects.</h1>
<p class="hero__desc">
Parse browser forms into structured JavaScript objects. Six adapters —
React hooks, vanilla DOM, jQuery, FormData, and more. One coherent API.
</p>
<div class="hero__actions">
<span class="hero__install">
<span class="hero__install-prefix">$</span>
<span id="hero-install-pkg">npm install @form2js/react react</span>
</span>
<a class="btn-primary" href="#playground">Try the playground ↓</a>
<a class="btn-ghost" href={apiDocsPath(basePath)}>API Docs →</a>
</div>
</section>
<script>
window.addEventListener("form2js:variant-change", (event) => {
if (!(event instanceof CustomEvent)) return;
const detail = event.detail;
const variantId = typeof detail?.variantId === "string" ? detail.variantId : null;
const firstPackage =
Array.isArray(detail?.packages) && typeof detail.packages[0] === "string"
? detail.packages[0]
: null;
const el = document.getElementById("hero-install-pkg");
if (!el) return;
if (variantId === "react") {
el.textContent = "npm install @form2js/react react";
return;
}
if (variantId === "jquery") {
el.textContent = "npm install @form2js/jquery jquery";
return;
}
if (firstPackage) {
el.textContent = `npm install ${firstPackage}`;
}
});
</script>
================================================
FILE: apps/docs/src/components/landing/InstallSection.astro
================================================
---
// apps/docs/src/components/landing/InstallSection.astro
---
<section class="section content-wrap" id="install">
<p class="section-eyebrow">Use in your project</p>
<div class="install-grid">
<div>
<h2 class="section-title">Install</h2>
<p class="section-desc">
Pick the adapter for your stack. All packages share the same
path-syntax so switching is painless.
</p>
</div>
<div>
<span id="install-cmd" class="install-cmd">npm install @form2js/react react</span>
<pre id="install-snippet" class="code-block">{`import { useForm2js } from '@form2js/react'
const { onSubmit, isSubmitting } = useForm2js(handler)
// nested names just work:
// <input name="user.email" />
// → { user: { email: '…' } }`}</pre>
<p id="install-alt-label" class="section-desc" hidden>Browser global via <script></p>
<pre id="install-alt-snippet" class="code-block" hidden></pre>
</div>
</div>
</section>
<script>
window.addEventListener("form2js:variant-change", (event) => {
if (!(event instanceof CustomEvent)) return;
const detail = event.detail;
const cmdEl = document.getElementById("install-cmd");
const snippetEl = document.getElementById("install-snippet");
const altLabelEl = document.getElementById("install-alt-label");
const altSnippetEl = document.getElementById("install-alt-snippet");
const variantId = typeof detail?.variantId === "string" ? detail.variantId : null;
let install = null;
switch (variantId) {
case "react":
install = {
command: "npm install @form2js/react react",
snippet: `import { useForm2js } from '@form2js/react'
const { onSubmit, isSubmitting } = useForm2js(handler)`,
};
break;
case "form":
install = {
command: "npm install @form2js/dom",
snippet: `import { formToObject } from '@form2js/dom'
const data = formToObject(formElement)`,
altLabel: "Browser global via <script>",
altSnippet: `<script src="https://unpkg.com/@form2js/dom/dist/standalone.global.js"><\/script>
<script>
const data = formToObject(formElement)
<\/script>`,
};
break;
case "jquery":
install = {
command: "npm install @form2js/jquery jquery",
snippet: `import { installToObjectPlugin } from '@form2js/jquery'
installToObjectPlugin($)
const data = $('form').toObject()`,
altLabel: "Browser global via <script>",
altSnippet: `<script src="https://code.jquery.com/jquery-3.7.1.min.js"><\/script>
<script src="https://unpkg.com/@form2js/jquery/dist/standalone.global.js"><\/script>
<script>
const data = $('form').toObject()
<\/script>`,
};
break;
case "js2form":
install = {
command: "npm install @form2js/js2form",
snippet: `import { objectToForm } from '@form2js/js2form'
objectToForm(formElement, { user: { name: 'Alice' } })`,
};
break;
case "core":
install = {
command: "npm install @form2js/core",
snippet: `import { entriesToObject } from '@form2js/core'
const data = entriesToObject([
{ key: 'user.name', value: 'Alice' }
])`,
};
break;
case "form-data":
install = {
command: "npm install @form2js/form-data",
snippet: `import { formDataToObject } from '@form2js/form-data'
const data = formDataToObject(new FormData(formElement))`,
};
break;
default:
install = null;
}
if (cmdEl && install?.command) cmdEl.textContent = install.command;
if (snippetEl && install?.snippet) snippetEl.textContent = install.snippet;
if (altLabelEl) altLabelEl.hidden = !install?.altLabel;
if (altSnippetEl) altSnippetEl.hidden = !install?.altSnippet;
if (altLabelEl && install?.altLabel) altLabelEl.textContent = install.altLabel;
if (altSnippetEl) altSnippetEl.textContent = install?.altSnippet ?? "";
});
</script>
================================================
FILE: apps/docs/src/components/playground/PlaygroundShell.tsx
================================================
// apps/docs/src/components/playground/PlaygroundShell.tsx
import React, { useEffect, useState } from "react";
import { VARIANT_IDS, variantsById } from "./variant-registry";
import { ResultPanel } from "./ResultPanel";
import type { ErrorInfo, OutputState, VariantDefinition, VariantId } from "./types";
import { VariantHeader } from "./VariantHeader";
function getRequestedRenderFault(): { variantId: VariantId; message: string } | null {
if (typeof window === "undefined") return null;
const requestedFault = new URLSearchParams(window.location.search).get("__fault");
if (!requestedFault) return null;
const [requestedVariantId, source] = requestedFault.split(":");
if (source !== "render" || !VARIANT_IDS.includes(requestedVariantId as VariantId)) return null;
const variantId = requestedVariantId as VariantId;
return { variantId, message: `Injected render fault for ${variantsById[variantId].label}` };
}
function getActiveVariantId(): VariantId {
if (typeof window === "undefined") return "react";
const current = new URLSearchParams(window.location.search).get("variant");
if (current && VARIANT_IDS.includes(current as VariantId)) return current as VariantId;
return "react";
}
function createInitialOutputStates(
variantId: VariantId
): Partial<Record<VariantId, OutputState>> {
return { [variantId]: variantsById[variantId].createInitialOutputState() };
}
function dispatchVariantChange(variantId: VariantId): void {
if (typeof window === "undefined") return;
const variant = variantsById[variantId];
window.dispatchEvent(
new CustomEvent("form2js:variant-change", {
detail: { variantId, packages: variant.packages }
})
);
}
interface VariantErrorBoundaryProps {
children: React.ReactNode;
onError: (errorInfo: ErrorInfo) => void;
}
interface VariantErrorBoundaryState { hasError: boolean; }
class VariantErrorBoundary extends React.Component<VariantErrorBoundaryProps, VariantErrorBoundaryState> {
state: VariantErrorBoundaryState = { hasError: false };
static getDerivedStateFromError(): VariantErrorBoundaryState { return { hasError: true }; }
componentDidCatch(error: Error): void {
this.props.onError({ message: error.message, source: "render" });
}
render(): React.ReactNode {
if (this.state.hasError) return null;
return this.props.children;
}
}
export function PlaygroundShell(): React.JSX.Element {
const [activeVariantId, setActiveVariantId] = useState<VariantId>(() => getActiveVariantId());
const [mountedVariantIds, setMountedVariantIds] = useState<VariantId[]>(() => [getActiveVariantId()]);
const [outputStates, setOutputStates] = useState<Partial<Record<VariantId, OutputState>>>(() =>
createInitialOutputStates(getActiveVariantId())
);
const [failedVariants, setFailedVariants] = useState<Partial<Record<VariantId, ErrorInfo>>>({});
useEffect(() => {
dispatchVariantChange(activeVariantId);
}, []);
useEffect(() => {
if (!mountedVariantIds.includes(activeVariantId)) {
setMountedVariantIds((current) => [...current, activeVariantId]);
}
setOutputStates((current) => {
if (current[activeVariantId]) return current;
return { ...current, [activeVariantId]: variantsById[activeVariantId].createInitialOutputState() };
});
}, [activeVariantId, mountedVariantIds]);
useEffect(() => {
const requestedFault = getRequestedRenderFault();
if (requestedFault?.variantId !== activeVariantId || failedVariants[activeVariantId] !== undefined) {
return;
}
handleVariantFailure(activeVariantId, { message: requestedFault.message, source: "render" });
}, [activeVariantId, failedVariants]);
const activeVariant = variantsById[activeVariantId];
const activeOutputState = outputStates[activeVariantId] ?? activeVariant.createInitialOutputState();
const variants = VARIANT_IDS.map((variantId) => variantsById[variantId]);
function handleVariantFailure(variantId: VariantId, errorInfo: ErrorInfo): void {
setFailedVariants((current) => ({ ...current, [variantId]: errorInfo }));
setOutputStates((current) => {
return Object.fromEntries(
Object.entries(current).filter(([currentVariantId]) => currentVariantId !== variantId)
) as Partial<Record<VariantId, OutputState>>;
});
}
function selectVariant(variantId: VariantId): void {
if (typeof window !== "undefined") {
const nextUrl = new URL(window.location.href);
nextUrl.searchParams.set("variant", variantId);
window.history.replaceState({}, "", `${nextUrl.pathname}${nextUrl.search}`);
dispatchVariantChange(variantId);
}
setActiveVariantId(variantId);
}
return (
<section aria-label="Unified playground" className="pg-shell">
<VariantHeader activeId={activeVariantId} onSelect={selectVariant} variants={variants} />
<div className="pg-body">
<div className="pg-form-col">
<p className="pg-variant-summary">{activeVariant.summary}</p>
{mountedVariantIds.map((variantId) => {
const variant: VariantDefinition = variantsById[variantId];
const isActive = variantId === activeVariantId;
if (failedVariants[variantId]) return null;
return (
<div hidden={!isActive} key={variantId}>
<VariantErrorBoundary
onError={(errorInfo) => {
handleVariantFailure(variantId, errorInfo);
}}
>
<variant.Component
isActive={isActive}
onOutputChange={(outputState) => {
setOutputStates((current) => ({ ...current, [variantId]: outputState }));
}}
reportFatalError={(errorInfo) => {
handleVariantFailure(variantId, errorInfo);
}}
/>
</VariantErrorBoundary>
</div>
);
})}
{failedVariants[activeVariantId] && (
<section aria-label="Failed variant">
<p>{activeVariant.label} failed to load.</p>
<p>{failedVariants[activeVariantId]?.message}</p>
</section>
)}
</div>
<div className="pg-output-col">
<ResultPanel outputState={activeOutputState} />
</div>
</div>
</section>
);
}
================================================
FILE: apps/docs/src/components/playground/ReactInspectorPanel.tsx
================================================
import React from "react";
import type { ReactOutputState } from "./types";
interface ReactInspectorPanelProps {
outputState: ReactOutputState;
}
export function ReactInspectorPanel({ outputState }: ReactInspectorPanelProps): React.JSX.Element {
const metaEntries = outputState.meta ? Object.entries(outputState.meta) : [];
const hasParsedPayload = outputState.parsedPayload !== null;
return (
<section aria-label="React output">
<h2>Submit state</h2>
<p>{outputState.statusMessage}</p>
<p>isSubmitting: {String(outputState.submitFlags.isSubmitting)}</p>
<p>isError: {String(outputState.submitFlags.isError)}</p>
<p>isSuccess: {String(outputState.submitFlags.isSuccess)}</p>
{metaEntries.length > 0 ? (
<dl>
{metaEntries.map(([key, value]) => (
<React.Fragment key={key}>
<dt>{key}</dt>
<dd>{value === null ? "null" : typeof value === "boolean" ? String(value) : value}</dd>
</React.Fragment>
))}
</dl>
) : null}
{hasParsedPayload ? <pre>{JSON.stringify(outputState.parsedPayload, null, 2)}</pre> : null}
{outputState.error ? <p>{outputState.error.message}</p> : null}
</section>
);
}
================================================
FILE: apps/docs/src/components/playground/ResultPanel.tsx
================================================
// apps/docs/src/components/playground/ResultPanel.tsx
import React from "react";
import type { OutputState } from "./types";
interface ResultPanelProps {
outputState: OutputState;
}
function formatZodErrors(error: { message: string }): string[] {
// The error message from react-variant is pre-formatted as "path: msg\npath: msg"
return error.message.split("\n").filter(Boolean);
}
export function ResultPanel({ outputState }: ResultPanelProps): React.JSX.Element {
const statusClass = `status-${outputState.status}`;
if (outputState.kind === "react") {
const { submitFlags, error, parsedPayload, meta } = outputState;
const errorLines = error ? formatZodErrors(error) : [];
const metaEntries = meta ? Object.entries(meta) : [];
return (
<div>
<p className="result-eyebrow">Output</p>
<h2>Submit state</h2>
<span className={`status-badge ${statusClass}`}>
{outputState.statusMessage}
</span>
<div className="result-flags">
<span className="flag-key">isSubmitting</span>
<span className={submitFlags.isSubmitting ? "flag-true" : "flag-false"}>
{String(submitFlags.isSubmitting)}
</span>
<span className="flag-key">isError</span>
<span className={submitFlags.isError ? "flag-true" : "flag-false"}>
{String(submitFlags.isError)}
</span>
<span className="flag-key">isSuccess</span>
<span className={submitFlags.isSuccess ? "flag-true" : "flag-false"}>
{String(submitFlags.isSuccess)}
</span>
</div>
{metaEntries.length > 0 && (
<dl>
{metaEntries.map(([key, value]) => (
<React.Fragment key={key}>
<dt>{key}</dt>
<dd>{value === null ? "null" : typeof value === "boolean" ? String(value) : value}</dd>
</React.Fragment>
))}
</dl>
)}
{errorLines.length > 0 && (
<div className="result-errors">
{errorLines.map((line, i) => (
<p key={i} className="result-error-item">
{line}
</p>
))}
</div>
)}
{parsedPayload !== null ? (
<pre className="result-json">{JSON.stringify(parsedPayload, null, 2)}</pre>
) : (
<p className="result-empty">
{outputState.status === "idle" ? "Submit the form to see parsed output." : ""}
</p>
)}
</div>
);
}
// standard kind
const { errorMessage, parsedPayload, statusMessage } = outputState;
return (
<div>
<p className="result-eyebrow">Output</p>
<h2>Parsed result</h2>
<span className={`status-badge ${statusClass}`}>{statusMessage}</span>
{errorMessage && (
<div className="result-errors">
<p className="result-error-item">{errorMessage}</p>
</div>
)}
{parsedPayload !== null ? (
<pre className="result-json">{JSON.stringify(parsedPayload, null, 2)}</pre>
) : (
<p className="result-empty">
{outputState.status === "idle" ? "Run the variant to see parsed output." : ""}
</p>
)}
</div>
);
}
================================================
FILE: apps/docs/src/components/playground/StandardResultPanel.tsx
================================================
import React from "react";
import type { StandardOutputState } from "./types";
interface StandardResultPanelProps {
outputState: StandardOutputState;
}
export function StandardResultPanel({ outputState }: StandardResultPanelProps): React.JSX.Element {
const hasParsedPayload = outputState.parsedPayload !== null;
return (
<section aria-label="Standard output">
<h2>Parsed result</h2>
<p>{outputState.statusMessage}</p>
{outputState.errorMessage ? <p>{outputState.errorMessage}</p> : null}
{hasParsedPayload ? <pre>{JSON.stringify(outputState.parsedPayload, null, 2)}</pre> : null}
</section>
);
}
================================================
FILE: apps/docs/src/components/playground/VariantHeader.tsx
================================================
// apps/docs/src/components/playground/VariantHeader.tsx
import React from "react";
import type { VariantDefinition, VariantId } from "./types";
interface VariantHeaderProps {
activeId: VariantId;
variants: VariantDefinition[];
onSelect: (variantId: VariantId) => void;
}
export function VariantHeader({ activeId, variants, onSelect }: VariantHeaderProps): React.JSX.Element {
return (
<div className="pg-tabs" aria-label="Variant switcher">
{variants.map((variant) => (
<button
key={variant.id}
aria-pressed={variant.id === activeId}
className="pg-tab-btn"
data-variant-id={variant.id}
onClick={() => {
onSelect(variant.id);
}}
type="button"
>
{variant.label}
</button>
))}
</div>
);
}
================================================
FILE: apps/docs/src/components/playground/bootstrap/jquery-bootstrap.ts
================================================
import $ from "jquery";
import { installToObjectPlugin } from "@form2js/jquery";
type JQueryWithPlugin = typeof $ & {
fn: {
toObject?: unknown;
};
};
let installedPlugin: unknown = null;
export function ensureJqueryBootstrap(): unknown {
const jquery = $ as JQueryWithPlugin;
if (typeof jquery.fn.toObject !== "function") {
installToObjectPlugin(jquery);
}
installedPlugin = jquery.fn.toObject ?? null;
return installedPlugin;
}
================================================
FILE: apps/docs/src/components/playground/types.ts
================================================
import type { ReactNode } from "react";
export type VariantKind = "react" | "standard";
export type OutputStatus = "idle" | "running" | "success" | "error";
export type VariantId = "react" | "form" | "jquery" | "js2form" | "core" | "form-data";
export interface ErrorInfo {
message: string;
source: "render" | "effect" | "bootstrap" | "event" | "async";
detail?: string;
}
export interface ReactOutputState {
kind: "react";
status: OutputStatus;
statusMessage: string;
submitFlags: {
isSubmitting: boolean;
isError: boolean;
isSuccess: boolean;
};
error: { message: string; detail?: string } | null;
parsedPayload: unknown;
meta?: Record<string, ReactNode>;
}
export interface StandardOutputState {
kind: "standard";
status: OutputStatus;
statusMessage: string;
errorMessage: string | null;
parsedPayload: unknown;
}
export type OutputState = ReactOutputState | StandardOutputState;
export interface VariantComponentProps {
isActive: boolean;
onOutputChange: (outputState: OutputState) => void;
reportFatalError: (errorInfo: ErrorInfo) => void;
}
export interface VariantDefinition {
id: VariantId;
kind: VariantKind;
label: string;
summary: string;
packages: string[];
createInitialOutputState: () => OutputState;
Component: (props: VariantComponentProps) => ReactNode;
}
================================================
FILE: apps/docs/src/components/playground/variant-registry.ts
================================================
import type { OutputState, VariantDefinition, VariantId } from "./types";
import { CoreVariant } from "./variants/core-variant";
import { FormDataVariant } from "./variants/form-data-variant";
import { FormVariant } from "./variants/form-variant";
import { JQueryVariant } from "./variants/jquery-variant";
import { Js2FormVariant } from "./variants/js2form-variant";
import { ReactVariant } from "./variants/react-variant";
function createStandardIdle(statusMessage: string): OutputState {
return {
kind: "standard",
status: "idle",
statusMessage,
errorMessage: null,
parsedPayload: null
};
}
function createReactIdle(statusMessage: string): OutputState {
return {
kind: "react",
status: "idle",
statusMessage,
submitFlags: {
isSubmitting: false,
isError: false,
isSuccess: false
},
error: null,
parsedPayload: null
};
}
export const variants = [
{
id: "react",
kind: "react",
label: "React",
summary: "Submit forms with schema-aware async state.",
packages: ["@form2js/react"],
createInitialOutputState: () => createReactIdle("Ready to submit.")
},
{
id: "form",
kind: "standard",
label: "Form",
summary: "Parse a plain browser form with @form2js/dom or form2js().",
packages: ["@form2js/dom"],
createInitialOutputState: () => createStandardIdle("Ready to parse the form.")
},
{
id: "jquery",
kind: "standard",
label: "jQuery",
summary: "Use the jQuery plugin adapter with selectable modes.",
packages: ["@form2js/jquery"],
createInitialOutputState: () => createStandardIdle("Ready to run the plugin.")
},
{
id: "js2form",
kind: "standard",
label: "js2form",
summary: "Apply object data back into form controls.",
packages: ["@form2js/js2form"],
createInitialOutputState: () => createStandardIdle("Ready to apply object data.")
},
{
id: "core",
kind: "standard",
label: "Core",
summary: "Parse raw key/value entries into nested objects.",
packages: ["@form2js/core"],
createInitialOutputState: () => createStandardIdle("Ready to parse entry data.")
},
{
id: "form-data",
kind: "standard",
label: "FormData",
summary: "Convert FormData-like entries into structured objects.",
packages: ["@form2js/form-data"],
createInitialOutputState: () => createStandardIdle("Ready to parse form data.")
}
] satisfies Omit<VariantDefinition, "Component">[];
export const VARIANT_IDS = variants.map((variant) => variant.id) satisfies VariantId[];
const variantComponents: Record<VariantId, VariantDefinition["Component"]> = {
react: ReactVariant,
form: FormVariant,
jquery: JQueryVariant,
js2form: Js2FormVariant,
core: CoreVariant,
"form-data": FormDataVariant
};
export const variantsById = Object.fromEntries(
variants.map((variant) => [
variant.id,
{
...variant,
Component: variantComponents[variant.id]
}
])
) as Record<VariantId, VariantDefinition>;
================================================
FILE: apps/docs/src/components/playground/variants/core-variant.tsx
================================================
// apps/docs/src/components/playground/variants/core-variant.tsx
import React, { useRef } from "react";
import { entriesToObject } from "@form2js/core";
import type { StandardOutputState, VariantComponentProps } from "../types";
const INITIAL_ENTRIES_JSON = `[
{ "key": "person.name.first", "value": "Moist" },
{ "key": "person.name.last", "value": "von Lipwig" },
{ "key": "person.city", "value": "ankh-morpork" },
{ "key": "person.guild", "value": "thieves" },
{ "key": "person.tags[]", "value": "crime" },
{ "key": "person.tags[]", "value": "banking" }
]`;
function createErrorState(message: string): StandardOutputState {
return { kind: "standard", status: "error", statusMessage: "core parse failed.", errorMessage: message, parsedPayload: null };
}
function createSuccessState(parsedPayload: unknown): StandardOutputState {
return { kind: "standard", status: "success", statusMessage: "@form2js/core -> entriesToObject(entry objects)", errorMessage: null, parsedPayload };
}
function formatVariantError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
export function CoreVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {
const jsonInputRef = useRef<HTMLTextAreaElement>(null);
function handleRun(): void {
const jsonInput = jsonInputRef.current;
if (!jsonInput) return;
let parsed: { key: string; value: unknown }[];
try {
parsed = JSON.parse(jsonInput.value) as { key: string; value: unknown }[];
} catch {
onOutputChange(createErrorState("JSON parse error: please provide valid entry-object JSON before parsing core entries."));
return;
}
try {
onOutputChange(createSuccessState(entriesToObject(parsed)));
} catch (error: unknown) {
onOutputChange(createErrorState(`Core conversion failed: ${formatVariantError(error)}`));
}
}
return (
<section aria-label="Core variant">
<div className="pg-field">
<label className="pg-label" htmlFor="core-entries">entry objects</label>
<textarea
className="pg-textarea"
defaultValue={INITIAL_ENTRIES_JSON}
id="core-entries"
name="core-entries-json"
ref={jsonInputRef}
rows={10}
/>
</div>
<div className="pg-btns">
<button className="pg-btn" onClick={handleRun} type="button">Run @form2js/core</button>
</div>
</section>
);
}
================================================
FILE: apps/docs/src/components/playground/variants/form-data-variant.tsx
================================================
// apps/docs/src/components/playground/variants/form-data-variant.tsx
import React, { useRef } from "react";
import { formDataToObject } from "@form2js/form-data";
import type { StandardOutputState, VariantComponentProps } from "../types";
function createIdleState(): StandardOutputState {
return { kind: "standard", status: "idle", statusMessage: "Ready to parse form data.", errorMessage: null, parsedPayload: null };
}
function createSuccessState(parsedPayload: unknown): StandardOutputState {
return { kind: "standard", status: "success", statusMessage: "@form2js/form-data -> formDataToObject(form)", errorMessage: null, parsedPayload };
}
export function FormDataVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {
const formRef = useRef<HTMLFormElement>(null);
function handleSubmit(event: React.SyntheticEvent<HTMLFormElement>): void {
event.preventDefault();
const form = formRef.current;
if (!form) { onOutputChange(createIdleState()); return; }
onOutputChange(createSuccessState(formDataToObject(new FormData(form))));
}
function handleReset(): void {
formRef.current?.reset();
onOutputChange(createIdleState());
}
return (
<section aria-label="FormData variant">
<form ref={formRef} onSubmit={handleSubmit}>
<div className="pg-field">
<label className="pg-label" htmlFor="fd-first">person.name.first</label>
<input className="pg-input" defaultValue="Tiffany" id="fd-first" name="person.name.first" type="text" />
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="fd-last">person.name.last</label>
<input className="pg-input" defaultValue="Aching" id="fd-last" name="person.name.last" type="text" />
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="fd-city">person.city</label>
<select className="pg-select" defaultValue="quirm" id="fd-city" name="person.city">
<option value="ankh-morpork">Ankh-Morpork</option>
<option value="lancre">Lancre</option>
<option value="quirm">Quirm</option>
<option value="sto-lat">Sto Lat</option>
</select>
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="fd-guild">person.guild</label>
<select className="pg-select" defaultValue="witches" id="fd-guild" name="person.guild">
<option value="watchman">watchman</option>
<option value="witches">witches</option>
<option value="assassins">assassins</option>
<option value="thieves">thieves</option>
</select>
</div>
<fieldset className="pg-fieldset">
<legend>person.tags[]</legend>
<label className="pg-check-label">
<input defaultChecked name="person.tags[]" type="checkbox" value="witch" />witch
</label>
<label className="pg-check-label">
<input name="person.tags[]" type="checkbox" value="shepherding" />shepherding
</label>
</fieldset>
<div className="pg-btns">
<button className="pg-btn" type="submit">Run @form2js/form-data</button>
<button className="pg-btn pg-btn-secondary" onClick={handleReset} type="button">Reset</button>
</div>
</form>
</section>
);
}
================================================
FILE: apps/docs/src/components/playground/variants/form-variant.tsx
================================================
// apps/docs/src/components/playground/variants/form-variant.tsx
import React, { useRef } from "react";
import { form2js, formToObject } from "@form2js/dom";
import type { StandardOutputState, VariantComponentProps } from "../types";
function createIdleState(): StandardOutputState {
return { kind: "standard", status: "idle", statusMessage: "Ready to parse the form.", errorMessage: null, parsedPayload: null };
}
function createSuccessState(statusMessage: string, parsedPayload: unknown): StandardOutputState {
return { kind: "standard", status: "success", statusMessage, errorMessage: null, parsedPayload };
}
export function FormVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {
const formRef = useRef<HTMLFormElement>(null);
function handleSubmit(event: React.SyntheticEvent<HTMLFormElement>): void {
event.preventDefault();
const form = formRef.current;
if (!form) { onOutputChange(createIdleState()); return; }
onOutputChange(createSuccessState("@form2js/dom -> formToObject(form)", formToObject(form)));
}
function handleLegacyRun(): void {
const form = formRef.current;
if (!form) { onOutputChange(createIdleState()); return; }
onOutputChange(createSuccessState("@form2js/dom -> form2js(form)", form2js(form)));
}
function handleReset(): void {
formRef.current?.reset();
onOutputChange(createIdleState());
}
return (
<section aria-label="Form variant">
<form ref={formRef} onSubmit={handleSubmit}>
<div className="pg-field">
<label className="pg-label" htmlFor="fv-first">person.name.first</label>
<input className="pg-input" defaultValue="Esme" id="fv-first" name="person.name.first" type="text" />
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="fv-last">person.name.last</label>
<input className="pg-input" defaultValue="Weatherwax" id="fv-last" name="person.name.last" type="text" />
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="fv-city">person.city</label>
<select className="pg-select" defaultValue="lancre" id="fv-city" name="person.city">
<option value="ankh-morpork">Ankh-Morpork</option>
<option value="lancre">Lancre</option>
<option value="quirm">Quirm</option>
<option value="sto-lat">Sto Lat</option>
</select>
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="fv-guild">person.guild</label>
<select className="pg-select" defaultValue="witches" id="fv-guild" name="person.guild">
<option value="watchman">watchman</option>
<option value="witches">witches</option>
<option value="assassins">assassins</option>
<option value="thieves">thieves</option>
</select>
</div>
<fieldset className="pg-fieldset">
<legend>person.tags[]</legend>
<label className="pg-check-label">
<input defaultChecked name="person.tags[]" type="checkbox" value="witch" />witch
</label>
<label className="pg-check-label">
<input defaultChecked name="person.tags[]" type="checkbox" value="headology" />headology
</label>
<label className="pg-check-label">
<input name="person.tags[]" type="checkbox" value="crime" />crime
</label>
</fieldset>
<label className="pg-check-label" style={{ marginBottom: "1rem" }}>
<input name="person.approved" type="checkbox" value="true" />person.approved
</label>
<div className="pg-btns">
<button className="pg-btn" type="submit">Run @form2js/dom</button>
<button className="pg-btn pg-btn-secondary" onClick={handleReset} type="button">Reset</button>
</div>
</form>
<div style={{ marginTop: "0.75rem" }}>
<button className="pg-btn pg-btn-secondary" onClick={handleLegacyRun} type="button">
Run form2js() (legacy)
</button>
</div>
</section>
);
}
================================================
FILE: apps/docs/src/components/playground/variants/jquery-variant.tsx
================================================
// apps/docs/src/components/playground/variants/jquery-variant.tsx
import React, { useEffect, useRef } from "react";
import $ from "jquery";
import { ensureJqueryBootstrap } from "../bootstrap/jquery-bootstrap";
import type { StandardOutputState, VariantComponentProps } from "../types";
type JQueryToObjectOptions = { mode?: "first" | "all" | "combine" };
type JQueryCollectionWithToObject = JQuery & {
toObject?: (options?: JQueryToObjectOptions) => unknown;
};
function createIdleState(): StandardOutputState {
return { kind: "standard", status: "idle", statusMessage: "Ready to run the plugin.", errorMessage: null, parsedPayload: null };
}
function createSuccessState(statusMessage: string, parsedPayload: unknown): StandardOutputState {
return { kind: "standard", status: "success", statusMessage, errorMessage: null, parsedPayload };
}
function getParsedPayload(
root: HTMLDivElement,
mode: "first" | "all" | "combine"
): unknown {
const collection = $(root).find(".jq-slice") as JQueryCollectionWithToObject;
return collection.toObject?.({ mode }) ?? null;
}
export function JQueryVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {
const sourceRef = useRef<HTMLDivElement>(null);
const modeRef = useRef<HTMLSelectElement>(null);
useEffect(() => { ensureJqueryBootstrap(); }, []);
function handleRun(): void {
ensureJqueryBootstrap();
const root = sourceRef.current;
const mode = (modeRef.current?.value ?? "combine") as "first" | "all" | "combine";
if (!root) return;
const parsedPayload = getParsedPayload(root, mode);
onOutputChange(createSuccessState(`@form2js/jquery -> $(".jq-slice").toObject({ mode: "${mode}" })`, parsedPayload));
}
function handleReset(): void {
const root = sourceRef.current;
if (root) {
root.querySelectorAll<HTMLFormElement>("form").forEach((form) => {
form.reset();
});
}
onOutputChange(createIdleState());
}
return (
<section aria-label="jQuery variant">
<div ref={sourceRef}>
<form className="jq-slice">
<div className="pg-field">
<label className="pg-label">person.first</label>
<input className="pg-input" defaultValue="Gytha" name="person.first" type="text" />
</div>
<div className="pg-field">
<label className="pg-label">person.last</label>
<input className="pg-input" defaultValue="Ogg" name="person.last" type="text" />
</div>
</form>
<form className="jq-slice">
<div className="pg-field">
<label className="pg-label">person.city</label>
<select className="pg-select" defaultValue="lancre" name="person.city">
<option value="ankh-morpork">Ankh-Morpork</option>
<option value="lancre">Lancre</option>
<option value="quirm">Quirm</option>
</select>
</div>
<div className="pg-field">
<label className="pg-label">person.guild</label>
<select className="pg-select" defaultValue="witches" name="person.guild">
<option value="watchman">watchman</option>
<option value="witches">witches</option>
<option value="assassins">assassins</option>
</select>
</div>
</form>
</div>
<div className="pg-field" style={{ marginTop: "0.75rem" }}>
<label className="pg-label" htmlFor="jq-mode">Mode</label>
<select className="pg-select" defaultValue="combine" id="jq-mode" name="jquery-mode" ref={modeRef}>
<option value="first">first</option>
<option value="all">all</option>
<option value="combine">combine</option>
</select>
</div>
<div className="pg-btns">
<button className="pg-btn" onClick={handleRun} type="button">Run @form2js/jquery</button>
<button className="pg-btn pg-btn-secondary" onClick={handleReset} type="button">Reset</button>
</div>
</section>
);
}
================================================
FILE: apps/docs/src/components/playground/variants/js2form-variant.tsx
================================================
// apps/docs/src/components/playground/variants/js2form-variant.tsx
import React, { useRef } from "react";
import { formToObject } from "@form2js/dom";
import { objectToForm } from "@form2js/js2form";
import type { StandardOutputState, VariantComponentProps } from "../types";
const INITIAL_JSON = `{
"person": {
"name": {
"first": "Tiffany",
"last": "Aching"
},
"city": "quirm",
"tags": ["witch"]
}
}`;
function createIdleState(): StandardOutputState {
return { kind: "standard", status: "idle", statusMessage: "Ready to apply object data.", errorMessage: null, parsedPayload: null };
}
function createErrorState(message: string): StandardOutputState {
return { kind: "standard", status: "error", statusMessage: "js2form apply failed.", errorMessage: message, parsedPayload: null };
}
function createSuccessState(parsedPayload: unknown): StandardOutputState {
return { kind: "standard", status: "success", statusMessage: "@form2js/js2form -> objectToForm(...), then formToObject(...)", errorMessage: null, parsedPayload };
}
function formatVariantError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
export function Js2FormVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {
const formRef = useRef<HTMLFormElement>(null);
const jsonInputRef = useRef<HTMLTextAreaElement>(null);
function handleApply(): void {
const form = formRef.current;
const jsonInput = jsonInputRef.current;
if (!form || !jsonInput) { onOutputChange(createIdleState()); return; }
let parsed: unknown;
try {
parsed = JSON.parse(jsonInput.value) as unknown;
} catch {
onOutputChange(createErrorState("JSON parse error: please provide valid JSON before applying js2form."));
return;
}
try {
objectToForm(form, parsed);
onOutputChange(createSuccessState(formToObject(form)));
} catch (error: unknown) {
onOutputChange(createErrorState(`js2form runtime error: ${formatVariantError(error)}`));
}
}
function handleReset(): void {
const form = formRef.current;
const jsonInput = jsonInputRef.current;
if (form) form.reset();
if (jsonInput) jsonInput.value = INITIAL_JSON;
onOutputChange(createIdleState());
}
return (
<section aria-label="js2form variant">
<div className="pg-field">
<label className="pg-label" htmlFor="j2f-json">input.json</label>
<textarea className="pg-textarea" defaultValue={INITIAL_JSON} id="j2f-json" name="js2form-json" ref={jsonInputRef} rows={10} />
</div>
<form ref={formRef}>
<div className="pg-field">
<label className="pg-label" htmlFor="j2f-first">person.name.first</label>
<input className="pg-input" defaultValue="Esme" id="j2f-first" name="person.name.first" type="text" />
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="j2f-last">person.name.last</label>
<input className="pg-input" defaultValue="Weatherwax" id="j2f-last" name="person.name.last" type="text" />
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="j2f-city">person.city</label>
<select className="pg-select" defaultValue="lancre" id="j2f-city" name="person.city">
<option value="ankh-morpork">Ankh-Morpork</option>
<option value="lancre">Lancre</option>
<option value="quirm">Quirm</option>
</select>
</div>
<fieldset className="pg-fieldset">
<legend>person.tags[]</legend>
<label className="pg-check-label">
<input defaultChecked name="person.tags[]" type="checkbox" value="witch" />witch
</label>
<label className="pg-check-label">
<input defaultChecked name="person.tags[]" type="checkbox" value="headology" />headology
</label>
</fieldset>
</form>
<div className="pg-btns">
<button className="pg-btn" onClick={handleApply} type="button">Apply js2form</button>
<button className="pg-btn pg-btn-secondary" onClick={handleReset} type="button">Reset form</button>
</div>
</section>
);
}
================================================
FILE: apps/docs/src/components/playground/variants/react-variant.tsx
================================================
// apps/docs/src/components/playground/variants/react-variant.tsx
import React, { useEffect, useRef, useState } from "react";
import { z } from "zod";
import { useForm2js } from "@form2js/react";
import type { ReactOutputState, VariantComponentProps } from "../types";
const SubmitPayloadSchema = z.object({
person: z.object({
name: z.object({
first: z.string().min(1, "First name is required."),
last: z.string().min(1, "Last name is required.")
}),
email: z.string().regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, "Email must be valid."),
age: z.coerce.number().int().min(18, "Age must be at least 18."),
guild: z.string().min(1, "Guild is required."),
interests: z.array(z.string()).default([])
})
});
type SubmitPayload = z.infer<typeof SubmitPayloadSchema>;
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function formatError(error: unknown): string {
if (error instanceof z.ZodError) {
return error.issues.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`).join("\n");
}
if (error instanceof Error) return error.message;
return String(error);
}
function createOutputState(
isSubmitting: boolean,
isError: boolean,
isSuccess: boolean,
error: unknown,
lastSuccessfulPayload: SubmitPayload | null
): ReactOutputState {
let status: ReactOutputState["status"] = "idle";
let statusMessage = "Ready to submit.";
if (isSubmitting) { status = "running"; statusMessage = "Callback running"; }
else if (isError) { status = "error"; statusMessage = "Submit failed"; }
else if (isSuccess){ status = "success"; statusMessage = "Callback resolved"; }
return {
kind: "react",
status,
statusMessage,
submitFlags: { isSubmitting, isError, isSuccess },
error: isError ? { message: formatError(error) } : null,
parsedPayload: lastSuccessfulPayload,
meta: { submitMode: "onSubmit", validationEnabled: true }
};
}
export function ReactVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {
const [lastSuccessfulPayload, setLastSuccessfulPayload] = useState<SubmitPayload | null>(null);
const onOutputChangeRef = useRef(onOutputChange);
const forceErrorRef = useRef(false);
const formRef = useRef<HTMLFormElement>(null);
const { onSubmit, isSubmitting, isError, error, isSuccess, reset } = useForm2js(
async (data: SubmitPayload) => {
await sleep(850);
if (forceErrorRef.current) {
forceErrorRef.current = false;
throw new Error("Simulated server error.");
}
setLastSuccessfulPayload(data);
},
{ schema: SubmitPayloadSchema }
);
useEffect(() => { onOutputChangeRef.current = onOutputChange; }, [onOutputChange]);
useEffect(() => {
onOutputChangeRef.current(createOutputState(isSubmitting, isError, isSuccess, error, lastSuccessfulPayload));
}, [error, isError, isSubmitting, isSuccess, lastSuccessfulPayload]);
useEffect(() => {
if (!isSubmitting && (isError || isSuccess)) {
forceErrorRef.current = false;
}
}, [isError, isSubmitting, isSuccess]);
function handleReset(): void {
forceErrorRef.current = false;
reset();
setLastSuccessfulPayload(null);
}
function handleForceError(): void {
const form = formRef.current;
if (!form) {
return;
}
if (!form.reportValidity()) {
forceErrorRef.current = false;
return;
}
forceErrorRef.current = true;
form.requestSubmit();
}
return (
<section aria-label="React variant">
<form
ref={formRef}
onSubmit={(event) => { void onSubmit(event); }}
>
<div className="pg-field">
<label className="pg-label" htmlFor="rv-first">person.name.first</label>
<input className="pg-input" defaultValue="Sam" id="rv-first" name="person.name.first" type="text" />
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="rv-last">person.name.last</label>
<input className="pg-input" defaultValue="Vimes" id="rv-last" name="person.name.last" type="text" />
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="rv-email">person.email</label>
<input className="pg-input" defaultValue="sam.vimes@ankh-morpork.gov" id="rv-email" name="person.email" type="email" />
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="rv-age">person.age</label>
<input className="pg-input" defaultValue={45} id="rv-age" min={0} name="person.age" type="number" />
</div>
<div className="pg-field">
<label className="pg-label" htmlFor="rv-guild">person.guild</label>
<select className="pg-select" defaultValue="watchman" id="rv-guild" name="person.guild">
<option value="watchman">watchman</option>
<option value="witches">witches</option>
<option value="assassins">assassins</option>
<option value="thieves">thieves</option>
</select>
</div>
<fieldset className="pg-fieldset">
<legend>person.interests[]</legend>
<label className="pg-check-label">
<input defaultChecked name="person.interests[]" type="checkbox" value="city-watch" />
city-watch
</label>
<label className="pg-check-label">
<input defaultChecked name="person.interests[]" type="checkbox" value="cigars" />
cigars
</label>
<label className="pg-check-label">
<input name="person.interests[]" type="checkbox" value="headology" />
headology
</label>
</fieldset>
<div className="pg-btns">
<button className="pg-btn" disabled={isSubmitting} type="submit">
{isSubmitting ? "Submitting…" : "Submit"}
</button>
<button className="pg-btn pg-btn-secondary" onClick={handleReset} type="button">
Reset state
</button>
<button
className="pg-btn pg-btn-danger"
disabled={isSubmitting}
onClick={handleForceError}
type="button"
>
{isSubmitting ? "Submitting…" : "Force Error"}
</button>
</div>
</form>
</section>
);
}
================================================
FILE: apps/docs/src/env.d.ts
================================================
/// <reference types="astro/client" />
================================================
FILE: apps/docs/src/layouts/ApiDocsLayout.astro
================================================
---
import { ApiPackageNav } from "../components/api/ApiPackageNav";
import { ApiToc } from "../components/api/ApiToc";
import type { ApiPackageEntry } from "../lib/api-packages";
import type { ApiHeading } from "../lib/api-docs-source";
import DocsShell from "./DocsShell.astro";
interface Props {
title: string;
introHtml: string;
bodyHtml: string;
headings: ApiHeading[];
packages: ApiPackageEntry[];
activePackageSlug?: ApiPackageEntry["slug"];
}
const { title, introHtml, bodyHtml, headings, packages, activePackageSlug } =
Astro.props;
const basePath = import.meta.env.BASE_URL;
---
<DocsShell title={`${title} | form2js`}>
<section class="api-docs">
<aside class="api-docs__nav">
<ApiPackageNav
activeSlug={activePackageSlug}
basePath={basePath}
packages={packages}
/>
</aside>
<article class="api-docs__content">
<header class="api-docs__header">
<p class="hero__eyebrow">API Docs</p>
<h1>{title}</h1>
{introHtml ? <div class="api-docs__intro" set:html={introHtml} /> : null}
</header>
<slot name="summary" />
<div class="api-docs__body" set:html={bodyHtml} />
</article>
{headings.length > 0 ? (
<aside class="api-docs__sidebar">
<ApiToc client:load headings={headings} />
</aside>
) : null}
</section>
</DocsShell>
================================================
FILE: apps/docs/src/layouts/DesignShell.astro
================================================
---
// apps/docs/src/layouts/DesignShell.astro
import { apiDocsPath } from "../lib/site-routes";
import "../styles/landing.css";
import "../styles/playground.css";
interface Props {
title?: string;
theme: "dark-brutalism" | "outrun-sunset" | "terminal-noir" | "linear-dark" | "editorial";
}
const { title = "form2js", theme } = Astro.props;
const basePath = import.meta.env.BASE_URL;
const THEME_FONTS: Record<string, string> = {
"dark-brutalism":
"https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800;900&family=DM+Mono:wght@300;400;500&display=swap",
"outrun-sunset":
"https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=Syne:wght@700;900&family=DM+Mono:wght@300;400;500&display=swap",
"terminal-noir":
"https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap",
"linear-dark":
"https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;900&family=DM+Mono:wght@300;400;500&display=swap",
editorial:
"https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=DM+Mono:wght@300;400;500&display=swap",
};
const fontsHref = THEME_FONTS[theme];
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title} — form2js</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
{fontsHref && <link href={fontsHref} rel="stylesheet" />}
</head>
<body>
<header class="site-header">
<a class="site-brand" href={basePath}>
<span class="site-brand__badge">f2j</span>
form2js
</a>
<nav class="site-nav" aria-label="Primary">
<a href={apiDocsPath(basePath)}>API Docs</a>
<a href="https://github.com/maxatwork/form2js">Github</a>
</nav>
</header>
<main>
<slot />
</main>
</body>
</html>
================================================
FILE: apps/docs/src/layouts/DocsShell.astro
================================================
---
import "../styles/global.css";
import { homepagePath, migrationGuidePath } from "../lib/site-routes";
interface Props {
title?: string;
}
const { title = "form2js" } = Astro.props;
const basePath = import.meta.env.BASE_URL;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
</head>
<body>
<div class="site-shell">
<header class="site-header">
<a class="site-brand" href={homepagePath(basePath)}>
<span class="site-brand__logo">f2j</span>
<span class="site-brand__name">form2js</span>
</a>
<nav class="site-nav" aria-label="Primary">
<a href={homepagePath(basePath)}>Overview</a>
<a href={migrationGuidePath(basePath)}>Migration</a>
<a href="https://github.com/maxatwork/form2js">Github</a>
</nav>
</header>
<main>
<slot />
</main>
</div>
</body>
</html>
================================================
FILE: apps/docs/src/lib/api-docs-source.ts
================================================
import { readFile } from "node:fs/promises";
import rehypeStringify from "rehype-stringify";
import remarkGfm from "remark-gfm";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
import {
apiIndexMarkdownPath,
getApiPackageByMarkdownBasename
} from "./api-packages";
import {
apiDocsPath,
apiPackageDocsPath,
homepagePath,
migrationGuidePath
} from "./site-routes";
export interface ApiHeading {
depth: 2 | 3;
slug: string;
text: string;
}
export interface ApiDocsSource {
title: string;
introMarkdown: string;
introHtml: string;
bodyMarkdown: string;
bodyHtml: string;
headings: ApiHeading[];
}
interface ParseOptions {
basePath: string;
}
type MarkdownNode = {
type: string;
url?: string;
depth?: number;
value?: string;
children?: MarkdownNode[];
position?: {
start?: { offset?: number };
end?: { offset?: number };
};
data?: Record<string, unknown>;
};
function slugify(text: string): string {
const slug = text
.toLowerCase()
.replace(/[`"]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.replace(/-{2,}/g, "-");
return slug || "section";
}
function rewriteMarkdownLink(url: string, basePath: string): string {
if (
url.startsWith("#") ||
url.startsWith("http://") ||
url.startsWith("https://") ||
url.startsWith("mailto:")
) {
return url;
}
const [pathname, hash] = url.split("#");
const suffix = hash ? `#${hash}` : "";
const normalizedPathname = pathname
.replace(/^(?:\.\.\/)?docs\//, "")
.replace(/^\.\//, "");
if (
normalizedPathname === "README.md" ||
pathname === "../README.md"
) {
return `${homepagePath(basePath)}${suffix}`;
}
if (normalizedPathname === "migrate.md") {
return `${migrationGuidePath(basePath)}${suffix}`;
}
if (normalizedPathname === "api.md" || normalizedPathname === "api-index.md") {
return `${apiDocsPath(basePath)}${suffix}`;
}
const apiPackage = getApiPackageByMarkdownBasename(normalizedPathname);
if (apiPackage) {
return `${apiPackageDocsPath(basePath, apiPackage.slug)}${suffix}`;
}
return url;
}
function visit(
node: MarkdownNode,
callback: (currentNode: MarkdownNode) => void
): void {
callback(node);
for (const child of node.children ?? []) {
visit(child, callback);
}
}
function collectText(node: MarkdownNode): string {
if (node.type === "text" || node.type === "inlineCode") {
return node.value ?? "";
}
return (node.children ?? []).map((child) => collectText(child)).join("");
}
function renderMarkdownNodes(nodes: MarkdownNode[]): string {
if (nodes.length === 0) {
return "";
}
const processor = unified().use(remarkRehype).use(rehypeStringify);
const htmlTree = processor.runSync({
type: "root",
children: nodes,
} as never);
return processor.stringify(htmlTree).trim();
}
function extractMarkdownSlice(
markdown: string,
startOffset: number,
endOffset?: number
): string {
return markdown.slice(startOffset, endOffset).trim();
}
export function parseApiDocsMarkdown(
markdown: string,
options: ParseOptions
): ApiDocsSource {
const parser = unified().use(remarkParse).use(remarkGfm);
const tree = parser.parse(markdown) as MarkdownNode;
const rootChildren = tree.children ?? [];
const titleNode = rootChildren[0];
if (titleNode?.type !== "heading" || titleNode.depth !== 1) {
throw new Error("API docs markdown must start with an H1 heading.");
}
const title = collectText(titleNode);
const contentNodes = rootChildren.slice(1);
const firstBodyNodeIndex = contentNodes.findIndex(
(node) => node.type === "heading" && node.depth === 2
);
const introNodes =
firstBodyNodeIndex === -1
? contentNodes
: contentNodes.slice(0, firstBodyNodeIndex);
const bodyNodes =
firstBodyNodeIndex === -1 ? [] : contentNodes.slice(firstBodyNodeIndex);
const introStartOffset = titleNode.position?.end?.offset ?? 0;
const bodyStartOffset = bodyNodes[0]?.position?.start?.offset;
const introMarkdown = extractMarkdownSlice(
markdown,
introStartOffset,
bodyStartOffset
);
const bodyMarkdown =
bodyStartOffset === undefined
? ""
: extractMarkdownSlice(markdown, bodyStartOffset);
const headings: ApiHeading[] = [];
const slugCounts = new Map<string, number>();
for (const node of [...introNodes, ...bodyNodes]) {
visit(node, (currentNode) => {
if (currentNode.type === "link" && currentNode.url) {
currentNode.url = rewriteMarkdownLink(
currentNode.url,
options.basePath
);
}
if (
currentNode.type !== "heading" ||
(currentNode.depth !== 2 && currentNode.depth !== 3)
) {
return;
}
if (!bodyNodes.includes(node)) {
return;
}
const text = collectText(currentNode);
const baseSlug = slugify(text);
const nextCount = (slugCounts.get(baseSlug) ?? 0) + 1;
slugCounts.set(baseSlug, nextCount);
const slug = nextCount === 1 ? baseSlug : `${baseSlug}-${nextCount}`;
currentNode.data = {
...(currentNode.data ?? {}),
hProperties: {
id: slug,
},
};
headings.push({
depth: currentNode.depth,
slug,
text,
});
});
}
return {
title,
introMarkdown,
introHtml: renderMarkdownNodes(introNodes),
bodyMarkdown,
bodyHtml: renderMarkdownNodes(bodyNodes),
headings,
};
}
export async function loadApiDocsSource(
options: { basePath?: string; markdownPath?: string } = {}
): Promise<ApiDocsSource> {
const markdownPath = options.markdownPath ?? apiIndexMarkdownPath;
const markdown = await readFile(markdownPath, "utf8");
return parseApiDocsMarkdown(markdown, {
basePath: options.basePath ?? "/",
});
}
================================================
FILE: apps/docs/src/lib/api-packages.ts
================================================
import path from "node:path";
export type ApiPackageSlug =
| "core"
| "dom"
| "form-data"
| "react"
| "js2form"
| "jquery";
export interface ApiPackageEntry {
slug: ApiPackageSlug;
packageName: string;
summary: string;
markdownPath: string;
}
function resolveDocsPath(filename: string): string {
return path.resolve(process.cwd(), "..", "..", "docs", filename);
}
export const apiIndexMarkdownPath = resolveDocsPath("api-index.md");
export const apiPackages: ApiPackageEntry[] = [
{
slug: "core",
packageName: "@form2js/core",
summary: "Turn path-like key/value pairs into nested objects and flatten them back into entries.",
markdownPath: resolveDocsPath("api-core.md")
},
{
slug: "dom",
packageName: "@form2js/dom",
summary: "Parse browser form controls into an object while preserving native submission behavior.",
markdownPath: resolveDocsPath("api-dom.md")
},
{
slug: "form-data",
packageName: "@form2js/form-data",
summary: "Parse FormData and tuple entries with the same path rules used by the core parser.",
markdownPath: resolveDocsPath("api-form-data.md")
},
{
slug: "react",
packageName: "@form2js/react",
summary: "Handle React form submission with parsed payloads, optional schema validation, and submit state.",
markdownPath: resolveDocsPath("api-react.md")
},
{
slug: "js2form",
packageName: "@form2js/js2form",
summary: "Push nested object data back into matching DOM form controls.",
markdownPath: resolveDocsPath("api-js2form.md")
},
{
slug: "jquery",
packageName: "@form2js/jquery",
summary: "Install a jQuery plugin wrapper around the DOM parser for legacy form handling flows.",
markdownPath: resolveDocsPath("api-jquery.md")
}
];
export function getApiPackageBySlug(slug: string): ApiPackageEntry | undefined {
return apiPackages.find((entry) => entry.slug === slug);
}
export function getApiPackageByMarkdownBasename(
basename: string
): ApiPackageEntry | undefined {
return apiPackages.find(
(entry) => path.basename(entry.markdownPath) === basename
);
}
================================================
FILE: apps/docs/src/lib/site-routes.ts
================================================
function normalizeBase(basePath: string): string {
if (basePath === "/") {
return "/";
}
const withLeadingSlash = basePath.startsWith("/") ? basePath : `/${basePath}`;
return withLeadingSlash.endsWith("/") ? withLeadingSlash : `${withLeadingSlash}/`;
}
export function homepagePath(basePath: string): string {
return normalizeBase(basePath);
}
export function migrationGuidePath(basePath: string): string {
return `${normalizeBase(basePath)}migrate/`;
}
export function apiDocsPath(basePath: string): string {
return `${normalizeBase(basePath)}api/`;
}
export function apiPackageDocsPath(basePath: string, slug: string): string {
return `${apiDocsPath(basePath)}${encodeURIComponent(slug)}/`;
}
export function homepageVariantPath(basePath: string, variant: string): string {
return `${normalizeBase(basePath)}?variant=${encodeURIComponent(variant)}`;
}
================================================
FILE: apps/docs/src/pages/api/[package].astro
================================================
---
import ApiDocsLayout from "../../layouts/ApiDocsLayout.astro";
import { apiPackages, type ApiPackageEntry } from "../../lib/api-packages";
import { loadApiDocsSource } from "../../lib/api-docs-source";
interface Props {
apiPackage: ApiPackageEntry;
}
export function getStaticPaths() {
return apiPackages.map((apiPackage) => ({
params: { package: apiPackage.slug },
props: { apiPackage }
}));
}
const { apiPackage } = Astro.props;
const apiDocsSource = await loadApiDocsSource({
basePath: import.meta.env.BASE_URL,
markdownPath: apiPackage.markdownPath
});
---
<ApiDocsLayout
activePackageSlug={apiPackage.slug}
bodyHtml={apiDocsSource.bodyHtml}
headings={apiDocsSource.headings}
introHtml={apiDocsSource.introHtml}
packages={apiPackages}
title={apiDocsSource.title}
/>
================================================
FILE: apps/docs/src/pages/api/index.astro
================================================
---
import { ApiPackageSummaryList } from "../../components/api/ApiPackageSummaryList";
import ApiDocsLayout from "../../layouts/ApiDocsLayout.astro";
import { apiPackages } from "../../lib/api-packages";
import { loadApiDocsSource } from "../../lib/api-docs-source";
const apiDocsSource = await loadApiDocsSource({
basePath: import.meta.env.BASE_URL
});
---
<ApiDocsLayout
bodyHtml={apiDocsSource.bodyHtml}
headings={apiDocsSource.headings}
introHtml={apiDocsSource.introHtml}
packages={apiPackages}
title={apiDocsSource.title}
>
<ApiPackageSummaryList
slot="summary"
basePath={import.meta.env.BASE_URL}
packages={apiPackages}
/>
</ApiDocsLayout>
================================================
FILE: apps/docs/src/pages/index.astro
================================================
---
// apps/docs/src/pages/1.astro
import "../styles/themes/dark-brutalism.css";
import DesignShell from "../layouts/DesignShell.astro";
import Hero from "../components/landing/Hero.astro";
import InstallSection from "../components/landing/InstallSection.astro";
import ApiDocsCta from "../components/landing/ApiDocsCta.astro";
import { PlaygroundShell } from "../components/playground/PlaygroundShell";
---
<DesignShell title="form2js - form serialization library" theme="dark-brutalism">
<Hero />
<section class="content-wrap section" id="playground">
<p class="section-eyebrow">Playground</p>
<h2 class="section-title">Try it live</h2>
<PlaygroundShell client:load />
</section>
<InstallSection />
<ApiDocsCta />
</DesignShell>
================================================
FILE: apps/docs/src/pages/migrate.astro
================================================
---
import path from "node:path";
import { ApiToc } from "../components/api/ApiToc";
import { loadApiDocsSource } from "../lib/api-docs-source";
import DocsShell from "../layouts/DocsShell.astro";
const migrationSource = await loadApiDocsSource({
basePath: import.meta.env.BASE_URL,
markdownPath: path.resolve(process.cwd(), "..", "..", "docs", "migrate.md")
});
---
<DocsShell title={`${migrationSource.title} | form2js`}>
<section class="api-docs api-docs--content-sidebar">
<article class="api-docs__content">
<header class="api-docs__header">
<p class="hero__eyebrow">Migration Guide</p>
<h1>{migrationSource.title}</h1>
{migrationSource.introHtml ? (
<div class="api-docs__intro" set:html={migrationSource.introHtml} />
) : null}
</header>
<div class="api-docs__body" set:html={migrationSource.bodyHtml} />
</article>
{migrationSource.headings.length > 0 ? (
<aside class="api-docs__sidebar">
<ApiToc client:load headings={migrationSource.headings} />
</aside>
) : null}
</section>
</DocsShell>
================================================
FILE: apps/docs/src/styles/global.css
================================================
:root {
color-scheme: dark;
--bg: #0c0e14;
--panel: #12151e;
--panel-2: #1a1d2b;
--text: #e4e6ef;
--muted: #8b8fa3;
--accent: #00e5c8;
--border: rgba(255, 255, 255, 0.08);
--mono: "JetBrains Mono", "Fira Code", monospace;
--ui: "Inter", "Segoe UI", sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--ui);
}
a {
color: inherit;
}
.site-shell {
min-height: 100vh;
}
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
background: var(--panel);
}
.site-brand {
display: inline-flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
}
.site-brand__logo {
padding: 0.2rem 0.5rem;
border: 1px solid rgba(0, 229, 200, 0.2);
border-radius: 0.4rem;
color: var(--accent);
background: rgba(0, 229, 200, 0.08);
font-family: var(--mono);
}
.site-brand__name {
font-weight: 600;
}
.site-nav {
display: inline-flex;
gap: 1rem;
}
.site-nav a {
color: var(--muted);
text-decoration: none;
}
.site-nav a:hover,
.site-nav a:focus-visible {
color: var(--text);
}
.hero {
width: min(72rem, calc(100% - 3rem));
margin: 0 auto;
padding: 4rem 0;
}
.hero__eyebrow {
margin: 0 0 0.75rem;
color: var(--accent);
font-family: var(--mono);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.hero h1 {
margin: 0;
font-size: clamp(2.5rem, 8vw, 5rem);
}
.hero p:last-child {
max-width: 42rem;
color: var(--muted);
line-height: 1.6;
}
.api-docs {
width: min(96rem, calc(100% - 3rem));
margin: 0 auto;
padding: 3rem 0 4rem;
display: grid;
grid-template-columns: 16rem minmax(0, 1fr) 18rem;
gap: 2rem;
}
.api-docs--content-sidebar {
grid-template-columns: minmax(0, 1fr) 18rem;
}
.api-docs__nav,
.api-docs__sidebar {
position: sticky;
top: 1rem;
align-self: start;
}
.api-docs__content {
min-width: 0;
}
.api-docs__header h1 {
margin: 0;
font-size: clamp(2rem, 6vw, 3.75rem);
}
.api-docs__intro,
.api-docs__body {
color: var(--muted);
line-height: 1.75;
}
.api-docs__intro {
margin-top: 1.5rem;
}
.api-docs__body {
margin-top: 2rem;
}
.api-package-nav,
.api-toc,
.api-package-summary {
border: 1px solid var(--border);
border-radius: 1rem;
background: var(--panel);
}
.api-docs__body h2,
.api-docs__body h3 {
color: var(--text);
scroll-margin-top: 6rem;
}
.api-docs__body pre {
overflow-x: auto;
padding: 1rem 1.25rem;
border: 1px solid var(--border);
border-radius: 1rem;
background: var(--panel);
}
.api-docs__body code {
font-family: var(--mono);
}
.api-docs__body table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
}
.api-docs__body th,
.api-docs__body td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
.api-package-nav {
padding: 1rem;
}
.api-package-nav__eyebrow,
.api-toc__eyebrow {
margin: 0 0 0.75rem;
color: var(--accent);
font-family: var(--mono);
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.8rem;
}
.api-package-nav__list,
.api-toc__list,
.api-toc__sublist {
margin: 0;
padding: 0;
list-style: none;
}
.api-package-nav__list {
display: grid;
gap: 0.5rem;
}
.api-package-nav__link {
display: block;
padding: 0.6rem 0.75rem;
border-radius: 0.75rem;
color: var(--muted);
text-decoration: none;
}
.api-package-nav__link[aria-current="page"] {
background: color-mix(in srgb, var(--accent) 18%, transparent);
color: var(--text);
}
.api-toc {
padding: 1rem;
}
.api-toc__list,
.api-toc__sublist {
margin: 0;
padding: 0;
list-style: none;
}
.api-toc__list {
display: grid;
gap: 0.75rem;
}
.api-toc__sublist {
margin-top: 0.5rem;
padding-left: 0.75rem;
display: grid;
gap: 0.4rem;
}
.api-toc__link,
.api-toc__sublink {
color: var(--muted);
text-decoration: none;
}
.api-toc__link[aria-current="true"],
.api-toc__sublink[aria-current="true"] {
color: var(--text);
}
.api-package-summary-list {
margin-top: 2rem;
}
.api-package-summary-list h2 {
margin-bottom: 1rem;
}
.api-package-summary-list__items {
display: grid;
gap: 1rem;
}
.api-package-summary {
padding: 1.25rem;
}
.api-package-summary h3 {
margin: 0 0 0.5rem;
}
.api-package-summary p {
margin: 0;
color: var(--muted);
line-height: 1.6;
}
@media (max-width: 900px) {
.api-docs {
grid-template-columns: 1fr;
}
.api-docs__nav {
position: static;
order: -1;
}
.api-docs__sidebar {
position: static;
}
}
================================================
FILE: apps/docs/src/styles/landing.css
================================================
/* apps/docs/src/styles/landing.css */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
min-height: 100%;
}
body {
background: var(--c-bg);
color: var(--c-text);
font-family: var(--f-ui);
}
a {
color: inherit;
}
/* ── Nav ── */
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2.5rem;
border-bottom: 1px solid var(--c-border);
}
.site-brand {
display: inline-flex;
align-items: center;
gap: 0.6rem;
text-decoration: none;
color: var(--c-text);
font-family: var(--f-display);
font-weight: 700;
font-size: 1rem;
}
.site-brand__badge {
padding: 0.15rem 0.5rem;
background: var(--c-accent-glow);
border: 1px solid color-mix(in srgb, var(--c-accent) 35%, transparent);
color: var(--c-accent);
font-family: var(--f-mono);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.03em;
}
.site-nav {
display: flex;
align-items: center;
gap: 1.5rem;
}
.site-nav a {
color: var(--c-muted);
text-decoration: none;
font-size: 0.85rem;
transition: color 0.15s;
}
.site-nav a:hover {
color: var(--c-text);
}
/* ── Hero ── */
.hero {
width: min(76rem, calc(100% - 3rem));
margin: 0 auto;
padding: 5rem 0 3.5rem;
}
.hero__eyebrow {
font-family: var(--f-mono);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--c-accent);
margin-bottom: 1.25rem;
}
.hero__title {
font-family: var(--f-display);
font-size: clamp(3rem, 9vw, 6rem);
font-weight: 900;
line-height: 0.92;
letter-spacing: -0.02em;
color: var(--c-text);
margin-bottom: 1.25rem;
}
.hero__desc {
font-size: 0.95rem;
color: var(--c-muted);
line-height: 1.7;
max-width: 48ch;
margin-bottom: 2rem;
font-family: var(--f-ui);
}
.hero__actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
align-items: center;
}
.hero__install {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.9rem;
background: var(--c-panel);
border: 1px solid var(--c-border);
font-family: var(--f-mono);
font-size: 0.8rem;
color: var(--c-text);
}
.hero__install-prefix {
color: var(--c-muted);
}
.btn-primary {
display: inline-block;
padding: 0.55rem 1.5rem;
background: var(--c-accent);
color: var(--c-bg);
font-family: var(--f-ui);
font-size: 0.85rem;
font-weight: 700;
text-decoration: none;
border: none;
cursor: pointer;
transition: opacity 0.15s;
}
.btn-primary:hover {
opacity: 0.85;
}
.btn-ghost {
display: inline-block;
padding: 0.55rem 1.5rem;
background: transparent;
color: var(--c-muted);
font-family: var(--f-ui);
font-size: 0.85rem;
font-weight: 600;
text-decoration: none;
border: 1px solid var(--c-border);
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.btn-ghost:hover {
color: var(--c-text);
border-color: var(--c-text);
}
/* ── Page sections ── */
.content-wrap {
width: min(76rem, calc(100% - 3rem));
margin: 0 auto;
}
.section {
padding: 3rem 0;
border-top: 1px solid var(--c-border);
}
.section-eyebrow {
font-family: var(--f-mono);
font-size: 0.7rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--c-accent);
margin-bottom: 0.75rem;
}
.section-title {
font-family: var(--f-display);
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 900;
color: var(--c-text);
margin-bottom: 0.75rem;
}
.section-desc {
font-size: 0.9rem;
color: var(--c-muted);
line-height: 1.65;
max-width: 54ch;
margin-bottom: 1.5rem;
}
/* ── Install section ── */
.install-grid {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 3rem;
align-items: start;
}
@media (max-width: 680px) {
.install-grid {
grid-template-columns: 1fr;
}
.hero {
padding: 3rem 0 2.5rem;
}
.site-header {
padding: 1rem 1.25rem;
}
}
.code-block {
background: var(--c-panel-2);
border: 1px solid var(--c-border);
padding: 1.25rem 1.5rem;
font-family: var(--f-mono);
font-size: 0.78rem;
line-height: 1.65;
overflow-x: auto;
color: var(--c-muted);
}
.code-block .kw {
color: var(--c-accent);
}
.code-block .fn {
color: var(--c-accent-2);
}
.code-block .str {
color: var(--c-text);
opacity: 0.7;
}
.code-block .cmt {
color: var(--c-muted);
font-style: italic;
}
.install-cmd {
font-family: var(--f-mono);
font-size: 0.82rem;
color: var(--c-accent);
margin-bottom: 0.5rem;
display: block;
}
.install-cmd::before {
content: "$ ";
color: var(--c-muted);
}
================================================
FILE: apps/docs/src/styles/playground.css
================================================
/* apps/docs/src/styles/playground.css */
/* ── Shell ── */
.pg-shell {
border: 1px solid var(--c-border);
overflow: hidden;
}
/* ── Tab bar ── */
.pg-tabs {
display: flex;
border-bottom: 1px solid var(--c-border);
background: var(--c-panel);
overflow-x: auto;
scrollbar-width: none;
}
.pg-tabs::-webkit-scrollbar { display: none; }
.pg-tab-btn {
padding: 0.6rem 1rem;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
color: var(--c-muted);
font-family: var(--f-mono);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.06em;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
transition: color 0.15s;
}
.pg-tab-btn[aria-pressed="true"] {
color: var(--c-accent);
border-bottom-color: var(--c-accent);
}
.pg-tab-btn:hover:not([aria-pressed="true"]) { color: var(--c-text); }
/* ── Two-column split ── */
.pg-body {
display: grid;
grid-template-columns: 1fr 1fr;
min-height: 340px;
}
@media (max-width: 760px) {
.pg-body { grid-template-columns: 1fr; }
.pg-form-col { border-right: none; }
}
.pg-form-col {
padding: 1.5rem;
border-right: 1px solid var(--c-border);
background: var(--c-panel);
overflow-y: auto;
}
.pg-output-col {
padding: 1.5rem;
background: var(--c-panel-2);
overflow-y: auto;
}
/* ── Form fields ── */
.pg-field { margin-bottom: 1rem; }
.pg-label {
display: block;
font-family: var(--f-mono);
font-size: 0.7rem;
color: var(--c-muted);
margin-bottom: 0.3rem;
letter-spacing: 0.05em;
}
.pg-input,
.pg-select,
.pg-textarea {
width: 100%;
padding: 0.4rem 0.6rem;
background: var(--c-bg);
border: 1px solid var(--c-border);
color: var(--c-text);
font-family: var(--f-mono);
font-size: 0.8rem;
outline: none;
transition: border-color 0.15s;
-webkit-appearance: none;
}
.pg-input:focus,
.pg-select:focus,
.pg-textarea:focus { border-color: var(--c-accent); }
.pg-textarea {
min-height: 220px;
resize: vertical;
line-height: 1.55;
}
.pg-fieldset {
border: 1px solid var(--c-border);
padding: 0.6rem 0.9rem 0.6rem;
margin-bottom: 1rem;
}
.pg-fieldset legend {
font-family: var(--f-mono);
font-size: 0.7rem;
color: var(--c-muted);
padding: 0 0.3rem;
letter-spacing: 0.05em;
}
.pg-check-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-family: var(--f-mono);
font-size: 0.78rem;
color: var(--c-text);
margin: 0.35rem 0;
cursor: pointer;
}
/* ── Buttons ── */
.pg-btns {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 1.25rem;
}
.pg-btn {
padding: 0.4rem 1rem;
background: var(--c-accent);
color: var(--c-bg);
font-family: var(--f-mono);
font-size: 0.73rem;
font-weight: 700;
letter-spacing: 0.06em;
border: none;
cursor: pointer;
transition: opacity 0.15s;
}
.pg-btn:hover { opacity: 0.82; }
.pg-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.pg-btn-secondary {
background: transparent;
color: var(--c-muted);
border: 1px solid var(--c-border);
}
.pg-btn-secondary:hover { color: var(--c-text); border-color: var(--c-text); opacity: 1; }
.pg-btn-danger {
background: transparent;
color: #ff5555;
border: 1px solid rgba(255, 85, 85, 0.3);
}
.pg-btn-danger:hover { border-color: #ff5555; opacity: 1; }
.pg-btn-danger:disabled { opacity: 0.35; cursor: not-allowed; }
/* ── Output panel (ResultPanel) ── */
.result-eyebrow {
font-family: var(--f-mono);
font-size: 0.65rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--c-muted);
margin-bottom: 1rem;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-family: var(--f-mono);
font-size: 0.73rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 1rem;
}
.status-badge::before {
content: '';
display: block;
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
}
.status-idle { color: var(--c-muted); }
.status-running { color: var(--c-accent); animation: badge-pulse 1s steps(1) infinite; }
.status-success { color: #22c55e; }
.status-error { color: #ff5555; }
@keyframes badge-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.result-flags {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.2rem 0.75rem;
font-family: var(--f-mono);
font-size: 0.73rem;
margin-bottom: 1rem;
align-items: baseline;
}
.flag-key { color: var(--c-muted); }
.flag-true { color: var(--c-accent); }
.flag-false { color: var(--c-muted); opacity: 0.5; }
.result-errors {
margin-bottom: 1rem;
padding: 0.75rem 0.9rem;
background: rgba(255, 85, 85, 0.06);
border: 1px solid rgba(255, 85, 85, 0.2);
}
.result-error-item {
font-family: var(--f-mono);
font-size: 0.73rem;
color: #ff7777;
margin: 0.2rem 0;
}
.result-json {
font-family: var(--f-mono);
font-size: 0.76rem;
color: var(--c-text);
background: var(--c-panel);
border: 1px solid var(--c-border);
padding: 0.75rem 1rem;
overflow-x: auto;
white-space: pre;
line-height: 1.55;
}
.result-msg {
font-family: var(--f-mono);
font-size: 0.78rem;
color: var(--c-muted);
line-height: 1.5;
}
.result-empty {
font-family: var(--f-mono);
font-size: 0.73rem;
color: var(--c-muted);
opacity: 0.5;
font-style: italic;
}
.pg-variant-summary {
font-family: var(--f-mono);
font-size: 0.72rem;
color: var(--c-muted);
margin-bottom: 1rem;
line-height: 1.5;
}
================================================
FILE: apps/docs/src/styles/themes/dark-brutalism.css
================================================
/* apps/docs/src/styles/themes/dark-brutalism.css */
:root {
color-scheme: dark;
--c-bg: #0c0c0c;
--c-panel: #111111;
--c-panel-2: #0a0a0a;
--c-text: #ffffff;
--c-muted: #555555;
--c-accent: #c6ff00;
--c-accent-2: #c6ff00;
--c-accent-glow: rgba(198, 255, 0, 0.18);
--c-border: #1e1e1e;
--f-ui: 'Syne', sans-serif;
--f-mono: 'DM Mono', 'Fira Code', monospace;
--f-display: 'Syne', sans-serif;
}
================================================
FILE: apps/docs/src/styles/themes/editorial.css
================================================
/* apps/docs/src/styles/themes/editorial.css */
:root {
color-scheme: light;
--c-bg: #faf9f6;
--c-panel: #f0ece0;
--c-panel-2: #1a1a1a;
--c-text: #1a1a1a;
--c-muted: #888888;
--c-accent: #ff6600;
--c-accent-2: #1a1a1a;
--c-accent-glow: transparent;
--c-border: #cccccc;
--f-ui: 'Libre Baskerville', Georgia, serif;
--f-mono: 'DM Mono', 'Fira Code', monospace;
--f-display: 'Bebas Neue', 'Arial Black', sans-serif;
}
================================================
FILE: apps/docs/src/styles/themes/linear-dark.css
================================================
/* apps/docs/src/styles/themes/linear-dark.css */
:root {
color-scheme: dark;
--c-bg: #060609;
--c-panel: rgba(255, 255, 255, 0.025);
--c-panel-2: rgba(255, 255, 255, 0.04);
--c-text: #ffffff;
--c-muted: rgba(255, 255, 255, 0.4);
--c-accent: #5e56ff;
--c-accent-2: #9747ff;
--c-accent-glow: rgba(94, 86, 255, 0.22);
--c-border: rgba(255, 255, 255, 0.07);
--f-ui: 'Geist', 'Segoe UI', sans-serif;
--f-mono: 'DM Mono', 'Fira Code', monospace;
--f-display: 'Geist', 'Segoe UI', sans-serif;
}
================================================
FILE: apps/docs/src/styles/themes/outrun-sunset.css
================================================
/* apps/docs/src/styles/themes/outrun-sunset.css */
:root {
color-scheme: dark;
--c-bg: #050a1a;
--c-panel: #080f26;
--c-panel-2: #060c1e;
--c-text: #ffffff;
--c-muted: rgba(255, 255, 255, 0.35);
--c-accent: #ff8c00;
--c-accent-2: #ff2d55;
--c-accent-glow: rgba(255, 100, 0, 0.22);
--c-border: rgba(255, 100, 0, 0.2);
--f-ui: 'Syne', sans-serif;
--f-mono: 'DM Mono', 'Fira Code', monospace;
--f-display: 'Dela Gothic One', 'Syne', sans-serif;
}
================================================
FILE: apps/docs/src/styles/themes/terminal-noir.css
================================================
/* apps/docs/src/styles/themes/terminal-noir.css */
:root {
color-scheme: dark;
--c-bg: #030403;
--c-panel: #040c04;
--c-panel-2: #020802;
--c-text: #22ff44;
--c-muted: #2a6b2a;
--c-accent: #22ff44;
--c-accent-2: #22ff44;
--c-accent-glow: rgba(34, 255, 68, 0.18);
--c-border: #0f2e0f;
--f-ui: 'Space Mono', monospace;
--f-mono: 'Space Mono', monospace;
--f-display: 'Space Mono', monospace;
}
================================================
FILE: apps/docs/test/api-docs-page.test.tsx
================================================
// @vitest-environment jsdom
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { ApiPackageNav } from "../src/components/api/ApiPackageNav";
import { ApiPackageSummaryList } from "../src/components/api/ApiPackageSummaryList";
import { ApiToc } from "../src/components/api/ApiToc";
describe("ApiPackageNav", () => {
it("renders package links and marks the active package", () => {
const markup = renderToStaticMarkup(
<ApiPackageNav
activeSlug="react"
basePath="/form2js/"
packages={[
{
slug: "core",
packageName: "@form2js/core"
},
{
slug: "react",
packageName: "@form2js/react"
}
]}
/>
);
expect(markup).toContain("Packages");
expect(markup).toContain('href="/form2js/api/react/"');
expect(markup).toContain('aria-current="page"');
});
});
describe("ApiPackageSummaryList", () => {
it("renders package summaries with package routes", () => {
const markup = renderToStaticMarkup(
<ApiPackageSummaryList
basePath="/form2js/"
packages={[
{
slug: "dom",
packageName: "@form2js/dom",
summary: "DOM parsing"
}
]}
/>
);
expect(markup).toContain("@form2js/dom");
expect(markup).toContain("DOM parsing");
expect(markup).toContain('href="/form2js/api/dom/"');
});
});
describe("ApiToc", () => {
it("renders nested section links and marks the active section", () => {
const markup = renderToStaticMarkup(
<ApiToc
headings={[
{ depth: 2, slug: "package-index", text: "Package Index" },
{ depth: 3, slug: "common-tasks", text: "Common tasks" },
{ depth: 2, slug: "api", text: "API" }
]}
initialActiveSlug="common-tasks"
/>
);
expect(markup).toContain("On this page");
expect(markup).toContain('href="#package-index"');
expect(markup).toContain('href="#common-tasks"');
expect(markup).toContain('aria-current="true"');
});
});
================================================
FILE: apps/docs/test/api-docs-source.test.ts
================================================
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { parseApiDocsMarkdown } from "../src/lib/api-docs-source";
const testDir = path.dirname(fileURLToPath(import.meta.url));
const apiIndexMarkdown = readFileSync(path.resolve(testDir, "../../../docs/api-index.md"), "utf8");
const apiCoreMarkdown = readFileSync(path.resolve(testDir, "../../../docs/api-core.md"), "utf8");
const apiDomMarkdown = readFileSync(path.resolve(testDir, "../../../docs/api-dom.md"), "utf8");
const apiFormDataMarkdown = readFileSync(path.resolve(testDir, "../../../docs/api-form-data.md"), "utf8");
const apiJqueryMarkdown = readFileSync(path.resolve(testDir, "../../../docs/api-jquery.md"), "utf8");
const apiJs2formMarkdown = readFileSync(path.resolve(testDir, "../../../docs/api-js2form.md"), "utf8");
const apiReactMarkdown = readFileSync(path.resolve(testDir, "../../../docs/api-react.md"), "utf8");
const migrationMarkdown = readFileSync(path.resolve(testDir, "../../../docs/migrate.md"), "utf8");
const readmeMarkdown = readFileSync(path.resolve(testDir, "../../../README.md"), "utf8");
describe("parseApiDocsMarkdown", () => {
it("extracts the H1 title, intro copy, headings, and rewrites package markdown links", () => {
const source = parseApiDocsMarkdown(
`# React API
Intro with [index](api-index.md).
## Installation
Text with [core](api-core.md).
### npm
More text.
`,
{ basePath: "/form2js/" }
);
expect(source.title).toBe("React API");
expect(source.introMarkdown).toContain("Intro with");
expect(source.introHtml).toContain('href="/form2js/api/"');
expect(source.bodyHtml).toContain('href="/form2js/api/core/"');
expect(source.bodyHtml).toContain('id="installation"');
expect(source.bodyHtml).toContain('id="npm"');
expect(source.headings).toEqual([
{ depth: 2, slug: "installation", text: "Installation" },
{ depth: 3, slug: "npm", text: "npm" }
]);
});
it("returns an empty intro when the first section starts immediately after the title", () => {
const source = parseApiDocsMarkdown(
`# API Title
## Section
Text.
`,
{ basePath: "/" }
);
expect(source.introMarkdown).toBe("");
expect(source.introHtml).toBe("");
});
it("deduplicates repeated heading slugs", () => {
const source = parseApiDocsMarkdown(
`# API Title
Intro
## Repeated Name
### Repeated Name
## Repeated Name
`,
{ basePath: "/" }
);
expect(source.headings).toEqual([
{ depth: 2, slug: "repeated-name", text: "Repeated Name" },
{ depth: 3, slug: "repeated-name-2", text: "Repeated Name" },
{ depth: 2, slug: "repeated-name-3", text: "Repeated Name" }
]);
});
it("throws when the markdown does not start with an H1", () => {
expect(() =>
parseApiDocsMarkdown(
`## Missing title
Text.
`,
{ basePath: "/" }
)
).toThrow("API docs markdown must start with an H1 heading.");
});
it("documents the split api markdown sources and updates the readme source link", () => {
expect(apiIndexMarkdown).toContain("# form2js API Reference");
expect(apiCoreMarkdown).toContain("## Installation");
expect(apiCoreMarkdown).toContain("## General Example");
expect(apiCoreMarkdown).toContain("## Types and Properties");
expect(apiCoreMarkdown).toContain("npm install @form2js/core");
expect(apiCoreMarkdown).toContain("### Schema validation");
expect(apiCoreMarkdown).toContain("entriesToObject(rawEntries, { schema: PersonSchema })");
expect(apiDomMarkdown).toContain("https://unpkg.com/@form2js/dom/dist/standalone.global.js");
expect(apiDomMarkdown).toContain("### `useIdIfEmptyName`");
expect(apiDomMarkdown).toContain("### `nodeCallback`");
expect(apiFormDataMarkdown).toContain("### Schema validation");
expect(apiFormDataMarkdown).toContain("formDataToObject(formData, { schema: PersonSchema })");
expect(apiJqueryMarkdown).toContain("https://unpkg.com/@form2js/jquery/dist/standalone.global.js");
expect(apiJqueryMarkdown).toContain("### `mode: \"all\"`");
expect(apiJs2formMarkdown).toContain("### `shouldClean: false`");
expect(apiJs2formMarkdown).toContain("### `useIdIfEmptyName`");
expect(apiReactMarkdown).toContain("npm install @form2js/react react");
expect(readmeMarkdown).toContain("[API Reference Source](docs/api-index.md)");
});
it("parses the migration guide markdown and rewrites package links", () => {
const source = parseApiDocsMarkdown(migrationMarkdown, {
basePath: "/form2js/"
});
expect(source.title).toBe("Migrate from Legacy form2js");
expect(source.introMarkdown).toContain("single `form2js` script");
expect(source.bodyHtml).toContain('href="/form2js/api/dom/"');
expect(source.bodyHtml).toContain('href="/form2js/api/jquery/"');
expect(source.bodyHtml).toContain('href="/form2js/api/form-data/"');
expect(source.headings).toEqual(
expect.arrayContaining([
expect.objectContaining({ slug: "quick-chooser", text: "Quick Chooser" }),
expect.objectContaining({ slug: "legacy-api-mapping", text: "Legacy API Mapping" }),
expect.objectContaining({ slug: "where-to-go-now", text: "Where To Go Now" })
])
);
});
});
================================================
FILE: apps/docs/test/data-variants.test.tsx
================================================
// apps/docs/test/data-variants.test.tsx
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { PlaygroundShell } from "../src/components/playground/PlaygroundShell";
import type { OutputState, VariantComponentProps } from "../src/components/playground/types";
import { CoreVariant } from "../src/components/playground/variants/core-variant";
import { FormDataVariant } from "../src/components/playground/variants/form-data-variant";
declare global {
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
}
interface RenderResult {
container: HTMLDivElement;
root: ReturnType<typeof createRoot>;
getLastOutputState: () => OutputState | null;
}
function renderVariant(Component: (props: VariantComponentProps) => React.ReactNode): RenderResult {
let lastOutputState: OutputState | null = null;
const container = document.createElement("div");
document.body.append(container);
const root = createRoot(container);
act(() => {
root.render(
<Component
isActive
onOutputChange={(outputState) => { lastOutputState = outputState; }}
reportFatalError={(errorInfo) => { throw new Error(`Unexpected fatal error: ${errorInfo.message}`); }}
/>
);
});
return { container, root, getLastOutputState: () => lastOutputState };
}
describe("parser playground variants", () => {
beforeEach(() => { globalThis.IS_REACT_ACT_ENVIRONMENT = true; });
afterEach(() => {
document.body.innerHTML = "";
vi.restoreAllMocks();
});
it("runs the core variant from seeded entry objects (Moist von Lipwig)", () => {
const view = renderVariant(CoreVariant);
const jsonInput = view.container.querySelector<HTMLTextAreaElement>('textarea[name="core-entries-json"]');
const runButton = [...view.container.querySelectorAll("button")].find((button) =>
button.textContent?.includes("@form2js/core")
);
expect(jsonInput?.value).toContain('"key": "person.name.first"');
expect(jsonInput?.value).toContain("Moist");
act(() => {
runButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(view.getLastOutputState()).toEqual({
kind: "standard",
status: "success",
statusMessage: "@form2js/core -> entriesToObject(entry objects)",
errorMessage: null,
parsedPayload: {
person: {
city: "ankh-morpork",
guild: "thieves",
name: { first: "Moist", last: "von Lipwig" },
tags: ["crime", "banking"]
}
}
});
act(() => { view.root.unmount(); });
});
it("runs the form-data variant from a submitted form (Tiffany Aching)", () => {
const view = renderVariant(FormDataVariant);
const firstNameInput = view.container.querySelector<HTMLInputElement>('input[name="person.name.first"]');
const form = view.container.querySelector("form");
expect(firstNameInput?.value).toBe("Tiffany");
expect(form).not.toBeNull();
act(() => {
form?.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
});
expect(view.getLastOutputState()).toEqual({
kind: "standard",
status: "success",
statusMessage: "@form2js/form-data -> formDataToObject(form)",
errorMessage: null,
parsedPayload: {
person: {
city: "quirm",
guild: "witches",
name: { first: "Tiffany", last: "Aching" },
tags: ["witch"]
}
}
});
act(() => { view.root.unmount(); });
});
it("reports runtime merge failures from the core parser instead of labeling them as JSON parse errors", () => {
const view = renderVariant(CoreVariant);
const jsonInput = view.container.querySelector<HTMLTextAreaElement>('textarea[name="core-entries-json"]');
const runButton = [...view.container.querySelectorAll("button")].find((button) =>
button.textContent?.includes("@form2js/core")
);
act(() => {
if (!jsonInput) throw new Error("Missing core JSON input.");
jsonInput.value = JSON.stringify([
{ key: "person", value: "watch" },
{ key: "person.name.first", value: "Sam" }
]);
jsonInput.dispatchEvent(new Event("input", { bubbles: true }));
runButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(view.getLastOutputState()).toEqual({
kind: "standard",
status: "error",
statusMessage: "core parse failed.",
errorMessage: "Core conversion failed: Expected object-like container while setting nested path",
parsedPayload: null
});
act(() => { view.root.unmount(); });
});
it("preserves core output when switching away and back through the shell", () => {
const container = document.createElement("div");
document.body.append(container);
const root = createRoot(container);
act(() => { root.render(<PlaygroundShell />); });
const coreButton = container.querySelector('button[data-variant-id="core"]');
act(() => { coreButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); });
const runButton = [...container.querySelectorAll("button")].find((button) =>
button.textContent?.includes("@form2js/core")
);
act(() => { runButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); });
expect(container.textContent).toContain("@form2js/core -> entriesToObject(entry objects)");
expect(container.textContent).toContain("von Lipwig");
const formButton = container.querySelector('button[data-variant-id="form"]');
act(() => { formButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); });
act(() => { coreButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); });
expect(container.textContent).toContain("@form2js/core -> entriesToObject(entry objects)");
expect(container.textContent).toContain("von Lipwig");
act(() => { root.unmount(); });
});
});
================================================
FILE: apps/docs/test/docs-pipeline.test.ts
================================================
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const testDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(testDir, "../../..");
const turboConfig = JSON.parse(readFileSync(path.join(repoRoot, "turbo.json"), "utf8")) as {
tasks?: Record<string, { dependsOn?: string[] }>;
};
describe("docs CI task graph", () => {
it("builds workspace package dependencies before docs lint and typecheck", () => {
expect(turboConfig.tasks?.lint?.dependsOn).toEqual(expect.arrayContaining(["^build"]));
expect(turboConfig.tasks?.typecheck?.dependsOn).toEqual(expect.arrayContaining(["^build"]));
});
});
================================================
FILE: apps/docs/test/docs-root-scripts.test.ts
================================================
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const testDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(testDir, "../../..");
const rootPackageJson = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8")) as {
scripts?: Record<string, string>;
};
describe("root docs scripts", () => {
it("builds workspace packages before local docs commands that depend on package dist output", () => {
expect(rootPackageJson.scripts?.docs).toContain("npm run build:packages");
expect(rootPackageJson.scripts?.["docs:build"]).toContain("npm run build:packages");
expect(rootPackageJson.scripts?.["test:docs"]).toContain("npm run build:packages");
});
});
================================================
FILE: apps/docs/test/homepage-shell.test.ts
================================================
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const testDir = path.dirname(fileURLToPath(import.meta.url));
const homepageSource = readFileSync(path.resolve(testDir, "../src/pages/index.astro"), "utf8");
const installSectionSource = readFileSync(
path.resolve(testDir, "../src/components/landing/InstallSection.astro"),
"utf8"
);
const docsShellSource = readFileSync(path.resolve(testDir, "../src/layouts/DocsShell.astro"), "utf8");
const readmeSource = readFileSync(path.resolve(testDir, "../../../README.md"), "utf8");
describe("docs homepage shell", () => {
it("wires the landing page sections together", () => {
expect(homepageSource).toContain("<Hero />");
expect(homepageSource).toContain('id="playground"');
expect(homepageSource).toContain("<InstallSection />");
expect(homepageSource).toContain("<ApiDocsCta />");
expect(readFileSync(path.resolve(testDir, "../src/components/landing/Hero.astro"), "utf8")).toContain(
"npm install @form2js/react react"
);
});
it("includes npm and standalone install guidance for supported variants", () => {
expect(installSectionSource).toContain("npm install @form2js/react react");
expect(installSectionSource).toContain("https://unpkg.com/@form2js/dom/dist/standalone.global.js");
expect(installSectionSource).toContain("https://unpkg.com/@form2js/jquery/dist/standalone.global.js");
});
it("surfaces the migration guide in the shared docs chrome and the README", () => {
expect(docsShellSource).toContain("migrationGuidePath");
expect(docsShellSource).toContain(">Migration<");
expect(readmeSource).toContain("Migrating from legacy form2js?");
expect(readmeSource).toContain("https://maxatwork.github.io/form2js/migrate/");
expect(readmeSource).toContain("## Migration from Legacy");
});
});
================================================
FILE: apps/docs/test/playground-shell.test.tsx
================================================
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OutputState, VariantComponentProps, VariantDefinition, VariantId } from "../src/components/playground/types";
function createReactOutput(statusMessage: string): OutputState {
return {
kind: "react",
status: "success",
statusMessage,
submitFlags: {
isSubmitting: false,
isError: false,
isSuccess: true
},
error: null,
parsedPayload: { ok: true },
meta: {
submitMode: "onSubmit",
validationEnabled: true
}
};
}
function createStandardOutput(statusMessage: string): OutputState {
return {
kind: "standard",
status: "success",
statusMessage,
errorMessage: null,
parsedPayload: { ok: true }
};
}
function makeVariant(
id: VariantId,
kind: VariantDefinition["kind"],
label: string,
statusMessage: string,
options?: {
throwWhenActive?: boolean;
}
): VariantDefinition {
function Component(props: VariantComponentProps) {
if (options?.throwWhenActive && props.isActive) {
throw new Error(`${label} render exploded`);
}
return (
<div data-testid={`${id}-variant`}>
<span>{props.isActive ? `${label} active` : `${label} hidden`}</span>
<button
onClick={() => {
props.onOutputChange(
kind === "react" ? createReactOutput(statusMessage) : createStandardOutput(statusMessage)
);
}}
type="button"
>
Emit {label}
</button>
<button
onClick={() => {
props.reportFatalError({
message: `${label} crashed`,
source: "event"
});
}}
type="button"
>
Fail {label}
</button>
</div>
);
}
return {
id,
kind,
label,
summary: `${label} summary`,
packages: [`@form2js/${id}`],
createInitialOutputState: () =>
kind === "react"
? {
kind: "react",
status: "idle",
statusMessage: `Ready for ${label}`,
submitFlags: {
isSubmitting: false,
isError: false,
isSuccess: false
},
error: null,
parsedPayload: null
}
: {
kind: "standard",
status: "idle",
statusMessage: `Ready for ${label}`,
errorMessage: null,
parsedPayload: null
},
Component
};
}
function resetMockVariants(): void {
mockVariantsById.react = makeVariant("react", "react", "React", "React complete");
}
const { mockVariantsById } = vi.hoisted(() => ({
mockVariantsById: {
react: makeVariant("react", "react", "React", "React complete"),
form: makeVariant("form", "standard", "Form", "Form complete"),
jquery: makeVariant("jquery", "standard", "jQuery", "jQuery complete"),
js2form: makeVariant("js2form", "standard", "js2form", "js2form complete"),
core: makeVariant("core", "standard", "Core", "Core complete"),
"form-data": makeVariant("form-data", "standard", "FormData", "FormData complete")
} satisfies Record<VariantId, VariantDefinition>
}));
vi.mock("../src/components/playground/variant-registry", () => ({
VARIANT_IDS: ["react", "form", "jquery", "js2form", "core", "form-data"],
variantsById: mockVariantsById
}));
import { PlaygroundShell } from "../src/components/playground/PlaygroundShell";
declare global {
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
}
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
resetMockVariants();
vi.spyOn(console, "error").mockImplementation(() => undefined);
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
window.history.replaceState({}, "", "/");
});
afterEach(() => {
act(() => {
root.unmount();
});
container.remove();
resetMockVariants();
vi.restoreAllMocks();
});
function renderShell(): void {
act(() => {
root.render(<PlaygroundShell />);
});
}
describe("PlaygroundShell", () => {
it("renders the active variant and chooses the output panel by kind", () => {
window.history.replaceState({}, "", "/?variant=react");
renderShell();
expect(container.textContent).toContain("React summary");
expect(container.textContent).toContain("React active");
expect(container.textContent).toContain("Submit state");
expect(container.textContent).toContain("Ready for React");
});
it("updates the query string when switching variants", () => {
window.history.replaceState({}, "", "/docs/start?variant=react#top");
renderShell();
const formButton = container.querySelector('button[data-variant-id="form"]');
expect(formButton).not.toBeNull();
act(() => {
formButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(window.location.pathname).toBe("/docs/start");
expect(window.location.search).toBe("?variant=form");
expect(window.location.hash).toBe("");
expect(container.textContent).toContain("Form active");
expect(container.textContent).toContain("Parsed result");
});
it("does not render placeholder payload output while a variant is idle", () => {
window.history.replaceState({}, "", "/?variant=react");
renderShell();
expect(container.textContent).toContain("Ready for React");
expect(container.textContent).not.toContain("null");
const formButton = container.querySelector('button[data-variant-id="form"]');
act(() => {
formButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("Ready for Form");
expect(container.textContent).not.toContain("null");
});
it("preserves emitted output state when switching away and back", () => {
renderShell();
const emitReact = [...container.querySelectorAll("button")].find((button) => button.textContent === "Emit React");
expect(emitReact).not.toBeUndefined();
act(() => {
emitReact?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("React complete");
expect(container.textContent).toContain("submitMode");
expect(container.textContent).toContain("onSubmit");
expect(container.textContent).toContain("validationEnabled");
expect(container.textContent).toContain("true");
const formButton = container.querySelector('button[data-variant-id="form"]');
act(() => {
formButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("React hidden");
const reactButton = container.querySelector('button[data-variant-id="react"]');
act(() => {
reactButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("React complete");
});
it("renders a failed-state fallback and keeps it when revisiting the variant", () => {
renderShell();
const emitReact = [...container.querySelectorAll("button")].find((button) => button.textContent === "Emit React");
const failReact = [...container.querySelectorAll("button")].find((button) => button.textContent === "Fail React");
expect(emitReact).not.toBeUndefined();
expect(failReact).not.toBeUndefined();
act(() => {
emitReact?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("React complete");
act(() => {
failReact?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("React crashed");
expect(container.textContent).not.toContain("React complete");
expect(container.textContent).not.toContain("submitMode");
expect(container.textContent).not.toContain("React active");
const formButton = container.querySelector('button[data-variant-id="form"]');
act(() => {
formButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const reactButton = container.querySelector('button[data-variant-id="react"]');
act(() => {
reactButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("React crashed");
expect(container.textContent).not.toContain("React complete");
expect(container.textContent).not.toContain("submitMode");
});
it("isolates a thrown active-variant render failure and keeps the switcher usable", async () => {
mockVariantsById.react = makeVariant("react", "react", "React", "React complete", {
throwWhenActive: true
});
renderShell();
await act(async () => {
await Promise.resolve();
});
expect(container.textContent).toContain("React failed");
expect(container.textContent).toContain("React render exploded");
expect(container.textContent).not.toContain("Form failed");
const formButton = container.querySelector('button[data-variant-id="form"]');
expect(formButton).not.toBeNull();
act(() => {
formButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.textContent).toContain("Form active");
expect(container.textContent).toContain("Parsed result");
const reactButton = container.querySelector('button[data-variant-id="react"]');
act(() => {
reactButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await act(async () => {
await Promise.resolve();
});
expect(container.textContent).toContain("React failed");
expect(container.textContent).toContain("React render exploded");
});
});
================================================
FILE: apps/docs/test/playground-styles.test.ts
================================================
import { readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const stylesheetPath = path.resolve(import.meta.dirname, "../src/styles/playground.css");
const stylesheet = readFileSync(stylesheetPath, "utf8");
describe("playground responsive styles", () => {
it("drops the desktop form-column divider in the one-column mobile layout", () => {
expect(stylesheet).toContain("@media (max-width: 760px)");
expect(stylesheet).toContain(".pg-form-col { border-right: none; }");
});
});
================================================
FILE: apps/docs/test/react-variant.test.tsx
================================================
// apps/docs/test/react-variant.test.tsx
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OutputState, VariantComponentProps } from "../src/components/playground/types";
import { ReactVariant } from "../src/components/playground/variants/react-variant";
declare global {
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
}
interface RenderResult {
container: HTMLDivElement;
root: ReturnType<typeof createRoot>;
getLastOutputState: () => OutputState | null;
}
function renderVariant(Component: (props: VariantComponentProps) => React.ReactNode): RenderResult {
let lastOutputState: OutputState | null = null;
const container = document.createElement("div");
document.body.append(container);
const root = createRoot(container);
act(() => {
root.render(
<Component
isActive
onOutputChange={(outputState) => { lastOutputState = outputState; }}
reportFatalError={(errorInfo) => { throw new Error(`Unexpected fatal error: ${errorInfo.message}`); }}
/>
);
});
return { container, root, getLastOutputState: () => lastOutputState };
}
async function submitForm(container: HTMLDivElement): Promise<void> {
const form = container.querySelector("form");
if (!form) throw new Error("Missing React variant form.");
await act(async () => {
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
await Promise.resolve();
});
}
describe("ReactVariant", () => {
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
vi.useFakeTimers();
});
afterEach(() => {
document.body.innerHTML = "";
vi.useRealTimers();
vi.restoreAllMocks();
});
it("submits the seeded valid form and emits react output states", async () => {
const view = renderVariant(ReactVariant);
const firstNameInput = view.container.querySelector<HTMLInputElement>('input[name="person.name.first"]');
const emailInput = view.container.querySelector<HTMLInputElement>('input[name="person.email"]');
expect(firstNameInput?.value).toBe("Sam");
expect(emailInput?.value).toBe("sam.vimes@ankh-morpork.gov");
await submitForm(view.container);
expect(view.getLastOutputState()).toMatchObject({
kind: "react",
status: "running",
submitFlags: { isSubmitting: true, isError: false, isSuccess: false },
parsedPayload: null,
meta: { submitMode: "onSubmit", validationEnabled: true }
});
await act(async () => {
vi.advanceTimersByTime(900);
await Promise.resolve();
});
expect(view.getLastOutputState()).toEqual({
kind: "react",
status: "success",
statusMessage: "Callback resolved",
submitFlags: { isSubmitting: false, isError: false, isSuccess: true },
error: null,
parsedPayload: {
person: {
age: 45,
email: "sam.vimes@ankh-morpork.gov",
guild: "watchman",
interests: ["city-watch", "cigars"],
name: { first: "Sam", last: "Vimes" }
}
},
meta: { submitMode: "onSubmit", validationEnabled: true }
});
act(() => { view.root.unmount(); });
});
it("emits validation failure output when seeded fields become invalid", async () => {
const view = renderVariant(ReactVariant);
const emailInput = view.container.querySelector<HTMLInputElement>('input[name="person.email"]');
act(() => {
if (!emailInput) throw new Error("Missing email input.");
emailInput.value = "bad-email";
emailInput.dispatchEvent(new Event("input", { bubbles: true }));
});
await submitForm(view.container);
expect(view.getLastOutputState()).toEqual({
kind: "react",
status: "error",
statusMessage: "Submit failed",
submitFlags: { isSubmitting: false, isError: true, isSuccess: false },
error: { message: "person.email: Email must be valid." },
parsedPayload: null,
meta: { submitMode: "onSubmit", validationEnabled: true }
});
act(() => { view.root.unmount(); });
});
it("emits callback failure output when the Force Error button is clicked", async () => {
const view = renderVariant(ReactVariant);
const forceErrorButton = [...view.container.querySelectorAll("button")].find((button) =>
button.textContent?.includes("Force Error")
);
expect(forceErrorButton).toBeDefined();
act(() => {
forceErrorButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(view.getLastOutputState()).toMatchObject({
kind: "react",
status: "running"
});
await act(async () => {
vi.advanceTimersByTime(900);
await Promise.resolve();
});
expect(view.getLastOutputState()).toEqual({
kind: "react",
status: "error",
statusMessage: "Submit failed",
submitFlags: { isSubmitting: false, isError: true, isSuccess: false },
error: { message: "Simulated server error." },
parsedPayload: null,
meta: { submitMode: "onSubmit", validationEnabled: true }
});
act(() => { view.root.unmount(); });
});
it("clears a queued force-error after a validation failure so the next valid submit succeeds", async () => {
const view = renderVariant(ReactVariant);
const forceErrorButton = [...view.container.querySelectorAll("button")].find((button) =>
button.textContent?.includes("Force Error")
);
const emailInput = view.container.querySelector<HTMLInputElement>('input[name="person.email"]');
expect(forceErrorButton).toBeDefined();
act(() => {
if (!emailInput) throw new Error("Missing email input.");
emailInput.value = "bad-email";
emailInput.dispatchEvent(new Event("input", { bubbles: true }));
forceErrorButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
act(() => {
if (!emailInput) throw new Error("Missing email input.");
emailInput.value = "sam.vimes@ankh-morpork.gov";
emailInput.dispatchEvent(new Event("input", { bubbles: true }));
});
await submitForm(view.container);
expect(view.getLastOutputState()).toMatchObject({
kind: "react",
status: "running"
});
await act(async () => {
vi.advanceTimersByTime(900);
await Promise.resolve();
});
expect(view.getLastOutputState()).toMatchObject({
kind: "react",
status: "success",
statusMessage: "Callback resolved",
submitFlags: { isSubmitting: false, isError: false, isSuccess: true },
error: null,
meta: { submitMode: "onSubmit", validationEnabled: true }
});
act(() => { view.root.unmount(); });
});
it("resets back to idle and clears the last successful payload", async () => {
const view = renderVariant(ReactVariant);
const resetButton = [...view.container.querySelectorAll("button")].find((button) =>
button.textContent?.includes("Reset state")
);
await submitForm(view.container);
await act(async () => {
vi.advanceTimersByTime(900);
await Promise.resolve();
});
expect(view.getLastOutputState()).toMatchObject({ kind: "react", status: "success" });
act(() => {
resetButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(view.getLastOutputState()).toEqual({
kind: "react",
status: "idle",
statusMessage: "Ready to submit.",
submitFlags: { isSubmitting: false, isError: false, isSuccess: false },
error: null,
parsedPayload: null,
meta: { submitMode: "onSubmit", validationEnabled: true }
});
act(() => { view.root.unmount(); });
});
});
================================================
FILE: apps/docs/test/site-routes.test.ts
================================================
import { describe, expect, it } from "vitest";
import {
apiDocsPath,
apiPackageDocsPath,
homepagePath,
homepageVariantPath,
migrationGuidePath
} from "../src/lib/site-routes";
describe("site routes", () => {
it("builds homepage, migration, and api paths under a base path", () => {
expect(homepagePath("/form2js/")).toBe("/form2js/");
expect(migrationGuidePath("/form2js/")).toBe("/form2js/migrate/");
expect(migrationGuidePath("/")).toBe("/migrate/");
expect(apiDocsPath("/form2js/")).toBe("/form2js/api/");
expect(apiPackageDocsPath("/form2js/", "react")).toBe("/form2js/api/react/");
expect(apiPackageDocsPath("/", "form-data")).toBe("/api/form-data/");
});
it("adds variant query params to the homepage only", () => {
expect(homepageVariantPath("/form2js/", "react")).toBe("/form2js/?variant=react");
expect(homepageVariantPath("/", "form-data")).toBe("/?variant=form-data");
});
});
================================================
FILE: apps/docs/test/standard-variants.test.tsx
================================================
// apps/docs/test/standard-variants.test.tsx
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OutputState, VariantComponentProps } from "../src/components/playground/types";
import { ensureJqueryBootstrap } from "../src/components/playground/bootstrap/jquery-bootstrap";
import { FormVariant } from "../src/components/playground/variants/form-variant";
import { JQueryVariant } from "../src/components/playground/variants/jquery-variant";
import { Js2FormVariant } from "../src/components/playground/variants/js2form-variant";
declare global {
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
}
interface RenderResult {
container: HTMLDivElement;
root: ReturnType<typeof createRoot>;
getLastOutputState: () => OutputState | null;
render: () => void;
}
function renderVariant(Component: (props: VariantComponentProps) => React.ReactNode): RenderResult {
let lastOutputState: OutputState | null = null;
const container = document.createElement("div");
document.body.append(container);
const root = createRoot(container);
function render(): void {
act(() => {
root.render(
<Component
isActive
onOutputChange={(outputState) => { lastOutputState = outputState; }}
reportFatalError={(errorInfo) => { throw new Error(`Unexpected fatal error: ${errorInfo.message}`); }}
/>
);
});
}
render();
return { container, root, render, getLastOutputState: () => lastOutputState };
}
describe("standard playground variants", () => {
beforeEach(() => { globalThis.IS_REACT_ACT_ENVIRONMENT = true; });
afterEach(() => {
document.body.innerHTML = "";
vi.restoreAllMocks();
});
it("runs the form variant with seeded controls via form submit", () => {
const view = renderVariant(FormVariant);
const firstNameInput = view.container.querySelector<HTMLInputElement>('input[name="person.name.first"]');
const lastNameInput = view.container.querySelector<HTMLInputElement>('input[name="person.name.last"]');
const form = view.container.querySelector("form");
expect(firstNameInput?.value).toBe("Esme");
expect(lastNameInput?.value).toBe("Weatherwax");
expect(form).not.toBeNull();
act(() => {
form?.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
});
expect(view.getLastOutputState()).toEqual({
kind: "standard",
status: "success",
statusMessage: "@form2js/dom -> formToObject(form)",
errorMessage: null,
parsedPayload: {
person: {
city: "lancre",
guild: "witches",
name: { first: "Esme", last: "Weatherwax" },
tags: ["witch", "headology"]
}
}
});
act(() => { view.root.unmount(); });
});
it("runs the jquery variant in combine mode and installs the plugin idempotently", () => {
const beforeInstall = ensureJqueryBootstrap();
const secondInstall = ensureJqueryBootstrap();
expect(secondInstall).toBe(beforeInstall);
const view = renderVariant(JQueryVariant);
const modeSelect = view.container.querySelector<HTMLSelectElement>('select[name="jquery-mode"]');
const runButton = [...view.container.querySelectorAll("button")].find((button) =>
button.textContent?.includes("@form2js/jquery")
);
expect(modeSelect?.value).toBe("combine");
act(() => {
runButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(view.getLastOutputState()).toEqual({
kind: "standard",
status: "success",
statusMessage: '@form2js/jquery -> $(".jq-slice").toObject({ mode: "combine" })',
errorMessage: null,
parsedPayload: {
person: {
city: "lancre",
first: "Gytha",
guild: "witches",
last: "Ogg"
}
}
});
act(() => { view.root.unmount(); });
});
it("applies js2form data, reports invalid JSON, and resets back to idle", () => {
const view = renderVariant(Js2FormVariant);
const jsonInput = view.container.querySelector<HTMLTextAreaElement>('textarea[name="js2form-json"]');
const firstNameInput = view.container.querySelector<HTMLInputElement>('input[name="person.name.first"]');
const applyButton = [...view.container.querySelectorAll("button")].find((button) =>
button.textContent?.includes("Apply js2form")
);
const resetButton = [...view.container.querySelectorAll("button")].find((button) =>
button.textContent?.includes("Reset form")
);
expect(jsonInput?.value).toContain('"first": "Tiffany"');
expect(firstNameInput?.value).toBe("Esme");
act(() => {
if (!jsonInput) throw new Error("Missing js2form JSON input.");
jsonInput.value = "{";
jsonInput.dispatchEvent(new Event("input", { bubbles: true }));
applyButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(view.getLastOutputState()).toEqual({
kind: "standard",
status: "error",
statusMessage: "js2form apply failed.",
errorMessage: "JSON parse error: please provide valid JSON before applying js2form.",
parsedPayload: null
});
act(() => {
if (!jsonInput) throw new Error("Missing js2form JSON input.");
jsonInput.value = `{
"person": {
"name": { "first": "Tiffany", "last": "Aching" },
"city": "quirm",
"tags": ["witch"]
}
}`;
jsonInput.dispatchEvent(new Event("input", { bubbles: true }));
applyButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(view.getLastOutputState()).toEqual({
kind: "standard",
status: "success",
statusMessage: "@form2js/js2form -> objectToForm(...), then formToObject(...)",
errorMessage: null,
parsedPayload: {
person: { city: "quirm", name: { first: "Tiffany", last: "Aching" }, tags: ["witch"] }
}
});
expect(firstNameInput?.value).toBe("Tiffany");
act(() => {
resetButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(firstNameInput?.value).toBe("Esme");
expect(view.getLastOutputState()).toEqual({
kind: "standard",
status: "idle",
statusMessage: "Ready to apply object data.",
errorMessage: null,
parsedPayload: null
});
act(() => { view.root.unmount(); });
});
});
================================================
FILE: apps/docs/test/variant-registry.test.ts
================================================
import { describe, expect, it } from "vitest";
import { VARIANT_IDS, variantsById } from "../src/components/playground/variant-registry";
describe("variant registry", () => {
it("registers all expected variants", () => {
expect(VARIANT_IDS).toEqual([
"react",
"form",
"jquery",
"js2form",
"core",
"form-data"
]);
});
it("provides seeded idle output state for every variant", () => {
for (const id of VARIANT_IDS) {
const variant = variantsById[id];
expect(variant.id).toBe(id);
expect(variant.label.length).toBeGreaterThan(0);
expect(variant.summary.length).toBeGreaterThan(0);
expect(variant.packages.length).toBeGreaterThan(0);
expect(variant.createInitialOutputState().status).toBe("idle");
expect(variant.createInitialOutputState().kind).toBe(variant.kind);
}
});
});
================================================
FILE: apps/docs/test-e2e/api-docs.spec.ts
================================================
import { expect, test } from "@playwright/test";
test("api docs index links to package pages and package toc anchors work on mobile", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/api/");
await expect(page.getByRole("heading", { name: "form2js API Reference" })).toBeVisible();
await expect(
page.getByLabel("API packages").getByRole("link", { name: "@form2js/react" })
).toBeVisible();
await page.getByLabel("API packages").getByRole("link", { name: "@form2js/react" }).click();
await expect(page).toHaveURL(/\/api\/react\/$/);
await expect(page.getByRole("heading", { name: "@form2js/react" })).toBeVisible();
await expect(page.getByText("On this page")).toBeVisible();
await page.getByLabel("On this page").getByRole("link", { name: "Installation" }).click();
await expect(page).toHaveURL(/#installation$/);
});
================================================
FILE: apps/docs/test-e2e/homepage.spec.ts
================================================
import { expect, test } from "@playwright/test";
test("homepage switcher supports keyboard navigation and preserves parser output", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("link", { name: "API Docs", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Core" })).toBeVisible();
const coreButton = page.locator('button[data-variant-id="core"]');
await coreButton.focus();
await coreButton.press("Space");
await expect(coreButton).toHaveAttribute("aria-pressed", "true");
await expect(page.locator("#install-cmd")).toContainText("npm install @form2js/core");
await expect(page.locator("#install-snippet")).toContainText("entriesToObject");
await page.getByRole("button", { name: "Run @form2js/core" }).click();
const resultJson = page.locator(".result-json");
await expect(page.getByText("@form2js/core -> entriesToObject(entry objects)")).toBeVisible();
await expect(resultJson).toContainText("von Lipwig");
await page.locator('button[data-variant-id="form"]').click();
await page.locator('button[data-variant-id="core"]').click();
await expect(page.getByText("@form2js/core -> entriesToObject(entry objects)")).toBeVisible();
await expect(resultJson).toContainText("von Lipwig");
});
test("fault-injected variant failure keeps the switcher usable", async ({ page }) => {
await page.goto("/?variant=react&__fault=react:render");
await expect(page.getByText("React failed to load.")).toBeVisible();
await expect(page.getByText("Injected render fault for React")).toBeVisible();
await page.locator('button[data-variant-id="form"]').click();
await expect(page.getByRole("button", { name: "Run @form2js/dom" })).toBeVisible();
});
================================================
FILE: apps/docs/tsconfig.json
================================================
{
"extends": "astro/tsconfigs/strict",
"include": [
".astro/types.d.ts",
"src/**/*",
"test/**/*",
"test-e2e/**/*"
]
}
================================================
FILE: apps/docs/vitest.config.ts
================================================
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["test/**/*.{test,spec}.{ts,tsx}"],
exclude: ["test-e2e/**"]
}
});
================================================
FILE: changeset/config.json
================================================
{
"note": "Canonical changesets config lives at .changeset/config.json"
}
================================================
FILE: docs/api-core.md
================================================
# @form2js/core
`@form2js/core` is the path parsing engine behind the rest of the package family. Use it when you already have key/value entries, need to turn them into nested objects, or want to flatten nested data back into entry form.
## Installation
```bash
npm install @form2js/core
```
Standalone/global build is not shipped for this package.
## General Example
```ts
import { entriesToObject, objectToEntries } from "@form2js/core";
const data = entriesToObject([
{ key: "person.name.first", value: "Esme" },
{ key: "person.roles[]", value: "witch" },
]);
const flat = objectToEntries(data);
```
## Types and Properties
### Exported Surface
| Export | Kind | What it does |
| --- | --- | --- |
| `createMergeContext` | function | Creates merge state used while parsing indexed arrays. |
| `setPathValue` | function | Applies one path/value into an object tree. |
| `entriesToObject` | function | Main parser for iterable entries. |
| `objectToEntries` | function | Flattens nested object/array data into `{ key, value }` entries. |
| `processNameValues` | function | Compatibility helper for `{ name, value }` input. |
| `Entry`, `EntryInput`, `EntryValue`, `NameValuePair`, `ObjectTree`, `ParseOptions`, `MergeContext`, `MergeOptions`, `SchemaValidator`, `ValidationOptions`, `InferSchemaOutput` | types | Public type surface for parser inputs, options, and results. |
```ts
export function createMergeContext(): MergeContext;
export function setPathValue(
target: ObjectTree,
path: string,
value: EntryValue,
options?: MergeOptions
): ObjectTree;
export function entriesToObject(entries: Iterable<EntryInput>, options?: ParseOptions): ObjectTree;
export function entriesToObject<TSchema extends SchemaValidator>(
entries: Iterable<EntryInput>,
options: ParseOptions & { schema: TSchema }
): InferSchemaOutput<TSchema>;
export function objectToEntries(value: unknown): Entry[];
export function processNameValues(
nameValues: Iterable<NameValuePair>,
skipEmpty?: boolean,
delimiter?: string
): ObjectTree;
```
### Options And Defaults
| Option | Default | Where | Why this matters |
| --- | --- | --- | --- |
| `delimiter` | `"."` | `entriesToObject`, `setPathValue`, `processNameValues` | Controls how dot-like path chunks are split. |
| `skipEmpty` | `true` | `entriesToObject`, `processNameValues` | Drops `""` and `null` values unless you opt out. |
| `allowUnsafePathSegments` | `false` | `entriesToObject`, `setPathValue` | Blocks prototype-pollution path segments unless you explicitly trust the source. |
| `schema` | unset | `entriesToObject` | Runs `schema.parse(parsedObject)` and returns schema output type. |
| `context` | fresh merge context | `setPathValue` | Keeps indexed array compaction stable across multiple writes. |
### Schema validation
Use `schema` when you want parsing and validation in the same step. The parser only requires a structural `{ parse(unknown) }` contract, so this works with Zod and similar validators.
```ts
import { z } from "zod";
import { entriesToObject } from "@form2js/core";
const PersonSchema = z.object({
person: z.object({
age: z.coerce.number().int().min(0),
email: z.string().email()
})
});
const rawEntries = [
{ key: "person.age", value: "17" },
{ key: "person.email", value: "esk@example.com" }
];
const result = entriesToObject(rawEntries, { schema: PersonSchema });
```
### `skipEmpty: false`
Opt out of the default empty-value filtering when blank strings are meaningful in your payload.
```ts
import { entriesToObject } from "@form2js/core";
const result = entriesToObject(
[{ key: "person.nickname", value: "" }],
{ skipEmpty: false }
);
```
### Behavior Notes
- Indexed array keys are compacted by encounter order, not preserved by numeric index.
- `EntryInput` accepts `[key, value]`, `{ key, value }`, and `{ name, value }`.
- If `schema` is provided, parser output is passed to `schema.parse()` and schema errors are rethrown.
- `objectToEntries` emits bracket indexes for arrays such as `emails[0]` and only serializes own enumerable properties.
================================================
FILE: docs/api-dom.md
================================================
# @form2js/dom
`@form2js/dom` solves the browser side of the problem: walk a form or DOM subtree, extract the submitted values, and return the parsed object. Use it when you want native form semantics without writing the extraction logic yourself.
## Installation
```bash
npm install @form2js/dom
```
Standalone via `unpkg`:
```html
<script src="https://unpkg.com/@form2js/dom/dist/standalone.global.js"></script>
<script>
const data = formToObject(formElement);
// or form2js(formElement)
</script>
```
## General Example
```ts
import { formToObject } from "@form2js/dom";
const result = formToObject(document.getElementById("profileForm"), {
useIdIfEmptyName: true,
getDisabled: false,
});
```
## Types and Properties
### Exported Surface
| Export | Kind | What it does |
| --- | --- | --- |
| `NodeCallbackResult` | interface | Custom extraction payload (`name` or `key` plus `value`). |
| `FormToObjectNodeCallback` | type | Callback type used during node walk. |
| `ExtractOptions` | interface | Options for pair extraction only. |
| `FormToObjectOptions` | interface | Extraction options plus parser options. |
| `RootNodeInput` | type | Supported root inputs such as `id`, `Node`, `NodeList`, arrays, and collections. |
| `extractPairs` | function | Traverses DOM and returns path/value entries. |
| `formToObject` | function | High-level parser from DOM to object tree. |
| `form2js` | function | Compatibility wrapper around `formToObject`. |
```ts
export interface NodeCallbackResult {
name?: string;
key?: string;
value: unknown;
}
export const SKIP_NODE: unique symbol;
export type FormToObjectNodeCallback = (
node: Node
) => NodeCallbackResult | typeof SKIP_NODE | false | null | undefined;
export interface ExtractOptions {
nodeCallback?: FormToObjectNodeCallback;
useIdIfEmptyName?: boolean;
getDisabled?: boolean;
document?: Document;
}
export interface FormToObjectOptions extends ExtractOptions, ParseOptions {}
export function extractPairs(rootNode: RootNodeInput, options?: ExtractOptions): Entry[];
export function formToObject(rootNode: RootNodeInput, options?: FormToObjectOptions): ObjectTree;
```
### Options And Defaults
| Option | Default | Where | Why this matters |
| --- | --- | --- | --- |
| `delimiter` | `"."` | `formToObject`, `form2js` | Matches parser path semantics. |
| `skipEmpty` | `true` | `formToObject`, `form2js` | Skips `""` and `null` values by default. |
| `allowUnsafePathSegments` | `false` | `formToObject`, `form2js` | Rejects unsafe path segments before object merging. |
| `useIdIfEmptyName` | `false` | extraction and wrappers | Lets `id` act as field key when `name` is empty. |
| `getDisabled` | `false` | extraction and wrappers | Disabled controls, including disabled fieldset descendants, are ignored unless enabled explicitly. |
| `nodeCallback` | unset | extraction and wrappers | Use it for custom field extraction from specific nodes. |
| `document` | ambient/global document | extraction and wrappers | Required outside browser globals. |
### `useIdIfEmptyName`
Enable this when a form control is keyed by `id` rather than `name`, which is common in older markup or UI builders.
```ts
import { formToObject } from "@form2js/dom";
const result = formToObject(document.getElementById("profileForm"), {
useIdIfEmptyName: true
});
```
### `nodeCallback`
Use `nodeCallback` to rewrite or skip specific nodes before the default extraction logic runs.
```ts
import { formToObject, SKIP_NODE } from "@form2js/dom";
const result = formToObject(document.getElementById("profileForm"), {
nodeCallback(node) {
if (!(node instanceof HTMLInputElement)) {
return;
}
if (node.type === "hidden" && node.name === "csrfToken") {
return SKIP_NODE;
}
if (node.name === "person.age") {
return { key: node.name, value: Number(node.value) };
}
}
});
```
### Behavior Notes
- `select name="colors[]"` is emitted as key `colors`; the trailing `[]` is removed for selects.
- Checkbox and radio values follow native browser submission semantics:
- checked controls emit their string `value`
- unchecked controls are omitted
- omitted indexed controls do not reserve compacted array slots, so preserve row identity with another submitted field when it matters
- Button-like inputs (`button`, `reset`, `submit`, `image`) are excluded from extraction.
- You can merge multiple roots (`NodeList`, arrays, `HTMLCollection`) into one object.
- If the callback returns `SKIP_NODE`, that node is excluded from extraction entirely.
- If the callback returns `{ key | name, value }`, that value is used directly for that node.
================================================
FILE: docs/api-form-data.md
================================================
# @form2js/form-data
`@form2js/form-data` is the server-friendly adapter for the same parsing rules used by the DOM package. Use it when your input is a `FormData` instance or a plain iterable of form-like key/value tuples.
## Installation
```bash
npm install @form2js/form-data
```
Standalone/global build is not shipped for this package.
## General Example
```ts
import { formDataToObject } from "@form2js/form-data";
const result = formDataToObject([
["person.name.first", "Sam"],
["person.roles[]", "captain"],
]);
```
## Types and Properties
### Exported Surface
| Export | Kind | What it does |
| --- | --- | --- |
| `KeyValueEntryInput` | type alias | Alias of core `EntryInput`. |
| `FormDataToObjectOptions` | interface | Parser options for form-data conversion. |
| `entriesToObject` | function | Adapter to the core parser. |
| `formDataToObject` | function | Parses `FormData` or iterable form-data entries. |
| `EntryInput`, `ObjectTree`, `ParseOptions`, `SchemaValidator`, `ValidationOptions`, `InferSchemaOutput` | type re-export | Core types re-exported for convenience. |
```ts
export type KeyValueEntryInput = EntryInput;
export interface FormDataToObjectOptions extends ParseOptions {}
export function entriesToObject(entries: Iterable<KeyValueEntryInput>, options?: ParseOptions): ObjectTree;
export function formDataToObject(
formData: FormData | Iterable<readonly [string, FormDataEntryValue]>,
options?: FormDataToObjectOptions
): ObjectTree;
```
### Options And Defaults
| Option | Default | Why this matters |
| --- | --- | --- |
| `delimiter` | `"."` | Keeps path splitting aligned with core and DOM behavior. |
| `skipEmpty` | `true` | Drops empty string and `null` values unless disabled. |
| `allowUnsafePathSegments` | `false` | Rejects unsafe path segments before object merging. |
| `schema` | unset | Runs `schema.parse(parsedObject)` after parsing and returns schema output type. |
### Schema validation
Use the same schema pattern on `FormData` input when you want validated server-side parsing without importing `@form2js/core` separately.
```ts
import { z } from "zod";
import { formDataToObject } from "@form2js/form-data";
const PersonSchema = z.object({
person: z.object({
age: z.coerce.number().int().min(0)
})
});
const formData = new FormData();
formData.set("person.age", "12");
const result = formDataToObject(formData, { schema: PersonSchema });
```
### Behavior Notes
- Parsing rules are the same as `@form2js/core`.
- Accepts either a real `FormData` object or any iterable of readonly key/value tuples.
- Schema validation is optional and uses only a structural `{ parse(unknown) }` contract.
================================================
FILE: docs/api-index.md
================================================
# form2js API Reference
This section is for developers who want the exact API surface of the current `@form2js/*` packages, plus the defaults and edge cases that usually matter when you wire forms into real applications.
If you want the broader project overview first, start with [README.md](README.md).
## Who this is for
- Developers choosing between the `@form2js/*` packages
- Teams migrating from the legacy `form2js` flow to package-specific APIs
- Anyone who needs exact options, exported types, and behavior notes
## Package Guide
- [`@form2js/core`](api-core.md): parse path-like entries into nested objects and flatten them back out
- [`@form2js/dom`](api-dom.md): turn browser form controls into an object
- [`@form2js/form-data`](api-form-data.md): parse `FormData` or tuple entries with the same path rules
- [`@form2js/react`](api-react.md): handle React form submission with parsing, validation, and submit state
- [`@form2js/js2form`](api-js2form.md): push nested object data back into form controls
- [`@form2js/jquery`](api-jquery.md): install a jQuery plugin on top of the DOM parser
## Shared Naming Rules
These rules apply across parser-based packages such as `core`, `dom`, and `form-data`.
- Dot paths build nested objects: `person.name.first` becomes `{ person: { name: { first: ... } } }`
- Repeated `[]` pushes into arrays in encounter order: `roles[]`
- Indexed arrays are compacted in first-seen order: `items[8]`, `items[5]` becomes indexes `0`, `1`
- Rails-style brackets are supported: `rails[field][value]`
- By default, empty string and `null` are skipped (`skipEmpty: true`)
- Unsafe key path segments (`__proto__`, `prototype`, `constructor`) are rejected by default
================================================
FILE: docs/api-jquery.md
================================================
# @form2js/jquery
`@form2js/jquery` is the legacy-friendly adapter for projects that still rely on jQuery forms. Use it when you want `$.fn.toObject()` on top of the DOM parser without rewriting the rest of the form handling code.
## Installation
```bash
npm install @form2js/jquery jquery
```
Standalone via `unpkg`:
```html
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://unpkg.com/@form2js/jquery/dist/standalone.global.js"></script>
<script>
const data = $("#profileForm").toObject({ mode: "first" });
</script>
```
## General Example
```ts
import $ from "jquery";
import { installToObjectPlugin } from "@form2js/jquery";
installToObjectPlugin($);
const data = $("#profileForm").toObject({ mode: "first" });
```
## Types and Properties
### Exported Surface
| Export | Kind | What it does |
| --- | --- | --- |
| `ToObjectMode` | type | `"first" | "all" | "combine"` |
| `ToObjectOptions` | interface | Plugin options mapped to `@form2js/dom` behavior. |
| `installToObjectPlugin` | function | Adds `toObject()` to `$.fn` if missing. |
| `maybeAutoInstallPlugin` | function | Installs the plugin only when a jQuery-like scope is detected. |
```ts
export type ToObjectMode = "first" | "all" | "combine";
export interface ToObjectOptions {
mode?: ToObjectMode;
delimiter?: string;
skipEmpty?: boolean;
allowUnsafePathSegments?: boolean;
nodeCallback?: FormToObjectNodeCallback;
useIdIfEmptyName?: boolean;
getDisabled?: boolean;
}
export function installToObjectPlugin($: JQueryLike): void;
export function maybeAutoInstallPlugin(scope?: unknown): void;
```
### Options And Defaults
| Option | Default | Why this matters |
| --- | --- | --- |
| `mode` | `"first"` | Controls whether you parse one match, all matches, or merge all matches. |
| `delimiter` | `"."` | Same path splitting behavior as the other packages. |
| `skipEmpty` | `true` | Keeps default parser behavior for empty values. |
| `allowUnsafePathSegments` | `false` | Rejects unsafe path segments before object merging. |
| `useIdIfEmptyName` | `false` | Lets the plugin fall back to `id` where needed. |
| `getDisabled` | `false` | Disabled controls are skipped unless enabled. |
| `nodeCallback` | unset | Hook for custom extraction through the DOM package semantics. |
### `mode: "all"`
Use `all` when the selector can match multiple forms or repeated field groups and you want one parsed object per match.
```ts
const result = $(".profile-form").toObject({ mode: "all" });
```
### Behavior Notes
- `installToObjectPlugin` is idempotent; it does not overwrite an existing `$.fn.toObject`.
- `mode: "all"` returns an array of objects, one per matched element.
- `mode: "combine"` passes all matched root nodes together into the DOM parser.
================================================
FILE: docs/api-js2form.md
================================================
# @form2js/js2form
`@form2js/js2form` moves data in the opposite direction: take a nested object and write it into matching form controls. Use it when you need to prefill a form, restore saved draft state, or sync object data back into existing DOM controls.
## Installation
```bash
npm install @form2js/js2form
```
Standalone/global build is not shipped for this package.
## General Example
```ts
import { objectToForm } from "@form2js/js2form";
objectToForm("profileForm", {
person: {
name: { first: "Tiffany", last: "Aching" },
roles: ["witch"],
},
});
```
## Types and Properties
### Exported Surface
| Export | Kind | What it does |
| --- | --- | --- |
| `RootNodeInput` | type | Root as element id, node, `null`, or `undefined`. |
| `ObjectToFormNodeCallback` | type | Write-time callback for per-node assignment control. |
| `ObjectToFormOptions` | interface | Options for name normalization, cleaning, and document resolution. |
| `SupportedField`, `SupportedFieldCollection`, `FieldMap` | types | Field typing used by mapping and assignment helpers. |
| `flattenDataForForm` | function | Flattens object data to an entry list. |
| `mapFieldsByName` | function | Builds a normalized name-to-field mapping. |
| `objectToForm` | function | Populates matching fields from object data. |
| `js2form` | function | Compatibility wrapper around `objectToForm`. |
| `normalizeName` | function | Normalizes field names and compacts indexed arrays. |
```ts
export interface ObjectToFormOptions {
delimiter?: string;
nodeCallback?: ObjectToFormNodeCallback;
useIdIfEmptyName?: boolean;
shouldClean?: boolean;
document?: Document;
}
export function flattenDataForForm(data: unknown): Entry[];
export function mapFieldsByName(
rootNode: RootNodeInput,
options?: Pick<ObjectToFormOptions, "delimiter" | "useIdIfEmptyName" | "shouldClean" | "document">
): FieldMap;
export function objectToForm(rootNode: RootNodeInput, data: unknown, options?: ObjectToFormOptions): void;
```
### Options And Defaults
| Option | Default | Where | Why this matters |
| --- | --- | --- | --- |
| `delimiter` | `"."` | `objectToForm`, `mapFieldsByName`, `js2form` | Must match how your input keys are structured. |
| `useIdIfEmptyName` | `false` | `objectToForm`, `mapFieldsByName`, `js2form` | Useful when form controls are keyed by `id` instead of `name`. |
| `shouldClean` | `true` | `objectToForm`, `mapFieldsByName` | Clears form state before applying incoming values. |
| `document` | ambient/global document | all root-resolving APIs | Needed when running with a DOM shim. |
| `nodeCallback` | unset | `objectToForm`, `js2form` options | Called before default assignment; return `false` to skip default assignment for that node. |
### `shouldClean: false`
Disable cleaning when you want to layer partial data onto an existing form without clearing unrelated controls first.
```ts
import { objectToForm } from "@form2js/js2form";
objectToForm(
"profileForm",
{
person: {
name: { first: "Tiffany" }
}
},
{ shouldClean: false }
);
```
### `useIdIfEmptyName`
Match fields by `id` when the markup does not provide stable `name` attributes.
```ts
import { objectToForm } from "@form2js/js2form";
objectToForm(
document.getElementById("profileForm"),
{
firstName: "Agnes"
},
{ useIdIfEmptyName: true }
);
```
### Behavior Notes
- `objectToForm` is a no-op when the root cannot be resolved.
- Checkbox and radio groups are matched with `[]` and non-`[]` name fallbacks.
- Name normalization compacts sparse indexes to sequential indexes during matching.
- For multi-select names like `colors[]`, matching includes `[]` and bare-name fallbacks without creating one map key per option.
- Form updates set values, checked state, and selected state, but do not dispatch synthetic events.
================================================
FILE: docs/api-react.md
================================================
# @form2js/react
`@form2js/react` wraps the form-data parser in a React submit hook. Use it when you want a single `onSubmit` handler that parses form input, optionally validates it with a schema, and tracks submit state without wiring your own state machine.
## Installation
```bash
npm install @form2js/react react
```
Standalone/global build is not shipped for this package.
## General Example
```tsx
import { z } from "zod";
import { useForm2js } from "@form2js/react";
const schema = z.object({
person: z.object({
email: z.string().email()
})
});
export function SignupForm(): React.JSX.Element {
const { onSubmit, isSubmitting, isError, error, isSuccess, reset } = useForm2js(
async (data) => {
await sendFormData(data);
},
{ schema }
);
return (
<form
onSubmit={(event) => {
void onSubmit(event);
}}
>
<input name="person.email" type="email" defaultValue="sam.vimes@ankh.city" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</button>
{isError ? <p>{String(error)}</p> : null}
{isSuccess ? <p>Saved</p> : null}
<button type="button" onClick={reset}>
Reset state
</button>
</form>
);
}
```
## Types and Properties
### Exported Surface
| Export | Kind | What it does |
| --- | --- | --- |
| `UseForm2jsData` | type | Infers submit payload from the optional schema. |
| `UseForm2jsSubmit` | type | Submit callback signature. |
| `UseForm2jsOptions` | interface | Parser options plus optional schema. |
| `UseForm2jsResult` | interface | Hook return state and handlers. |
| `useForm2js` | function | Creates submit handler and submit state machine for forms. |
```ts
export type UseForm2jsSubmit<TSchema extends SchemaValidator | undefined = undefined> = (
data: UseForm2jsData<TSchema>
) => Promise<void> | void;
export interface UseForm2jsOptions<TSchema extends SchemaValidator | undefined = undefined>
extends ParseOptions {
schema?: TSchema;
}
export interface UseForm2jsResult {
onSubmit: (event: SyntheticEvent<HTMLFormElement, SubmitEvent>) => Promise<void>;
isSubmitting: boolean;
isError: boolean;
error: unknown;
isSuccess: boolean;
reset: () => void;
}
```
### Options And Defaults
| Option | Default | Why this matters |
| --- | --- | --- |
| `delimiter` | `"."` | Keeps parser path splitting aligned with the other packages. |
| `skipEmpty` | `true` | Drops empty string and `null` values unless disabled. |
| `allowUnsafePathSegments` | `false` | Keeps parser hardened by default. |
| `schema` | unset | If set, the parsed payload is run through `schema.parse(...)` before the submit callback. |
### Behavior Notes
- `onSubmit` always calls `event.preventDefault()`.
- Re-submit attempts are ignored while a submit promise is still pending.
- Validation and submit errors are both surfaced through `error` and `isError`.
- `reset()` clears `isError`, `error`, and `isSuccess`.
================================================
FILE: docs/migrate.md
================================================
# Migrate from Legacy form2js
If you built around the old single `form2js` script or the archived jQuery plugin flow, the main change is that modern form2js is now a small package family. You install only the part you need instead of pulling one browser-era bundle into every environment.
The legacy code and historical examples still live in the [legacy branch](https://github.com/maxatwork/form2js/tree/legacy), but new work should move to the current packages and docs.
## Quick Chooser
| If your legacy code does this | Use now | Notes |
| --- | --- | --- |
| `form2js(form)` in the browser | [`@form2js/dom`](api-dom.md) | Closest direct replacement. Exports both `formToObject()` and a compatibility `form2js()` wrapper. |
| `$("#form").toObject()` in jQuery | [`@form2js/jquery`](api-jquery.md) | Keeps the plugin shape while using the modern DOM parser underneath. |
| Parse `FormData` on the server or in browser pipelines | [`@form2js/form-data`](api-form-data.md) | Best fit for fetch actions, loaders, workers, and Node. |
| Handle submit state in React | [`@form2js/react`](api-react.md) | Wraps form parsing in a hook with async submit state and optional schema validation. |
| Push object data back into a form | [`@form2js/js2form`](api-js2form.md) | Modern replacement for the old "object back into fields" helpers around the ecosystem. |
| Work directly with path/value entries | [`@form2js/core`](api-core.md) | Lowest-level parser and formatter. |
## What Changed
- The archived project exposed one browser-oriented `form2js(rootNode, delimiter, skipEmpty, nodeCallback, useIdIfEmptyName)` entry point.
- The current project splits that behavior by environment and responsibility.
- Browser DOM extraction lives in `@form2js/dom`.
- jQuery compatibility lives in `@form2js/jquery`.
- `FormData`, React, object-to-form, and low-level entry parsing each have their own package.
That split is the point of the rewrite: smaller installs, clearer environment boundaries, and first-class TypeScript/ESM support without making every user drag along legacy browser assumptions.
## Legacy API Mapping
Legacy browser code usually looked like this:
```js
var data = form2js(rootNode, ".", true, nodeCallback, false);
```
Modern browser code should usually look like this:
```ts
import { formToObject } from "@form2js/dom";
const data = formToObject(rootNode, {
delimiter: ".",
skipEmpty: true,
nodeCallback,
useIdIfEmptyName: false
});
```
If you want the smallest possible migration diff, `@form2js/dom` also exports a compatibility wrapper:
```ts
import { form2js } from "@form2js/dom";
const data = form2js(rootNode, ".", true, nodeCallback, false);
```
Parameter mapping:
| Legacy parameter | Modern equivalent |
| --- | --- |
| `rootNode` | `rootNode` |
| `delimiter` | `options.delimiter` |
| `skipEmpty` | `options.skipEmpty` |
| `nodeCallback` | `options.nodeCallback` |
| `useIdIfEmptyName` | `options.useIdIfEmptyName` |
The main migration decision is not the parameter mapping. It is choosing the right package for the environment where parsing now happens.
## Browser Migration
For plain browser forms, install `@form2js/dom`:
```bash
npm install @form2js/dom
```
Module usage:
```ts
import { formToObject } from "@form2js/dom";
const data = formToObject(document.getElementById("profileForm"));
```
Standalone usage is still available for the DOM package:
```html
<script src="https://unpkg.com/@form2js/dom/dist/standalone.global.js"></script>
<script>
const data = formToObject(document.getElementById("profileForm"));
// or form2js(document.getElementById("profileForm"))
</script>
```
## jQuery Migration
If your codebase still expects `$.fn.toObject()`, move to `@form2js/jquery` instead of rebuilding that glue yourself.
```bash
npm install @form2js/jquery jquery
```
```ts
import $ from "jquery";
import { installToObjectPlugin } from "@form2js/jquery";
installToObjectPlugin($);
const data = $("#profileForm").toObject({ mode: "first" });
```
Standalone usage is also available:
```html
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://unpkg.com/@form2js/jquery/dist/standalone.global.js"></script>
<script>
const data = $("#profileForm").toObject({ mode: "combine" });
</script>
```
## Behavior Differences To Check
- `skipEmpty` still defaults to `true`, so empty strings and `null` values are skipped unless you opt out.
- Disabled controls are ignored by default. Set `getDisabled: true` only if you really want them parsed.
- Unsafe path segments such as `__proto__`, `prototype`, and `constructor` are rejected by default in the modern parser.
- Only `@form2js/dom` and `@form2js/jquery` ship standalone browser globals. The other packages are module-only.
- React and `FormData` use cases now have dedicated packages instead of being squeezed through the DOM entry point.
## Where To Go Now
If the legacy code used browser DOM access only because that was the only option at the time, this is the modern package map:
- Use [`@form2js/form-data`](api-form-data.md) when your app already has `FormData`, request entries, or server-side action handlers.
- Use [`@form2js/react`](api-react.md) when you want submit-state handling around parsing in React.
- Use [`@form2js/js2form`](api-js2form.md) when you need to populate forms from nested objects.
- Use [`@form2js/core`](api-core.md) when you already have raw key/value pairs and just need the parser rules.
## Migration Checklist
1. Identify whether the old code is DOM-based, jQuery-based, React-based, or really just `FormData` processing.
2. Swap the legacy package or script include for the specific current package.
3. Move old positional arguments to an options object where appropriate.
4. Re-test any custom `nodeCallback` logic and any flows that depend on disabled or empty fields.
5. Replace browser-only parsing with `@form2js/form-data` or `@form2js/react` when the parsing no longer needs direct DOM traversal.
================================================
FILE: eslint.config.js
================================================
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import tseslint from "typescript-eslint";
const tsconfigRootDir = path.dirname(fileURLToPath(import.meta.url));
export default tseslint.config(
{
ignores: [
"**/dist/**",
"**/coverage/**",
"**/node_modules/**",
".turbo/**",
"package-lock.json"
]
},
js.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
files: ["**/*.ts", "**/*.tsx", "**/*.cts", "**/*.mts"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir
}
},
rules: {
"@typescript-eslint/consistent-type-imports": [
"error",
{
"prefer": "type-imports"
}
],
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-return": "error",
"@typescript-eslint/prefer-regexp-exec": "off",
"@typescript-eslint/prefer-nullish-coalescing": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/no-unnecessary-condition": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/prefer-for-of": "off",
"@typescript-eslint/consistent-indexed-object-style": "off",
"@typescript-eslint/no-base-to-string": "off",
"@typescript-eslint/non-nullable-type-assertion-style": "off"
}
}
);
================================================
FILE: package.json
================================================
{
"name": "form2js-monorepo",
"private": true,
"type": "module",
"packageManager": "npm@11.6.2",
"workspaces": [
"packages/*",
"apps/*"
],
"engines": {
"node": ">=18"
},
"scripts": {
"build": "turbo run build",
"build:packages": "turbo run build --filter=@form2js/*",
"docs": "npm run build:packages && npm -w @form2js/docs run dev",
"docs:build": "npm run build:packages && npm -w @form2js/docs run build",
"test": "npm run test:packages && npm run test:integration && npm run test:docs",
"test:docs": "npm run build:packages && DOCS_E2E_PORT=4329 npm -w @form2js/docs run test:e2e",
"test:packages": "turbo run test",
"test:integration": "vitest run test/integration",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"clean": "turbo run clean && rimraf node_modules .turbo",
"pack:dry-run": "npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/core && npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/dom && npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/form-data && npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/js2form && npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/jquery && npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/react",
"changeset": "changeset",
"release": "changeset publish",
"scope:rewrite": "node scripts/rewrite-scope.mjs",
"bump-version": "node scripts/bump-version.mjs"
},
"overrides": {
"minimatch": {
"brace-expansion": "1.1.13"
},
"@typescript-eslint/typescript-estree": {
"minimatch": {
"brace-expansion": "2.0.3"
}
},
"micromatch": {
"picomatch": "2.3.2"
},
"astro": {
"@rollup/pluginutils": {
"picomatch": "4.0.4"
}
},
"vite": {
"picomatch": "4.0.4"
},
"@rollup/pluginutils": {
"picomatch": "4.0.4"
},
"tinyglobby": {
"picomatch": "4.0.4"
},
"vitest": {
"picomatch": "4.0.4"
},
"smol-toml": "1.6.1",
"yaml": "2.8.3"
},
"devDependencies": {
"@changesets/cli": "^2.29.7",
"@eslint/js": "^9.22.0",
"@types/jsdom": "^21.1.7",
"eslint": "^9.22.0",
"jsdom": "^26.0.0",
"picomatch": "^4.0.4",
"rimraf": "^6.0.1",
"tsup": "^8.5.0",
"tsx": "^4.20.5",
"turbo": "^2.5.6",
"typescript": "^5.9.2",
"typescript-eslint": "^8.35.1",
"vitest": "^4.1.2"
}
}
================================================
FILE: packages/core/CHANGELOG.md
================================================
# @form2js/core
## 3.3.0
### Minor Changes
- Restore Rails-style bracket path compatibility across parsing and form population, align DOM checkbox and radio handling with native browser submission behavior, and add explicit `SKIP_NODE` callback support for DOM extraction.
## 3.2.2
### Patch Changes
- Republish the package metadata and package-level README improvements across all public packages after fixing publish configuration.
## 3.2.1
### Patch Changes
- Add npm package metadata (`license`, `bugs`, `keywords`) and publish package-level README files for all public packages.
## 3.2.0
### Minor Changes
- 4d2f923: Added optional schema validation support (structural `parse`) to parsing APIs in core and form-data, plus a new `@form2js/react` package with `useForm2js` for async submit state management in React forms.
## 3.1.1
### Patch Changes
- 77b8543: Hardened parser security by rejecting unsafe key path segments by default and aligned DOM extraction with HTML semantics (disabled fieldsets, no button-like inputs). Added a writer hook, improved multi-select mapping, tightened ESLint, added a version bump script, and aligned packages to 3.1.0 with updated docs, tests, and repo/website links.
New Features
Added allowUnsafePathSegments option across core, dom, form-data, and jQuery plugin to explicitly allow trusted unsafe segments.
Added nodeCallback to objectToForm/js2form; returning false skips default assignment for that node.
Improved multi-select handling: matches names with [] and bare-
gitextract_xzm58w39/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .github/ │ └── workflows/ │ ├── ci.yml │ ├── pages.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── apps/ │ └── docs/ │ ├── astro.config.mjs │ ├── package.json │ ├── playwright.config.ts │ ├── src/ │ │ ├── components/ │ │ │ ├── api/ │ │ │ │ ├── ApiPackageNav.tsx │ │ │ │ ├── ApiPackageSummaryList.tsx │ │ │ │ └── ApiToc.tsx │ │ │ ├── landing/ │ │ │ │ ├── ApiDocsCta.astro │ │ │ │ ├── Hero.astro │ │ │ │ └── InstallSection.astro │ │ │ └── playground/ │ │ │ ├── PlaygroundShell.tsx │ │ │ ├── ReactInspectorPanel.tsx │ │ │ ├── ResultPanel.tsx │ │ │ ├── StandardResultPanel.tsx │ │ │ ├── VariantHeader.tsx │ │ │ ├── bootstrap/ │ │ │ │ └── jquery-bootstrap.ts │ │ │ ├── types.ts │ │ │ ├── variant-registry.ts │ │ │ └── variants/ │ │ │ ├── core-variant.tsx │ │ │ ├── form-data-variant.tsx │ │ │ ├── form-variant.tsx │ │ │ ├── jquery-variant.tsx │ │ │ ├── js2form-variant.tsx │ │ │ └── react-variant.tsx │ │ ├── env.d.ts │ │ ├── layouts/ │ │ │ ├── ApiDocsLayout.astro │ │ │ ├── DesignShell.astro │ │ │ └── DocsShell.astro │ │ ├── lib/ │ │ │ ├── api-docs-source.ts │ │ │ ├── api-packages.ts │ │ │ └── site-routes.ts │ │ ├── pages/ │ │ │ ├── api/ │ │ │ │ ├── [package].astro │ │ │ │ └── index.astro │ │ │ ├── index.astro │ │ │ └── migrate.astro │ │ └── styles/ │ │ ├── global.css │ │ ├── landing.css │ │ ├── playground.css │ │ └── themes/ │ │ ├── dark-brutalism.css │ │ ├── editorial.css │ │ ├── linear-dark.css │ │ ├── outrun-sunset.css │ │ └── terminal-noir.css │ ├── test/ │ │ ├── api-docs-page.test.tsx │ │ ├── api-docs-source.test.ts │ │ ├── data-variants.test.tsx │ │ ├── docs-pipeline.test.ts │ │ ├── docs-root-scripts.test.ts │ │ ├── homepage-shell.test.ts │ │ ├── playground-shell.test.tsx │ │ ├── playground-styles.test.ts │ │ ├── react-variant.test.tsx │ │ ├── site-routes.test.ts │ │ ├── standard-variants.test.tsx │ │ └── variant-registry.test.ts │ ├── test-e2e/ │ │ ├── api-docs.spec.ts │ │ └── homepage.spec.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── changeset/ │ └── config.json ├── docs/ │ ├── api-core.md │ ├── api-dom.md │ ├── api-form-data.md │ ├── api-index.md │ ├── api-jquery.md │ ├── api-js2form.md │ ├── api-react.md │ └── migrate.md ├── eslint.config.js ├── package.json ├── packages/ │ ├── core/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── test/ │ │ │ └── core.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── dom/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── standalone.ts │ │ ├── test/ │ │ │ └── dom.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── form-data/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── test/ │ │ │ └── form-data.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── jquery/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── standalone.ts │ │ ├── test/ │ │ │ └── jquery.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── js2form/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── test/ │ │ │ └── js2form.test.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ └── react/ │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src/ │ │ └── index.ts │ ├── test/ │ │ └── react.test.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── scripts/ │ ├── bump-version.mjs │ └── rewrite-scope.mjs ├── test/ │ └── integration/ │ ├── bump-version.test.ts │ ├── dependency-security.test.ts │ ├── form-flow.test.ts │ ├── package-metadata.test.ts │ └── workflow-node-version.test.ts ├── tsconfig.base.json └── turbo.json
SYMBOL INDEX (282 symbols across 45 files)
FILE: apps/docs/src/components/api/ApiPackageNav.tsx
type ApiPackageNavEntry (line 6) | type ApiPackageNavEntry = Pick<ApiPackageEntry, "slug" | "packageName">;
type ApiPackageNavProps (line 8) | interface ApiPackageNavProps {
function ApiPackageNav (line 14) | function ApiPackageNav({
FILE: apps/docs/src/components/api/ApiPackageSummaryList.tsx
type ApiPackageSummaryEntry (line 6) | type ApiPackageSummaryEntry = Pick<
type ApiPackageSummaryListProps (line 11) | interface ApiPackageSummaryListProps {
function ApiPackageSummaryList (line 16) | function ApiPackageSummaryList({
FILE: apps/docs/src/components/api/ApiToc.tsx
type ApiTocProps (line 5) | interface ApiTocProps {
type TocGroup (line 10) | interface TocGroup {
function groupHeadings (line 15) | function groupHeadings(headings: ApiHeading[]): TocGroup[] {
function ApiToc (line 33) | function ApiToc({ headings, initialActiveSlug }: ApiTocProps): React.JSX...
FILE: apps/docs/src/components/playground/PlaygroundShell.tsx
function getRequestedRenderFault (line 9) | function getRequestedRenderFault(): { variantId: VariantId; message: str...
function getActiveVariantId (line 19) | function getActiveVariantId(): VariantId {
function createInitialOutputStates (line 26) | function createInitialOutputStates(
function dispatchVariantChange (line 32) | function dispatchVariantChange(variantId: VariantId): void {
type VariantErrorBoundaryProps (line 42) | interface VariantErrorBoundaryProps {
type VariantErrorBoundaryState (line 46) | interface VariantErrorBoundaryState { hasError: boolean; }
class VariantErrorBoundary (line 48) | class VariantErrorBoundary extends React.Component<VariantErrorBoundaryP...
method getDerivedStateFromError (line 50) | static getDerivedStateFromError(): VariantErrorBoundaryState { return ...
method componentDidCatch (line 51) | componentDidCatch(error: Error): void {
method render (line 54) | render(): React.ReactNode {
function PlaygroundShell (line 60) | function PlaygroundShell(): React.JSX.Element {
FILE: apps/docs/src/components/playground/ReactInspectorPanel.tsx
type ReactInspectorPanelProps (line 5) | interface ReactInspectorPanelProps {
function ReactInspectorPanel (line 9) | function ReactInspectorPanel({ outputState }: ReactInspectorPanelProps):...
FILE: apps/docs/src/components/playground/ResultPanel.tsx
type ResultPanelProps (line 5) | interface ResultPanelProps {
function formatZodErrors (line 9) | function formatZodErrors(error: { message: string }): string[] {
function ResultPanel (line 14) | function ResultPanel({ outputState }: ResultPanelProps): React.JSX.Eleme...
FILE: apps/docs/src/components/playground/StandardResultPanel.tsx
type StandardResultPanelProps (line 5) | interface StandardResultPanelProps {
function StandardResultPanel (line 9) | function StandardResultPanel({ outputState }: StandardResultPanelProps):...
FILE: apps/docs/src/components/playground/VariantHeader.tsx
type VariantHeaderProps (line 5) | interface VariantHeaderProps {
function VariantHeader (line 11) | function VariantHeader({ activeId, variants, onSelect }: VariantHeaderPr...
FILE: apps/docs/src/components/playground/bootstrap/jquery-bootstrap.ts
type JQueryWithPlugin (line 4) | type JQueryWithPlugin = typeof $ & {
function ensureJqueryBootstrap (line 12) | function ensureJqueryBootstrap(): unknown {
FILE: apps/docs/src/components/playground/types.ts
type VariantKind (line 3) | type VariantKind = "react" | "standard";
type OutputStatus (line 4) | type OutputStatus = "idle" | "running" | "success" | "error";
type VariantId (line 5) | type VariantId = "react" | "form" | "jquery" | "js2form" | "core" | "for...
type ErrorInfo (line 7) | interface ErrorInfo {
type ReactOutputState (line 13) | interface ReactOutputState {
type StandardOutputState (line 27) | interface StandardOutputState {
type OutputState (line 35) | type OutputState = ReactOutputState | StandardOutputState;
type VariantComponentProps (line 37) | interface VariantComponentProps {
type VariantDefinition (line 43) | interface VariantDefinition {
FILE: apps/docs/src/components/playground/variant-registry.ts
function createStandardIdle (line 9) | function createStandardIdle(statusMessage: string): OutputState {
function createReactIdle (line 19) | function createReactIdle(statusMessage: string): OutputState {
constant VARIANT_IDS (line 85) | const VARIANT_IDS = variants.map((variant) => variant.id) satisfies Vari...
FILE: apps/docs/src/components/playground/variants/core-variant.tsx
constant INITIAL_ENTRIES_JSON (line 7) | const INITIAL_ENTRIES_JSON = `[
function createErrorState (line 16) | function createErrorState(message: string): StandardOutputState {
function createSuccessState (line 20) | function createSuccessState(parsedPayload: unknown): StandardOutputState {
function formatVariantError (line 24) | function formatVariantError(error: unknown): string {
function CoreVariant (line 32) | function CoreVariant({ onOutputChange }: VariantComponentProps): React.J...
FILE: apps/docs/src/components/playground/variants/form-data-variant.tsx
function createIdleState (line 7) | function createIdleState(): StandardOutputState {
function createSuccessState (line 11) | function createSuccessState(parsedPayload: unknown): StandardOutputState {
function FormDataVariant (line 15) | function FormDataVariant({ onOutputChange }: VariantComponentProps): Rea...
FILE: apps/docs/src/components/playground/variants/form-variant.tsx
function createIdleState (line 7) | function createIdleState(): StandardOutputState {
function createSuccessState (line 11) | function createSuccessState(statusMessage: string, parsedPayload: unknow...
function FormVariant (line 15) | function FormVariant({ onOutputChange }: VariantComponentProps): React.J...
FILE: apps/docs/src/components/playground/variants/jquery-variant.tsx
type JQueryToObjectOptions (line 8) | type JQueryToObjectOptions = { mode?: "first" | "all" | "combine" };
type JQueryCollectionWithToObject (line 9) | type JQueryCollectionWithToObject = JQuery & {
function createIdleState (line 13) | function createIdleState(): StandardOutputState {
function createSuccessState (line 17) | function createSuccessState(statusMessage: string, parsedPayload: unknow...
function getParsedPayload (line 21) | function getParsedPayload(
function JQueryVariant (line 29) | function JQueryVariant({ onOutputChange }: VariantComponentProps): React...
FILE: apps/docs/src/components/playground/variants/js2form-variant.tsx
constant INITIAL_JSON (line 8) | const INITIAL_JSON = `{
function createIdleState (line 19) | function createIdleState(): StandardOutputState {
function createErrorState (line 23) | function createErrorState(message: string): StandardOutputState {
function createSuccessState (line 27) | function createSuccessState(parsedPayload: unknown): StandardOutputState {
function formatVariantError (line 31) | function formatVariantError(error: unknown): string {
function Js2FormVariant (line 39) | function Js2FormVariant({ onOutputChange }: VariantComponentProps): Reac...
FILE: apps/docs/src/components/playground/variants/react-variant.tsx
type SubmitPayload (line 21) | type SubmitPayload = z.infer<typeof SubmitPayloadSchema>;
function sleep (line 23) | function sleep(ms: number): Promise<void> {
function formatError (line 27) | function formatError(error: unknown): string {
function createOutputState (line 35) | function createOutputState(
function ReactVariant (line 59) | function ReactVariant({ onOutputChange }: VariantComponentProps): React....
FILE: apps/docs/src/lib/api-docs-source.ts
type ApiHeading (line 19) | interface ApiHeading {
type ApiDocsSource (line 25) | interface ApiDocsSource {
type ParseOptions (line 34) | interface ParseOptions {
type MarkdownNode (line 38) | type MarkdownNode = {
function slugify (line 51) | function slugify(text: string): string {
function rewriteMarkdownLink (line 62) | function rewriteMarkdownLink(url: string, basePath: string): string {
function visit (line 101) | function visit(
function collectText (line 112) | function collectText(node: MarkdownNode): string {
function renderMarkdownNodes (line 120) | function renderMarkdownNodes(nodes: MarkdownNode[]): string {
function extractMarkdownSlice (line 134) | function extractMarkdownSlice(
function parseApiDocsMarkdown (line 142) | function parseApiDocsMarkdown(
function loadApiDocsSource (line 231) | async function loadApiDocsSource(
FILE: apps/docs/src/lib/api-packages.ts
type ApiPackageSlug (line 3) | type ApiPackageSlug =
type ApiPackageEntry (line 11) | interface ApiPackageEntry {
function resolveDocsPath (line 18) | function resolveDocsPath(filename: string): string {
function getApiPackageBySlug (line 63) | function getApiPackageBySlug(slug: string): ApiPackageEntry | undefined {
function getApiPackageByMarkdownBasename (line 67) | function getApiPackageByMarkdownBasename(
FILE: apps/docs/src/lib/site-routes.ts
function normalizeBase (line 1) | function normalizeBase(basePath: string): string {
function homepagePath (line 10) | function homepagePath(basePath: string): string {
function migrationGuidePath (line 14) | function migrationGuidePath(basePath: string): string {
function apiDocsPath (line 18) | function apiDocsPath(basePath: string): string {
function apiPackageDocsPath (line 22) | function apiPackageDocsPath(basePath: string, slug: string): string {
function homepageVariantPath (line 26) | function homepageVariantPath(basePath: string, variant: string): string {
FILE: apps/docs/test/data-variants.test.tsx
type RenderResult (line 17) | interface RenderResult {
function renderVariant (line 23) | function renderVariant(Component: (props: VariantComponentProps) => Reac...
FILE: apps/docs/test/playground-shell.test.tsx
function createReactOutput (line 9) | function createReactOutput(statusMessage: string): OutputState {
function createStandardOutput (line 28) | function createStandardOutput(statusMessage: string): OutputState {
function makeVariant (line 38) | function makeVariant(
function resetMockVariants (line 111) | function resetMockVariants(): void {
function renderShell (line 159) | function renderShell(): void {
FILE: apps/docs/test/react-variant.test.tsx
type RenderResult (line 15) | interface RenderResult {
function renderVariant (line 21) | function renderVariant(Component: (props: VariantComponentProps) => Reac...
function submitForm (line 38) | async function submitForm(container: HTMLDivElement): Promise<void> {
FILE: apps/docs/test/standard-variants.test.tsx
type RenderResult (line 18) | interface RenderResult {
function renderVariant (line 25) | function renderVariant(Component: (props: VariantComponentProps) => Reac...
FILE: packages/core/src/index.ts
constant SUB_ARRAY_REGEXP (line 15) | const SUB_ARRAY_REGEXP = /^\[\d+?\]/;
constant SUB_OBJECT_REGEXP (line 16) | const SUB_OBJECT_REGEXP = /^[a-zA-Z_][a-zA-Z_0-9]*/;
constant PATH_TOKEN_REGEXP (line 17) | const PATH_TOKEN_REGEXP = /[a-zA-Z_][a-zA-Z0-9_]*/g;
constant UNSAFE_PATH_SEGMENTS (line 18) | const UNSAFE_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constru...
type BracketMatch (line 20) | interface BracketMatch {
function isRecord (line 26) | function isRecord(value: unknown): value is Record<string, unknown> {
function createIndexRecord (line 30) | function createIndexRecord(): Record<string, unknown> {
function hasOwnRecordValue (line 34) | function hasOwnRecordValue(record: Record<string, unknown>, key: string)...
function getOwnRecordValue (line 38) | function getOwnRecordValue(record: Record<string, unknown>, key: string)...
function setOwnRecordValue (line 42) | function setOwnRecordValue(record: Record<string, unknown>, key: string,...
function findBracketMatches (line 51) | function findBracketMatches(input: string): BracketMatch[] {
function normalizeEntry (line 77) | function normalizeEntry(entry: EntryInput): Entry {
function shouldSkipValue (line 94) | function shouldSkipValue(value: EntryValue, skipEmpty: boolean): boolean {
function findUnsafePathToken (line 98) | function findUnsafePathToken(part: string): string | null {
function assertPathIsSafe (line 113) | function assertPathIsSafe(nameParts: string[], allowUnsafePathSegments: ...
function splitNameIntoParts (line 129) | function splitNameIntoParts(name: string, delimiter: string): string[] {
function ensureNamedArray (line 183) | function ensureNamedArray(container: unknown, arrayName: string): unknow...
function pushToNamedArray (line 202) | function pushToNamedArray(container: unknown, arrayName: string, value: ...
function createMergeContext (line 208) | function createMergeContext(): MergeContext {
function setPathValue (line 212) | function setPathValue(
function entriesToObject (line 295) | function entriesToObject(
function objectToNameValues (line 326) | function objectToNameValues(obj: unknown): NameValuePair[] {
function getSubValues (line 356) | function getSubValues(subObject: unknown, name: string): NameValuePair[] {
function objectToEntries (line 375) | function objectToEntries(value: unknown): Entry[] {
function processNameValues (line 382) | function processNameValues(
FILE: packages/core/src/types.ts
type EntryValue (line 1) | type EntryValue = unknown;
type Entry (line 3) | interface Entry {
type NameValuePair (line 8) | interface NameValuePair {
type ParseOptions (line 13) | interface ParseOptions {
type SchemaValidator (line 19) | interface SchemaValidator<TOutput = unknown> {
type InferSchemaOutput (line 23) | type InferSchemaOutput<TSchema> = TSchema extends SchemaValidator<infer ...
type ValidationOptions (line 27) | interface ValidationOptions<TSchema extends SchemaValidator = SchemaVali...
type MergeContext (line 31) | interface MergeContext {
type MergeOptions (line 35) | interface MergeOptions {
type ObjectTree (line 41) | type ObjectTree = Record<string, unknown>;
type EntryInput (line 43) | type EntryInput =
FILE: packages/core/test/core.test.ts
method parse (line 250) | parse(value: unknown) {
method parse (line 271) | parse() {
FILE: packages/dom/src/index.ts
type NodeCallbackResult (line 3) | interface NodeCallbackResult {
constant SKIP_NODE (line 9) | const SKIP_NODE = Symbol("form2js.skipNode");
type FormToObjectNodeCallback (line 11) | type FormToObjectNodeCallback = (node: Node) => NodeCallbackResult | typ...
type ExtractOptions (line 13) | interface ExtractOptions {
type FormToObjectOptions (line 20) | interface FormToObjectOptions extends ExtractOptions, ParseOptions {}
type RootNodeInput (line 22) | type RootNodeInput =
function isNodeObject (line 31) | function isNodeObject(value: unknown): value is Node {
function isElementNode (line 40) | function isElementNode(node: Node): node is Element {
function nodeNameIs (line 44) | function nodeNameIs(node: Node, expected: string): boolean {
function isInputNode (line 48) | function isInputNode(node: Node): node is HTMLInputElement {
function isTextareaNode (line 52) | function isTextareaNode(node: Node): node is HTMLTextAreaElement {
function isSelectNode (line 56) | function isSelectNode(node: Node): node is HTMLSelectElement {
function isNodeDisabled (line 60) | function isNodeDisabled(node: Node): boolean {
function isFormControlNode (line 64) | function isFormControlNode(node: Node): node is HTMLInputElement | HTMLT...
function getFirstLegendChild (line 68) | function getFirstLegendChild(fieldset: Element): Element | null {
function isDisabledByAncestorFieldset (line 79) | function isDisabledByAncestorFieldset(node: Element): boolean {
function isEffectivelyDisabledControl (line 102) | function isEffectivelyDisabledControl(node: Node): boolean {
function isButtonLikeInputType (line 114) | function isButtonLikeInputType(inputType: string): boolean {
function isNodeListLike (line 118) | function isNodeListLike(value: RootNodeInput): value is Node[] | NodeLis...
function getDocumentFromRoot (line 126) | function getDocumentFromRoot(rootNode: RootNodeInput, fallback?: Documen...
function resolveRootNode (line 149) | function resolveRootNode(rootNode: RootNodeInput, options: ExtractOption...
function getFieldName (line 158) | function getFieldName(node: Node, useIdIfEmptyName: boolean): string {
function getSelectedOptionValue (line 176) | function getSelectedOptionValue(selectNode: HTMLSelectElement): string |...
function getFieldValue (line 194) | function getFieldValue(fieldNode: Node, getDisabled: boolean): unknown {
function getSubFormValues (line 239) | function getSubFormValues(rootNode: Node, options: ExtractOptions): Entr...
function extractNodeValues (line 254) | function extractNodeValues(node: Node, options: ExtractOptions): Entry[]...
function getFormValues (line 290) | function getFormValues(rootNode: Node, options: ExtractOptions): Entry[] {
function extractPairs (line 303) | function extractPairs(rootNode: RootNodeInput, options: ExtractOptions =...
function formToObject (line 330) | function formToObject(rootNode: RootNodeInput, options: FormToObjectOpti...
function form2js (line 349) | function form2js(
FILE: packages/dom/src/standalone.ts
type DomStandaloneGlobals (line 3) | interface DomStandaloneGlobals {
FILE: packages/dom/test/dom.test.ts
method nodeCallback (line 54) | nodeCallback(node) {
method nodeCallback (line 76) | nodeCallback(node) {
method nodeCallback (line 101) | nodeCallback(node) {
FILE: packages/form-data/src/index.ts
type KeyValueEntryInput (line 11) | type KeyValueEntryInput = EntryInput;
type FormDataToObjectOptions (line 13) | interface FormDataToObjectOptions extends ParseOptions {}
function entriesToObject (line 20) | function entriesToObject(
function formDataToObject (line 35) | function formDataToObject(
FILE: packages/form-data/test/form-data.test.ts
method parse (line 52) | parse(value: unknown) {
method parse (line 73) | parse() {
FILE: packages/jquery/src/index.ts
type ToObjectMode (line 3) | type ToObjectMode = "first" | "all" | "combine";
type ToObjectOptions (line 5) | interface ToObjectOptions {
type JQueryCollectionLike (line 15) | interface JQueryCollectionLike {
type JQueryLike (line 20) | interface JQueryLike {
function isNodeObject (line 25) | function isNodeObject(value: unknown): value is Node {
function getFnObject (line 29) | function getFnObject($: JQueryLike): Record<string, unknown> {
type ResolvedToObjectOptions (line 33) | interface ResolvedToObjectOptions {
function applySettings (line 43) | function applySettings(options?: ToObjectOptions): ResolvedToObjectOptio...
function isJQueryLike (line 60) | function isJQueryLike(value: unknown): value is JQueryLike {
function installToObjectPlugin (line 64) | function installToObjectPlugin($: JQueryLike): void {
function maybeAutoInstallPlugin (line 132) | function maybeAutoInstallPlugin(scope: unknown = globalThis): void {
FILE: packages/jquery/src/standalone.ts
type JQueryGlobal (line 3) | interface JQueryGlobal {
FILE: packages/jquery/test/jquery.test.ts
type ToObjectOptions (line 6) | type ToObjectOptions = {
type ToObjectResult (line 16) | type ToObjectResult = unknown;
type StubCollection (line 18) | type StubCollection = {
type SelectorInput (line 25) | type SelectorInput = string | Element | Element[];
type StubJQuery (line 27) | type StubJQuery = ((input: SelectorInput) => StubCollection) & {
function createCollection (line 32) | function createCollection(elements: Element[], fn: Record<string, unknow...
function createStubJQuery (line 53) | function createStubJQuery(): StubJQuery {
method nodeCallback (line 143) | nodeCallback(node) {
FILE: packages/js2form/src/index.ts
constant ARRAY_ITEM_REGEXP (line 3) | const ARRAY_ITEM_REGEXP = /\[[0-9]+?\]$/;
constant LAST_INDEXED_ARRAY_REGEXP (line 4) | const LAST_INDEXED_ARRAY_REGEXP = /(.*)(\[)([0-9]*)(\])$/;
constant ARRAY_OF_ARRAYS_REGEXP (line 5) | const ARRAY_OF_ARRAYS_REGEXP = /\[([0-9]+)\]\[([0-9]+)\]/g;
type RootNodeInput (line 7) | type RootNodeInput = string | Node | null | undefined;
type ObjectToFormNodeCallback (line 8) | type ObjectToFormNodeCallback = ((node: Node) => unknown) | null | undef...
type ObjectToFormOptions (line 10) | interface ObjectToFormOptions {
type SupportedField (line 18) | type SupportedField = HTMLInputElement | HTMLTextAreaElement | HTMLSelec...
type SupportedFieldCollection (line 19) | type SupportedFieldCollection = SupportedField | SupportedField[];
type FieldMap (line 20) | type FieldMap = Record<string, SupportedFieldCollection>;
type ArrayIndexesMap (line 22) | type ArrayIndexesMap = Record<
type BracketMatch (line 34) | interface BracketMatch {
function findBracketMatches (line 40) | function findBracketMatches(input: string): BracketMatch[] {
function isNodeObject (line 66) | function isNodeObject(value: unknown): value is Node {
function isElementNode (line 70) | function isElementNode(node: Node): node is Element {
function nodeNameIs (line 74) | function nodeNameIs(node: Node, expected: string): boolean {
function isInputNode (line 78) | function isInputNode(node: Node): node is HTMLInputElement {
function isTextareaNode (line 82) | function isTextareaNode(node: Node): node is HTMLTextAreaElement {
function isSelectNode (line 86) | function isSelectNode(node: Node): node is HTMLSelectElement {
function getDocumentFromRoot (line 90) | function getDocumentFromRoot(rootNode: RootNodeInput, fallback?: Documen...
function resolveRootNode (line 106) | function resolveRootNode(rootNode: RootNodeInput, options: ObjectToFormO...
function isSupportedField (line 119) | function isSupportedField(node: Node): node is SupportedField {
function shouldSkipNodeAssignment (line 123) | function shouldSkipNodeAssignment(node: Node, nodeCallback: ObjectToForm...
function normalizeName (line 127) | function normalizeName(name: string, delimiter: string, arrayIndexes: Ar...
function mergeField (line 239) | function mergeField(result: FieldMap, key: string, value: SupportedField...
function getFields (line 264) | function getFields(
function setValue (line 347) | function setValue(
function toPathEntries (line 394) | function toPathEntries(data: unknown): Entry[] {
function flattenDataForForm (line 401) | function flattenDataForForm(data: unknown): Entry[] {
function mapFieldsByName (line 405) | function mapFieldsByName(
function objectToForm (line 423) | function objectToForm(rootNode: RootNodeInput, data: unknown, options: O...
function js2form (line 462) | function js2form(
FILE: packages/js2form/test/js2form.test.ts
method nodeCallback (line 85) | nodeCallback(node) {
FILE: packages/react/src/index.ts
type UseForm2jsData (line 4) | type UseForm2jsData<TSchema extends SchemaValidator | undefined> =
type UseForm2jsSubmit (line 7) | type UseForm2jsSubmit<TSchema extends SchemaValidator | undefined = unde...
type UseForm2jsOptions (line 11) | interface UseForm2jsOptions<TSchema extends SchemaValidator | undefined ...
type UseForm2jsResult (line 16) | interface UseForm2jsResult {
function buildParseOptions (line 25) | function buildParseOptions(options: ParseOptions): ParseOptions {
function useForm2js (line 43) | function useForm2js<TSchema extends SchemaValidator | undefined = undefi...
FILE: packages/react/test/react.test.ts
type HarnessProps (line 14) | interface HarnessProps<TSchema extends SchemaValidator | undefined = und...
function Harness (line 21) | function Harness<TSchema extends SchemaValidator | undefined = undefined>(
type MountedHarness (line 38) | interface MountedHarness {
function renderHarness (line 56) | function renderHarness<TSchema extends SchemaValidator | undefined = und...
function dispatchSubmit (line 107) | function dispatchSubmit(form: HTMLFormElement): void {
function submitAndFlush (line 113) | async function submitAndFlush(form: HTMLFormElement): Promise<void> {
function createDeferred (line 120) | function createDeferred<TValue = undefined>(): {
method parse (line 139) | parse(value: unknown) {
method parse (line 164) | parse() {
FILE: scripts/bump-version.mjs
constant VERSION_PARTS_RE (line 6) | const VERSION_PARTS_RE = /^(\d+)\.(\d+)\.(\d+)$/;
constant SEMVER_RE (line 7) | const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+...
constant BUMP_TYPES (line 9) | const BUMP_TYPES = new Set(["patch", "minor", "major"]);
constant DEP_FIELDS (line 10) | const DEP_FIELDS = [
function usage (line 17) | function usage(exitCode = 1) {
function nextVersion (line 24) | function nextVersion(currentVersion, bumpType) {
function expandWorkspacePattern (line 52) | async function expandWorkspacePattern(pattern) {
function readJson (line 69) | async function readJson(filePath) {
function readJsonIfExists (line 74) | async function readJsonIfExists(filePath) {
function rewriteDependencyRange (line 86) | function rewriteDependencyRange(currentRange, targetVersion) {
function main (line 104) | async function main() {
FILE: scripts/rewrite-scope.mjs
constant ROOT (line 5) | const ROOT = process.cwd();
constant SOURCE_SCOPE (line 6) | const SOURCE_SCOPE = "@form2js";
constant SKIP_DIRS (line 7) | const SKIP_DIRS = new Set([".git", "node_modules", "dist", "coverage", "...
constant TEXT_EXTENSIONS (line 8) | const TEXT_EXTENSIONS = new Set([
function parseArgs (line 20) | function parseArgs(argv) {
function assertScope (line 46) | function assertScope(scope) {
function walk (line 56) | async function walk(dir, files = []) {
function rewriteDependencyMap (line 77) | function rewriteDependencyMap(map, targetScope) {
function rewritePackageJson (line 98) | async function rewritePackageJson(filePath, targetScope, dryRun) {
function rewriteTextFile (line 127) | async function rewriteTextFile(filePath, targetScope, dryRun) {
function main (line 142) | async function main() {
FILE: test/integration/bump-version.test.ts
function writeJson (line 12) | function writeJson(filePath: string, value: unknown) {
FILE: test/integration/dependency-security.test.ts
type Lockfile (line 7) | type Lockfile = {
function readLockfile (line 14) | function readLockfile(): Lockfile {
function compareSemver (line 18) | function compareSemver(left: string, right: string): number {
function isVulnerableVersion (line 34) | function isVulnerableVersion(name: string, version: string): boolean {
FILE: test/integration/package-metadata.test.ts
type PackageManifest (line 7) | type PackageManifest = {
function readManifest (line 26) | function readManifest(packageDir: string): PackageManifest {
FILE: test/integration/workflow-node-version.test.ts
function readWorkflow (line 9) | function readWorkflow(relativePath: string): string {
Condensed preview — 131 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (351K chars).
[
{
"path": ".changeset/README.md",
"chars": 78,
"preview": "# Changesets\n\nRun `npm run changeset` to add release notes and version bumps.\n"
},
{
"path": ".changeset/config.json",
"chars": 435,
"preview": "{\n \"$schema\": \"https://unpkg.com/@changesets/config@3.0.0/schema.json\",\n \"changelog\": \"@changesets/cli/changelog\",\n \""
},
{
"path": ".github/workflows/ci.yml",
"chars": 1407,
"preview": "name: CI\n\non:\n push:\n branches:\n - master\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n checks:\n n"
},
{
"path": ".github/workflows/pages.yml",
"chars": 1407,
"preview": "name: Deploy Docs Site\n\non:\n push:\n branches:\n - master\n paths:\n - \"apps/docs/**\"\n - \"docs/**\"\n "
},
{
"path": ".github/workflows/release.yml",
"chars": 962,
"preview": "name: Release\n\non:\n push:\n branches:\n - master\n\npermissions:\n contents: write\n pull-requests: write\n id-toke"
},
{
"path": ".gitignore",
"chars": 239,
"preview": "node_modules\n.npm\n.turbo\ndist\ncoverage\n*.tsbuildinfo\n.idea\n.hg\n.hgignore\n.astro\n**/.astro/\ndocs/triage-*\n.env\n.env.local"
},
{
"path": "LICENSE",
"chars": 1058,
"preview": "Copyright (c) 2010 Maxim Vasiliev\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this "
},
{
"path": "README.md",
"chars": 12125,
"preview": "# form2js\n\n🚀 **form2js is back — modernized and actively maintained.**\n\nOriginally created in 2010, now rewritten for mo"
},
{
"path": "apps/docs/astro.config.mjs",
"chars": 279,
"preview": "import { defineConfig } from \"astro/config\";\nimport react from \"@astrojs/react\";\n\nconst base = process.env.DOCS_BASE_PAT"
},
{
"path": "apps/docs/package.json",
"chars": 1082,
"preview": "{\n \"name\": \"@form2js/docs\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"astro dev\",\n \"build\": "
},
{
"path": "apps/docs/playwright.config.ts",
"chars": 731,
"preview": "import { defineConfig, devices } from \"@playwright/test\";\n\nconst docsE2eHost = process.env.DOCS_E2E_HOST ?? \"127.0.0.1\";"
},
{
"path": "apps/docs/src/components/api/ApiPackageNav.tsx",
"chars": 1040,
"preview": "import React from \"react\";\n\nimport type { ApiPackageEntry } from \"../../lib/api-packages\";\nimport { apiPackageDocsPath }"
},
{
"path": "apps/docs/src/components/api/ApiPackageSummaryList.tsx",
"chars": 993,
"preview": "import React from \"react\";\n\nimport type { ApiPackageEntry } from \"../../lib/api-packages\";\nimport { apiPackageDocsPath }"
},
{
"path": "apps/docs/src/components/api/ApiToc.tsx",
"chars": 3605,
"preview": "import React, { useEffect, useMemo, useState } from \"react\";\n\nimport type { ApiHeading } from \"../../lib/api-docs-source"
},
{
"path": "apps/docs/src/components/landing/ApiDocsCta.astro",
"chars": 560,
"preview": "---\n// apps/docs/src/components/landing/ApiDocsCta.astro\nimport { apiDocsPath } from \"../../lib/site-routes\";\nconst base"
},
{
"path": "apps/docs/src/components/landing/Hero.astro",
"chars": 1643,
"preview": "---\n// apps/docs/src/components/landing/Hero.astro\nimport { apiDocsPath } from \"../../lib/site-routes\";\nconst basePath ="
},
{
"path": "apps/docs/src/components/landing/InstallSection.astro",
"chars": 4020,
"preview": "---\n// apps/docs/src/components/landing/InstallSection.astro\n---\n\n<section class=\"section content-wrap\" id=\"install\">\n "
},
{
"path": "apps/docs/src/components/playground/PlaygroundShell.tsx",
"chars": 6415,
"preview": "// apps/docs/src/components/playground/PlaygroundShell.tsx\nimport React, { useEffect, useState } from \"react\";\n\nimport {"
},
{
"path": "apps/docs/src/components/playground/ReactInspectorPanel.tsx",
"chars": 1249,
"preview": "import React from \"react\";\n\nimport type { ReactOutputState } from \"./types\";\n\ninterface ReactInspectorPanelProps {\n out"
},
{
"path": "apps/docs/src/components/playground/ResultPanel.tsx",
"chars": 3259,
"preview": "// apps/docs/src/components/playground/ResultPanel.tsx\nimport React from \"react\";\nimport type { OutputState } from \"./ty"
},
{
"path": "apps/docs/src/components/playground/StandardResultPanel.tsx",
"chars": 641,
"preview": "import React from \"react\";\n\nimport type { StandardOutputState } from \"./types\";\n\ninterface StandardResultPanelProps {\n "
},
{
"path": "apps/docs/src/components/playground/VariantHeader.tsx",
"chars": 835,
"preview": "// apps/docs/src/components/playground/VariantHeader.tsx\nimport React from \"react\";\nimport type { VariantDefinition, Var"
},
{
"path": "apps/docs/src/components/playground/bootstrap/jquery-bootstrap.ts",
"chars": 456,
"preview": "import $ from \"jquery\";\nimport { installToObjectPlugin } from \"@form2js/jquery\";\n\ntype JQueryWithPlugin = typeof $ & {\n "
},
{
"path": "apps/docs/src/components/playground/types.ts",
"chars": 1344,
"preview": "import type { ReactNode } from \"react\";\n\nexport type VariantKind = \"react\" | \"standard\";\nexport type OutputStatus = \"idl"
},
{
"path": "apps/docs/src/components/playground/variant-registry.ts",
"chars": 3030,
"preview": "import type { OutputState, VariantDefinition, VariantId } from \"./types\";\nimport { CoreVariant } from \"./variants/core-v"
},
{
"path": "apps/docs/src/components/playground/variants/core-variant.tsx",
"chars": 2485,
"preview": "// apps/docs/src/components/playground/variants/core-variant.tsx\nimport React, { useRef } from \"react\";\nimport { entries"
},
{
"path": "apps/docs/src/components/playground/variants/form-data-variant.tsx",
"chars": 3396,
"preview": "// apps/docs/src/components/playground/variants/form-data-variant.tsx\nimport React, { useRef } from \"react\";\nimport { fo"
},
{
"path": "apps/docs/src/components/playground/variants/form-variant.tsx",
"chars": 4133,
"preview": "// apps/docs/src/components/playground/variants/form-variant.tsx\nimport React, { useRef } from \"react\";\nimport { form2js"
},
{
"path": "apps/docs/src/components/playground/variants/jquery-variant.tsx",
"chars": 4046,
"preview": "// apps/docs/src/components/playground/variants/jquery-variant.tsx\nimport React, { useEffect, useRef } from \"react\";\nimp"
},
{
"path": "apps/docs/src/components/playground/variants/js2form-variant.tsx",
"chars": 4268,
"preview": "// apps/docs/src/components/playground/variants/js2form-variant.tsx\nimport React, { useRef } from \"react\";\nimport { form"
},
{
"path": "apps/docs/src/components/playground/variants/react-variant.tsx",
"chars": 6375,
"preview": "// apps/docs/src/components/playground/variants/react-variant.tsx\nimport React, { useEffect, useRef, useState } from \"re"
},
{
"path": "apps/docs/src/env.d.ts",
"chars": 39,
"preview": "/// <reference types=\"astro/client\" />\n"
},
{
"path": "apps/docs/src/layouts/ApiDocsLayout.astro",
"chars": 1374,
"preview": "---\nimport { ApiPackageNav } from \"../components/api/ApiPackageNav\";\nimport { ApiToc } from \"../components/api/ApiToc\";\n"
},
{
"path": "apps/docs/src/layouts/DesignShell.astro",
"chars": 2010,
"preview": "---\n// apps/docs/src/layouts/DesignShell.astro\nimport { apiDocsPath } from \"../lib/site-routes\";\nimport \"../styles/landi"
},
{
"path": "apps/docs/src/layouts/DocsShell.astro",
"chars": 1020,
"preview": "---\nimport \"../styles/global.css\";\nimport { homepagePath, migrationGuidePath } from \"../lib/site-routes\";\n\ninterface Pro"
},
{
"path": "apps/docs/src/lib/api-docs-source.ts",
"chars": 5923,
"preview": "import { readFile } from \"node:fs/promises\";\nimport rehypeStringify from \"rehype-stringify\";\nimport remarkGfm from \"rema"
},
{
"path": "apps/docs/src/lib/api-packages.ts",
"chars": 2140,
"preview": "import path from \"node:path\";\n\nexport type ApiPackageSlug =\n | \"core\"\n | \"dom\"\n | \"form-data\"\n | \"react\"\n | \"js2for"
},
{
"path": "apps/docs/src/lib/site-routes.ts",
"chars": 882,
"preview": "function normalizeBase(basePath: string): string {\n if (basePath === \"/\") {\n return \"/\";\n }\n\n const withLeadingSla"
},
{
"path": "apps/docs/src/pages/api/[package].astro",
"chars": 808,
"preview": "---\nimport ApiDocsLayout from \"../../layouts/ApiDocsLayout.astro\";\nimport { apiPackages, type ApiPackageEntry } from \".."
},
{
"path": "apps/docs/src/pages/api/index.astro",
"chars": 678,
"preview": "---\nimport { ApiPackageSummaryList } from \"../../components/api/ApiPackageSummaryList\";\nimport ApiDocsLayout from \"../.."
},
{
"path": "apps/docs/src/pages/index.astro",
"chars": 755,
"preview": "---\n// apps/docs/src/pages/1.astro\nimport \"../styles/themes/dark-brutalism.css\";\nimport DesignShell from \"../layouts/Des"
},
{
"path": "apps/docs/src/pages/migrate.astro",
"chars": 1107,
"preview": "---\nimport path from \"node:path\";\n\nimport { ApiToc } from \"../components/api/ApiToc\";\nimport { loadApiDocsSource } from "
},
{
"path": "apps/docs/src/styles/global.css",
"chars": 4696,
"preview": ":root {\n color-scheme: dark;\n --bg: #0c0e14;\n --panel: #12151e;\n --panel-2: #1a1d2b;\n --text: #e4e6ef;\n --muted: #"
},
{
"path": "apps/docs/src/styles/landing.css",
"chars": 4548,
"preview": "/* apps/docs/src/styles/landing.css */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\n"
},
{
"path": "apps/docs/src/styles/playground.css",
"chars": 5473,
"preview": "/* apps/docs/src/styles/playground.css */\n\n/* ── Shell ── */\n.pg-shell {\n border: 1px solid var(--c-border);\n overflow"
},
{
"path": "apps/docs/src/styles/themes/dark-brutalism.css",
"chars": 420,
"preview": "/* apps/docs/src/styles/themes/dark-brutalism.css */\n:root {\n color-scheme: dark;\n --c-bg: #0c0c0c;\n --c-panel: #1111"
},
{
"path": "apps/docs/src/styles/themes/editorial.css",
"chars": 442,
"preview": "/* apps/docs/src/styles/themes/editorial.css */\n:root {\n color-scheme: light;\n --c-bg: #faf9f6;\n --c-panel: #f0ece0;\n"
},
{
"path": "apps/docs/src/styles/themes/linear-dark.css",
"chars": 515,
"preview": "/* apps/docs/src/styles/themes/linear-dark.css */\n:root {\n color-scheme: dark;\n --c-bg: #060609;\n --c-panel: rgba(255"
},
{
"path": "apps/docs/src/styles/themes/outrun-sunset.css",
"chars": 471,
"preview": "/* apps/docs/src/styles/themes/outrun-sunset.css */\n:root {\n color-scheme: dark;\n --c-bg: #050a1a;\n --c-panel: #080f2"
},
{
"path": "apps/docs/src/styles/themes/terminal-noir.css",
"chars": 419,
"preview": "/* apps/docs/src/styles/themes/terminal-noir.css */\n:root {\n color-scheme: dark;\n --c-bg: #030403;\n --c-panel: #040c0"
},
{
"path": "apps/docs/test/api-docs-page.test.tsx",
"chars": 2176,
"preview": "// @vitest-environment jsdom\n\nimport React from \"react\";\nimport { renderToStaticMarkup } from \"react-dom/server\";\nimport"
},
{
"path": "apps/docs/test/api-docs-source.test.ts",
"chars": 5369,
"preview": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport "
},
{
"path": "apps/docs/test/data-variants.test.tsx",
"chars": 6050,
"preview": "// apps/docs/test/data-variants.test.tsx\n// @vitest-environment jsdom\n\nimport React, { act } from \"react\";\nimport { crea"
},
{
"path": "apps/docs/test/docs-pipeline.test.ts",
"chars": 747,
"preview": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport "
},
{
"path": "apps/docs/test/docs-root-scripts.test.ts",
"chars": 822,
"preview": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport "
},
{
"path": "apps/docs/test/homepage-shell.test.ts",
"chars": 1934,
"preview": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport "
},
{
"path": "apps/docs/test/playground-shell.test.tsx",
"chars": 9961,
"preview": "// @vitest-environment jsdom\n\nimport React, { act } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport "
},
{
"path": "apps/docs/test/playground-styles.test.ts",
"chars": 549,
"preview": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport { describe, expect, it } from \"vitest\";\n\nc"
},
{
"path": "apps/docs/test/react-variant.test.tsx",
"chars": 7796,
"preview": "// apps/docs/test/react-variant.test.tsx\n// @vitest-environment jsdom\n\nimport React, { act } from \"react\";\nimport { crea"
},
{
"path": "apps/docs/test/site-routes.test.ts",
"chars": 941,
"preview": "import { describe, expect, it } from \"vitest\";\n\nimport {\n apiDocsPath,\n apiPackageDocsPath,\n homepagePath,\n homepage"
},
{
"path": "apps/docs/test/standard-variants.test.tsx",
"chars": 6513,
"preview": "// apps/docs/test/standard-variants.test.tsx\n// @vitest-environment jsdom\n\nimport React, { act } from \"react\";\nimport { "
},
{
"path": "apps/docs/test/variant-registry.test.ts",
"chars": 878,
"preview": "import { describe, expect, it } from \"vitest\";\n\nimport { VARIANT_IDS, variantsById } from \"../src/components/playground/"
},
{
"path": "apps/docs/test-e2e/api-docs.spec.ts",
"chars": 896,
"preview": "import { expect, test } from \"@playwright/test\";\n\ntest(\"api docs index links to package pages and package toc anchors wo"
},
{
"path": "apps/docs/test-e2e/homepage.spec.ts",
"chars": 1736,
"preview": "import { expect, test } from \"@playwright/test\";\n\ntest(\"homepage switcher supports keyboard navigation and preserves par"
},
{
"path": "apps/docs/tsconfig.json",
"chars": 140,
"preview": "{\n \"extends\": \"astro/tsconfigs/strict\",\n \"include\": [\n \".astro/types.d.ts\",\n \"src/**/*\",\n \"test/**/*\",\n \"t"
},
{
"path": "apps/docs/vitest.config.ts",
"chars": 173,
"preview": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n test: {\n include: [\"test/**/*.{test,sp"
},
{
"path": "changeset/config.json",
"chars": 76,
"preview": "{\n \"note\": \"Canonical changesets config lives at .changeset/config.json\"\n}\n"
},
{
"path": "docs/api-core.md",
"chars": 4093,
"preview": "# @form2js/core\n\n`@form2js/core` is the path parsing engine behind the rest of the package family. Use it when you alrea"
},
{
"path": "docs/api-dom.md",
"chars": 4663,
"preview": "# @form2js/dom\n\n`@form2js/dom` solves the browser side of the problem: walk a form or DOM subtree, extract the submitted"
},
{
"path": "docs/api-form-data.md",
"chars": 2681,
"preview": "# @form2js/form-data\n\n`@form2js/form-data` is the server-friendly adapter for the same parsing rules used by the DOM pac"
},
{
"path": "docs/api-index.md",
"chars": 1711,
"preview": "# form2js API Reference\n\nThis section is for developers who want the exact API surface of the current `@form2js/*` packa"
},
{
"path": "docs/api-jquery.md",
"chars": 2788,
"preview": "# @form2js/jquery\n\n`@form2js/jquery` is the legacy-friendly adapter for projects that still rely on jQuery forms. Use it"
},
{
"path": "docs/api-js2form.md",
"chars": 3841,
"preview": "# @form2js/js2form\n\n`@form2js/js2form` moves data in the opposite direction: take a nested object and write it into matc"
},
{
"path": "docs/api-react.md",
"chars": 2996,
"preview": "# @form2js/react\n\n`@form2js/react` wraps the form-data parser in a React submit hook. Use it when you want a single `onS"
},
{
"path": "docs/migrate.md",
"chars": 6009,
"preview": "# Migrate from Legacy form2js\n\nIf you built around the old single `form2js` script or the archived jQuery plugin flow, t"
},
{
"path": "eslint.config.js",
"chars": 1840,
"preview": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport js from \"@eslint/js\";\nimport tseslint fro"
},
{
"path": "package.json",
"chars": 2582,
"preview": "{\n \"name\": \"form2js-monorepo\",\n \"private\": true,\n \"type\": \"module\",\n \"packageManager\": \"npm@11.6.2\",\n \"workspaces\":"
},
{
"path": "packages/core/CHANGELOG.md",
"chars": 2236,
"preview": "# @form2js/core\n\n## 3.3.0\n\n### Minor Changes\n\n- Restore Rails-style bracket path compatibility across parsing and form p"
},
{
"path": "packages/core/README.md",
"chars": 480,
"preview": "# @form2js/core\n\nCore path parsing and object transformation logic for form-shaped data.\n\n## Install\n\n```bash\nnpm instal"
},
{
"path": "packages/core/package.json",
"chars": 1178,
"preview": "{\n \"name\": \"@form2js/core\",\n \"version\": \"3.4.0\",\n \"description\": \"Core path parsing and object transformation logic f"
},
{
"path": "packages/core/src/index.ts",
"chars": 10955,
"preview": "import type {\n Entry,\n EntryInput,\n EntryValue,\n InferSchemaOutput,\n MergeContext,\n MergeOptions,\n NameValuePair,"
},
{
"path": "packages/core/src/types.ts",
"chars": 1061,
"preview": "export type EntryValue = unknown;\n\nexport interface Entry {\n key: string;\n value: EntryValue;\n}\n\nexport interface Name"
},
{
"path": "packages/core/test/core.test.ts",
"chars": 8297,
"preview": "import { describe, expect, it } from \"vitest\";\nimport { entriesToObject, objectToEntries, processNameValues, setPathValu"
},
{
"path": "packages/core/tsconfig.json",
"chars": 158,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"lib\": [\"ES2020\"]\n },\n \"include\": [\"src/**/*.ts\""
},
{
"path": "packages/core/tsup.config.ts",
"chars": 206,
"preview": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n entry: {\n index: \"src/index.ts\"\n },\n format: "
},
{
"path": "packages/dom/CHANGELOG.md",
"chars": 2295,
"preview": "# @form2js/dom\n\n## 3.2.0\n\n### Minor Changes\n\n- Restore Rails-style bracket path compatibility across parsing and form po"
},
{
"path": "packages/dom/README.md",
"chars": 397,
"preview": "# @form2js/dom\n\nParse browser form controls into structured objects.\n\n## Install\n\n```bash\nnpm install @form2js/dom\n```\n\n"
},
{
"path": "packages/dom/package.json",
"chars": 1186,
"preview": "{\n \"name\": \"@form2js/dom\",\n \"version\": \"3.4.0\",\n \"description\": \"DOM extraction adapters for form2js.\",\n \"license\": "
},
{
"path": "packages/dom/src/index.ts",
"chars": 9507,
"preview": "import { entriesToObject, type Entry, type ObjectTree, type ParseOptions } from \"@form2js/core\";\n\nexport interface NodeC"
},
{
"path": "packages/dom/src/standalone.ts",
"chars": 401,
"preview": "import { SKIP_NODE, form2js, formToObject } from \"./index\";\n\ninterface DomStandaloneGlobals {\n formToObject?: typeof fo"
},
{
"path": "packages/dom/test/dom.test.ts",
"chars": 10862,
"preview": "// @vitest-environment jsdom\n\nimport { describe, expect, it } from \"vitest\";\nimport { SKIP_NODE, extractPairs, form2js, "
},
{
"path": "packages/dom/tsconfig.json",
"chars": 181,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]\n },\n \"i"
},
{
"path": "packages/dom/tsup.config.ts",
"chars": 409,
"preview": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig([\n {\n entry: {\n index: \"src/index.ts\"\n },\n"
},
{
"path": "packages/form-data/CHANGELOG.md",
"chars": 2287,
"preview": "# @form2js/form-data\n\n## 3.2.3\n\n### Patch Changes\n\n- Updated dependencies\n - @form2js/core@3.3.0\n\n## 3.2.2\n\n### Patch C"
},
{
"path": "packages/form-data/README.md",
"chars": 465,
"preview": "# @form2js/form-data\n\nConvert `FormData` and entry lists into structured objects.\n\n## Install\n\n```bash\nnpm install @form"
},
{
"path": "packages/form-data/package.json",
"chars": 1101,
"preview": "{\n \"name\": \"@form2js/form-data\",\n \"version\": \"3.4.0\",\n \"description\": \"FormData adapters for form2js.\",\n \"license\": "
},
{
"path": "packages/form-data/src/index.ts",
"chars": 1642,
"preview": "import {\n entriesToObject as coreEntriesToObject,\n type EntryInput,\n type InferSchemaOutput,\n type ObjectTree,\n typ"
},
{
"path": "packages/form-data/test/form-data.test.ts",
"chars": 2328,
"preview": "// @vitest-environment jsdom\n\nimport { describe, expect, it } from \"vitest\";\nimport { entriesToObject, formDataToObject "
},
{
"path": "packages/form-data/tsconfig.json",
"chars": 181,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]\n },\n \"i"
},
{
"path": "packages/form-data/tsup.config.ts",
"chars": 206,
"preview": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n entry: {\n index: \"src/index.ts\"\n },\n format: "
},
{
"path": "packages/jquery/CHANGELOG.md",
"chars": 2008,
"preview": "# @form2js/jquery\n\n## 3.1.5\n\n### Patch Changes\n\n- Updated dependencies\n - @form2js/dom@3.2.0\n\n## 3.1.4\n\n### Patch Chang"
},
{
"path": "packages/jquery/README.md",
"chars": 457,
"preview": "# @form2js/jquery\n\nUse form2js through a jQuery plugin adapter.\n\n## Install\n\n```bash\nnpm install @form2js/jquery jquery\n"
},
{
"path": "packages/jquery/package.json",
"chars": 1309,
"preview": "{\n \"name\": \"@form2js/jquery\",\n \"version\": \"3.4.0\",\n \"description\": \"jQuery plugin adapter for form2js.\",\n \"license\":"
},
{
"path": "packages/jquery/src/index.ts",
"chars": 3782,
"preview": "import { form2js, type FormToObjectNodeCallback, type RootNodeInput } from \"@form2js/dom\";\n\nexport type ToObjectMode = \""
},
{
"path": "packages/jquery/src/standalone.ts",
"chars": 298,
"preview": "import { maybeAutoInstallPlugin } from \"./index\";\n\ninterface JQueryGlobal {\n jQuery?: unknown;\n}\n\nconst scope = globalT"
},
{
"path": "packages/jquery/test/jquery.test.ts",
"chars": 4813,
"preview": "// @vitest-environment jsdom\n\nimport { describe, expect, it } from \"vitest\";\nimport { installToObjectPlugin, maybeAutoIn"
},
{
"path": "packages/jquery/tsconfig.json",
"chars": 224,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n \"typ"
},
{
"path": "packages/jquery/tsup.config.ts",
"chars": 438,
"preview": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig([\n {\n entry: {\n index: \"src/index.ts\"\n },\n"
},
{
"path": "packages/js2form/CHANGELOG.md",
"chars": 2299,
"preview": "# @form2js/js2form\n\n## 3.2.0\n\n### Minor Changes\n\n- Restore Rails-style bracket path compatibility across parsing and for"
},
{
"path": "packages/js2form/README.md",
"chars": 454,
"preview": "# @form2js/js2form\n\nPopulate form controls from plain object data.\n\n## Install\n\n```bash\nnpm install @form2js/js2form\n```"
},
{
"path": "packages/js2form/package.json",
"chars": 1081,
"preview": "{\n \"name\": \"@form2js/js2form\",\n \"version\": \"3.4.0\",\n \"description\": \"Populate DOM forms from plain objects.\",\n \"lice"
},
{
"path": "packages/js2form/src/index.ts",
"chars": 13144,
"preview": "import { objectToEntries, type Entry } from \"@form2js/core\";\n\nconst ARRAY_ITEM_REGEXP = /\\[[0-9]+?\\]$/;\nconst LAST_INDEX"
},
{
"path": "packages/js2form/test/js2form.test.ts",
"chars": 13436,
"preview": "// @vitest-environment jsdom\n\nimport { describe, expect, it } from \"vitest\";\nimport {\n flattenDataForForm,\n js2form,\n "
},
{
"path": "packages/js2form/tsconfig.json",
"chars": 181,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]\n },\n \"i"
},
{
"path": "packages/js2form/tsup.config.ts",
"chars": 206,
"preview": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n entry: {\n index: \"src/index.ts\"\n },\n format: "
},
{
"path": "packages/react/CHANGELOG.md",
"chars": 881,
"preview": "# @form2js/react\n\n## 3.2.3\n\n### Patch Changes\n\n- @form2js/form-data@3.2.3\n\n## 3.2.2\n\n### Patch Changes\n\n- Republish the "
},
{
"path": "packages/react/README.md",
"chars": 739,
"preview": "# @form2js/react\n\nHandle form submission with a small React hook built on form2js parsing.\n\n## Install\n\n```bash\nnpm inst"
},
{
"path": "packages/react/package.json",
"chars": 1296,
"preview": "{\n \"name\": \"@form2js/react\",\n \"version\": \"3.4.0\",\n \"description\": \"React hook adapter for form2js.\",\n \"license\": \"MI"
},
{
"path": "packages/react/src/index.ts",
"chars": 3053,
"preview": "import { formDataToObject, type InferSchemaOutput, type ObjectTree, type ParseOptions, type SchemaValidator } from \"@for"
},
{
"path": "packages/react/test/react.test.ts",
"chars": 8143,
"preview": "// @vitest-environment jsdom\n\nimport React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/clien"
},
{
"path": "packages/react/tsconfig.json",
"chars": 181,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]\n },\n \"i"
},
{
"path": "packages/react/tsup.config.ts",
"chars": 206,
"preview": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n entry: {\n index: \"src/index.ts\"\n },\n format: "
},
{
"path": "scripts/bump-version.mjs",
"chars": 5513,
"preview": "#!/usr/bin/env node\n\nimport { readFile, writeFile, readdir } from \"node:fs/promises\";\nimport path from \"node:path\";\n\ncon"
},
{
"path": "scripts/rewrite-scope.mjs",
"chars": 4291,
"preview": "#!/usr/bin/env node\nimport { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\n\nconst ROOT = process.cwd();"
},
{
"path": "test/integration/bump-version.test.ts",
"chars": 2069,
"preview": "import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport os from \"node:os\";\nimport path fro"
},
{
"path": "test/integration/dependency-security.test.ts",
"chars": 3035,
"preview": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport "
},
{
"path": "test/integration/form-flow.test.ts",
"chars": 1572,
"preview": "// @vitest-environment jsdom\n\nimport { describe, expect, it } from \"vitest\";\nimport { formToObject } from \"@form2js/dom\""
},
{
"path": "test/integration/package-metadata.test.ts",
"chars": 1710,
"preview": "import { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:ur"
},
{
"path": "test/integration/workflow-node-version.test.ts",
"chars": 895,
"preview": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport "
},
{
"path": "tsconfig.base.json",
"chars": 916,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": {\n \"@form2js/core\": [\"packages/core/src/index.ts\"],\n "
},
{
"path": "turbo.json",
"chars": 516,
"preview": "{\n \"$schema\": \"https://turbo.build/schema.json\",\n \"tasks\": {\n \"build\": {\n \"dependsOn\": [\n \"^build\"\n "
}
]
About this extraction
This page contains the full source code of the maxatwork/form2js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 131 files (316.8 KB), approximately 86.9k tokens, and a symbol index with 282 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.