From 2559d7f3c269560b15d0a8225a594218171cf951 Mon Sep 17 00:00:00 2001
From: maxatwork <397263+maxatwork@users.noreply.github.com>
Date: Mon, 23 Mar 2026 20:15:42 +0100
Subject: [PATCH] docs: add legacy migration guide
---
README.md | 17 +++
apps/docs/src/layouts/DocsShell.astro | 3 +-
apps/docs/src/lib/api-docs-source.ts | 11 +-
apps/docs/src/lib/site-routes.ts | 4 +
apps/docs/src/pages/migrate.astro | 32 ++++++
apps/docs/src/styles/global.css | 4 +
apps/docs/test/api-docs-source.test.ts | 20 ++++
apps/docs/test/homepage-shell.test.ts | 10 ++
apps/docs/test/site-routes.test.ts | 7 +-
docs/migrate.md | 145 +++++++++++++++++++++++++
10 files changed, 249 insertions(+), 4 deletions(-)
create mode 100644 apps/docs/src/pages/migrate.astro
create mode 100644 docs/migrate.md
diff --git a/README.md b/README.md
index dc5a8c5..d9a60fc 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,8 @@ Originally created in 2010, now rewritten for modern JavaScript, TypeScript, ESM
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.
@@ -15,8 +17,23 @@ It is not a serializer, not an ORM, and not a new religion. It just does this on
## 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 |
diff --git a/apps/docs/src/layouts/DocsShell.astro b/apps/docs/src/layouts/DocsShell.astro
index c26b6ef..df6303a 100644
--- a/apps/docs/src/layouts/DocsShell.astro
+++ b/apps/docs/src/layouts/DocsShell.astro
@@ -1,6 +1,6 @@
---
import "../styles/global.css";
-import { homepagePath } from "../lib/site-routes";
+import { homepagePath, migrationGuidePath } from "../lib/site-routes";
interface Props {
title?: string;
@@ -26,6 +26,7 @@ const basePath = import.meta.env.BASE_URL;
diff --git a/apps/docs/src/lib/api-docs-source.ts b/apps/docs/src/lib/api-docs-source.ts
index d30c1d5..f0eccab 100644
--- a/apps/docs/src/lib/api-docs-source.ts
+++ b/apps/docs/src/lib/api-docs-source.ts
@@ -9,7 +9,12 @@ import {
apiIndexMarkdownPath,
getApiPackageByMarkdownBasename
} from "./api-packages";
-import { apiDocsPath, apiPackageDocsPath, homepagePath } from "./site-routes";
+import {
+ apiDocsPath,
+ apiPackageDocsPath,
+ homepagePath,
+ migrationGuidePath
+} from "./site-routes";
export interface ApiHeading {
depth: 2 | 3;
@@ -77,6 +82,10 @@ function rewriteMarkdownLink(url: string, basePath: string): string {
return `${homepagePath(basePath)}${suffix}`;
}
+ if (normalizedPathname === "migrate.md") {
+ return `${migrationGuidePath(basePath)}${suffix}`;
+ }
+
if (normalizedPathname === "api.md" || normalizedPathname === "api-index.md") {
return `${apiDocsPath(basePath)}${suffix}`;
}
diff --git a/apps/docs/src/lib/site-routes.ts b/apps/docs/src/lib/site-routes.ts
index d64d471..b4aeba7 100644
--- a/apps/docs/src/lib/site-routes.ts
+++ b/apps/docs/src/lib/site-routes.ts
@@ -11,6 +11,10 @@ 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/`;
}
diff --git a/apps/docs/src/pages/migrate.astro b/apps/docs/src/pages/migrate.astro
new file mode 100644
index 0000000..101b196
--- /dev/null
+++ b/apps/docs/src/pages/migrate.astro
@@ -0,0 +1,32 @@
+---
+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")
+});
+---
+
+
+
+
+
+
Migration Guide
+
{migrationSource.title}
+ {migrationSource.introHtml ? (
+
+ ) : null}
+
+
+
+ {migrationSource.headings.length > 0 ? (
+
+ ) : null}
+
+
diff --git a/apps/docs/src/styles/global.css b/apps/docs/src/styles/global.css
index 99416ed..a76734d 100644
--- a/apps/docs/src/styles/global.css
+++ b/apps/docs/src/styles/global.css
@@ -116,6 +116,10 @@ a {
gap: 2rem;
}
+.api-docs--content-sidebar {
+ grid-template-columns: minmax(0, 1fr) 18rem;
+}
+
.api-docs__nav,
.api-docs__sidebar {
position: sticky;
diff --git a/apps/docs/test/api-docs-source.test.ts b/apps/docs/test/api-docs-source.test.ts
index a8d9455..bd95561 100644
--- a/apps/docs/test/api-docs-source.test.ts
+++ b/apps/docs/test/api-docs-source.test.ts
@@ -14,6 +14,7 @@ const apiFormDataMarkdown = readFileSync(path.resolve(testDir, "../../../docs/ap
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", () => {
@@ -115,4 +116,23 @@ Text.
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" })
+ ])
+ );
+ });
});
diff --git a/apps/docs/test/homepage-shell.test.ts b/apps/docs/test/homepage-shell.test.ts
index 668bc92..77a3754 100644
--- a/apps/docs/test/homepage-shell.test.ts
+++ b/apps/docs/test/homepage-shell.test.ts
@@ -10,6 +10,8 @@ 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", () => {
@@ -27,4 +29,12 @@ describe("docs homepage shell", () => {
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");
+ });
});
diff --git a/apps/docs/test/site-routes.test.ts b/apps/docs/test/site-routes.test.ts
index 8eb1721..8af0c12 100644
--- a/apps/docs/test/site-routes.test.ts
+++ b/apps/docs/test/site-routes.test.ts
@@ -4,12 +4,15 @@ import {
apiDocsPath,
apiPackageDocsPath,
homepagePath,
- homepageVariantPath
+ homepageVariantPath,
+ migrationGuidePath
} from "../src/lib/site-routes";
describe("site routes", () => {
- it("builds homepage and api paths under a base path", () => {
+ 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/");
diff --git a/docs/migrate.md b/docs/migrate.md
new file mode 100644
index 0000000..87c8e82
--- /dev/null
+++ b/docs/migrate.md
@@ -0,0 +1,145 @@
+# 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
+
+
+```
+
+## 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
+
+
+
+```
+
+## 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.