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.