@decocms/start 2.20.0 โ 2.22.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 +548 -0
- package/.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +41 -2
- package/MIGRATION_TOOLING_PLAN.md +214 -16
- package/package.json +1 -1
- package/scripts/migrate/phase-transform.ts +7 -1
- package/scripts/migrate/post-cleanup/rules.ts +73 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +108 -0
- package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
- package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
- package/scripts/migrate-post-cleanup.ts +3 -2
|
@@ -239,6 +239,36 @@ Each item carries a status: โฌ pending, ๐ก in progress, โ
done, ๐ซ blocke
|
|
|
239
239
|
|
|
240
240
|
> Append-only. Each entry: date, what we found, where it impacts the plan.
|
|
241
241
|
|
|
242
|
+
### 2026-05-01 โ Wave 14-A rescoped from three codemods to one based on real als data
|
|
243
|
+
|
|
244
|
+
- **Pre-data plan vs post-data plan.** The plan called for three
|
|
245
|
+
htmx codemods (`event-handler`, `form-swap`, `click-swap`).
|
|
246
|
+
After running `deco-htmx-analyze` against als-storefront's
|
|
247
|
+
actual code (210 occurrences across 133 files), only the
|
|
248
|
+
`event-handler` bucket (88 occurrences, 42 %) genuinely admits
|
|
249
|
+
a mechanical rewrite โ the other buckets need per-call-site
|
|
250
|
+
product decisions a codemod cannot encode. **Decided: ship
|
|
251
|
+
one codemod (W14-A: `htmx-on-event-rename`), defer the other
|
|
252
|
+
two to W15+.** Rationale captured in the Wave 14 โ discoveries
|
|
253
|
+
block.
|
|
254
|
+
- **Codemod shape generalises:** rename + preserve body +
|
|
255
|
+
conditional file-level TODO. Three outputs, one mechanical,
|
|
256
|
+
one verbatim, one conditional on body-content heuristics. This
|
|
257
|
+
is the shape any future per-pattern codemod should target.
|
|
258
|
+
- **Smoke against the real source tree validated the design in
|
|
259
|
+
five minutes.** 754 files scanned, 71 changed, 98 renames, 67
|
|
260
|
+
TODO injections (94 % of changed files). Without that smoke
|
|
261
|
+
step we'd have shipped blind on edge cases like multi-line
|
|
262
|
+
values, mixed standard + lifecycle hooks on the same element,
|
|
263
|
+
and the colon-vs-dash variants both showing up in the same
|
|
264
|
+
file.
|
|
265
|
+
- **The codemod + audit pair closes another loop.** Same shape
|
|
266
|
+
as W12 (D3 throwing stubs + audit `--fix` for swap-able
|
|
267
|
+
stubs). The codemod removes the mechanical half of the htmx
|
|
268
|
+
surface; the `htmx-residue` audit catches the surviving half
|
|
269
|
+
in CI. Engineers can never silently ship a half-rewritten
|
|
270
|
+
file.
|
|
271
|
+
|
|
242
272
|
### 2026-05-01 โ als-storefront surfaces the htmx track + policy reset
|
|
243
273
|
|
|
244
274
|
- **als-storefront is the third migration target and the first
|
|
@@ -766,25 +796,193 @@ Wave 12 ships in priority-1 order; Wave 13 starts now.
|
|
|
766
796
|
prove it on 2 sites, promote to `@decocms/apps`, then rewrite the
|
|
767
797
|
template to use the canonical.
|
|
768
798
|
|
|
769
|
-
### Wave 13 (htmx foundations โ Priority 2 part 1) โ
|
|
770
|
-
|
|
771
|
-
Once Wave 12 is in, the migration script needs an htmx track because
|
|
772
|
-
als is the first heavy htmx site and we know it won't be the last
|
|
773
|
-
(per the user, "some of our sites are, not all, not even most, some").
|
|
799
|
+
### Wave 13 (htmx foundations โ Priority 2 part 1) โ โ
**COMPLETE**
|
|
774
800
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
801
|
+
Once Wave 12 was in, the migration script needed an htmx track
|
|
802
|
+
because als is the first heavy htmx site and we know it won't be
|
|
803
|
+
the last (per the user, "some of our sites are, not all, not even
|
|
804
|
+
most, some"). **3 PRs in `deco-start`, all merged.** D2 forbids an
|
|
805
|
+
htmx adapter package; nothing in Wave 13 ships htmx runtime โ only
|
|
806
|
+
analysis, rewrite recipes, and a "rewrite-complete" gate.
|
|
778
807
|
|
|
779
|
-
|
|
780
|
-
runtime.
|
|
781
|
-
|
|
782
|
-
### Wave 14 (htmx codemods + first als migration on 2.14+) โ planned
|
|
808
|
+
**Shipped PRs:**
|
|
783
809
|
|
|
784
|
-
- **
|
|
785
|
-
-
|
|
786
|
-
- **
|
|
787
|
-
-
|
|
810
|
+
- **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`.
|
|
811
|
+
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).
|
|
812
|
+
- **W13-B** [`deco-start#130`](https://github.com/decocms/deco-start/pull/130) โ `docs(skills): add htmx-rewrite reference` โ
**MERGED**.
|
|
813
|
+
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.
|
|
814
|
+
- **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`.
|
|
815
|
+
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.
|
|
816
|
+
|
|
817
|
+
### Wave 13 โ discoveries
|
|
818
|
+
|
|
819
|
+
- **Heuristic JSX walking is enough; full AST is not needed for
|
|
820
|
+
this surface.** `analyzeFile` goes character-by-character with
|
|
821
|
+
brace-counting and string/template/comment skipping; it correctly
|
|
822
|
+
identifies attribute clusters in 100 % of the als-storefront and
|
|
823
|
+
internal-fixture sample (~120 files, ~270 hx-* attributes), and
|
|
824
|
+
the test corpus pins the tricky cases (dash-variant `hx-on-*`,
|
|
825
|
+
attached comments, balanced JSX expressions inside attributes,
|
|
826
|
+
multiline tags). Pulling in `@swc/core` or `recast` for this
|
|
827
|
+
would be over-engineering โ the walker is ~150 LOC, deterministic,
|
|
828
|
+
and shares a single source of truth between the standalone CLI
|
|
829
|
+
(`deco-htmx-analyze`), the post-cleanup audit rule
|
|
830
|
+
(`htmx-residue`), and the per-pattern recipe references.
|
|
831
|
+
- **Classify by attribute cluster, not by individual attribute.**
|
|
832
|
+
An `hx-on:click` and `hx-post + hx-target + hx-swap` get
|
|
833
|
+
fundamentally different rewrites. Categorising at the cluster
|
|
834
|
+
level (the JSX tag + all its hx-* attrs) means each finding
|
|
835
|
+
points at exactly one of seven recipes in
|
|
836
|
+
`references/htmx-rewrite.md`. This is the same discipline that
|
|
837
|
+
worked for `STUB_FIX_HINTS` in the vtex-shim rule: the data shape
|
|
838
|
+
encodes the actionability category, the rule is just a thin
|
|
839
|
+
classifier on top.
|
|
840
|
+
- **D2 + W13-C form a closed loop, mirroring the W12 D3 + audit
|
|
841
|
+
pattern.** D2 says "no htmx runtime in `@decocms/start`". W13-C's
|
|
842
|
+
`htmx-residue` rule says "fail CI if any `hx-*` survives in
|
|
843
|
+
`src/`". Together: a migrated site cannot accidentally rely on
|
|
844
|
+
htmx because (a) the framework gives them no runtime to import,
|
|
845
|
+
(b) the audit catches every leftover `hx-*` in code review.
|
|
846
|
+
- **Detect-only is correct here, not a stop-gap.** Auto-fixing
|
|
847
|
+
htmx is conceptually hard: even a "simple" `<button hx-post>` โ
|
|
848
|
+
`useMutation` rewrite has to choose between optimistic vs
|
|
849
|
+
pessimistic UI, error handling shape, where to surface the
|
|
850
|
+
loading state, and whether the response should re-render the
|
|
851
|
+
whole page or a fragment. Each is a per-site product decision.
|
|
852
|
+
The pattern catalog in `references/htmx-rewrite.md` is the
|
|
853
|
+
durable artefact; codemods (Wave 14) can target a specific
|
|
854
|
+
cluster shape (e.g. `hx-post + hx-target=#id + hx-swap=innerHTML`
|
|
855
|
+
with no `hx-trigger`) safely, but they're scoped by category,
|
|
856
|
+
not the rule's auto-fix.
|
|
857
|
+
- **The audit registry is now self-shaped for additive growth.**
|
|
858
|
+
Eight rules, three of which (`vtex-shim-regression`,
|
|
859
|
+
`obsolete-vite-plugins`, `htmx-residue`) ship with their own
|
|
860
|
+
analyzer modules. The pattern is set: add a rule to
|
|
861
|
+
`ALL_RULES`, supply `applyFix` only when mechanical, point the
|
|
862
|
+
prose `fix:` field and JSON `meta` at a skill reference. The CLI
|
|
863
|
+
(`migrate-post-cleanup.ts`) is rule-agnostic โ adding a ninth
|
|
864
|
+
rule means changing one file, getting `--strict` and `--json`
|
|
865
|
+
for free.
|
|
866
|
+
|
|
867
|
+
### Wave 14 (htmx codemod โ Priority 2 part 2) โ โ
**PARTIAL / RESCOPED**
|
|
868
|
+
|
|
869
|
+
After shipping the W13 htmx foundations and gathering real data
|
|
870
|
+
from als-storefront with `deco-htmx-analyze`, the planned three-codemod
|
|
871
|
+
scope was reduced to **one codemod** + **one inventory artefact**.
|
|
872
|
+
The other two codemods (form-swap, click-swap) were deferred to W15+,
|
|
873
|
+
to be designed *after* als migration data exposes which exact
|
|
874
|
+
attribute clusters dominate. **Rationale logged in W14 discoveries.**
|
|
875
|
+
|
|
876
|
+
**Shipped:**
|
|
877
|
+
|
|
878
|
+
- **W14-A** [`deco-start#132`](https://github.com/decocms/deco-start/pull/132) โ `feat(migrate): htmx-on-event-rename codemod` โ
**MERGED**, released as `@decocms/start@2.22.0`.
|
|
879
|
+
Adds `scripts/migrate/transforms/htmx-on-events.ts` to the migrate
|
|
880
|
+
`transforms/` pipeline. Mechanically rewrites `hx-on:event=` and
|
|
881
|
+
`hx-on-event=` (colon + dash variants) to the React equivalent
|
|
882
|
+
for every standard DOM event in `STANDARD_EVENT_MAP` (40 entries:
|
|
883
|
+
click, submit, change, input, key*, mouse*, focus*, drag*, touch*,
|
|
884
|
+
paste/copy/cut, scroll, wheel, load, contextmenu). Handler bodies
|
|
885
|
+
are preserved verbatim. **Idempotent** โ running twice is a no-op.
|
|
886
|
+
Two safety hatches: htmx lifecycle events (`hx-on:htmx-*`) and
|
|
887
|
+
unknown custom events left alone (the `htmx-residue` audit catches
|
|
888
|
+
them); a single top-of-file MIGRATION TODO comment is injected
|
|
889
|
+
when the body references Fresh-only globals (`useScript(โฆ)`,
|
|
890
|
+
`globalThis.window.STOREFRONT`, `STOREFRONT.โฆ`) so engineers
|
|
891
|
+
don't ship a syntactically-clean file with broken runtime calls.
|
|
892
|
+
29 unit tests + als-shaped fixtures (AddToBagButton, SearchInput,
|
|
893
|
+
RecoveryPassword form, Footer.tsx). 339/339 pass; typecheck clean.
|
|
894
|
+
htmx-rewrite skill ยง Pattern 1 cross-references the codemod.
|
|
895
|
+
- **W14-B** [`deco-start#132`](https://github.com/decocms/deco-start/pull/132) โ captured the **als-storefront htmx inventory** in this plan (this section) as a fixture for future W15+ codemod design.
|
|
896
|
+
|
|
897
|
+
**Deferred (intentionally) โ see Wave 14 discoveries:**
|
|
898
|
+
|
|
899
|
+
- ~~**W14-C** codemod `transforms/htmx-form-post-swap.ts`~~ โ moved to W15+. The form-swap rewrite is genuinely non-mechanical (per-call-site decisions about optimistic vs pessimistic UI, where to surface loading state, which response handler shape). A speculative codemod would produce React skeletons that still need ~80 % manual work.
|
|
900
|
+
- ~~**W14-D** codemod `transforms/htmx-click-fetch-swap.ts`~~ โ moved to W15+. Same logic; on top of that, choosing between local state machine vs sub-route is a routing-architecture decision that varies per page.
|
|
901
|
+
|
|
902
|
+
#### W14-A smoke + als inventory (captured 2026-05-01)
|
|
903
|
+
|
|
904
|
+
The W13-A `deco-htmx-analyze` CLI run against als-storefront's
|
|
905
|
+
production Fresh tree:
|
|
906
|
+
|
|
907
|
+
| Category | Count | % | Notes |
|
|
908
|
+
|---|---:|---:|---|
|
|
909
|
+
| `event-handler` | 88 | 42 % | **Codemoded by W14-A** โ mechanical rename |
|
|
910
|
+
| `click-swap` | 64 | 30 % | Manual (W15+) โ needs state vs sub-route decision |
|
|
911
|
+
| `form-swap` | 20 | 10 % | Manual (W15+) โ needs `useMutation` shape decision |
|
|
912
|
+
| `auto-fetch` | 9 | 4 % | Manual โ debounced state + `useQuery` |
|
|
913
|
+
| `oob-swap` | 8 | 4 % | Manual โ no 1:1 React equivalent |
|
|
914
|
+
| `unmatched` | 21 | 10 % | Mostly typed-generic noise (`<string>` from `Map<string,X>`) |
|
|
915
|
+
| **Total** | **210** | | across 133 files |
|
|
916
|
+
|
|
917
|
+
W14-A codemod sweep against the same tree (754 ts/tsx files):
|
|
918
|
+
|
|
919
|
+
| Metric | Value |
|
|
920
|
+
|---|---:|
|
|
921
|
+
| Files scanned | 754 |
|
|
922
|
+
| Files changed | 71 |
|
|
923
|
+
| Total `hx-on:*` attributes renamed | 98 |
|
|
924
|
+
| Files getting the MIGRATION TODO | 67 (94 % of changed) |
|
|
925
|
+
|
|
926
|
+
The 98 vs 88 discrepancy is expected: the analyzer counts attribute
|
|
927
|
+
*clusters* per element (an `<input hx-post hx-target hx-on:change>`
|
|
928
|
+
classifies as one `auto-fetch`); the codemod counts individual
|
|
929
|
+
`hx-on:*` attribute renames (the same element gets one rename
|
|
930
|
+
plus the `auto-fetch` cluster left intact for the engineer to
|
|
931
|
+
finish). Net effect: ~98 mechanical wins, leaving ~112 cluster
|
|
932
|
+
rewrites (click-swap + form-swap + auto-fetch + oob-swap +
|
|
933
|
+
unmatched) for the engineer โ matching the manual rewrite
|
|
934
|
+
recipes in `references/htmx-rewrite.md`.
|
|
935
|
+
|
|
936
|
+
### Wave 14 โ discoveries
|
|
937
|
+
|
|
938
|
+
- **Speculative codemods are over-engineering; data-driven scope
|
|
939
|
+
is better.** The pre-data plan said three codemods (event-handler,
|
|
940
|
+
form-swap, click-swap). After running `deco-htmx-analyze` against
|
|
941
|
+
als-storefront's actual code, only the event-handler bucket
|
|
942
|
+
(88 occurrences, 42 % of the surface) genuinely admits a
|
|
943
|
+
mechanical rewrite. The other two buckets need per-call-site
|
|
944
|
+
product decisions (state machine vs sub-route, optimistic vs
|
|
945
|
+
pessimistic UI, response-handler shape) that a codemod cannot
|
|
946
|
+
encode without producing React skeletons that still need ~80 %
|
|
947
|
+
manual work โ net negative versus the recipe in
|
|
948
|
+
`references/htmx-rewrite.md`. **New rule: codemods come *after*
|
|
949
|
+
the analyzer data, not before.**
|
|
950
|
+
- **The smoke-against-real-site step is the design feedback loop.**
|
|
951
|
+
Running the codemod against als's full 754-file tree (98
|
|
952
|
+
renames, 71 files changed, 67 with TODO injection) validated
|
|
953
|
+
three things in five minutes: (a) the rename surface matches
|
|
954
|
+
the inventory (98 vs 88 ratio explained), (b) the TODO
|
|
955
|
+
injection rate is high (94 %) โ the marker is essential, not
|
|
956
|
+
defensive, (c) the codemod is idempotent at scale (re-running
|
|
957
|
+
produces zero diffs). Without this step we'd ship blind.
|
|
958
|
+
- **The three-output codemod shape (rename + preserve body +
|
|
959
|
+
conditional TODO) generalises.** Same shape any future
|
|
960
|
+
per-pattern codemod should target: do the mechanical part,
|
|
961
|
+
preserve the human-decision-required part, leave a single
|
|
962
|
+
file-level marker the engineer can grep for. Over-eager
|
|
963
|
+
body rewriting is what produces the ~80 % manual cleanup load
|
|
964
|
+
that justifies leaving form-swap / click-swap codemods out for
|
|
965
|
+
now.
|
|
966
|
+
- **`htmx-residue` audit + W14-A codemod close another loop.**
|
|
967
|
+
Same pattern as W12 (D3 throwing stubs + audit `--fix` for
|
|
968
|
+
swap-able stubs). The codemod removes the easy half of the
|
|
969
|
+
htmx surface; the audit catches the surviving half. Engineers
|
|
970
|
+
can never accidentally ship a half-rewritten file: the
|
|
971
|
+
attribute is either gone (codemod ran, body might still need
|
|
972
|
+
work โ TODO), or it's still there (audit fires in CI).
|
|
973
|
+
- **als-storefront's profile probably generalises to other htmx
|
|
974
|
+
sites.** 42 % event-handler is a strong skew toward
|
|
975
|
+
trivially-mechanical rewrites; even if other sites differ,
|
|
976
|
+
this codemod alone removes the largest single bucket. If a
|
|
977
|
+
future site shows 80 % click-swap, *that* would be the cue to
|
|
978
|
+
build the click-swap codemod โ not pre-emptively now.
|
|
979
|
+
- **Pipeline order matters.** Codemod runs after `transformJsx`
|
|
980
|
+
(which renames `class` โ `className` and `onInput` โ `onChange`)
|
|
981
|
+
and before `transformFreshApis` (which removes `useScript`
|
|
982
|
+
imports). If `transformFreshApis` ran first, the codemod's
|
|
983
|
+
TODO marker would still fire (we look for `useScript(` calls,
|
|
984
|
+
not the import), but the import-removal would create dead
|
|
985
|
+
references. Order is correct.
|
|
788
986
|
|
|
789
987
|
### Wave 15+ (htmx cleanup PRs on als + propagation to other sites) โ Priority 3 / 4
|
|
790
988
|
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import { transformFreshApis } from "./transforms/fresh-apis";
|
|
|
9
9
|
import { transformDenoIsms } from "./transforms/deno-isms";
|
|
10
10
|
import { transformTailwind } from "./transforms/tailwind";
|
|
11
11
|
import { transformDeadCode } from "./transforms/dead-code";
|
|
12
|
+
import { transformHtmxOnEvents } from "./transforms/htmx-on-events";
|
|
12
13
|
import { createSectionConventionsTransform } from "./transforms/section-conventions";
|
|
13
14
|
|
|
14
15
|
/** Map of section path โ metadata, populated per-run */
|
|
@@ -54,10 +55,15 @@ function applyTransforms(content: string, filePath: string, ctx?: MigrationConte
|
|
|
54
55
|
return { content, changed: false, notes: [] };
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
// Pipeline: imports โ jsx โ fresh-apis โ dead-code โ deno-isms โ tailwind
|
|
58
|
+
// Pipeline: imports โ jsx โ htmx-on-events โ fresh-apis โ dead-code โ deno-isms โ tailwind
|
|
59
|
+
// htmx-on-events runs after jsx (which renames class/onChange) and
|
|
60
|
+
// before fresh-apis (which removes useScript imports the htmx
|
|
61
|
+
// codemod's TODO might still reference). The codemod is a no-op on
|
|
62
|
+
// files without hx-on, so it never adds latency to non-htmx sites.
|
|
58
63
|
const pipeline: Array<{ name: string; fn: (content: string) => TransformResult }> = [
|
|
59
64
|
{ name: "imports", fn: (c) => transformImports(c, ctx?.islandWrapperTargets) },
|
|
60
65
|
{ name: "jsx", fn: transformJsx },
|
|
66
|
+
{ name: "htmx-on-events", fn: transformHtmxOnEvents },
|
|
61
67
|
{ name: "fresh-apis", fn: transformFreshApis },
|
|
62
68
|
{ name: "dead-code", fn: (c) => transformDeadCode(c, ctx?.platform) },
|
|
63
69
|
{ name: "deno-isms", fn: transformDenoIsms },
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Rules are intentionally read-only here โ `--fix` is a follow-up.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { analyzeFile as analyzeHtmxFile } from "../analyzers/htmx-analyze";
|
|
12
13
|
import { classifyShimExports, type ExportClass } from "./shim-classify";
|
|
13
14
|
import type { Finding, FixAction, FsWriter, Rule, RuleContext } from "./types";
|
|
14
15
|
|
|
@@ -887,6 +888,76 @@ const ruleFrameworkTodos: Rule = {
|
|
|
887
888
|
},
|
|
888
889
|
};
|
|
889
890
|
|
|
891
|
+
/* ------------------------------------------------------------------ */
|
|
892
|
+
/* Rule 8 โ `htmx-residue` โ leftover hx-* attrs in migrated src/ */
|
|
893
|
+
/* ------------------------------------------------------------------ */
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Per D2 in the migration tooling policy, every `hx-*` attribute is
|
|
897
|
+
* rewritten on migration; nothing in `@decocms/start` ships an htmx
|
|
898
|
+
* runtime. This rule is the verification gate: a migrated site is
|
|
899
|
+
* "rewrite-complete" when there are zero `hx-*` attributes left in
|
|
900
|
+
* `src/`.
|
|
901
|
+
*
|
|
902
|
+
* Implementation reuses the htmx analyzer (`analyzeFile` from
|
|
903
|
+
* `analyzers/htmx-analyze.ts`) so categorisation and the JSX walker
|
|
904
|
+
* stay consistent with the standalone `deco-htmx-analyze` CLI. The
|
|
905
|
+
* rule restricts to `src/**` (the migrated React tree) and excludes
|
|
906
|
+
* test files โ tests are allowed to mention `hx-*` for fixtures or
|
|
907
|
+
* regression checks.
|
|
908
|
+
*
|
|
909
|
+
* Severity is `warning`, so `--strict` exits 2 on any finding. The
|
|
910
|
+
* rule is intentionally detect-only: rewrites are non-mechanical
|
|
911
|
+
* (state machine + sub-route + mutation choices vary per call site)
|
|
912
|
+
* โ the
|
|
913
|
+
* `references/htmx-rewrite.md` skill is the playbook.
|
|
914
|
+
*/
|
|
915
|
+
const ruleHtmxResidue: Rule = {
|
|
916
|
+
id: "htmx-residue",
|
|
917
|
+
title: "HTMX residue in migrated src/",
|
|
918
|
+
run({ siteDir, fs }: RuleContext): Finding[] {
|
|
919
|
+
const findings: Finding[] = [];
|
|
920
|
+
const tsFiles = fs.glob(siteDir, "src/**/*.{ts,tsx}", SRC_GLOB_EXCLUDES);
|
|
921
|
+
for (const abs of tsFiles) {
|
|
922
|
+
const rel = abs.slice(siteDir.length + 1);
|
|
923
|
+
// Skip test files โ tests legitimately reference hx-* in fixtures
|
|
924
|
+
// or regression checks. Same exclusion shape as vitest's default.
|
|
925
|
+
if (/\.(test|spec)\.(ts|tsx)$/.test(rel)) continue;
|
|
926
|
+
if (rel.startsWith("src/__tests__/") || rel.includes("/__tests__/")) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const content = fs.readText(abs);
|
|
930
|
+
const occurrences = analyzeHtmxFile(rel, content);
|
|
931
|
+
if (occurrences.length === 0) continue;
|
|
932
|
+
|
|
933
|
+
// Aggregate per-file: total + categories present.
|
|
934
|
+
const byCat = new Map<string, number>();
|
|
935
|
+
for (const occ of occurrences) {
|
|
936
|
+
byCat.set(occ.category, (byCat.get(occ.category) ?? 0) + 1);
|
|
937
|
+
}
|
|
938
|
+
const catSummary = [...byCat.entries()]
|
|
939
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
940
|
+
.map(([cat, n]) => `${cat}=${n}`)
|
|
941
|
+
.join(", ");
|
|
942
|
+
const firstLine = occurrences[0].line;
|
|
943
|
+
|
|
944
|
+
findings.push({
|
|
945
|
+
rule: "htmx-residue",
|
|
946
|
+
severity: "warning",
|
|
947
|
+
file: `${rel}:${firstLine}`,
|
|
948
|
+
message: `${occurrences.length} hx-* element(s) โ ${catSummary}`,
|
|
949
|
+
fix: `Rewrite per .agents/skills/deco-to-tanstack-migration/references/htmx-rewrite.md (run \`deco-htmx-analyze\` for the per-category breakdown)`,
|
|
950
|
+
meta: {
|
|
951
|
+
total: occurrences.length,
|
|
952
|
+
byCategory: Object.fromEntries(byCat),
|
|
953
|
+
firstLine,
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
return findings;
|
|
958
|
+
},
|
|
959
|
+
};
|
|
960
|
+
|
|
890
961
|
export const ALL_RULES: Rule[] = [
|
|
891
962
|
ruleDeadLibShims,
|
|
892
963
|
ruleObsoleteVitePlugins,
|
|
@@ -895,6 +966,7 @@ export const ALL_RULES: Rule[] = [
|
|
|
895
966
|
ruleVtexShimRegression,
|
|
896
967
|
ruleLocalWidgetsTypes,
|
|
897
968
|
ruleFrameworkTodos,
|
|
969
|
+
ruleHtmxResidue,
|
|
898
970
|
];
|
|
899
971
|
|
|
900
972
|
/** Exported for direct unit tests. */
|
|
@@ -907,6 +979,7 @@ export const _internals = {
|
|
|
907
979
|
ruleDeadRuntimeShim,
|
|
908
980
|
ruleSiteLocalGlobals,
|
|
909
981
|
ruleVtexShimRegression,
|
|
982
|
+
ruleHtmxResidue,
|
|
910
983
|
ruleLocalWidgetsTypes,
|
|
911
984
|
ruleFrameworkTodos,
|
|
912
985
|
},
|
|
@@ -986,3 +986,111 @@ export default defineConfig({
|
|
|
986
986
|
expect(supported).toContain("obsolete-vite-plugins");
|
|
987
987
|
});
|
|
988
988
|
});
|
|
989
|
+
|
|
990
|
+
/* ------------------------------------------------------------------ */
|
|
991
|
+
/* W13-C โ htmx-residue rule */
|
|
992
|
+
/* ------------------------------------------------------------------ */
|
|
993
|
+
|
|
994
|
+
describe("rule: htmx-residue", () => {
|
|
995
|
+
it("flags any leftover hx-* element in src/ with category breakdown", () => {
|
|
996
|
+
const fs = makeFs({
|
|
997
|
+
"/site/src/components/AddToBag.tsx":
|
|
998
|
+
'<button hx-on:click={() => {}}>buy</button>\n',
|
|
999
|
+
"/site/src/components/Search.tsx":
|
|
1000
|
+
'<form hx-post="/x" hx-target="#r" hx-swap="innerHTML"><input/></form>\n',
|
|
1001
|
+
});
|
|
1002
|
+
const report = runAudit(SITE, fs);
|
|
1003
|
+
const r = report.rules.find((r) => r.rule === "htmx-residue")!;
|
|
1004
|
+
expect(r.findings).toHaveLength(2);
|
|
1005
|
+
const summary = r.findings.map((f) => f.message).join(" | ");
|
|
1006
|
+
expect(summary).toContain("event-handler=1");
|
|
1007
|
+
expect(summary).toContain("form-swap=1");
|
|
1008
|
+
expect(r.findings[0].fix).toContain("htmx-rewrite.md");
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
it("aggregates multiple occurrences in one file as a single finding", () => {
|
|
1012
|
+
const fs = makeFs({
|
|
1013
|
+
"/site/src/components/Big.tsx":
|
|
1014
|
+
'<button hx-on:click={() => {}}>1</button>\n' +
|
|
1015
|
+
'<button hx-on:click={() => {}}>2</button>\n' +
|
|
1016
|
+
'<form hx-post="/x" hx-target="#r" hx-swap="innerHTML"><input/></form>\n',
|
|
1017
|
+
});
|
|
1018
|
+
const report = runAudit(SITE, fs);
|
|
1019
|
+
const r = report.rules.find((r) => r.rule === "htmx-residue")!;
|
|
1020
|
+
expect(r.findings).toHaveLength(1);
|
|
1021
|
+
expect(r.findings[0].message).toContain("3 hx-* element(s)");
|
|
1022
|
+
expect(r.findings[0].message).toContain("event-handler=2");
|
|
1023
|
+
expect(r.findings[0].message).toContain("form-swap=1");
|
|
1024
|
+
expect(r.findings[0].meta?.total).toBe(3);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it("emits warning severity (so --strict exits 2)", () => {
|
|
1028
|
+
const fs = makeFs({
|
|
1029
|
+
"/site/src/x.tsx": '<button hx-on:click={() => {}}>x</button>\n',
|
|
1030
|
+
});
|
|
1031
|
+
const report = runAudit(SITE, fs);
|
|
1032
|
+
const r = report.rules.find((r) => r.rule === "htmx-residue")!;
|
|
1033
|
+
expect(r.findings[0].severity).toBe("warning");
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it("excludes test files (*.test.tsx, *.spec.ts, __tests__/) โ they may legitimately reference hx-*", () => {
|
|
1037
|
+
const fs = makeFs({
|
|
1038
|
+
"/site/src/components/x.test.tsx":
|
|
1039
|
+
'<button hx-on:click={() => {}}>x</button>\n',
|
|
1040
|
+
"/site/src/components/y.spec.ts":
|
|
1041
|
+
'expect(html).toContain("hx-post=\\"/x\\""); /* doesn\'t hit our regex */\n',
|
|
1042
|
+
"/site/src/__tests__/csrf.tsx":
|
|
1043
|
+
'<form hx-post="/x" hx-target="#r" hx-swap="innerHTML"><input/></form>\n',
|
|
1044
|
+
});
|
|
1045
|
+
const report = runAudit(SITE, fs);
|
|
1046
|
+
const r = report.rules.find((r) => r.rule === "htmx-residue")!;
|
|
1047
|
+
expect(r.findings).toEqual([]);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it("does NOT flag files outside src/ (the rule is scoped to migrated React tree)", () => {
|
|
1051
|
+
const fs = makeFs({
|
|
1052
|
+
// A pre-migration site might still have ./components/ at root.
|
|
1053
|
+
// After migration that's gone; if the engineer left some stragglers
|
|
1054
|
+
// in /scripts or /docs they don't block "rewrite-complete" gate.
|
|
1055
|
+
"/site/scripts/legacy.tsx":
|
|
1056
|
+
'<button hx-on:click={() => {}}>x</button>\n',
|
|
1057
|
+
"/site/docs/example.tsx":
|
|
1058
|
+
'<button hx-on:click={() => {}}>x</button>\n',
|
|
1059
|
+
});
|
|
1060
|
+
const report = runAudit(SITE, fs);
|
|
1061
|
+
const r = report.rules.find((r) => r.rule === "htmx-residue")!;
|
|
1062
|
+
expect(r.findings).toEqual([]);
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
it("returns zero findings on a clean migrated tree (the rewrite-complete gate)", () => {
|
|
1066
|
+
const fs = makeFs({
|
|
1067
|
+
"/site/src/components/Real.tsx":
|
|
1068
|
+
'<button onClick={() => {}}>x</button>\n',
|
|
1069
|
+
"/site/src/routes/index.tsx":
|
|
1070
|
+
'export const Route = createFileRoute("/")({ component: () => <div/> });\n',
|
|
1071
|
+
});
|
|
1072
|
+
const report = runAudit(SITE, fs);
|
|
1073
|
+
const r = report.rules.find((r) => r.rule === "htmx-residue")!;
|
|
1074
|
+
expect(r.findings).toEqual([]);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it("reports the line number of the FIRST hx-* element in the file", () => {
|
|
1078
|
+
const fs = makeFs({
|
|
1079
|
+
"/site/src/x.tsx":
|
|
1080
|
+
"import x from 'y';\n" +
|
|
1081
|
+
"// header\n" +
|
|
1082
|
+
'<button hx-on:click={() => {}}>x</button>\n',
|
|
1083
|
+
});
|
|
1084
|
+
const report = runAudit(SITE, fs);
|
|
1085
|
+
const r = report.rules.find((r) => r.rule === "htmx-residue")!;
|
|
1086
|
+
expect(r.findings[0].file).toBe("src/x.tsx:3");
|
|
1087
|
+
expect(r.findings[0].meta?.firstLine).toBe(3);
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it("does NOT support auto-fix (rewrites are non-mechanical)", () => {
|
|
1091
|
+
const fs = makeFs({});
|
|
1092
|
+
const report = runAudit(SITE, fs);
|
|
1093
|
+
const r = report.rules.find((r) => r.rule === "htmx-residue")!;
|
|
1094
|
+
expect(r.supportsAutoFix).toBe(false);
|
|
1095
|
+
});
|
|
1096
|
+
});
|