@decocms/start 2.23.0 → 2.25.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/references/post-migration-cleanup.md +65 -9
- package/MIGRATION_TOOLING_PLAN.md +158 -0
- package/package.json +1 -1
- package/scripts/migrate/post-cleanup/rules.ts +202 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +236 -0
- package/scripts/migrate-post-cleanup.ts +4 -2
|
@@ -18,8 +18,10 @@ npx -p @decocms/start deco-post-cleanup
|
|
|
18
18
|
|
|
19
19
|
# Auto-apply mechanical fixes for the safe rules, then report what's left.
|
|
20
20
|
# Safe rules: dead-lib-shims, dead-runtime-shim, local-widgets-types,
|
|
21
|
-
# vtex-shim-regression (swap subset), obsolete-vite-plugins
|
|
22
|
-
#
|
|
21
|
+
# vtex-shim-regression (swap subset), obsolete-vite-plugins,
|
|
22
|
+
# local-framework-duplicate (auto-fixable subset of the registry).
|
|
23
|
+
# Other rules — and the warn-only entries of local-framework-duplicate —
|
|
24
|
+
# stay detect-only. They require human judgment.
|
|
23
25
|
npx -p @decocms/start deco-post-cleanup --fix
|
|
24
26
|
|
|
25
27
|
# Combine for CI: auto-fix safe rules, fail (exit 2) if warnings remain.
|
|
@@ -29,14 +31,16 @@ npx -p @decocms/start deco-post-cleanup --fix --strict
|
|
|
29
31
|
npx -p @decocms/start deco-post-cleanup --json
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
The audit covers all
|
|
34
|
+
The audit covers all 9 rules below and prints the exact file path +
|
|
33
35
|
suggested fix for each finding. With `--fix`, the safe rules
|
|
34
36
|
auto-apply: `rm` for dead files, regex-anchored import rewrites for
|
|
35
37
|
shadowed shims (`local-widgets-types`, `dead-runtime-shim`), the swap
|
|
36
|
-
subset of `vtex-shim-regression`,
|
|
37
|
-
inline plugin literals from `vite.config.ts
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
subset of `vtex-shim-regression`, JS-aware removal of obsolete
|
|
39
|
+
inline plugin literals from `vite.config.ts`, and rewrite-imports +
|
|
40
|
+
delete for the auto-fixable subset of `local-framework-duplicate`
|
|
41
|
+
(see § 8). The output explicitly tags rules that require manual work
|
|
42
|
+
as `(0 fixed, manual)`, so you always know what's left after auto-fix
|
|
43
|
+
runs.
|
|
40
44
|
|
|
41
45
|
Real-world signal: on baggagio, `--fix` produced a byte-identical
|
|
42
46
|
diff to the manual cleanup PR a human had just made (45 files,
|
|
@@ -398,7 +402,59 @@ In `--strict` mode any residue exits 2 — wire that into CI once a
|
|
|
398
402
|
site has finished its HTMX rewrite to prevent regressions sneaking
|
|
399
403
|
back in via copy-paste from a Fresh source.
|
|
400
404
|
|
|
401
|
-
## 8.
|
|
405
|
+
## 8. Drop site-local copies of framework code (`local-framework-duplicate`)
|
|
406
|
+
|
|
407
|
+
The audit's `local-framework-duplicate` rule encodes a registry of
|
|
408
|
+
files we expect sites to NOT carry locally because the canonical
|
|
409
|
+
implementation already ships in `@decocms/start`. New entries go in
|
|
410
|
+
`scripts/migrate/post-cleanup/rules.ts → FRAMEWORK_DUPLICATES`.
|
|
411
|
+
|
|
412
|
+
Two kinds of finding:
|
|
413
|
+
|
|
414
|
+
| Kind | Auto-fix | Example | What you do |
|
|
415
|
+
|---|---|---|---|
|
|
416
|
+
| **Pure dup** (`safeToAutoFix: true`) | YES | `src/sdk/clx.ts` matches `@decocms/start/sdk/clx` byte-for-byte | `--fix` rewrites every `from "~/sdk/clx"` to `from "@decocms/start/sdk/clx"` and deletes the file. Zero behavior change. |
|
|
417
|
+
| **Partial overlap** (`safeToAutoFix: false`) | NO | `src/sdk/useSendEvent.ts` (typed) overlaps `@decocms/start/sdk/analytics → useSendEvent` (permissive) | The rule emits a `warning` with a `reason` explaining the manual judgement: widen the framework export, accept type loss, or fork on purpose. Human picks. |
|
|
418
|
+
|
|
419
|
+
### How the rule fires
|
|
420
|
+
|
|
421
|
+
The site file must match every regex in `contentSignature` before
|
|
422
|
+
the rule treats it as the framework dup. This is conservative on
|
|
423
|
+
purpose — sites that genuinely forked the helper (added platform
|
|
424
|
+
logic, wrapped in something else) are skipped automatically.
|
|
425
|
+
|
|
426
|
+
### Current registry
|
|
427
|
+
|
|
428
|
+
| Site path | Canonical | Auto-fix? | Reason / status |
|
|
429
|
+
|---|---|---|---|
|
|
430
|
+
| `src/sdk/clx.ts` | `@decocms/start/sdk/clx` | yes | Identical implementation; baggagio's extra `clsx` alias has zero callers. |
|
|
431
|
+
| `src/sdk/useSendEvent.ts` | `@decocms/start/sdk/analytics` | no | Site copy uses `<E extends AnalyticsEvent>` generic; framework export is permissive. Replace 1:1 = type-safety loss. Either widen the framework first or accept the loss. |
|
|
432
|
+
| `src/matchers/location.ts` | `@decocms/start/matchers/builtins` | no | Framework's `registerBuiltinMatchers()` ships a richer location matcher (`request.cf` first, geo cookies fallback, headers fallback) plus 10 sibling matchers. Adopting changes behaviour — verify country-name lookup parity, swap `setup.ts`'s `customMatchers` entry. |
|
|
433
|
+
| `src/sdk/url.ts` | `@decocms/apps/commerce/sdk/url` | no | Site fork carries a positional `removeIdSku?: boolean` flag with hardcoded VTEX-specific keys. Canonical apps export uses `{ stripSearchParams: string[] }` (`@decocms/apps@1.9+`). Rewrite imports + each `relative(url, true)` call site → `relative(url, { stripSearchParams: ["idsku", "skuId"] })`, then delete the file. Auto-fix is gated because the call-site rewrite needs JSX/TS-aware transformation, not pure import rewrite. |
|
|
434
|
+
|
|
435
|
+
### Adding a new entry
|
|
436
|
+
|
|
437
|
+
When you spot a site carrying its own copy of code that lives in
|
|
438
|
+
`@decocms/start`, add an entry to `FRAMEWORK_DUPLICATES`:
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
{
|
|
442
|
+
id: "<short-stable-id>",
|
|
443
|
+
sitePath: "src/<path>.ts",
|
|
444
|
+
canonicalImport: "@decocms/start/<path>",
|
|
445
|
+
contentSignature: [/<regex 1>/, /<regex 2>/],
|
|
446
|
+
safeToAutoFix: true | false,
|
|
447
|
+
reason: "<required when not safeToAutoFix>",
|
|
448
|
+
description: "<one-liner used in finding message>",
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Per **D4** in the migration tooling policy, the framework promotion
|
|
453
|
+
itself happens at 3+ sites. This registry is the *enforcement* layer
|
|
454
|
+
once promoted: every other site picks up the convergence
|
|
455
|
+
automatically the next time `deco-post-cleanup --fix` runs.
|
|
456
|
+
|
|
457
|
+
## 9. Search for orphan `TODO: move into framework` comments
|
|
402
458
|
|
|
403
459
|
Real sites accumulate `TODO` comments like `// TODO: move into decoVitePlugin
|
|
404
460
|
in next @decocms/start release`. These are roadmap items the framework
|
|
@@ -415,7 +471,7 @@ For each hit, decide:
|
|
|
415
471
|
|
|
416
472
|
## Verification checklist
|
|
417
473
|
|
|
418
|
-
After completing 1-
|
|
474
|
+
After completing 1-9:
|
|
419
475
|
|
|
420
476
|
- [ ] `npm run typecheck` baseline matches pre-cleanup count (no new errors)
|
|
421
477
|
- [ ] `npm run dev` starts and `/`, `/some-pdp/p`, `/s?q=foo` all render
|
|
@@ -1128,6 +1128,164 @@ had no skill coverage. Wave 15-A closes both loops in one PR.
|
|
|
1128
1128
|
`useSendEvent`/`clx`/location-matcher imports, `relative()`
|
|
1129
1129
|
SKU-stripping extension. Sequenced after 15-A merges.
|
|
1130
1130
|
|
|
1131
|
+
### Wave 15-B-1 (cross-site convergence — `local-framework-duplicate` audit rule) — 🟡 **IN FLIGHT**
|
|
1132
|
+
|
|
1133
|
+
First slice of the cross-site-convergence backlog deferred from
|
|
1134
|
+
Wave 15-A. Concrete data first (per the user-rule "verify before
|
|
1135
|
+
designing"): the `useSendEvent`/`clx`/location-matcher promotion
|
|
1136
|
+
turned out to be *not* a "promote site code → framework" exercise.
|
|
1137
|
+
The framework already has each helper. The work is **enforcing the
|
|
1138
|
+
existing canonical** when sites carry their own copy.
|
|
1139
|
+
|
|
1140
|
+
Verified state (2026-05-01 grep against both sites):
|
|
1141
|
+
|
|
1142
|
+
| Item | casaevideo | baggagio | Action |
|
|
1143
|
+
|---|---|---|---|
|
|
1144
|
+
| `src/sdk/clx.ts` | absent (already canonical) | present, identical body + dead `clsx` alias (zero callers) | pure dup → **auto-fix** |
|
|
1145
|
+
| `src/sdk/useSendEvent.ts` | absent | present, **stricter** typing (`<E extends AnalyticsEvent>` generic) vs framework's permissive shape | **warn-only** (replacing 1:1 weakens types) |
|
|
1146
|
+
| `src/matchers/location.ts` | present, cookie-only subset of framework | absent | **warn-only** (framework's `registerBuiltinMatchers()` is a behavior superset; needs per-site verification of country-name lookup parity) |
|
|
1147
|
+
|
|
1148
|
+
So this is exactly *one* mechanically-applicable fix (`clx` in
|
|
1149
|
+
baggagio) plus two judgement calls. Hand-applying would be cheap;
|
|
1150
|
+
the value is making the audit *enforce* the convergence so the next
|
|
1151
|
+
copy-paste regression on any future site gets caught automatically.
|
|
1152
|
+
|
|
1153
|
+
**Shipped (one PR against `decocms/deco-start`):**
|
|
1154
|
+
|
|
1155
|
+
38. `feat(migrate): local-framework-duplicate audit rule with registry-driven enforcement` 🟡 **WAITING ON CI**
|
|
1156
|
+
- **New rule `local-framework-duplicate`** in
|
|
1157
|
+
`scripts/migrate/post-cleanup/rules.ts` driven by an exported
|
|
1158
|
+
`FRAMEWORK_DUPLICATES` registry. Each entry is `{ id,
|
|
1159
|
+
sitePath, canonicalImport, contentSignature: RegExp[],
|
|
1160
|
+
safeToAutoFix, reason?, description }`. The rule fires only
|
|
1161
|
+
when **every** content-signature regex matches the site file
|
|
1162
|
+
— conservative on purpose so genuinely-forked helpers are
|
|
1163
|
+
skipped.
|
|
1164
|
+
- **Auto-fix path** (when `safeToAutoFix: true`): rewrite all
|
|
1165
|
+
`from "~/<derived>"` importers to `from
|
|
1166
|
+
"<canonicalImport>"` via the existing `rewriteImportSpec`
|
|
1167
|
+
helper, then delete the file. Already-canonical importers
|
|
1168
|
+
are left untouched.
|
|
1169
|
+
- **Warn-only path** (when `safeToAutoFix: false`): rule still
|
|
1170
|
+
fires + populates the finding's `fix:` field with the
|
|
1171
|
+
`reason` so engineers see *why* auto-fix is gated and what
|
|
1172
|
+
they need to verify before manual cleanup.
|
|
1173
|
+
- **Three initial entries** in the registry, mapped 1:1 to the
|
|
1174
|
+
cross-site audit findings:
|
|
1175
|
+
| id | site path | canonical | auto-fix? |
|
|
1176
|
+
|---|---|---|---|
|
|
1177
|
+
| `clx` | `src/sdk/clx.ts` | `@decocms/start/sdk/clx` | **yes** |
|
|
1178
|
+
| `use-send-event` | `src/sdk/useSendEvent.ts` | `@decocms/start/sdk/analytics` | no (typing regression) |
|
|
1179
|
+
| `location-matcher` | `src/matchers/location.ts` | `@decocms/start/matchers/builtins` | no (behavior superset, parity check needed) |
|
|
1180
|
+
- **11 new tests** covering: pure-dup detection, fork detection
|
|
1181
|
+
(signature mismatch → no flag), warn-only entries, severity
|
|
1182
|
+
uniformity (warning for both kinds, so `--strict` gates
|
|
1183
|
+
everything), auto-fix happy-path (delete + rewrite both
|
|
1184
|
+
importers, leave canonical importers alone), warn-only
|
|
1185
|
+
auto-fix is a no-op (does NOT delete partial-overlap files),
|
|
1186
|
+
mixed coexistence (auto-fixable `clx` and warn-only
|
|
1187
|
+
`useSendEvent` in the same tree → only `clx` gets auto-fixed),
|
|
1188
|
+
`supportsAutoFix` flag is true (since rule has `applyFix`).
|
|
1189
|
+
- **CLI help text + `post-migration-cleanup.md` § 8** updated
|
|
1190
|
+
with the new rule's table and the "adding a new entry"
|
|
1191
|
+
section. Old § 8 (orphan TODO comments) renumbered to § 9.
|
|
1192
|
+
- 345 → 353 tests pass, typecheck clean, end-to-end disk smoke
|
|
1193
|
+
against a temp fixture confirmed: 2 importers rewritten + 1
|
|
1194
|
+
file deleted in one `--fix` run.
|
|
1195
|
+
- **Real-site smoke**:
|
|
1196
|
+
- **baggagio**: rule fires twice — `clx.ts` (auto-fixable),
|
|
1197
|
+
`useSendEvent.ts` (warn-only with the typed-generic reason).
|
|
1198
|
+
- **casaevideo**: rule fires once — `location.ts` (warn-only
|
|
1199
|
+
with the `registerBuiltinMatchers()` adoption hint).
|
|
1200
|
+
- **Net**: every future site that copy-pastes any of these three
|
|
1201
|
+
files gets a tight audit finding + auto-fix on the safe one.
|
|
1202
|
+
The registry pattern means adding a 4th cross-site duplicate
|
|
1203
|
+
is a single object literal — no new rule, no new tests
|
|
1204
|
+
scaffolding, no new doc section.
|
|
1205
|
+
|
|
1206
|
+
**Still in the cross-site backlog (sequenced behind 15-B-1):**
|
|
1207
|
+
|
|
1208
|
+
- **15-B-2** — `useSuggestions` framework helper (new export in
|
|
1209
|
+
`@decocms/start/sdk` typed by `Resolved<T>`, optional Sentry
|
|
1210
|
+
hook). Sites adopt incrementally; once 2+ adopt, add a registry
|
|
1211
|
+
entry pointing the legacy hand-rolled implementations at the
|
|
1212
|
+
canonical via `local-framework-duplicate`.
|
|
1213
|
+
- **15-B-3** — `useOffer` factory (D4 candidate; needs design pass
|
|
1214
|
+
for PIX/installment plugin slots).
|
|
1215
|
+
- **15-B-4** — `Picture` API unification (breaking; needs a
|
|
1216
|
+
picking-the-winner pass between casaevideo's and baggagio's
|
|
1217
|
+
shapes, plus a codemod for call sites).
|
|
1218
|
+
|
|
1219
|
+
### Wave 15-B-5 (canonical `relative()` + audit registry entry — apps + deco-start) — 🟡 **IN FLIGHT**
|
|
1220
|
+
|
|
1221
|
+
The smallest 15-B slice: extend `commerce/sdk/url.ts → relative()`
|
|
1222
|
+
with a generic options bag, then point the audit at the canonical
|
|
1223
|
+
so future site forks get caught automatically.
|
|
1224
|
+
|
|
1225
|
+
Verified state (2026-05-01 grep against baggagio-tanstack):
|
|
1226
|
+
|
|
1227
|
+
- `src/sdk/url.ts` carries a positional 2-arg fork (`relative(link,
|
|
1228
|
+
removeIdSku?: boolean)`) with VTEX-specific keys (`idsku`,
|
|
1229
|
+
`skuId`) hardcoded inside.
|
|
1230
|
+
- 9 importers in baggagio. ONE of them — `ProductCard.tsx` — uses
|
|
1231
|
+
the 2-arg form (via prop `removeIdSkuFromUrl`). The other 8 use
|
|
1232
|
+
the 1-arg form, identical to the apps canonical.
|
|
1233
|
+
- casaevideo doesn't carry a fork.
|
|
1234
|
+
|
|
1235
|
+
So the convergence is one apps-side extension + one audit registry
|
|
1236
|
+
entry. The single `ProductCard` call site rewrites by hand or by a
|
|
1237
|
+
future codemod (out of scope here).
|
|
1238
|
+
|
|
1239
|
+
**Shipped (two PRs):**
|
|
1240
|
+
|
|
1241
|
+
39. [`apps-start#36`](https://github.com/decocms/apps-start/pull/36) — `feat(commerce/sdk): extend relative() with stripSearchParams option` ✅ **MERGED** (will release as `@decocms/apps@1.9.x`).
|
|
1242
|
+
- **`commerce/sdk/url.ts`**: backwards-compatible second
|
|
1243
|
+
`RelativeOptions` argument with `stripSearchParams?:
|
|
1244
|
+
string[]` primitive. 1-arg callers (everyone in apps + 8/9
|
|
1245
|
+
of baggagio's call sites) unaffected. The byte-for-byte
|
|
1246
|
+
"://path-style" passthrough is locked in by an explicit
|
|
1247
|
+
backwards-compat test.
|
|
1248
|
+
- **Why generic, not `removeIdSku?: boolean`**: hardcoded VTEX
|
|
1249
|
+
key names belong at call sites, not in a generic commerce
|
|
1250
|
+
helper. `stripSearchParams: string[]` works for any platform.
|
|
1251
|
+
Sites pass `["idsku", "skuId"]` themselves — honest about
|
|
1252
|
+
where the platform knowledge lives.
|
|
1253
|
+
- **`commerce/__tests__/url.test.ts`** (new): 18 tests covering
|
|
1254
|
+
base behaviour (relative/absolute/undefined/empty/malformed
|
|
1255
|
+
via `toString()` thrower), `stripSearchParams` primitive
|
|
1256
|
+
(single, multi, empty, missing, repeated keys, all-stripped
|
|
1257
|
+
→ drop trailing `?`), and three explicit backwards-compat
|
|
1258
|
+
assertions.
|
|
1259
|
+
- 290/290 tests pass, typecheck + biome clean.
|
|
1260
|
+
|
|
1261
|
+
40. `feat(migrate): add url-relative entry to local-framework-duplicate registry` 🟡 **WAITING ON CI** (deco-start side).
|
|
1262
|
+
- **Registry entry** in `FRAMEWORK_DUPLICATES` for
|
|
1263
|
+
`src/sdk/url.ts` → `@decocms/apps/commerce/sdk/url`. Content
|
|
1264
|
+
signature anchored on the legacy positional `removeIdSku?:
|
|
1265
|
+
boolean` shape so sites that already adopted the canonical
|
|
1266
|
+
options-object aren't flagged.
|
|
1267
|
+
- **`safeToAutoFix: false`** — the call-site rewrite from
|
|
1268
|
+
positional `relative(url, true)` to `relative(url, {
|
|
1269
|
+
stripSearchParams: ["idsku", "skuId"] })` requires JSX/TS-
|
|
1270
|
+
aware transformation, not pure import rewrite. The finding's
|
|
1271
|
+
`fix:` field carries the exact recipe.
|
|
1272
|
+
- **Two new tests**: positive case (legacy fork → flagged with
|
|
1273
|
+
the correct hint), negative case (canonical-shaped local
|
|
1274
|
+
fork that already adopted the options object → NOT flagged,
|
|
1275
|
+
proves the signature-anchoring works).
|
|
1276
|
+
- **Skill doc § 8 table** updated with the 4th entry, including
|
|
1277
|
+
version pin (`@decocms/apps@1.9+`).
|
|
1278
|
+
- 355/355 tests pass, typecheck clean. Smoke against baggagio
|
|
1279
|
+
now fires 3 findings (was 2): clx, useSendEvent, url; smoke
|
|
1280
|
+
against casaevideo unchanged at 1 (location-matcher).
|
|
1281
|
+
|
|
1282
|
+
**Process note**: this is the first time we ran the apps-side and
|
|
1283
|
+
deco-start-side as a pair of PRs. The order matters — apps-start
|
|
1284
|
+
must merge first so the deco-start audit registry can point at
|
|
1285
|
+
the released canonical. The skill doc explicitly version-pins the
|
|
1286
|
+
canonical (`@decocms/apps@1.9+`) so engineers reading the audit
|
|
1287
|
+
output know whether they need to bump apps before adopting.
|
|
1288
|
+
|
|
1131
1289
|
### Wave 15+ (htmx cleanup PRs on als + propagation to other sites) — Priority 3 / 4
|
|
1132
1290
|
|
|
1133
1291
|
Each htmx pattern that survives the codemod becomes a per-pattern PR
|
package/package.json
CHANGED
|
@@ -943,6 +943,206 @@ const ruleFrameworkTodos: Rule = {
|
|
|
943
943
|
},
|
|
944
944
|
};
|
|
945
945
|
|
|
946
|
+
/* ------------------------------------------------------------------ */
|
|
947
|
+
/* Rule — `local-framework-duplicate` — site-local copy of fwk code */
|
|
948
|
+
/* ------------------------------------------------------------------ */
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Registry of files we expect sites to NOT carry locally because the
|
|
952
|
+
* canonical implementation already lives in `@decocms/start` (or a
|
|
953
|
+
* sibling apps package).
|
|
954
|
+
*
|
|
955
|
+
* Two flavours:
|
|
956
|
+
* - `safeToAutoFix: true` — site file is a behaviour-equivalent dup
|
|
957
|
+
* of the framework export. `--fix` rewrites every `from "~/<path>"`
|
|
958
|
+
* importer to `from "<canonicalImport>"` and deletes the file.
|
|
959
|
+
* - `safeToAutoFix: false` — site file *overlaps* with framework code
|
|
960
|
+
* but isn't a clean drop-in (different typing, partial coverage,
|
|
961
|
+
* stricter behaviour, etc.). The rule still flags it so the entry
|
|
962
|
+
* surfaces in audits, but never deletes — the `reason` explains why
|
|
963
|
+
* a human has to make the call.
|
|
964
|
+
*
|
|
965
|
+
* `contentSignature` regexes ALL must match the site file's contents
|
|
966
|
+
* before the rule fires. They are deliberately specific enough to
|
|
967
|
+
* avoid catching forks that happen to share a filename but have
|
|
968
|
+
* diverged.
|
|
969
|
+
*/
|
|
970
|
+
interface FrameworkDuplicate {
|
|
971
|
+
/** Stable id surfaced in finding meta and CLI/JSON output. */
|
|
972
|
+
id: string;
|
|
973
|
+
/** Site-relative path of the duplicated file (e.g. "src/sdk/clx.ts"). */
|
|
974
|
+
sitePath: string;
|
|
975
|
+
/** Canonical import to rewrite to. */
|
|
976
|
+
canonicalImport: string;
|
|
977
|
+
/**
|
|
978
|
+
* Heuristic content fingerprint. The site file must match every
|
|
979
|
+
* regex for the rule to consider it the framework dup.
|
|
980
|
+
*/
|
|
981
|
+
contentSignature: RegExp[];
|
|
982
|
+
/**
|
|
983
|
+
* When true, the rule's `applyFix` will rewrite all importers and
|
|
984
|
+
* delete the file. When false, the rule emits a warning only —
|
|
985
|
+
* `reason` explains the manual judgement required.
|
|
986
|
+
*/
|
|
987
|
+
safeToAutoFix: boolean;
|
|
988
|
+
/**
|
|
989
|
+
* Required when `safeToAutoFix: false`. Surfaces in the finding's
|
|
990
|
+
* `fix:` field so users see *why* the auto-fix is gated.
|
|
991
|
+
*/
|
|
992
|
+
reason?: string;
|
|
993
|
+
/**
|
|
994
|
+
* Human-readable one-liner shown in the finding message and used
|
|
995
|
+
* to compose the `fix:` hint when auto-fixable.
|
|
996
|
+
*/
|
|
997
|
+
description: string;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Add an entry here when:
|
|
1002
|
+
* - 1+ migrated sites carry their own copy of code that already
|
|
1003
|
+
* exists in `@decocms/start` (or a sibling apps package), AND
|
|
1004
|
+
* - the canonical version is at least feature-equivalent.
|
|
1005
|
+
*
|
|
1006
|
+
* Per D4 in the migration tooling policy, the framework promotion
|
|
1007
|
+
* itself happens at 3+ sites — but once promoted, this registry is
|
|
1008
|
+
* how we *enforce* convergence on the remaining sites.
|
|
1009
|
+
*/
|
|
1010
|
+
export const FRAMEWORK_DUPLICATES: FrameworkDuplicate[] = [
|
|
1011
|
+
{
|
|
1012
|
+
id: "clx",
|
|
1013
|
+
sitePath: "src/sdk/clx.ts",
|
|
1014
|
+
canonicalImport: "@decocms/start/sdk/clx",
|
|
1015
|
+
contentSignature: [
|
|
1016
|
+
/export\s+const\s+clx\s*=/,
|
|
1017
|
+
/args\.filter\(Boolean\)\.join/,
|
|
1018
|
+
],
|
|
1019
|
+
safeToAutoFix: true,
|
|
1020
|
+
description: "src/sdk/clx.ts duplicates @decocms/start/sdk/clx",
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
id: "use-send-event",
|
|
1024
|
+
sitePath: "src/sdk/useSendEvent.ts",
|
|
1025
|
+
canonicalImport: "@decocms/start/sdk/analytics",
|
|
1026
|
+
contentSignature: [
|
|
1027
|
+
/export\s+(?:const|function)\s+useSendEvent/,
|
|
1028
|
+
/data-event/,
|
|
1029
|
+
/encodeURIComponent/,
|
|
1030
|
+
],
|
|
1031
|
+
safeToAutoFix: false,
|
|
1032
|
+
reason:
|
|
1033
|
+
"site copy uses a typed AnalyticsEvent generic; the framework export is permissive. " +
|
|
1034
|
+
"Replacing 1:1 weakens type-safety. Either widen the framework export (preferred), or " +
|
|
1035
|
+
"rewrite call sites to drop the generic. Manual review required.",
|
|
1036
|
+
description:
|
|
1037
|
+
"src/sdk/useSendEvent.ts overlaps with @decocms/start/sdk/analytics → useSendEvent",
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
id: "location-matcher",
|
|
1041
|
+
sitePath: "src/matchers/location.ts",
|
|
1042
|
+
canonicalImport: "@decocms/start/matchers/builtins",
|
|
1043
|
+
contentSignature: [
|
|
1044
|
+
/registerMatcher\(\s*['"]website\/matchers\/location\.ts['"]/,
|
|
1045
|
+
/__cf_geo/,
|
|
1046
|
+
],
|
|
1047
|
+
safeToAutoFix: false,
|
|
1048
|
+
reason:
|
|
1049
|
+
"framework's registerBuiltinMatchers() ships a richer location matcher (request.cf + " +
|
|
1050
|
+
"geo cookies + headers + 10 sibling matchers). Adopting it changes behaviour: " +
|
|
1051
|
+
"verify country-name lookup parity (resolveCountryCode vs site's inline table) and " +
|
|
1052
|
+
"swap setup.ts's customMatchers entry to call registerBuiltinMatchers().",
|
|
1053
|
+
description:
|
|
1054
|
+
"src/matchers/location.ts overlaps with @decocms/start/matchers/builtins → registerBuiltinMatchers()",
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
id: "url-relative",
|
|
1058
|
+
sitePath: "src/sdk/url.ts",
|
|
1059
|
+
canonicalImport: "@decocms/apps/commerce/sdk/url",
|
|
1060
|
+
// Fingerprint: site fork carries a positional `removeIdSku?: boolean`
|
|
1061
|
+
// flag + hardcoded VTEX-specific keys (`idsku`, `skuId`). Canonical
|
|
1062
|
+
// apps export uses an options object — `{ stripSearchParams: string[] }`
|
|
1063
|
+
// — which is generic and platform-agnostic.
|
|
1064
|
+
contentSignature: [
|
|
1065
|
+
/export\s+const\s+relative\s*=/,
|
|
1066
|
+
/removeIdSku\s*\?\s*:\s*boolean/,
|
|
1067
|
+
/['"](idsku|skuId)['"]/,
|
|
1068
|
+
],
|
|
1069
|
+
safeToAutoFix: false,
|
|
1070
|
+
reason:
|
|
1071
|
+
"rewrite imports to '@decocms/apps/commerce/sdk/url'. " +
|
|
1072
|
+
"Each call site using the boolean form `relative(url, true)` becomes " +
|
|
1073
|
+
"`relative(url, { stripSearchParams: [\"idsku\", \"skuId\"] })`. " +
|
|
1074
|
+
"1-arg calls are unchanged. Then delete src/sdk/url.ts. " +
|
|
1075
|
+
"Auto-fix is gated because the call-site rewrite needs JSX/TS-aware " +
|
|
1076
|
+
"transformation (positional bool → options object), not pure import " +
|
|
1077
|
+
"rewrite.",
|
|
1078
|
+
description:
|
|
1079
|
+
"src/sdk/url.ts overlaps with @decocms/apps/commerce/sdk/url → relative() (extended in @decocms/apps@1.9+)",
|
|
1080
|
+
},
|
|
1081
|
+
];
|
|
1082
|
+
|
|
1083
|
+
const ruleLocalFrameworkDuplicate: Rule = {
|
|
1084
|
+
id: "local-framework-duplicate",
|
|
1085
|
+
title: "Site-local copy of framework code",
|
|
1086
|
+
run({ siteDir, fs }: RuleContext): Finding[] {
|
|
1087
|
+
const findings: Finding[] = [];
|
|
1088
|
+
for (const dup of FRAMEWORK_DUPLICATES) {
|
|
1089
|
+
const abs = `${siteDir}/${dup.sitePath}`;
|
|
1090
|
+
if (!fs.exists(abs)) continue;
|
|
1091
|
+
const content = fs.readText(abs);
|
|
1092
|
+
const matchesAll = dup.contentSignature.every((re) => re.test(content));
|
|
1093
|
+
if (!matchesAll) continue;
|
|
1094
|
+
|
|
1095
|
+
const fixMessage = dup.safeToAutoFix
|
|
1096
|
+
? `Auto-fixable: rewrite \`from "~/${stripExt(dup.sitePath.replace(/^src\//, ""))}"\` → \`from "${dup.canonicalImport}"\` and delete ${dup.sitePath}.`
|
|
1097
|
+
: dup.reason ?? "Manual review required.";
|
|
1098
|
+
|
|
1099
|
+
findings.push({
|
|
1100
|
+
rule: "local-framework-duplicate",
|
|
1101
|
+
severity: "warning",
|
|
1102
|
+
file: dup.sitePath,
|
|
1103
|
+
message: `${dup.description}${dup.safeToAutoFix ? " (pure dup)" : " (partial overlap)"}`,
|
|
1104
|
+
fix: fixMessage,
|
|
1105
|
+
meta: {
|
|
1106
|
+
id: dup.id,
|
|
1107
|
+
canonicalImport: dup.canonicalImport,
|
|
1108
|
+
safeToAutoFix: dup.safeToAutoFix,
|
|
1109
|
+
...(dup.reason ? { reason: dup.reason } : {}),
|
|
1110
|
+
},
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
return findings;
|
|
1114
|
+
},
|
|
1115
|
+
applyFix(ctx, findings, writer): FixAction[] {
|
|
1116
|
+
const actions: FixAction[] = [];
|
|
1117
|
+
for (const f of findings) {
|
|
1118
|
+
const id = f.meta?.id as string | undefined;
|
|
1119
|
+
const safe = f.meta?.safeToAutoFix === true;
|
|
1120
|
+
if (!safe || !id) continue;
|
|
1121
|
+
const dup = FRAMEWORK_DUPLICATES.find((d) => d.id === id);
|
|
1122
|
+
if (!dup) continue;
|
|
1123
|
+
|
|
1124
|
+
const siteImportSpec = `~/${stripExt(dup.sitePath.replace(/^src\//, ""))}`;
|
|
1125
|
+
const updated = rewriteImportSpec(
|
|
1126
|
+
ctx,
|
|
1127
|
+
writer,
|
|
1128
|
+
siteImportSpec,
|
|
1129
|
+
dup.canonicalImport,
|
|
1130
|
+
);
|
|
1131
|
+
writer.deleteFile(`${ctx.siteDir}/${dup.sitePath}`);
|
|
1132
|
+
actions.push({
|
|
1133
|
+
file: dup.sitePath,
|
|
1134
|
+
kind: "rewrite-imports+delete",
|
|
1135
|
+
detail: `rewrote ${updated.length} import(s) "${siteImportSpec}" → "${dup.canonicalImport}" and deleted ${dup.sitePath}`,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
return actions;
|
|
1139
|
+
},
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
function stripExt(path: string): string {
|
|
1143
|
+
return path.replace(/\.(ts|tsx|js|jsx|mjs)$/, "");
|
|
1144
|
+
}
|
|
1145
|
+
|
|
946
1146
|
/* ------------------------------------------------------------------ */
|
|
947
1147
|
/* Rule 8 — `htmx-residue` — leftover hx-* attrs in migrated src/ */
|
|
948
1148
|
/* ------------------------------------------------------------------ */
|
|
@@ -1021,6 +1221,7 @@ export const ALL_RULES: Rule[] = [
|
|
|
1021
1221
|
ruleVtexShimRegression,
|
|
1022
1222
|
ruleLocalWidgetsTypes,
|
|
1023
1223
|
ruleFrameworkTodos,
|
|
1224
|
+
ruleLocalFrameworkDuplicate,
|
|
1024
1225
|
ruleHtmxResidue,
|
|
1025
1226
|
];
|
|
1026
1227
|
|
|
@@ -1037,5 +1238,6 @@ export const _internals = {
|
|
|
1037
1238
|
ruleHtmxResidue,
|
|
1038
1239
|
ruleLocalWidgetsTypes,
|
|
1039
1240
|
ruleFrameworkTodos,
|
|
1241
|
+
ruleLocalFrameworkDuplicate,
|
|
1040
1242
|
},
|
|
1041
1243
|
};
|
|
@@ -666,6 +666,7 @@ describe("runAudit — totals", () => {
|
|
|
666
666
|
[
|
|
667
667
|
"dead-lib-shims",
|
|
668
668
|
"dead-runtime-shim",
|
|
669
|
+
"local-framework-duplicate",
|
|
669
670
|
"local-widgets-types",
|
|
670
671
|
"obsolete-vite-plugins",
|
|
671
672
|
"vtex-shim-regression",
|
|
@@ -1170,3 +1171,238 @@ describe("rule: htmx-residue", () => {
|
|
|
1170
1171
|
expect(r.supportsAutoFix).toBe(false);
|
|
1171
1172
|
});
|
|
1172
1173
|
});
|
|
1174
|
+
|
|
1175
|
+
/* ------------------------------------------------------------------ */
|
|
1176
|
+
/* W15-B-1 — local-framework-duplicate rule */
|
|
1177
|
+
/* ------------------------------------------------------------------ */
|
|
1178
|
+
|
|
1179
|
+
describe("rule: local-framework-duplicate", () => {
|
|
1180
|
+
it("flags src/sdk/clx.ts when content matches the framework export (auto-fixable)", () => {
|
|
1181
|
+
const fs = makeFs({
|
|
1182
|
+
"/site/src/sdk/clx.ts":
|
|
1183
|
+
"export const clx = (...args: (string | null | undefined | false)[]) =>\n" +
|
|
1184
|
+
' args.filter(Boolean).join(" ").replace(/\\s\\s+/g, " ");\n' +
|
|
1185
|
+
"export default clx;\n",
|
|
1186
|
+
});
|
|
1187
|
+
const report = runAudit(SITE, fs);
|
|
1188
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1189
|
+
expect(r.findings).toHaveLength(1);
|
|
1190
|
+
expect(r.findings[0].file).toBe("src/sdk/clx.ts");
|
|
1191
|
+
expect(r.findings[0].message).toContain("pure dup");
|
|
1192
|
+
expect(r.findings[0].meta?.id).toBe("clx");
|
|
1193
|
+
expect(r.findings[0].meta?.safeToAutoFix).toBe(true);
|
|
1194
|
+
expect(r.findings[0].meta?.canonicalImport).toBe("@decocms/start/sdk/clx");
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
it("flags src/sdk/clx.ts when site adds a clsx alias (signature still matches)", () => {
|
|
1198
|
+
const fs = makeFs({
|
|
1199
|
+
"/site/src/sdk/clx.ts":
|
|
1200
|
+
"export const clx = (...args: (string | null | undefined | false)[]) =>\n" +
|
|
1201
|
+
' args.filter(Boolean).join(" ").replace(/\\s\\s+/g, " ");\n' +
|
|
1202
|
+
"export const clsx = clx;\n" +
|
|
1203
|
+
"export default clx;\n",
|
|
1204
|
+
});
|
|
1205
|
+
const report = runAudit(SITE, fs);
|
|
1206
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1207
|
+
expect(r.findings).toHaveLength(1);
|
|
1208
|
+
expect(r.findings[0].meta?.id).toBe("clx");
|
|
1209
|
+
expect(r.findings[0].meta?.safeToAutoFix).toBe(true);
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
it("does NOT flag a clx.ts that has been forked (signature mismatch)", () => {
|
|
1213
|
+
const fs = makeFs({
|
|
1214
|
+
// Realistic fork: uses lodash-style cn from a different package.
|
|
1215
|
+
"/site/src/sdk/clx.ts":
|
|
1216
|
+
'import { cn } from "lodash";\nexport const clx = cn;\nexport default clx;\n',
|
|
1217
|
+
});
|
|
1218
|
+
const report = runAudit(SITE, fs);
|
|
1219
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1220
|
+
expect(r.findings).toEqual([]);
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
it("flags src/sdk/useSendEvent.ts as warn-only (typing regression risk)", () => {
|
|
1224
|
+
const fs = makeFs({
|
|
1225
|
+
"/site/src/sdk/useSendEvent.ts":
|
|
1226
|
+
'import { AnalyticsEvent } from "@decocms/apps/commerce/types";\n' +
|
|
1227
|
+
"export const useSendEvent = <E extends AnalyticsEvent>(\n" +
|
|
1228
|
+
" { event, on }: { event: E; on: 'click' | 'view' | 'change' },\n" +
|
|
1229
|
+
") => ({\n" +
|
|
1230
|
+
' "data-event": encodeURIComponent(JSON.stringify(event)),\n' +
|
|
1231
|
+
' "data-event-trigger": on,\n' +
|
|
1232
|
+
"});\n",
|
|
1233
|
+
});
|
|
1234
|
+
const report = runAudit(SITE, fs);
|
|
1235
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1236
|
+
expect(r.findings).toHaveLength(1);
|
|
1237
|
+
expect(r.findings[0].file).toBe("src/sdk/useSendEvent.ts");
|
|
1238
|
+
expect(r.findings[0].message).toContain("partial overlap");
|
|
1239
|
+
expect(r.findings[0].meta?.safeToAutoFix).toBe(false);
|
|
1240
|
+
expect(r.findings[0].fix).toContain("typed AnalyticsEvent generic");
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
it("flags src/sdk/url.ts as warn-only (positional bool → options object call-site rewrite)", () => {
|
|
1244
|
+
const fs = makeFs({
|
|
1245
|
+
"/site/src/sdk/url.ts":
|
|
1246
|
+
"export const relative = (\n" +
|
|
1247
|
+
" link?: string | undefined,\n" +
|
|
1248
|
+
" removeIdSku?: boolean,\n" +
|
|
1249
|
+
") => {\n" +
|
|
1250
|
+
' const linkUrl = link ? new URL(link, "http://localhost") : undefined;\n' +
|
|
1251
|
+
" if (linkUrl && removeIdSku) {\n" +
|
|
1252
|
+
' linkUrl.searchParams.delete("idsku");\n' +
|
|
1253
|
+
' linkUrl.searchParams.delete("skuId");\n' +
|
|
1254
|
+
" }\n" +
|
|
1255
|
+
" return linkUrl ? `${linkUrl.pathname}` : undefined;\n" +
|
|
1256
|
+
"};\n",
|
|
1257
|
+
});
|
|
1258
|
+
const report = runAudit(SITE, fs);
|
|
1259
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1260
|
+
expect(r.findings).toHaveLength(1);
|
|
1261
|
+
expect(r.findings[0].file).toBe("src/sdk/url.ts");
|
|
1262
|
+
expect(r.findings[0].meta?.id).toBe("url-relative");
|
|
1263
|
+
expect(r.findings[0].meta?.safeToAutoFix).toBe(false);
|
|
1264
|
+
expect(r.findings[0].meta?.canonicalImport).toBe(
|
|
1265
|
+
"@decocms/apps/commerce/sdk/url",
|
|
1266
|
+
);
|
|
1267
|
+
expect(r.findings[0].fix).toContain("stripSearchParams");
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
it("does NOT flag a forked url.ts that no longer carries the removeIdSku flag", () => {
|
|
1271
|
+
// A site that already adopted an options-object-shaped local helper
|
|
1272
|
+
// should not be flagged — the rule's signature is anchored on the
|
|
1273
|
+
// legacy positional-boolean shape.
|
|
1274
|
+
const fs = makeFs({
|
|
1275
|
+
"/site/src/sdk/url.ts":
|
|
1276
|
+
"export const relative = (link?: string, options?: { stripSearchParams?: string[] }) => {\n" +
|
|
1277
|
+
' if (!link) return undefined;\n' +
|
|
1278
|
+
' return new URL(link, "https://localhost").pathname;\n' +
|
|
1279
|
+
"};\n",
|
|
1280
|
+
});
|
|
1281
|
+
const report = runAudit(SITE, fs);
|
|
1282
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1283
|
+
// url-relative entry must NOT fire on the canonical-shaped local fork.
|
|
1284
|
+
const urlFinding = r.findings.find((f) => f.meta?.id === "url-relative");
|
|
1285
|
+
expect(urlFinding).toBeUndefined();
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it("flags src/matchers/location.ts as warn-only (behaviour-superset opportunity)", () => {
|
|
1289
|
+
const fs = makeFs({
|
|
1290
|
+
"/site/src/matchers/location.ts":
|
|
1291
|
+
'import { registerMatcher } from "@decocms/start/cms";\n' +
|
|
1292
|
+
"export function registerLocationMatcher(): void {\n" +
|
|
1293
|
+
' registerMatcher("website/matchers/location.ts", (rule, ctx) => {\n' +
|
|
1294
|
+
" const cookies = ctx.cookies ?? {};\n" +
|
|
1295
|
+
" const country = cookies.__cf_geo_country ?? '';\n" +
|
|
1296
|
+
" return Boolean(country);\n" +
|
|
1297
|
+
" });\n" +
|
|
1298
|
+
"}\n",
|
|
1299
|
+
});
|
|
1300
|
+
const report = runAudit(SITE, fs);
|
|
1301
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1302
|
+
expect(r.findings).toHaveLength(1);
|
|
1303
|
+
expect(r.findings[0].file).toBe("src/matchers/location.ts");
|
|
1304
|
+
expect(r.findings[0].meta?.safeToAutoFix).toBe(false);
|
|
1305
|
+
expect(r.findings[0].fix).toContain("registerBuiltinMatchers");
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
it("emits zero findings on a clean tree (no duplicates present)", () => {
|
|
1309
|
+
const fs = makeFs({
|
|
1310
|
+
"/site/src/sections/Hello.tsx":
|
|
1311
|
+
'import { clx } from "@decocms/start/sdk/clx";\nexport default () => <div className={clx("a")}>x</div>;\n',
|
|
1312
|
+
});
|
|
1313
|
+
const report = runAudit(SITE, fs);
|
|
1314
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1315
|
+
expect(r.findings).toEqual([]);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it("emits warning severity for both auto-fixable AND warn-only entries (--strict gates everything)", () => {
|
|
1319
|
+
const fs = makeFs({
|
|
1320
|
+
"/site/src/sdk/clx.ts":
|
|
1321
|
+
'export const clx = (...args: any[]) => args.filter(Boolean).join(" ").replace(/\\s\\s+/g, " ");\n',
|
|
1322
|
+
"/site/src/sdk/useSendEvent.ts":
|
|
1323
|
+
'export const useSendEvent = (e: any) => ({ "data-event": encodeURIComponent(JSON.stringify(e)) });\n',
|
|
1324
|
+
});
|
|
1325
|
+
const report = runAudit(SITE, fs);
|
|
1326
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1327
|
+
for (const f of r.findings) expect(f.severity).toBe("warning");
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
it("auto-fix rewrites importers using ~/sdk/clx and deletes the file", () => {
|
|
1331
|
+
const { fs, writer, store } = makeMutableFs({
|
|
1332
|
+
"/site/src/sdk/clx.ts":
|
|
1333
|
+
'export const clx = (...args: any[]) => args.filter(Boolean).join(" ").replace(/\\s\\s+/g, " ");\n',
|
|
1334
|
+
"/site/src/components/A.tsx":
|
|
1335
|
+
'import { clx } from "~/sdk/clx";\nexport default () => clx("x");\n',
|
|
1336
|
+
"/site/src/components/B.tsx":
|
|
1337
|
+
'import { clx } from "~/sdk/clx";\nimport React from "react";\nexport default () => clx("y");\n',
|
|
1338
|
+
"/site/src/components/Unrelated.tsx":
|
|
1339
|
+
'import { clx } from "@decocms/start/sdk/clx";\nexport default () => clx("z");\n',
|
|
1340
|
+
});
|
|
1341
|
+
const report = runAudit(SITE, fs, { writer });
|
|
1342
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1343
|
+
expect(r.fixes).toBeDefined();
|
|
1344
|
+
expect(r.fixes!.length).toBe(1);
|
|
1345
|
+
expect(r.fixes![0].kind).toBe("rewrite-imports+delete");
|
|
1346
|
+
expect(r.fixes![0].detail).toContain("rewrote 2 import(s)");
|
|
1347
|
+
// File deleted from the in-memory store
|
|
1348
|
+
expect(store["/site/src/sdk/clx.ts"]).toBeUndefined();
|
|
1349
|
+
// Importers rewritten
|
|
1350
|
+
expect(store["/site/src/components/A.tsx"]).toContain(
|
|
1351
|
+
'from "@decocms/start/sdk/clx"',
|
|
1352
|
+
);
|
|
1353
|
+
expect(store["/site/src/components/B.tsx"]).toContain(
|
|
1354
|
+
'from "@decocms/start/sdk/clx"',
|
|
1355
|
+
);
|
|
1356
|
+
// Already-canonical import untouched
|
|
1357
|
+
expect(store["/site/src/components/Unrelated.tsx"]).toContain(
|
|
1358
|
+
'from "@decocms/start/sdk/clx"',
|
|
1359
|
+
);
|
|
1360
|
+
expect(store["/site/src/components/Unrelated.tsx"]).not.toMatch(/~\/sdk\/clx/);
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
it("auto-fix is a no-op for warn-only entries (does NOT delete partial-overlap files)", () => {
|
|
1364
|
+
const { fs, writer, store } = makeMutableFs({
|
|
1365
|
+
"/site/src/sdk/useSendEvent.ts":
|
|
1366
|
+
'import { AnalyticsEvent } from "@decocms/apps/commerce/types";\n' +
|
|
1367
|
+
"export const useSendEvent = <E extends AnalyticsEvent>() => ({\n" +
|
|
1368
|
+
' "data-event": encodeURIComponent("x"),\n' +
|
|
1369
|
+
"});\n",
|
|
1370
|
+
"/site/src/matchers/location.ts":
|
|
1371
|
+
'import { registerMatcher } from "@decocms/start/cms";\n' +
|
|
1372
|
+
'registerMatcher("website/matchers/location.ts", () => Boolean(__cf_geo_country));\n',
|
|
1373
|
+
});
|
|
1374
|
+
const report = runAudit(SITE, fs, { writer });
|
|
1375
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1376
|
+
expect(r.findings.length).toBe(2);
|
|
1377
|
+
// Both fixes are no-ops because safeToAutoFix === false.
|
|
1378
|
+
expect(r.fixes ?? []).toEqual([]);
|
|
1379
|
+
// Files preserved.
|
|
1380
|
+
expect(store["/site/src/sdk/useSendEvent.ts"]).toBeDefined();
|
|
1381
|
+
expect(store["/site/src/matchers/location.ts"]).toBeDefined();
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
it("auto-fix runs only on auto-fixable entries when both kinds coexist", () => {
|
|
1385
|
+
const { fs, writer, store } = makeMutableFs({
|
|
1386
|
+
"/site/src/sdk/clx.ts":
|
|
1387
|
+
'export const clx = (...args: any[]) => args.filter(Boolean).join(" ").replace(/\\s\\s+/g, " ");\n',
|
|
1388
|
+
"/site/src/sdk/useSendEvent.ts":
|
|
1389
|
+
'export const useSendEvent = (e: any) => ({ "data-event": encodeURIComponent(JSON.stringify(e)) });\n',
|
|
1390
|
+
"/site/src/components/A.tsx":
|
|
1391
|
+
'import { clx } from "~/sdk/clx";\nexport default () => clx("x");\n',
|
|
1392
|
+
});
|
|
1393
|
+
const report = runAudit(SITE, fs, { writer });
|
|
1394
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1395
|
+
expect(r.findings.length).toBe(2);
|
|
1396
|
+
expect(r.fixes!.length).toBe(1); // only clx auto-fixed
|
|
1397
|
+
expect(r.fixes![0].file).toBe("src/sdk/clx.ts");
|
|
1398
|
+
expect(store["/site/src/sdk/clx.ts"]).toBeUndefined();
|
|
1399
|
+
expect(store["/site/src/sdk/useSendEvent.ts"]).toBeDefined();
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
it("supportsAutoFix is true (the rule has applyFix even though some entries are warn-only)", () => {
|
|
1403
|
+
const fs = makeFs({});
|
|
1404
|
+
const report = runAudit(SITE, fs);
|
|
1405
|
+
const r = report.rules.find((r) => r.rule === "local-framework-duplicate")!;
|
|
1406
|
+
expect(r.supportsAutoFix).toBe(true);
|
|
1407
|
+
});
|
|
1408
|
+
});
|
|
@@ -77,8 +77,10 @@ function showHelp() {
|
|
|
77
77
|
--source <dir> Site directory to audit (default: .)
|
|
78
78
|
--fix Auto-apply mechanical fixes for the safe rules
|
|
79
79
|
(dead-lib-shims, dead-runtime-shim, local-widgets-types,
|
|
80
|
-
vtex-shim-regression swap subset, obsolete-vite-plugins
|
|
81
|
-
|
|
80
|
+
vtex-shim-regression swap subset, obsolete-vite-plugins,
|
|
81
|
+
local-framework-duplicate auto-fixable subset).
|
|
82
|
+
Other rules — including htmx-residue and the warn-only
|
|
83
|
+
entries of local-framework-duplicate — stay detect-only.
|
|
82
84
|
--json Emit machine-readable JSON instead of pretty text
|
|
83
85
|
--strict Exit code 2 if any warning-severity findings exist
|
|
84
86
|
--help, -h Show this help
|