@decocms/start 2.19.0 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/deco-to-tanstack-migration/SKILL.md +1 -0
- package/.agents/skills/deco-to-tanstack-migration/references/htmx-rewrite.md +527 -0
- package/.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +63 -12
- package/MIGRATION_TOOLING_PLAN.md +142 -31
- package/package.json +3 -2
- package/scripts/htmx-analyze.ts +226 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
- package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
- package/scripts/migrate/post-cleanup/rules.ts +73 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +108 -0
- package/scripts/migrate-post-cleanup.ts +3 -2
|
@@ -688,40 +688,151 @@ Releases shipped from Wave 6:
|
|
|
688
688
|
- `@decocms/apps@1.7.0` — adds `vtex/hooks/createUseCart` factory
|
|
689
689
|
- `@decocms/start@2.8.0` (compile phase) → `2.9.0` (template shim) → `2.10.0` (per-site config)
|
|
690
690
|
|
|
691
|
-
### Wave 12 (kicked off 2026-05-01 after D1–D5 sign-off) — Priority 1 (framework + commerce)
|
|
691
|
+
### Wave 12 (kicked off 2026-05-01 after D1–D5 sign-off) — Priority 1 (framework + commerce) — ✅ **COMPLETE**
|
|
692
692
|
|
|
693
693
|
After surfacing als-storefront as the third migration target (heavy on
|
|
694
694
|
htmx, ~120 hx-* files, prior als-tanstack attempt thrown away), the
|
|
695
|
-
"wait for 3rd site" deferrals collapse. Wave 12
|
|
696
|
-
that als + casaevideo + baggagio
|
|
697
|
-
audit `--fix` work D3 forces us into.
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
- **W12-B** apps-start:
|
|
703
|
-
|
|
704
|
-
- **
|
|
705
|
-
-
|
|
706
|
-
- **W12-
|
|
707
|
-
|
|
708
|
-
- **W12-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
695
|
+
"wait for 3rd site" deferrals collapse. Wave 12 shipped the abstractions
|
|
696
|
+
that als + casaevideo + baggagio had already justified, plus the
|
|
697
|
+
audit `--fix` work D3 forces us into. **9 PRs across `deco-start` and
|
|
698
|
+
`apps-start`, all merged.**
|
|
699
|
+
|
|
700
|
+
**Shipped PRs:**
|
|
701
|
+
|
|
702
|
+
- **W12-A + W12-B** [`apps-start#33`](https://github.com/decocms/apps-start/pull/33) — `feat(vtex/hooks): add createUseUser + createUseWishlist factories` ✅ **MERGED**.
|
|
703
|
+
Mirrors the `createUseCart` shape: invoke-driven legacy state machine, signal-shaped public API (`.value`), independent instances per call. `createUseWishlist` also exposes the `legacyAddArgsToCanonical` and `findWishlistEntry` helpers so site code can keep its old `productId`/`productGroupId` swap convention while routing through the canonical `vtex.actions.{addToWishlist, removeFromWishlist}` signature. New unit tests assert factory shape, instance independence, the arg-swap convention, and the entry-finder helper.
|
|
704
|
+
- **(Lint unblock)** [`apps-start#34`](https://github.com/decocms/apps-start/pull/34) — `chore(lint): tighten shopify storefront.graphql.gen.ts types + biome formatting` ✅ **MERGED**.
|
|
705
|
+
Cleared pre-existing `noExplicitAny` failures in `shopify/utils/storefront/storefront.graphql.gen.ts` plus formatting drift in `vtex/__tests__/client-segment-cookie.test.ts` so subsequent Wave 12 PRs in `apps-start` could land on a green CI baseline. Replaced loose `any` types with a structured `ProductFilter` shape (derived from real consumers) and `unknown` elsewhere; ran `biome check --write` to fix import order + formatting.
|
|
706
|
+
- **W12-C** [`deco-start#123`](https://github.com/decocms/deco-start/pull/123) — `feat(migrate): D3 — generated stubs throw at runtime` ✅ **MERGED**, released as `@decocms/start@2.16.0`.
|
|
707
|
+
Implements **D3** verbatim. The migration-time stubs in `lib-utils.ts` for `toProduct`, `getISCookiesFromBag`, `getSegmentFromBag`, `withSegmentCookie` no longer return identity-cast values, empty objects, or empty `Headers` — they now `throw new Error(STUB_MSG)` with a per-symbol message that names the canonical replacement, the canonical signature, and the `deco-post-cleanup --fix` invocation. Tests assert the throwing bodies + that other functional helpers (e.g. `parseCookie`, `isFilterParam`) are untouched.
|
|
708
|
+
- **W12-D + W12-E** [`deco-start#124`](https://github.com/decocms/deco-start/pull/124) — `feat(audit): vtex-shim-regression --fix for swap-able symbols` ✅ **MERGED**, released as `@decocms/start@2.17.0`.
|
|
709
|
+
Auto-fixes the swap subset of the regression rule: when every imported symbol from a `~/lib/vtex-*` shim has a `kind: "swap"` hint pointing to the same canonical module, the rule rewrites the `from "..."` clause to canonical and leaves the named-import list (including `as`-aliases) verbatim. Mixed swap + refactor surfaces, and shims that mix stubs with real impls (e.g. `isFilterParam`), are deliberately left for manual fix. 6 new tests + skill doc § 5 update.
|
|
710
|
+
- **W12-i** [`deco-start#125`](https://github.com/decocms/deco-start/pull/125) — `feat(migrate): scaffold useUser + useWishlist as factory shims (vtex)` ✅ **MERGED**.
|
|
711
|
+
Updates the migration script's `hooks.ts` template so freshly migrated VTEX sites get 3-line factory shims (`export const { useUser, resetUser } = createUseUser({ invoke })`) instead of 200-LOC singletons that were copy-pasted by the old migration. Non-VTEX sites still get the legacy stubs but with docstrings pointing at the factories for parity context. 4 new tests cover both branches and a line-count budget.
|
|
712
|
+
- **W12-F** [`deco-start#127`](https://github.com/decocms/deco-start/pull/127) — `feat(audit): obsolete-vite-plugins --fix` ✅ **MERGED**.
|
|
713
|
+
JS-aware applyFix for the rule. Walks `vite.config.ts` with a brace-counter that skips strings, template literals (including `${...}` interpolation), and line/block comments, so nested `{}` inside `config()` / `load()` / `resolveId()` bodies do not throw off boundary detection. Removes the inline literal + trailing `,\n` + the contiguous block of `// ...` comments immediately attached above. Idempotent. 7 new tests + smoke verified against real casaevideo `vite.config.ts` (162 LOC → both plugins gone, 2503 bytes / ~74 LOC removed, structurally identical to baggagio's already-clean shape, post-fix audit returns 0 findings).
|
|
714
|
+
- **W12-G** [`apps-start#35`](https://github.com/decocms/apps-start/pull/35) — `docs: add AGENTS.md cross-linking the canonical migration policy` ✅ **MERGED**.
|
|
715
|
+
Adds an AGENTS.md to `apps-start` so any agent or contributor opening that repo knows the canonical migration policy lives in `decocms/deco-start` and what D1–D5 mean specifically inside `apps-start` (especially D4: site-local apps live in the *site*, not in `apps-start`). Architecture overview + cross-link table.
|
|
716
|
+
- **W12-H** [`deco-start#126`](https://github.com/decocms/deco-start/pull/126) — `feat(migrate): scaffold migration-tooling-policy pointer rule` ✅ **MERGED**, released as `@decocms/start@2.18.0`.
|
|
717
|
+
Migration scaffold phase now writes `.cursor/rules/migration-tooling-policy.mdc` into every newly migrated site. The pointer is `alwaysApply: true`, links to the canonical rule and plan in `decocms/deco-start`, includes a one-line-per-decision D1–D5 table scoped to the site, and points at the `deco-post-cleanup --fix` / `--strict` commands. Length budget under 3 KB so it stays a pointer, not a copy. 8 new tests.
|
|
718
|
+
|
|
719
|
+
Wave 12 ships in priority-1 order; Wave 13 starts now.
|
|
720
|
+
|
|
721
|
+
### Wave 12 — discoveries
|
|
722
|
+
|
|
723
|
+
- **D3 + audit `--fix` is a closed loop, not an either/or.** The
|
|
724
|
+
symmetry that `--fix` for swap-able stubs (W12-D/E) combined with
|
|
725
|
+
throwing stubs (W12-C) produces is significant: the moment a
|
|
726
|
+
migrated site runs anything that hits a stub'd symbol, it throws
|
|
727
|
+
with an actionable error pointing at the canonical replacement; the
|
|
728
|
+
same moment, `--fix` knows what `from "..."` clause to rewrite. The
|
|
729
|
+
user no longer has a "silent regression" failure mode.
|
|
730
|
+
- **Per-symbol fix-hint table now has 5 consumers.** It's read by:
|
|
731
|
+
the rule's prose `fix:` field, the rule's `meta.fixHints`
|
|
732
|
+
structured payload, the runtime stub error message, the `--fix`
|
|
733
|
+
rewriter (selects swap candidates), and the skill doc table. Adding
|
|
734
|
+
a 5th, 6th, Nth stub means appending one entry to `STUB_FIX_HINTS`
|
|
735
|
+
— every consumer picks up the new symbol for free.
|
|
736
|
+
- **Site-level policy enforcement at scaffold time, not runtime.**
|
|
737
|
+
W12-H ships the canonical D1–D5 policy *as a pointer* into every
|
|
738
|
+
new site. Cursor sessions in those sites load the rule with
|
|
739
|
+
`alwaysApply: true`, so they know the policy without us having to
|
|
740
|
+
pull a copy of the rule into each repo. Drift-free by construction
|
|
741
|
+
— the canonical rule changes upstream and the pointer keeps
|
|
742
|
+
pointing.
|
|
743
|
+
- **Brace-balanced parsing + comment attachment makes
|
|
744
|
+
`obsolete-vite-plugins` `--fix` safe at scale.** The casaevideo
|
|
745
|
+
smoke test confirmed the approach handles real-world vite configs
|
|
746
|
+
with multi-line plugins, attached comments describing the
|
|
747
|
+
workaround, template literals containing `}`, and nested
|
|
748
|
+
`rollupOptions`. Idempotency falls out of "rule found 0 plugins
|
|
749
|
+
→ no findings → no fix actions". This is the pattern for any
|
|
750
|
+
future `--fix` that needs to surgically edit a file: extract a
|
|
751
|
+
span helper, write surface-level tests, smoke against a real
|
|
752
|
+
production file, ship.
|
|
753
|
+
- **`apps-start` had latent CI debt.** W12 surfaced pre-existing
|
|
754
|
+
`noExplicitAny` failures in `shopify/utils/storefront/storefront.graphql.gen.ts`
|
|
755
|
+
that had been failing on `main` for an unknown duration. The lint
|
|
756
|
+
unblock PR (`apps-start#34`) is the kind of "passing through" fix
|
|
757
|
+
that should land first whenever a CI gate is red. Don't paper
|
|
758
|
+
over it.
|
|
759
|
+
- **Factories migrate cleaner than templates.** Comparing
|
|
760
|
+
`apps-start#33` (factories) to `deco-start#125` (template that
|
|
761
|
+
*consumes* the factories), the factory PR is the larger artifact
|
|
762
|
+
but the template PR shipped 4 lines into each generated file.
|
|
763
|
+
Investing once in a well-shaped factory pays a 50:1 multiplier on
|
|
764
|
+
every site that runs the migration after that point. This is the
|
|
765
|
+
D4 promotion path working end-to-end: build it once at site level,
|
|
766
|
+
prove it on 2 sites, promote to `@decocms/apps`, then rewrite the
|
|
767
|
+
template to use the canonical.
|
|
768
|
+
|
|
769
|
+
### Wave 13 (htmx foundations — Priority 2 part 1) — ✅ **COMPLETE**
|
|
770
|
+
|
|
771
|
+
Once Wave 12 was in, the migration script needed an htmx track
|
|
772
|
+
because als is the first heavy htmx site and we know it won't be
|
|
773
|
+
the last (per the user, "some of our sites are, not all, not even
|
|
774
|
+
most, some"). **3 PRs in `deco-start`, all merged.** D2 forbids an
|
|
775
|
+
htmx adapter package; nothing in Wave 13 ships htmx runtime — only
|
|
776
|
+
analysis, rewrite recipes, and a "rewrite-complete" gate.
|
|
777
|
+
|
|
778
|
+
**Shipped PRs:**
|
|
779
|
+
|
|
780
|
+
- **W13-A** [`deco-start#129`](https://github.com/decocms/deco-start/pull/129) — `feat(migrate): htmx surface analyzer` ✅ **MERGED**, released as `@decocms/start@2.20.0`.
|
|
781
|
+
Adds `scripts/migrate/analyzers/htmx-analyze.ts` (per-file walker + classifier) and the `deco-htmx-analyze` CLI. The walker is heuristic JSX (regex for `hx-*` attrs, brace-balanced traversal back to the opening tag, forward to the closing `>` / `/>`) — skips strings, template literals, JSX expression slots, and balanced `{...}` blocks. Each occurrence is classified into one of seven categories (`event-handler`, `form-swap`, `click-swap`, `auto-fetch`, `oob-swap`, `boost`, `unmatched`) based on the attribute cluster, not individual attrs (recipes apply to clusters, not attrs in isolation). CLI emits per-category counts, top tags, sample line numbers, and a one-line migration recipe; `--json` for tooling. 24 tests covering classification (all 7 categories + tie-breaks + dash-variant `hx-on`) and real als-shaped fixtures (AddToBagButton, SearchInput, EmailAndPassword, ForgotPassword).
|
|
782
|
+
- **W13-B** [`deco-start#130`](https://github.com/decocms/deco-start/pull/130) — `docs(skills): add htmx-rewrite reference` ✅ **MERGED**.
|
|
783
|
+
Per-pattern playbook at `.agents/skills/deco-to-tanstack-migration/references/htmx-rewrite.md`. For each of the seven categories: a "Before" snippet pulled directly from als (so the recipe is grounded in what an engineer is actually staring at), an "After" snippet using the canonical TanStack Start patterns (`useState` + `useCart`, `useNavigate`, `useMutation`, sub-routes), an explicit decision criterion when more than one path is reasonable (e.g. local state machine vs. sub-route for `click-swap`), and a "Gotchas" block enumerating the failure modes humans actually hit (focus loss, double-submit, hydration mismatch, etc.). Cross-linked from `SKILL.md`'s problem table.
|
|
784
|
+
- **W13-C** [`deco-start#131`](https://github.com/decocms/deco-start/pull/131) — `feat(audit): htmx-residue rule` ✅ **MERGED**, released as `@decocms/start@2.21.0`.
|
|
785
|
+
Eighth audit rule. Reuses `analyzeFile` from `analyzers/htmx-analyze.ts` to scan `src/**/*.{ts,tsx}` (excluding `*.test.tsx` / `*.spec.ts` / `__tests__/`) and emits one warning per file with a category breakdown (`event-handler=2, form-swap=1`). Severity is `warning` so `--strict` exits 2 — the "rewrite-complete" CI gate. The fix string points at `references/htmx-rewrite.md`. Intentionally **detect-only** — rewrites are non-mechanical (state machine vs. sub-route vs. mutation choices vary per call site), so `--fix` wiring would be misleading; the skill is the playbook. 7 new tests cover aggregation, severity, test-file exclusion, scope (`src/` only), zero-finding gate, line-number reporting, and `supportsAutoFix: false`. Skill doc § 7 added explaining the rule + when to wire it into CI; help text updated.
|
|
786
|
+
|
|
787
|
+
### Wave 13 — discoveries
|
|
788
|
+
|
|
789
|
+
- **Heuristic JSX walking is enough; full AST is not needed for
|
|
790
|
+
this surface.** `analyzeFile` goes character-by-character with
|
|
791
|
+
brace-counting and string/template/comment skipping; it correctly
|
|
792
|
+
identifies attribute clusters in 100 % of the als-storefront and
|
|
793
|
+
internal-fixture sample (~120 files, ~270 hx-* attributes), and
|
|
794
|
+
the test corpus pins the tricky cases (dash-variant `hx-on-*`,
|
|
795
|
+
attached comments, balanced JSX expressions inside attributes,
|
|
796
|
+
multiline tags). Pulling in `@swc/core` or `recast` for this
|
|
797
|
+
would be over-engineering — the walker is ~150 LOC, deterministic,
|
|
798
|
+
and shares a single source of truth between the standalone CLI
|
|
799
|
+
(`deco-htmx-analyze`), the post-cleanup audit rule
|
|
800
|
+
(`htmx-residue`), and the per-pattern recipe references.
|
|
801
|
+
- **Classify by attribute cluster, not by individual attribute.**
|
|
802
|
+
An `hx-on:click` and `hx-post + hx-target + hx-swap` get
|
|
803
|
+
fundamentally different rewrites. Categorising at the cluster
|
|
804
|
+
level (the JSX tag + all its hx-* attrs) means each finding
|
|
805
|
+
points at exactly one of seven recipes in
|
|
806
|
+
`references/htmx-rewrite.md`. This is the same discipline that
|
|
807
|
+
worked for `STUB_FIX_HINTS` in the vtex-shim rule: the data shape
|
|
808
|
+
encodes the actionability category, the rule is just a thin
|
|
809
|
+
classifier on top.
|
|
810
|
+
- **D2 + W13-C form a closed loop, mirroring the W12 D3 + audit
|
|
811
|
+
pattern.** D2 says "no htmx runtime in `@decocms/start`". W13-C's
|
|
812
|
+
`htmx-residue` rule says "fail CI if any `hx-*` survives in
|
|
813
|
+
`src/`". Together: a migrated site cannot accidentally rely on
|
|
814
|
+
htmx because (a) the framework gives them no runtime to import,
|
|
815
|
+
(b) the audit catches every leftover `hx-*` in code review.
|
|
816
|
+
- **Detect-only is correct here, not a stop-gap.** Auto-fixing
|
|
817
|
+
htmx is conceptually hard: even a "simple" `<button hx-post>` →
|
|
818
|
+
`useMutation` rewrite has to choose between optimistic vs
|
|
819
|
+
pessimistic UI, error handling shape, where to surface the
|
|
820
|
+
loading state, and whether the response should re-render the
|
|
821
|
+
whole page or a fragment. Each is a per-site product decision.
|
|
822
|
+
The pattern catalog in `references/htmx-rewrite.md` is the
|
|
823
|
+
durable artefact; codemods (Wave 14) can target a specific
|
|
824
|
+
cluster shape (e.g. `hx-post + hx-target=#id + hx-swap=innerHTML`
|
|
825
|
+
with no `hx-trigger`) safely, but they're scoped by category,
|
|
826
|
+
not the rule's auto-fix.
|
|
827
|
+
- **The audit registry is now self-shaped for additive growth.**
|
|
828
|
+
Eight rules, three of which (`vtex-shim-regression`,
|
|
829
|
+
`obsolete-vite-plugins`, `htmx-residue`) ship with their own
|
|
830
|
+
analyzer modules. The pattern is set: add a rule to
|
|
831
|
+
`ALL_RULES`, supply `applyFix` only when mechanical, point the
|
|
832
|
+
prose `fix:` field and JSON `meta` at a skill reference. The CLI
|
|
833
|
+
(`migrate-post-cleanup.ts`) is rule-agnostic — adding a ninth
|
|
834
|
+
rule means changing one file, getting `--strict` and `--json`
|
|
835
|
+
for free.
|
|
725
836
|
|
|
726
837
|
### Wave 14 (htmx codemods + first als migration on 2.14+) — planned
|
|
727
838
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/start",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"bin": {
|
|
8
8
|
"deco-migrate": "./scripts/migrate.ts",
|
|
9
|
-
"deco-post-cleanup": "./scripts/migrate-post-cleanup.ts"
|
|
9
|
+
"deco-post-cleanup": "./scripts/migrate-post-cleanup.ts",
|
|
10
|
+
"deco-htmx-analyze": "./scripts/htmx-analyze.ts"
|
|
10
11
|
},
|
|
11
12
|
"exports": {
|
|
12
13
|
".": "./src/index.ts",
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* HTMX surface analyzer — CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* Inventories the `hx-*` surface of a Deco storefront so the engineer
|
|
6
|
+
* (or the next codemod wave) knows exactly what shapes are out there
|
|
7
|
+
* before starting the rewrite to React. Per D2 in the migration
|
|
8
|
+
* tooling policy, all htmx is rewritten on migration; no runtime is
|
|
9
|
+
* shipped in `@decocms/start`.
|
|
10
|
+
*
|
|
11
|
+
* Usage (from a site directory):
|
|
12
|
+
* npx -p @decocms/start deco-htmx-analyze
|
|
13
|
+
* npx -p @decocms/start deco-htmx-analyze --source /path/to/site
|
|
14
|
+
* npx -p @decocms/start deco-htmx-analyze --json
|
|
15
|
+
*
|
|
16
|
+
* Options:
|
|
17
|
+
* --source <dir> Site directory to analyze (default: current directory)
|
|
18
|
+
* --json Emit machine-readable JSON instead of pretty text
|
|
19
|
+
* --top <n> Show top N files by occurrence count (default: 20)
|
|
20
|
+
* --help, -h Show this help
|
|
21
|
+
*
|
|
22
|
+
* Wave 13-A. Read-only. Codemods land in Wave 14.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
import { realFsAdapter } from "./migrate/post-cleanup/runner";
|
|
27
|
+
import {
|
|
28
|
+
analyzeHtmx,
|
|
29
|
+
type HtmxCategory,
|
|
30
|
+
type HtmxInventory,
|
|
31
|
+
} from "./migrate/analyzers/htmx-analyze";
|
|
32
|
+
import { banner, bold, cyan, gray, green, red, yellow } from "./migrate/colors";
|
|
33
|
+
|
|
34
|
+
interface CliOpts {
|
|
35
|
+
source: string;
|
|
36
|
+
json: boolean;
|
|
37
|
+
top: number;
|
|
38
|
+
help: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseArgs(args: string[]): CliOpts {
|
|
42
|
+
let source = ".";
|
|
43
|
+
let json = false;
|
|
44
|
+
let top = 20;
|
|
45
|
+
let help = false;
|
|
46
|
+
for (let i = 0; i < args.length; i++) {
|
|
47
|
+
switch (args[i]) {
|
|
48
|
+
case "--source":
|
|
49
|
+
source = args[++i];
|
|
50
|
+
break;
|
|
51
|
+
case "--json":
|
|
52
|
+
json = true;
|
|
53
|
+
break;
|
|
54
|
+
case "--top":
|
|
55
|
+
top = Number.parseInt(args[++i] ?? "20", 10);
|
|
56
|
+
if (Number.isNaN(top) || top < 0) top = 20;
|
|
57
|
+
break;
|
|
58
|
+
case "--help":
|
|
59
|
+
case "-h":
|
|
60
|
+
help = true;
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
console.error(`Unknown argument: ${args[i]}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { source, json, top, help };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printHelp(): void {
|
|
71
|
+
console.log(`deco-htmx-analyze
|
|
72
|
+
|
|
73
|
+
Inventory the htmx surface (hx-* attributes) of a Deco storefront.
|
|
74
|
+
|
|
75
|
+
Usage:
|
|
76
|
+
npx -p @decocms/start deco-htmx-analyze [options]
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--source <dir> Site directory to analyze (default: cwd)
|
|
80
|
+
--json Emit machine-readable JSON
|
|
81
|
+
--top <n> Top N files by occurrence count (default: 20)
|
|
82
|
+
--help, -h Show this help
|
|
83
|
+
|
|
84
|
+
The output is read-only. Codemods that rewrite htmx to React are a
|
|
85
|
+
planned follow-up — see the deco-to-tanstack-migration skill for the
|
|
86
|
+
per-pattern rewrite recipes.
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const CATEGORY_DESCRIPTIONS: Record<HtmxCategory, string> = {
|
|
91
|
+
"event-handler": "hx-on:* with no fetch attr — pure client-side handler",
|
|
92
|
+
"form-swap": "hx-post on a <form> with hx-target/hx-swap",
|
|
93
|
+
"click-swap": "hx-get/hx-post on a button with hx-target",
|
|
94
|
+
"auto-fetch": "hx-trigger=keyup/intersect/etc on input or auto-fired element",
|
|
95
|
+
"oob-swap": "hx-swap-oob / hx-select-oob — out-of-band patches",
|
|
96
|
+
boost: "hx-boost=true — link prefetch, already-SPA in TanStack Start",
|
|
97
|
+
unmatched: "hx-* attribute set that didn't match a known pattern",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const CATEGORY_RECIPES: Record<HtmxCategory, string> = {
|
|
101
|
+
"event-handler":
|
|
102
|
+
"replace hx-on:click={useScript(...)} with onClick={() => { ... }}",
|
|
103
|
+
"form-swap":
|
|
104
|
+
"<form onSubmit> + useMutation/server function; render result with state",
|
|
105
|
+
"click-swap":
|
|
106
|
+
"setState/setView + conditional render, or sub-route via TanStack Router",
|
|
107
|
+
"auto-fetch":
|
|
108
|
+
"debounced state + useQuery; for intersect use IntersectionObserver",
|
|
109
|
+
"oob-swap":
|
|
110
|
+
"manual: out-of-band has no 1:1; refactor to broadcast event + listener",
|
|
111
|
+
boost: "replace <a hx-boost> with TanStack Router <Link> (already SPA)",
|
|
112
|
+
unmatched: "manual review",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const CATEGORY_ORDER: HtmxCategory[] = [
|
|
116
|
+
"event-handler",
|
|
117
|
+
"click-swap",
|
|
118
|
+
"form-swap",
|
|
119
|
+
"auto-fetch",
|
|
120
|
+
"boost",
|
|
121
|
+
"oob-swap",
|
|
122
|
+
"unmatched",
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
function printText(inv: HtmxInventory, top: number): void {
|
|
126
|
+
banner("HTMX surface analysis");
|
|
127
|
+
|
|
128
|
+
if (inv.totalOccurrences === 0) {
|
|
129
|
+
console.log(green("✓ No hx-* attributes found.\n"));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`${bold("Files with hx-* usage:")} ${inv.totalFiles}`);
|
|
134
|
+
console.log(`${bold("Total occurrences:")} ${inv.totalOccurrences}\n`);
|
|
135
|
+
|
|
136
|
+
console.log(bold("By category:"));
|
|
137
|
+
for (const cat of CATEGORY_ORDER) {
|
|
138
|
+
const count = inv.byCategory[cat];
|
|
139
|
+
if (count === 0) continue;
|
|
140
|
+
const label = `${cat.padEnd(15)}`;
|
|
141
|
+
const desc = gray(CATEGORY_DESCRIPTIONS[cat]);
|
|
142
|
+
console.log(` ${cyan(label)} ${String(count).padStart(4)} ${desc}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`\n${bold("Migration recipes:")}`);
|
|
146
|
+
for (const cat of CATEGORY_ORDER) {
|
|
147
|
+
if (inv.byCategory[cat] === 0) continue;
|
|
148
|
+
console.log(` ${cyan(cat.padEnd(15))} ${gray(CATEGORY_RECIPES[cat])}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`\n${bold(`Top ${top} files by occurrence:`)}`);
|
|
152
|
+
const slice = inv.files.slice(0, top);
|
|
153
|
+
const widest = Math.max(...slice.map((f) => f.file.length), 30);
|
|
154
|
+
for (const f of slice) {
|
|
155
|
+
const detail = CATEGORY_ORDER.filter((c) => f.byCategory[c] > 0)
|
|
156
|
+
.map((c) => `${c}=${f.byCategory[c]}`)
|
|
157
|
+
.join(", ");
|
|
158
|
+
console.log(
|
|
159
|
+
` ${f.file.padEnd(widest)} ${String(f.total).padStart(3)} ${gray(detail)}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (inv.files.length > top) {
|
|
164
|
+
console.log(gray(` …and ${inv.files.length - top} more file(s)`));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`\n${bold("Sample call sites:")}`);
|
|
168
|
+
for (const cat of CATEGORY_ORDER) {
|
|
169
|
+
const samples = inv.samples[cat];
|
|
170
|
+
if (samples.length === 0) continue;
|
|
171
|
+
console.log(` ${cyan(cat)}`);
|
|
172
|
+
for (const s of samples) {
|
|
173
|
+
const attrs = s.attrs.join(", ");
|
|
174
|
+
console.log(
|
|
175
|
+
` ${gray(`${s.file}:${s.line}`)} <${s.tag}> [${attrs}]`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const hasOob = inv.byCategory["oob-swap"] > 0;
|
|
181
|
+
const hasUnmatched = inv.byCategory.unmatched > 0;
|
|
182
|
+
if (hasOob || hasUnmatched) {
|
|
183
|
+
console.log();
|
|
184
|
+
if (hasOob) {
|
|
185
|
+
console.log(
|
|
186
|
+
yellow(
|
|
187
|
+
"⚠ oob-swap occurrences require manual rewrite — no 1:1 React equivalent.",
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (hasUnmatched) {
|
|
192
|
+
console.log(
|
|
193
|
+
yellow(
|
|
194
|
+
"⚠ unmatched occurrences require manual review — see Sample call sites.",
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
console.log();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function main(argv: string[]): number {
|
|
203
|
+
const opts = parseArgs(argv);
|
|
204
|
+
if (opts.help) {
|
|
205
|
+
printHelp();
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const sourceDir = path.resolve(opts.source);
|
|
210
|
+
const inv = analyzeHtmx(sourceDir, realFsAdapter);
|
|
211
|
+
|
|
212
|
+
if (opts.json) {
|
|
213
|
+
console.log(JSON.stringify(inv, null, 2));
|
|
214
|
+
} else {
|
|
215
|
+
printText(inv, opts.top);
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
process.exit(main(process.argv.slice(2)));
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error(red(`✗ deco-htmx-analyze failed: ${(err as Error).message}`));
|
|
224
|
+
if (process.env.DEBUG) console.error((err as Error).stack);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|