@endora-commerce/cli 0.100.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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/bin/endora.d.ts +28 -0
- package/dist/bin/endora.d.ts.map +1 -0
- package/dist/bin/endora.js +926 -0
- package/dist/bin/endora.js.map +1 -0
- package/dist/check/estate.d.ts +189 -0
- package/dist/check/estate.d.ts.map +1 -0
- package/dist/check/estate.js +1037 -0
- package/dist/check/estate.js.map +1 -0
- package/dist/check/hosts.d.ts +25 -0
- package/dist/check/hosts.d.ts.map +1 -0
- package/dist/check/hosts.js +1347 -0
- package/dist/check/hosts.js.map +1 -0
- package/dist/check/index.d.ts +60 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +111 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/layout.d.ts +136 -0
- package/dist/check/layout.d.ts.map +1 -0
- package/dist/check/layout.js +262 -0
- package/dist/check/layout.js.map +1 -0
- package/dist/check/ledger.d.ts +98 -0
- package/dist/check/ledger.d.ts.map +1 -0
- package/dist/check/ledger.js +173 -0
- package/dist/check/ledger.js.map +1 -0
- package/dist/check/peer-owners.d.ts +116 -0
- package/dist/check/peer-owners.d.ts.map +1 -0
- package/dist/check/peer-owners.js +225 -0
- package/dist/check/peer-owners.js.map +1 -0
- package/dist/check/report.d.ts +33 -0
- package/dist/check/report.d.ts.map +1 -0
- package/dist/check/report.js +107 -0
- package/dist/check/report.js.map +1 -0
- package/dist/check/run.d.ts +147 -0
- package/dist/check/run.d.ts.map +1 -0
- package/dist/check/run.js +111 -0
- package/dist/check/run.js.map +1 -0
- package/dist/checks.d.ts +17 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +17 -0
- package/dist/checks.js.map +1 -0
- package/dist/dev/index.d.ts +83 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +298 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/generate/divergence.d.ts +38 -0
- package/dist/generate/divergence.d.ts.map +1 -0
- package/dist/generate/divergence.js +237 -0
- package/dist/generate/divergence.js.map +1 -0
- package/dist/generate/index.d.ts +90 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +369 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs/declaration.d.ts +18 -0
- package/dist/inputs/declaration.d.ts.map +1 -0
- package/dist/inputs/declaration.js +64 -0
- package/dist/inputs/declaration.js.map +1 -0
- package/dist/inputs/env-file.d.ts +73 -0
- package/dist/inputs/env-file.d.ts.map +1 -0
- package/dist/inputs/env-file.js +134 -0
- package/dist/inputs/env-file.js.map +1 -0
- package/dist/inputs/prompt.d.ts +21 -0
- package/dist/inputs/prompt.d.ts.map +1 -0
- package/dist/inputs/prompt.js +59 -0
- package/dist/inputs/prompt.js.map +1 -0
- package/dist/inputs/resolve.d.ts +163 -0
- package/dist/inputs/resolve.d.ts.map +1 -0
- package/dist/inputs/resolve.js +290 -0
- package/dist/inputs/resolve.js.map +1 -0
- package/dist/install/host.d.ts +27 -0
- package/dist/install/host.d.ts.map +1 -0
- package/dist/install/host.js +90 -0
- package/dist/install/host.js.map +1 -0
- package/dist/install/index.d.ts +173 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +793 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/wizard.d.ts +144 -0
- package/dist/install/wizard.d.ts.map +1 -0
- package/dist/install/wizard.js +362 -0
- package/dist/install/wizard.js.map +1 -0
- package/dist/lib/admin-artefacts.d.ts +70 -0
- package/dist/lib/admin-artefacts.d.ts.map +1 -0
- package/dist/lib/admin-artefacts.js +354 -0
- package/dist/lib/admin-artefacts.js.map +1 -0
- package/dist/lib/admin-surfaces.d.ts +298 -0
- package/dist/lib/admin-surfaces.d.ts.map +1 -0
- package/dist/lib/admin-surfaces.js +669 -0
- package/dist/lib/admin-surfaces.js.map +1 -0
- package/dist/lib/delegated-composer.d.ts +85 -0
- package/dist/lib/delegated-composer.d.ts.map +1 -0
- package/dist/lib/delegated-composer.js +241 -0
- package/dist/lib/delegated-composer.js.map +1 -0
- package/dist/lib/divergence-artefacts.d.ts +305 -0
- package/dist/lib/divergence-artefacts.d.ts.map +1 -0
- package/dist/lib/divergence-artefacts.js +828 -0
- package/dist/lib/divergence-artefacts.js.map +1 -0
- package/dist/lib/divergence.d.ts +337 -0
- package/dist/lib/divergence.d.ts.map +1 -0
- package/dist/lib/divergence.js +1005 -0
- package/dist/lib/divergence.js.map +1 -0
- package/dist/lib/docs-artefacts.d.ts +395 -0
- package/dist/lib/docs-artefacts.d.ts.map +1 -0
- package/dist/lib/docs-artefacts.js +781 -0
- package/dist/lib/docs-artefacts.js.map +1 -0
- package/dist/lib/emitted-exports.d.ts +21 -0
- package/dist/lib/emitted-exports.d.ts.map +1 -0
- package/dist/lib/emitted-exports.js +96 -0
- package/dist/lib/emitted-exports.js.map +1 -0
- package/dist/lib/emitted-freshness.d.ts +112 -0
- package/dist/lib/emitted-freshness.d.ts.map +1 -0
- package/dist/lib/emitted-freshness.js +288 -0
- package/dist/lib/emitted-freshness.js.map +1 -0
- package/dist/lib/entity-index-artefact.d.ts +95 -0
- package/dist/lib/entity-index-artefact.d.ts.map +1 -0
- package/dist/lib/entity-index-artefact.js +210 -0
- package/dist/lib/entity-index-artefact.js.map +1 -0
- package/dist/lib/instance-build-inputs.d.ts +108 -0
- package/dist/lib/instance-build-inputs.d.ts.map +1 -0
- package/dist/lib/instance-build-inputs.js +165 -0
- package/dist/lib/instance-build-inputs.js.map +1 -0
- package/dist/lib/module-docs.d.ts +473 -0
- package/dist/lib/module-docs.d.ts.map +1 -0
- package/dist/lib/module-docs.js +711 -0
- package/dist/lib/module-docs.js.map +1 -0
- package/dist/lib/module-package-subpaths.d.ts +56 -0
- package/dist/lib/module-package-subpaths.d.ts.map +1 -0
- package/dist/lib/module-package-subpaths.js +223 -0
- package/dist/lib/module-package-subpaths.js.map +1 -0
- package/dist/lib/module-packages.d.ts +200 -0
- package/dist/lib/module-packages.d.ts.map +1 -0
- package/dist/lib/module-packages.js +580 -0
- package/dist/lib/module-packages.js.map +1 -0
- package/dist/lib/module-population.d.ts +129 -0
- package/dist/lib/module-population.d.ts.map +1 -0
- package/dist/lib/module-population.js +172 -0
- package/dist/lib/module-population.js.map +1 -0
- package/dist/lib/module-roots.d.ts +337 -0
- package/dist/lib/module-roots.d.ts.map +1 -0
- package/dist/lib/module-roots.js +586 -0
- package/dist/lib/module-roots.js.map +1 -0
- package/dist/lib/nested-checkouts.d.ts +33 -0
- package/dist/lib/nested-checkouts.d.ts.map +1 -0
- package/dist/lib/nested-checkouts.js +160 -0
- package/dist/lib/nested-checkouts.js.map +1 -0
- package/dist/lib/platform-root.d.ts +43 -0
- package/dist/lib/platform-root.d.ts.map +1 -0
- package/dist/lib/platform-root.js +134 -0
- package/dist/lib/platform-root.js.map +1 -0
- package/dist/lib/platform-surface.d.ts +235 -0
- package/dist/lib/platform-surface.d.ts.map +1 -0
- package/dist/lib/platform-surface.js +393 -0
- package/dist/lib/platform-surface.js.map +1 -0
- package/dist/lib/port-registrations.d.ts +223 -0
- package/dist/lib/port-registrations.d.ts.map +1 -0
- package/dist/lib/port-registrations.js +532 -0
- package/dist/lib/port-registrations.js.map +1 -0
- package/dist/lib/read-size.d.ts +154 -0
- package/dist/lib/read-size.d.ts.map +1 -0
- package/dist/lib/read-size.js +182 -0
- package/dist/lib/read-size.js.map +1 -0
- package/dist/lib/registration-owners.d.ts +79 -0
- package/dist/lib/registration-owners.d.ts.map +1 -0
- package/dist/lib/registration-owners.js +77 -0
- package/dist/lib/registration-owners.js.map +1 -0
- package/dist/lib/release-index.d.ts +53 -0
- package/dist/lib/release-index.d.ts.map +1 -0
- package/dist/lib/release-index.js +162 -0
- package/dist/lib/release-index.js.map +1 -0
- package/dist/lib/repeating-timers.d.ts +79 -0
- package/dist/lib/repeating-timers.d.ts.map +1 -0
- package/dist/lib/repeating-timers.js +189 -0
- package/dist/lib/repeating-timers.js.map +1 -0
- package/dist/lib/source-text.d.ts +34 -0
- package/dist/lib/source-text.d.ts.map +1 -0
- package/dist/lib/source-text.js +80 -0
- package/dist/lib/source-text.js.map +1 -0
- package/dist/lib/specifiers.d.ts +20 -0
- package/dist/lib/specifiers.d.ts.map +1 -0
- package/dist/lib/specifiers.js +130 -0
- package/dist/lib/specifiers.js.map +1 -0
- package/dist/lib/sql-tables.d.ts +166 -0
- package/dist/lib/sql-tables.d.ts.map +1 -0
- package/dist/lib/sql-tables.js +464 -0
- package/dist/lib/sql-tables.js.map +1 -0
- package/dist/lib/switchable-modules.d.ts +54 -0
- package/dist/lib/switchable-modules.d.ts.map +1 -0
- package/dist/lib/switchable-modules.js +104 -0
- package/dist/lib/switchable-modules.js.map +1 -0
- package/dist/lib/tailwind-sources.d.ts +136 -0
- package/dist/lib/tailwind-sources.d.ts.map +1 -0
- package/dist/lib/tailwind-sources.js +307 -0
- package/dist/lib/tailwind-sources.js.map +1 -0
- package/dist/lib/ui-layer.d.ts +54 -0
- package/dist/lib/ui-layer.d.ts.map +1 -0
- package/dist/lib/ui-layer.js +57 -0
- package/dist/lib/ui-layer.js.map +1 -0
- package/dist/lib/workspace-packages.d.ts +186 -0
- package/dist/lib/workspace-packages.d.ts.map +1 -0
- package/dist/lib/workspace-packages.js +351 -0
- package/dist/lib/workspace-packages.js.map +1 -0
- package/dist/new-instance/deploy.d.ts +211 -0
- package/dist/new-instance/deploy.d.ts.map +1 -0
- package/dist/new-instance/deploy.js +1381 -0
- package/dist/new-instance/deploy.js.map +1 -0
- package/dist/new-instance/docs-toolchain.d.ts +66 -0
- package/dist/new-instance/docs-toolchain.d.ts.map +1 -0
- package/dist/new-instance/docs-toolchain.js +69 -0
- package/dist/new-instance/docs-toolchain.js.map +1 -0
- package/dist/new-instance/host.d.ts +124 -0
- package/dist/new-instance/host.d.ts.map +1 -0
- package/dist/new-instance/host.js +276 -0
- package/dist/new-instance/host.js.map +1 -0
- package/dist/new-instance/index.d.ts +118 -0
- package/dist/new-instance/index.d.ts.map +1 -0
- package/dist/new-instance/index.js +567 -0
- package/dist/new-instance/index.js.map +1 -0
- package/dist/new-instance/modules.d.ts +188 -0
- package/dist/new-instance/modules.d.ts.map +1 -0
- package/dist/new-instance/modules.js +392 -0
- package/dist/new-instance/modules.js.map +1 -0
- package/dist/new-instance/template.d.ts +505 -0
- package/dist/new-instance/template.d.ts.map +1 -0
- package/dist/new-instance/template.js +1886 -0
- package/dist/new-instance/template.js.map +1 -0
- package/dist/new-module/emit.d.ts +67 -0
- package/dist/new-module/emit.d.ts.map +1 -0
- package/dist/new-module/emit.js +1393 -0
- package/dist/new-module/emit.js.map +1 -0
- package/dist/new-module/host.d.ts +67 -0
- package/dist/new-module/host.d.ts.map +1 -0
- package/dist/new-module/host.js +224 -0
- package/dist/new-module/host.js.map +1 -0
- package/dist/new-module/index.d.ts +32 -0
- package/dist/new-module/index.d.ts.map +1 -0
- package/dist/new-module/index.js +193 -0
- package/dist/new-module/index.js.map +1 -0
- package/dist/new-module/spec.d.ts +188 -0
- package/dist/new-module/spec.d.ts.map +1 -0
- package/dist/new-module/spec.js +404 -0
- package/dist/new-module/spec.js.map +1 -0
- package/dist/new-module/text.d.ts +15 -0
- package/dist/new-module/text.d.ts.map +1 -0
- package/dist/new-module/text.js +22 -0
- package/dist/new-module/text.js.map +1 -0
- package/dist/new-storefront/dockerfile.d.ts +20 -0
- package/dist/new-storefront/dockerfile.d.ts.map +1 -0
- package/dist/new-storefront/dockerfile.js +131 -0
- package/dist/new-storefront/dockerfile.js.map +1 -0
- package/dist/new-storefront/gitignore.d.ts +23 -0
- package/dist/new-storefront/gitignore.d.ts.map +1 -0
- package/dist/new-storefront/gitignore.js +39 -0
- package/dist/new-storefront/gitignore.js.map +1 -0
- package/dist/new-storefront/index.d.ts +93 -0
- package/dist/new-storefront/index.d.ts.map +1 -0
- package/dist/new-storefront/index.js +329 -0
- package/dist/new-storefront/index.js.map +1 -0
- package/dist/new-storefront/npmrc.d.ts +98 -0
- package/dist/new-storefront/npmrc.d.ts.map +1 -0
- package/dist/new-storefront/npmrc.js +217 -0
- package/dist/new-storefront/npmrc.js.map +1 -0
- package/dist/new-storefront/reference.d.ts +189 -0
- package/dist/new-storefront/reference.d.ts.map +1 -0
- package/dist/new-storefront/reference.js +430 -0
- package/dist/new-storefront/reference.js.map +1 -0
- package/dist/new-storefront/rewrite.d.ts +171 -0
- package/dist/new-storefront/rewrite.d.ts.map +1 -0
- package/dist/new-storefront/rewrite.js +701 -0
- package/dist/new-storefront/rewrite.js.map +1 -0
- package/dist/release-index.json +284 -0
- package/dist/rules/action-route-permissions.d.ts +141 -0
- package/dist/rules/action-route-permissions.d.ts.map +1 -0
- package/dist/rules/action-route-permissions.js +556 -0
- package/dist/rules/action-route-permissions.js.map +1 -0
- package/dist/rules/bundle-pairing.d.ts +74 -0
- package/dist/rules/bundle-pairing.d.ts.map +1 -0
- package/dist/rules/bundle-pairing.js +281 -0
- package/dist/rules/bundle-pairing.js.map +1 -0
- package/dist/rules/channel-resolution.d.ts +17 -0
- package/dist/rules/channel-resolution.d.ts.map +1 -0
- package/dist/rules/channel-resolution.js +382 -0
- package/dist/rules/channel-resolution.js.map +1 -0
- package/dist/rules/command-coverage.d.ts +210 -0
- package/dist/rules/command-coverage.d.ts.map +1 -0
- package/dist/rules/command-coverage.js +714 -0
- package/dist/rules/command-coverage.js.map +1 -0
- package/dist/rules/container-imports.d.ts +60 -0
- package/dist/rules/container-imports.d.ts.map +1 -0
- package/dist/rules/container-imports.js +158 -0
- package/dist/rules/container-imports.js.map +1 -0
- package/dist/rules/default-language-prose.d.ts +212 -0
- package/dist/rules/default-language-prose.d.ts.map +1 -0
- package/dist/rules/default-language-prose.js +710 -0
- package/dist/rules/default-language-prose.js.map +1 -0
- package/dist/rules/diacritic-folds.d.ts +238 -0
- package/dist/rules/diacritic-folds.d.ts.map +1 -0
- package/dist/rules/diacritic-folds.js +681 -0
- package/dist/rules/diacritic-folds.js.map +1 -0
- package/dist/rules/entity-tenant-classification.d.ts +171 -0
- package/dist/rules/entity-tenant-classification.d.ts.map +1 -0
- package/dist/rules/entity-tenant-classification.js +323 -0
- package/dist/rules/entity-tenant-classification.js.map +1 -0
- package/dist/rules/entry-presence.d.ts +142 -0
- package/dist/rules/entry-presence.d.ts.map +1 -0
- package/dist/rules/entry-presence.js +339 -0
- package/dist/rules/entry-presence.js.map +1 -0
- package/dist/rules/entry-scope.d.ts +91 -0
- package/dist/rules/entry-scope.d.ts.map +1 -0
- package/dist/rules/entry-scope.js +404 -0
- package/dist/rules/entry-scope.js.map +1 -0
- package/dist/rules/env-inputs.d.ts +222 -0
- package/dist/rules/env-inputs.d.ts.map +1 -0
- package/dist/rules/env-inputs.js +951 -0
- package/dist/rules/env-inputs.js.map +1 -0
- package/dist/rules/kernel-boundary.d.ts +37 -0
- package/dist/rules/kernel-boundary.d.ts.map +1 -0
- package/dist/rules/kernel-boundary.js +195 -0
- package/dist/rules/kernel-boundary.js.map +1 -0
- package/dist/rules/nul-bytes.d.ts +233 -0
- package/dist/rules/nul-bytes.d.ts.map +1 -0
- package/dist/rules/nul-bytes.js +332 -0
- package/dist/rules/nul-bytes.js.map +1 -0
- package/dist/rules/platform-surface.d.ts +479 -0
- package/dist/rules/platform-surface.d.ts.map +1 -0
- package/dist/rules/platform-surface.js +749 -0
- package/dist/rules/platform-surface.js.map +1 -0
- package/dist/rules/port-catches.d.ts +225 -0
- package/dist/rules/port-catches.d.ts.map +1 -0
- package/dist/rules/port-catches.js +1374 -0
- package/dist/rules/port-catches.js.map +1 -0
- package/dist/rules/port-shape.d.ts +213 -0
- package/dist/rules/port-shape.d.ts.map +1 -0
- package/dist/rules/port-shape.js +670 -0
- package/dist/rules/port-shape.js.map +1 -0
- package/dist/rules/queue-names.d.ts +108 -0
- package/dist/rules/queue-names.d.ts.map +1 -0
- package/dist/rules/queue-names.js +395 -0
- package/dist/rules/queue-names.js.map +1 -0
- package/dist/rules/singleton-identity.d.ts +205 -0
- package/dist/rules/singleton-identity.d.ts.map +1 -0
- package/dist/rules/singleton-identity.js +830 -0
- package/dist/rules/singleton-identity.js.map +1 -0
- package/dist/rules/subscribe-seam.d.ts +121 -0
- package/dist/rules/subscribe-seam.d.ts.map +1 -0
- package/dist/rules/subscribe-seam.js +594 -0
- package/dist/rules/subscribe-seam.js.map +1 -0
- package/dist/rules/transaction-context.d.ts +40 -0
- package/dist/rules/transaction-context.d.ts.map +1 -0
- package/dist/rules/transaction-context.js +294 -0
- package/dist/rules/transaction-context.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI check — this repository folds diacritics in exactly one place (issue #240).
|
|
3
|
+
*
|
|
4
|
+
* ## The defect, four times in a year
|
|
5
|
+
*
|
|
6
|
+
* `packages/contracts/src/text-normalization.ts` exists because the obvious
|
|
7
|
+
* one-liner
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* input.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase()
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* is **wrong for Polish** and reads as if it were right. `ł` is U+0142, a
|
|
14
|
+
* standalone code point with no canonical decomposition: NFD leaves it exactly
|
|
15
|
+
* where it was, so the strip has nothing to remove and the fold silently does
|
|
16
|
+
* nothing for every word carrying one. Same for `Ø đ ð þ ß æ œ`. The shared
|
|
17
|
+
* fold decomposes, strips, and **then** maps those letters, which is the whole
|
|
18
|
+
* reason it is longer than one line. (The map runs last rather than first on
|
|
19
|
+
* purpose: `Ǿ` decomposes to `Ø` plus an acute, so a map applied first never
|
|
20
|
+
* sees the letter the decomposition is about to expose.)
|
|
21
|
+
*
|
|
22
|
+
* Four private copies of the one-liner had grown by issue #236 — the ⌘K
|
|
23
|
+
* palette, the page-builder drawer, the combobox and the multi-select — and
|
|
24
|
+
* every one of them shipped the bug: a Puck block named `Nagłówek` was not
|
|
25
|
+
* found by typing `naglowek`, and `Metody płatności` was not found by typing
|
|
26
|
+
* `platnosci`. They were repaired by !745, and the only thing standing between
|
|
27
|
+
* that repair and a fifth copy was a paragraph in a file header. This check is
|
|
28
|
+
* that paragraph, made executable.
|
|
29
|
+
*
|
|
30
|
+
* ## What it refuses, and why it refuses the decomposition rather than the bug
|
|
31
|
+
*
|
|
32
|
+
* Three literal shapes, anywhere in the population except the helper's own path.
|
|
33
|
+
* The first two are the **explicit** fold:
|
|
34
|
+
*
|
|
35
|
+
* 1. **`decomposition`** — a literal naming a decomposing normalization form,
|
|
36
|
+
* `NFD` or `NFKD`. `NFC` and `NFKC` are **not** in the rule: they compose,
|
|
37
|
+
* so they never expose the combining marks a fold goes on to strip.
|
|
38
|
+
* 2. **`diacritic-strip`** — a pattern that removes combining marks: the
|
|
39
|
+
* `\p{Diacritic}` / `\p{Mn}` / `\p{M}` property escapes, the
|
|
40
|
+
* `\u0300-\u036f` block range in either escape spelling, and the same
|
|
41
|
+
* range written as raw combining characters (which is how
|
|
42
|
+
* `feed-template-io.service.ts` spells it, invisible to a grep for the
|
|
43
|
+
* escape).
|
|
44
|
+
*
|
|
45
|
+
* It refuses the **decomposition**, not "a decomposition immediately followed
|
|
46
|
+
* by a strip", because a rule with a proximity window is a rule an author
|
|
47
|
+
* escapes by moving one line into a helper — and half of a fold is still a
|
|
48
|
+
* fold. Both halves are reported independently for the same reason: a fold
|
|
49
|
+
* split across two functions leaves each half alone, and each half alone is
|
|
50
|
+
* still a second implementation of the thing that has one owner.
|
|
51
|
+
*
|
|
52
|
+
* There is no legitimate non-folding reason to decompose a string anywhere in
|
|
53
|
+
* this repository. If one arrives — grapheme-aware truncation is the plausible
|
|
54
|
+
* candidate — it goes in the ledger with its reason, not into a widening of the
|
|
55
|
+
* rule.
|
|
56
|
+
*
|
|
57
|
+
* ## The third shape, and why a check needed one (issue #244)
|
|
58
|
+
*
|
|
59
|
+
* 3. **`slug-run`** — a `.replace()` that collapses a **run of characters
|
|
60
|
+
* outside ASCII alphanumerics** to a separator: `/[^a-z0-9]+/g` to `'-'`,
|
|
61
|
+
* and the variants below. Slug construction, in other words.
|
|
62
|
+
*
|
|
63
|
+
* The first two signals share a defect that neither of them can see, and it is
|
|
64
|
+
* the reason issue #244 exists: **their population is defined by the presence
|
|
65
|
+
* of the thing they check.** A site that folds nothing writes no `NFD` and no
|
|
66
|
+
* `\p{Diacritic}`, matches neither predicate, is not in the population, and
|
|
67
|
+
* reports clean. Two sites were live on `master` for a year that way —
|
|
68
|
+
* `cms-template-layout.ts`'s `codeFromTemplateName` and `pim_ergonode`'s
|
|
69
|
+
* `slugFromSourceCode` — each going straight from `toLowerCase()` to
|
|
70
|
+
* `[^a-z0-9]+`, so that `Żółw` produced `w`, `Świeże Ćwikła` produced
|
|
71
|
+
* `wie-e-wik-a` and `KAT_ŁĄCZNIKI_01` produced `kat-czniki-01`. That is the
|
|
72
|
+
* exact bug the first two signals exist to catch, and this check printed
|
|
73
|
+
* `violations=0` throughout. Issue #245 repaired the sites; the blindness
|
|
74
|
+
* survived it, because the next hand-rolled slug builder written without a fold
|
|
75
|
+
* would be just as invisible.
|
|
76
|
+
*
|
|
77
|
+
* So the third signal keys on something **present in both the good case and the
|
|
78
|
+
* bad one**: the slug builder itself. A site that folds correctly and one that
|
|
79
|
+
* folds not at all both write the run collapse, so the run collapse is where a
|
|
80
|
+
* missing fold becomes observable. Concretely the rule is *"slug construction
|
|
81
|
+
* has one owner"*, `slugify` in `@endora-commerce/contracts`, and not *"a slug builder must
|
|
82
|
+
* fold"* — which is deliberate, and is what keeps the predicate honest:
|
|
83
|
+
*
|
|
84
|
+
* - **It needs no dataflow.** "Did the value reaching this `.replace()` pass
|
|
85
|
+
* through a fold?" cannot be answered by reading the expression: the fold
|
|
86
|
+
* may be a call up the chain, a caller two frames away, or a local assigned
|
|
87
|
+
* three statements earlier. A predicate that only read the same expression
|
|
88
|
+
* would report every correct site that folds elsewhere, and one that
|
|
89
|
+
* followed the value would have to say where it stops. Keying on the
|
|
90
|
+
* builder removes the question rather than answering it badly.
|
|
91
|
+
* - **It is strictly stronger.** `BlogPostEditor`'s local `slugify` folded
|
|
92
|
+
* correctly via `normalize` and was still the ninth private copy of a
|
|
93
|
+
* generator that has one owner — issue #245 unified eight and missed it.
|
|
94
|
+
* This signal reported it, and the repair is this rule's first finding.
|
|
95
|
+
*
|
|
96
|
+
* ### What a run collapse is, and the two spellings of it
|
|
97
|
+
*
|
|
98
|
+
* A `.replace(pattern, separator)` whose pattern holds a **negated character
|
|
99
|
+
* class over ASCII alphanumerics** — `[^a-z0-9]`, `[^a-zA-Z0-9]`,
|
|
100
|
+
* `[^A-Za-z0-9_-]`, `[^\w]`, in a regular-expression literal or in a
|
|
101
|
+
* `new RegExp('...')` — and whose separator is a non-empty string or an
|
|
102
|
+
* identifier. The run itself is either:
|
|
103
|
+
*
|
|
104
|
+
* - **quantified in place** — `[^a-z0-9]+`, `[^a-z0-9]{2,}`. All eight sites
|
|
105
|
+
* issue #245 unified were written this way; or
|
|
106
|
+
* - **collapsed in a second move later in the same call chain** —
|
|
107
|
+
* `.replace(/[^a-z0-9_]/g, '_').replace(/_{2,}/g, '_')`, which is how
|
|
108
|
+
* `pim_ergonode`'s two key derivations spell it. One expression, so this is
|
|
109
|
+
* a structural window rather than a line-proximity one.
|
|
110
|
+
*
|
|
111
|
+
* ### The population is honest, and that was measured rather than asserted
|
|
112
|
+
*
|
|
113
|
+
* The obvious wider predicate — *any* negated ASCII-alphanumeric class — matches
|
|
114
|
+
* **9** sites in this tree, and 7 of them legitimately need no fold: a payment
|
|
115
|
+
* hash seed that must not change, an XML element name, a DOM `id`, a
|
|
116
|
+
* Meilisearch index name, a test database name. A ledger that is mostly
|
|
117
|
+
* exceptions teaches people to add entries rather than to think, so that
|
|
118
|
+
* predicate was rejected.
|
|
119
|
+
*
|
|
120
|
+
* Requiring the **run collapse** is what removes them, and it is not a
|
|
121
|
+
* convenience: collapsing a run to one separator is slug grammar, while
|
|
122
|
+
* deleting the run (to `''`) or substituting one-for-one preserves length and
|
|
123
|
+
* position, which is identifier grammar. Measured over the 322 `.replace()`
|
|
124
|
+
* calls whose pattern this check can read, it matches **4** sites — the helper
|
|
125
|
+
* (exempt), one repair and two ledgered —
|
|
126
|
+
* and **none of them is an exception to the rule**: both ledger entries are the
|
|
127
|
+
* same defect deferred for a stated data reason, not sites the rule does not
|
|
128
|
+
* reach. That is the difference the population had to show before this signal
|
|
129
|
+
* was worth shipping.
|
|
130
|
+
*
|
|
131
|
+
* ### What it cannot see, stated here rather than discovered later
|
|
132
|
+
*
|
|
133
|
+
* - A collapse whose class is not lexically ASCII-alphanumeric:
|
|
134
|
+
* `new RegExp(variable)`, a class assembled from a template literal, a
|
|
135
|
+
* builder written with `split`/`join` or `Intl.Segmenter` rather than
|
|
136
|
+
* `replace`.
|
|
137
|
+
* - A two-move collapse whose halves sit in **different statements**
|
|
138
|
+
* (`const a = x.replace(...); return a.replace(/-{2,}/g, '-');`). The window
|
|
139
|
+
* is one call chain and goes no further, in the idiom of
|
|
140
|
+
* `check-port-catches`, which follows a gate one hop through `this` and
|
|
141
|
+
* refuses to go further.
|
|
142
|
+
* - A bare `[^a-z0-9]` with **no** run collapse anywhere. It still deletes
|
|
143
|
+
* `ł`, and it is out of the population on purpose: matching it is what pulls
|
|
144
|
+
* in the seven sanitisers above.
|
|
145
|
+
* - Whether a matched site **needs** a fold at all. That is a human judgement
|
|
146
|
+
* and it belongs in a ledger reason, not in a predicate.
|
|
147
|
+
*
|
|
148
|
+
* ## Literal nodes, so a comment is out of the population by construction
|
|
149
|
+
*
|
|
150
|
+
* The predicate reads **literal nodes** of the TypeScript AST, in the idiom of
|
|
151
|
+
* `check-module-boundary`'s SQL predicate. That is not an optimisation: both
|
|
152
|
+
* `text-normalization.ts` files quote the wrong one-liner in their headers,
|
|
153
|
+
* `AppShell.tsx` quotes it again to say why it does not use it, and two admin
|
|
154
|
+
* tests quote it a third time. A text-level check reports all five and gets
|
|
155
|
+
* switched off within a week. A node-level one cannot see a comment at all.
|
|
156
|
+
*
|
|
157
|
+
* ## The population is the whole tree, because the fold is now reachable
|
|
158
|
+
*
|
|
159
|
+
* (This paragraph used to carry a caveat, and the `slug-run` signal above is
|
|
160
|
+
* what retired it: the first two signals see a fold written **outside** the
|
|
161
|
+
* helper, so a slugifier with **no fold step at all** wrote nothing for them to
|
|
162
|
+
* see. Two shipped that way for a year, deleting `ł` rather than folding it,
|
|
163
|
+
* while this check read clean over both. Issue #245's shared `slugify` repaired
|
|
164
|
+
* the sites and this signal closes the hole they went through.)
|
|
165
|
+
*
|
|
166
|
+
* It was `admin/src` and `admin/test` alone when this check landed, and the
|
|
167
|
+
* header said why each other tree was out: none of them could import
|
|
168
|
+
* `admin/src/lib/text-normalization.ts`. That was true and it was the defect,
|
|
169
|
+
* not a property of the trees. The correct fold had been in `@endora-commerce/contracts`
|
|
170
|
+
* the whole time — inside `normalizeOrganizationName`, named after one caller,
|
|
171
|
+
* which is why six authors wrote their own instead of finding it. Issue #240
|
|
172
|
+
* extracted it as `foldDiacritics`, and the four packages below can all import
|
|
173
|
+
* it, so the rule "use the shared fold" now has something to mean everywhere:
|
|
174
|
+
*
|
|
175
|
+
* - **`admin/`** — where all four private copies grew (issue #236), each an
|
|
176
|
+
* import away from the helper. `admin/src/lib/text-normalization.ts` is now
|
|
177
|
+
* a two-line composition over the shared fold, not a second copy of it.
|
|
178
|
+
* - **`backend/`** — held the last two ledgered folds until issue #245 routed
|
|
179
|
+
* them, and five more slug generators, through `slugify` in
|
|
180
|
+
* `@endora-commerce/contracts`. It folds nowhere of its own now, and holds the two
|
|
181
|
+
* ledgered `slug-run` sites.
|
|
182
|
+
* - **`storefront/`** — folds nowhere today and builds no slug. It is in the
|
|
183
|
+
* population so that the first one written there is the one that gets
|
|
184
|
+
* refused, which is the only moment the rule is cheap to keep.
|
|
185
|
+
* - **`packages/`** — holds the shared fold and the shared generator, both
|
|
186
|
+
* excluded by that one exact path. Every other package is scanned like any
|
|
187
|
+
* other consumer.
|
|
188
|
+
*
|
|
189
|
+
* `EXCLUDED_SUBTREES` names the two places that must be able to write the
|
|
190
|
+
* shapes this check refuses: the checks themselves and their companion tests.
|
|
191
|
+
* Both are declared with reasons rather than pruned by a glob, for the same
|
|
192
|
+
* reason the roots are.
|
|
193
|
+
*
|
|
194
|
+
* Out of the population and staying out: `specs/` (contract sketches, not code
|
|
195
|
+
* that runs) and the repository-root build config, neither of which folds
|
|
196
|
+
* anything.
|
|
197
|
+
*
|
|
198
|
+
* ## The exclusion is path-anchored
|
|
199
|
+
*
|
|
200
|
+
* `SHARED_FOLD_HELPER` is one exact repo-relative path. It is deliberately not
|
|
201
|
+
* a name rule ("a file called `text-normalization.ts`") and not a directory
|
|
202
|
+
* rule ("anything under `lib/`"): issue #197's lesson is that a second file can
|
|
203
|
+
* satisfy a name-shaped exemption and thereby appoint itself the owner of the
|
|
204
|
+
* fold, which is the precise failure this check exists to prevent. There are
|
|
205
|
+
* two files called `text-normalization.ts` in the tree today — the fold in
|
|
206
|
+
* `packages/contracts/` and the admin's trim over it — and exactly one of them
|
|
207
|
+
* is exempt.
|
|
208
|
+
*
|
|
209
|
+
* The same path is the vacuous-pass guard's subject. The helper must exist and
|
|
210
|
+
* must still contain **all three** shapes, or the check exits 2 — because a
|
|
211
|
+
* helper that no longer folds means either the file moved (and the exclusion
|
|
212
|
+
* now exempts nothing) or the analysis stopped seeing folds (and every green
|
|
213
|
+
* after that is worthless). The third shape joined that guard for the same
|
|
214
|
+
* reason and buys one more: `slugify` is what the failure message tells every
|
|
215
|
+
* author to import, so a run in which nothing in the helper parses as a slug
|
|
216
|
+
* builder is a run whose remedy does not exist. A check whose own exemption is
|
|
217
|
+
* its self-test cannot go quietly blind.
|
|
218
|
+
*
|
|
219
|
+
* Usage: `tsx scripts/check-diacritic-folds.ts [--list]`
|
|
220
|
+
* Exit 0 = no unledgered fold and no unledgered slug run; exit 1 = at least
|
|
221
|
+
* one, or a stale ledger entry; exit 2 = nothing was read, so a pass would be
|
|
222
|
+
* vacuous.
|
|
223
|
+
*
|
|
224
|
+
* ## One analysis, two hosts
|
|
225
|
+
*
|
|
226
|
+
* This file is the analysis (`specs/101-endora-check/contracts/package-scope-layout.md`
|
|
227
|
+
* §6). `backend/scripts/check-diacritic-folds.ts` hosts it over this
|
|
228
|
+
* repository's four population roots and holds both ledgers, which are
|
|
229
|
+
* statements about *this* tree's debt; `endora check` hosts it over one module
|
|
230
|
+
* package, whose whole tree is one root. So the roots are a parameter with this
|
|
231
|
+
* repository's as the default, and both ledgers are arguments rather than values
|
|
232
|
+
* this file reads — a ledger read from inside the analysis is a ledger only one
|
|
233
|
+
* host can have.
|
|
234
|
+
*
|
|
235
|
+
* {@link SHARED_FOLD_HELPER} stays a repo-relative constant and is *right* to
|
|
236
|
+
* never match a package-relative key: a package has no exempt file. It imports
|
|
237
|
+
* `foldDiacritics` and `slugify` from `@endora-commerce/contracts` like every
|
|
238
|
+
* other consumer, which is what the exemption exists to make true.
|
|
239
|
+
*/
|
|
240
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
241
|
+
import { join } from 'node:path';
|
|
242
|
+
import ts from 'typescript';
|
|
243
|
+
/**
|
|
244
|
+
* The one file allowed to fold, as an exact repo-relative path.
|
|
245
|
+
*
|
|
246
|
+
* Path-anchored on purpose — see the header. A second `text-normalization.ts`
|
|
247
|
+
* elsewhere in the tree is a violation, which is the point; the admin file of
|
|
248
|
+
* that name is in the population and passes because it imports rather than
|
|
249
|
+
* folds.
|
|
250
|
+
*
|
|
251
|
+
* It moved here from `admin/src/lib/` when issue #240 extracted `foldDiacritics`
|
|
252
|
+
* into `@endora-commerce/contracts`. That move is what let the population widen: an
|
|
253
|
+
* exemption is only worth anything where the exempted file is reachable, and
|
|
254
|
+
* neither `backend/` nor `storefront/` could ever have imported the admin one.
|
|
255
|
+
*/
|
|
256
|
+
export const SHARED_FOLD_HELPER = 'packages/contracts/src/text-normalization.ts';
|
|
257
|
+
/**
|
|
258
|
+
* The trees scanned, and why each is in the population.
|
|
259
|
+
*
|
|
260
|
+
* Declared as data so the reason travels with the decision: a tree that is not
|
|
261
|
+
* here is a tree this check makes no claim about, and the next reader gets to
|
|
262
|
+
* see which claim was dropped rather than inferring it from a glob.
|
|
263
|
+
*/
|
|
264
|
+
export const POPULATION_ROOTS = {
|
|
265
|
+
admin: 'Where all four private copies grew (issue #236), each one an import away ' +
|
|
266
|
+
'from a fold it could not find. Its own tests are in: a test that recomputes ' +
|
|
267
|
+
'an expected value with its own fold is a fifth copy that happens to be green.',
|
|
268
|
+
backend: 'Held the last two ledgered folds, both carrying the `l` bug, until issue ' +
|
|
269
|
+
'#245 routed them through the shared slug generator. It folds nowhere of ' +
|
|
270
|
+
'its own now, and is scanned so the next one written here is refused.',
|
|
271
|
+
storefront: 'Folds nowhere today, which is exactly when a rule is cheap to keep: the ' +
|
|
272
|
+
'first fold written here is refused before it can be copied.',
|
|
273
|
+
packages: 'Holds the shared fold itself, exempt by exact path. Every other package is ' +
|
|
274
|
+
'a consumer like any other and is scanned like one.',
|
|
275
|
+
};
|
|
276
|
+
/** File extensions parsed. Anything else is not a module that can import the helper. */
|
|
277
|
+
export const SOURCE_EXTENSIONS = ['.ts', '.tsx'];
|
|
278
|
+
/**
|
|
279
|
+
* Subtrees inside a population root that are excluded, with the reason.
|
|
280
|
+
*
|
|
281
|
+
* Both entries are places that must be able to **write** the shapes this check
|
|
282
|
+
* refuses, because naming them is their job. They are exact path prefixes, not
|
|
283
|
+
* a `scripts` directory-name rule: a module directory called `scripts` would
|
|
284
|
+
* silently exempt itself under the latter, which is issue #197's lesson applied
|
|
285
|
+
* one level up from `SHARED_FOLD_HELPER`.
|
|
286
|
+
*/
|
|
287
|
+
export const EXCLUDED_SUBTREES = {
|
|
288
|
+
'backend/scripts': 'The checks themselves, this one included. A check that cannot spell the ' +
|
|
289
|
+
'pattern it refuses cannot refuse it.',
|
|
290
|
+
'backend/test/unit/scripts': "The checks' companion tests. Every fixture in them is a deliberate " +
|
|
291
|
+
'spelling of a refused shape, and a red proof that cannot be written is ' +
|
|
292
|
+
'issue #130 restated.',
|
|
293
|
+
'packages/cli/src/rules': 'The same reason as `backend/scripts`, at the second address the checks ' +
|
|
294
|
+
'now have: a relocated analysis is the check itself, and this one spells ' +
|
|
295
|
+
'`NFD`, `NFKD` and the combining-mark range because refusing them is its ' +
|
|
296
|
+
'job (feature 101, Phase 2). Excluded as an exact path prefix rather than ' +
|
|
297
|
+
'by a `rules` directory-name rule, for issue #197\'s reason — a module ' +
|
|
298
|
+
'directory called `rules` would otherwise exempt itself.',
|
|
299
|
+
'packages/cli/test': "The relocated analyses' companion tests, and the exact twin of " +
|
|
300
|
+
'`backend/test/unit/scripts` above: every fixture in them is a deliberate ' +
|
|
301
|
+
'spelling of a refused shape, and a red proof that cannot be written is ' +
|
|
302
|
+
'issue #130 restated. It is not a precaution — the merge request that ' +
|
|
303
|
+
'relocated this analysis wrote a fold into a red proof here and this check ' +
|
|
304
|
+
'reported it, which is the same finding `backend/scripts` was excluded for.',
|
|
305
|
+
};
|
|
306
|
+
/** Directory names pruned wherever they occur under a population root. */
|
|
307
|
+
export const SKIPPED_DIRECTORIES = {
|
|
308
|
+
node_modules: "Installed dependencies: third-party code, not this repository's source.",
|
|
309
|
+
dist: 'Build output, reproduced from source by the build.',
|
|
310
|
+
'.next': "Next.js build output, regenerated by the storefront's build.",
|
|
311
|
+
coverage: 'Coverage report output, regenerated by every run.',
|
|
312
|
+
};
|
|
313
|
+
/** The two explicit-fold kinds, which {@link DIACRITIC_FOLDS_ALLOWED} answers for. */
|
|
314
|
+
export const EXPLICIT_FOLD_KINDS = new Set([
|
|
315
|
+
'decomposition',
|
|
316
|
+
'diacritic-strip',
|
|
317
|
+
]);
|
|
318
|
+
/** Which ledger answers for a finding of this kind. */
|
|
319
|
+
export function ledgerFor(kind, folds, slugRuns) {
|
|
320
|
+
return EXPLICIT_FOLD_KINDS.has(kind) ? folds : slugRuns;
|
|
321
|
+
}
|
|
322
|
+
/** The decomposing normalization forms. `NFC` / `NFKC` compose and are not folds. */
|
|
323
|
+
const DECOMPOSING_FORMS = new Set(['NFD', 'NFKD']);
|
|
324
|
+
/**
|
|
325
|
+
* Spellings of "remove the combining marks".
|
|
326
|
+
*
|
|
327
|
+
* Every one of them appears in this tree or in a repaired copy of it. The raw
|
|
328
|
+
* range matters most: `feed-template-io.service.ts` writes the class with the
|
|
329
|
+
* combining characters themselves rather than with escapes, so a check grepping
|
|
330
|
+
* for `\u0300` reads that file clean.
|
|
331
|
+
*/
|
|
332
|
+
const DIACRITIC_PATTERNS = [
|
|
333
|
+
/\\p\{Diacritic\}/u,
|
|
334
|
+
/\\p\{Mn\}/u,
|
|
335
|
+
/\\p\{Nonspacing_Mark\}/u,
|
|
336
|
+
/\\p\{M\}/u,
|
|
337
|
+
/\\p\{Mark\}/u,
|
|
338
|
+
/\\p\{InCombiningDiacriticalMarks\}/u,
|
|
339
|
+
// The U+0300–U+036F block, in both escape spellings and in either case.
|
|
340
|
+
/\\u\{?0*300\}?\s*-\s*\\u\{?0*36[fF]\}?/u,
|
|
341
|
+
// The same range written as the characters themselves.
|
|
342
|
+
/[\u0300-\u036f]\s*-\s*[\u0300-\u036f]/u,
|
|
343
|
+
];
|
|
344
|
+
/** Does this literal's text name a combining-mark class? */
|
|
345
|
+
export function isDiacriticPattern(text) {
|
|
346
|
+
return DIACRITIC_PATTERNS.some((pattern) => pattern.test(text));
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* A negated character class, tolerating an escaped `]` inside it.
|
|
350
|
+
*
|
|
351
|
+
* `[^\]a-z0-9]` is legal and rare; a naive `\[\^[^\]]*\]` stops at the escaped
|
|
352
|
+
* bracket and reads the class as `[^\]`, which holds no ranges and so silently
|
|
353
|
+
* clears the site. Consuming `\\.` first is one character of regex and removes
|
|
354
|
+
* the whole shape from the "cannot see" list.
|
|
355
|
+
*/
|
|
356
|
+
const NEGATED_CLASS = /\[\^((?:\\.|[^\]\\])*)\]/;
|
|
357
|
+
/** The quantifier that turns a class into a **run**: `+`, `{2,}`, `{1,4}`. */
|
|
358
|
+
const RUN_QUANTIFIER = /^(?:\+|\{\d+,\d*\})/;
|
|
359
|
+
/**
|
|
360
|
+
* The negated ASCII-alphanumeric class in a pattern's source, if it has one.
|
|
361
|
+
*
|
|
362
|
+
* "ASCII alphanumeric" is a letter range **and** a digit range (`a-z0-9`,
|
|
363
|
+
* `a-zA-Z0-9`, `A-Za-z0-9_-`), or the `\w` shorthand, which is the same set
|
|
364
|
+
* plus `_`. Extra members are allowed and are the norm — six of the nine sites
|
|
365
|
+
* measured keep `_` or `-` — because what matters is that every character
|
|
366
|
+
* outside ASCII is on the wrong side of the negation.
|
|
367
|
+
*
|
|
368
|
+
* A class that names **Unicode** properties is deliberately not one of these:
|
|
369
|
+
* `[^\p{L}\p{N}]` keeps its accented letters, so it is not the defect and not in
|
|
370
|
+
* the population.
|
|
371
|
+
*/
|
|
372
|
+
export function negatedAsciiClass(pattern) {
|
|
373
|
+
const match = NEGATED_CLASS.exec(pattern);
|
|
374
|
+
if (match === null)
|
|
375
|
+
return undefined;
|
|
376
|
+
const members = match[1] ?? '';
|
|
377
|
+
const asciiAlphanumeric = (/a-z/i.test(members) && /0-9/.test(members)) || /\\w/.test(members);
|
|
378
|
+
if (!asciiAlphanumeric)
|
|
379
|
+
return undefined;
|
|
380
|
+
const tail = pattern.slice(match.index + match[0].length);
|
|
381
|
+
const quantifier = RUN_QUANTIFIER.exec(tail);
|
|
382
|
+
return {
|
|
383
|
+
text: `${match[0]}${quantifier?.[0] ?? ''}`,
|
|
384
|
+
quantified: quantifier !== null,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/** Escape a literal so it can be matched inside a constructed pattern. */
|
|
388
|
+
function escapeForRegExp(literal) {
|
|
389
|
+
return literal.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&');
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Does this pattern collapse a **run of the separator** — `/-+/`, `/_{2,}/`,
|
|
393
|
+
* `/[-]+$/`?
|
|
394
|
+
*
|
|
395
|
+
* This is the second half of the two-move spelling. An anchored edge trim
|
|
396
|
+
* (`/^-+/`, `/-+$/`) counts, and that is a decision rather than an oversight: a
|
|
397
|
+
* site that emits a separator and then quantifies it anywhere is producing a
|
|
398
|
+
* separator-delimited value, which is the grammar the rule is about. It changes
|
|
399
|
+
* no finding on the tree this landed against, where the two two-move sites both
|
|
400
|
+
* spell it `/_{2,}/`.
|
|
401
|
+
*/
|
|
402
|
+
export function collapsesSeparatorRun(pattern, separator) {
|
|
403
|
+
if (separator.length === 0)
|
|
404
|
+
return false;
|
|
405
|
+
const escaped = escapeForRegExp(separator);
|
|
406
|
+
return new RegExp(`(?:\\[${escaped}\\]|${escaped})(?:\\+|\\{\\d+,\\d*\\})`).test(pattern);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The pattern source of a `.replace()`'s first argument, where it is literal.
|
|
410
|
+
*
|
|
411
|
+
* A regular-expression literal (its body, without the delimiters and flags) or a
|
|
412
|
+
* `new RegExp('…')` built from a string. A **bare string** first argument is not
|
|
413
|
+
* one: `replace` treats it as a literal substring rather than a pattern, so
|
|
414
|
+
* `'[^a-z0-9]+'` there matches those characters verbatim and collapses nothing.
|
|
415
|
+
*/
|
|
416
|
+
export function replacePatternSource(node, file) {
|
|
417
|
+
if (ts.isRegularExpressionLiteral(node)) {
|
|
418
|
+
const end = node.text.lastIndexOf('/');
|
|
419
|
+
return end > 0 ? node.text.slice(1, end) : undefined;
|
|
420
|
+
}
|
|
421
|
+
if (ts.isNewExpression(node) && node.expression.getText(file) === 'RegExp') {
|
|
422
|
+
const first = node.arguments?.[0];
|
|
423
|
+
return first !== undefined && ts.isStringLiteralLike(first) ? first.text : undefined;
|
|
424
|
+
}
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* The separator a `.replace()` writes, where it is one.
|
|
429
|
+
*
|
|
430
|
+
* A non-empty string literal, or an identifier / property access — `slugify`
|
|
431
|
+
* itself writes `.replace(/[^a-z0-9]+/g, separator)`, and a copy of it would
|
|
432
|
+
* too. `''` is **not** a separator: deleting a run preserves nothing and is the
|
|
433
|
+
* shape four legitimate sanitisers in this tree use. A replacer *function* is
|
|
434
|
+
* not one either; nothing in the population writes a slug that way.
|
|
435
|
+
*
|
|
436
|
+
* Returns `null` for the identifier case, which is a separator whose text this
|
|
437
|
+
* check does not know — enough for the one-move spelling, not enough to look for
|
|
438
|
+
* a second move over it.
|
|
439
|
+
*/
|
|
440
|
+
export function replaceSeparator(node) {
|
|
441
|
+
if (ts.isStringLiteralLike(node))
|
|
442
|
+
return node.text.length > 0 ? node.text : undefined;
|
|
443
|
+
if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node))
|
|
444
|
+
return null;
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Is there a later `.replace()` in the **same call chain** that collapses a run
|
|
449
|
+
* of `separator`?
|
|
450
|
+
*
|
|
451
|
+
* The window is one expression and stops there. A collapse split across two
|
|
452
|
+
* statements is out of the population, and the header says so rather than
|
|
453
|
+
* leaving a reader to find out — the same bound `check-port-catches` draws when
|
|
454
|
+
* it follows a gate one hop through `this` and no further.
|
|
455
|
+
*/
|
|
456
|
+
function chainCollapsesSeparator(call, separator, file) {
|
|
457
|
+
let current = call;
|
|
458
|
+
while (ts.isPropertyAccessExpression(current.parent) &&
|
|
459
|
+
current.parent.expression === current &&
|
|
460
|
+
ts.isCallExpression(current.parent.parent) &&
|
|
461
|
+
current.parent.parent.expression === current.parent) {
|
|
462
|
+
const access = current.parent;
|
|
463
|
+
const next = current.parent.parent;
|
|
464
|
+
if (access.name.text === 'replace' && next.arguments.length === 2) {
|
|
465
|
+
const pattern = replacePatternSource(next.arguments[0], file);
|
|
466
|
+
if (pattern !== undefined && collapsesSeparatorRun(pattern, separator))
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
current = next;
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Is this call a slug run — a `.replace()` collapsing everything outside ASCII
|
|
475
|
+
* alphanumerics to a separator?
|
|
476
|
+
*
|
|
477
|
+
* Exported so a red proof can enter here as well as at {@link analyzeSource}.
|
|
478
|
+
* Returns the class as written, which is what the failure message names.
|
|
479
|
+
*/
|
|
480
|
+
export function slugRunOf(node, file) {
|
|
481
|
+
if (!ts.isCallExpression(node))
|
|
482
|
+
return undefined;
|
|
483
|
+
if (!ts.isPropertyAccessExpression(node.expression))
|
|
484
|
+
return undefined;
|
|
485
|
+
if (node.expression.name.text !== 'replace' || node.arguments.length !== 2)
|
|
486
|
+
return undefined;
|
|
487
|
+
const pattern = replacePatternSource(node.arguments[0], file);
|
|
488
|
+
if (pattern === undefined)
|
|
489
|
+
return undefined;
|
|
490
|
+
const negated = negatedAsciiClass(pattern);
|
|
491
|
+
if (negated === undefined)
|
|
492
|
+
return undefined;
|
|
493
|
+
const separator = replaceSeparator(node.arguments[1]);
|
|
494
|
+
if (separator === undefined)
|
|
495
|
+
return undefined;
|
|
496
|
+
if (negated.quantified)
|
|
497
|
+
return negated.text;
|
|
498
|
+
if (separator === null)
|
|
499
|
+
return undefined;
|
|
500
|
+
return chainCollapsesSeparator(node, separator, file) ? negated.text : undefined;
|
|
501
|
+
}
|
|
502
|
+
/** Is this path in the population — a source file under a root, not the helper? */
|
|
503
|
+
export function isScannablePath(path, roots = POPULATION_ROOTS) {
|
|
504
|
+
if (path === SHARED_FOLD_HELPER)
|
|
505
|
+
return false;
|
|
506
|
+
if (!SOURCE_EXTENSIONS.some((extension) => path.endsWith(extension)))
|
|
507
|
+
return false;
|
|
508
|
+
if (Object.keys(EXCLUDED_SUBTREES).some((subtree) => path.startsWith(`${subtree}/`))) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
const segments = path.split('/');
|
|
512
|
+
if (segments.slice(0, -1).some((segment) => SKIPPED_DIRECTORIES[segment] !== undefined)) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
return Object.keys(roots).some((root) => path.startsWith(`${root}/`));
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* One file's source, analysed once.
|
|
519
|
+
*
|
|
520
|
+
* **This is the top of the analysis**: source text in, findings and the judged
|
|
521
|
+
* population out. It parses rather than greps, so a `normalize('NFD')` quoted in
|
|
522
|
+
* a doc block — which four files in this tree do, on purpose, to explain why
|
|
523
|
+
* they do not use it — is not a literal node and cannot be seen. The same holds
|
|
524
|
+
* for the third signal: a doc block quoting `.replace(/[^a-z0-9]+/g, '-')` to
|
|
525
|
+
* explain the defect is not a call node.
|
|
526
|
+
*
|
|
527
|
+
* One parse for all three signals, and for the site count: two walks over the
|
|
528
|
+
* same tree would let the population and the findings drift apart, which is the
|
|
529
|
+
* one thing `sites=` exists to prevent.
|
|
530
|
+
*/
|
|
531
|
+
export function analyzeFile(source, path) {
|
|
532
|
+
const file = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
533
|
+
const found = [];
|
|
534
|
+
let replaceSites = 0;
|
|
535
|
+
const at = (node) => {
|
|
536
|
+
const position = file.getLineAndCharacterOfPosition(node.getStart(file));
|
|
537
|
+
return { line: position.line + 1, column: position.character + 1 };
|
|
538
|
+
};
|
|
539
|
+
const visit = (node) => {
|
|
540
|
+
if (ts.isStringLiteralLike(node)) {
|
|
541
|
+
if (DECOMPOSING_FORMS.has(node.text)) {
|
|
542
|
+
found.push({ path, kind: 'decomposition', ...at(node), literal: node.text });
|
|
543
|
+
}
|
|
544
|
+
else if (isDiacriticPattern(node.text)) {
|
|
545
|
+
// `new RegExp('\\p{Diacritic}', 'gu')` — the pattern arrives as a string.
|
|
546
|
+
found.push({ path, kind: 'diacritic-strip', ...at(node), literal: node.text });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else if (ts.isRegularExpressionLiteral(node) && isDiacriticPattern(node.text)) {
|
|
550
|
+
found.push({ path, kind: 'diacritic-strip', ...at(node), literal: node.text });
|
|
551
|
+
}
|
|
552
|
+
if (ts.isCallExpression(node) &&
|
|
553
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
554
|
+
node.expression.name.text === 'replace' &&
|
|
555
|
+
node.arguments.length === 2 &&
|
|
556
|
+
replacePatternSource(node.arguments[0], file) !== undefined) {
|
|
557
|
+
replaceSites += 1;
|
|
558
|
+
const slugRun = slugRunOf(node, file);
|
|
559
|
+
if (slugRun !== undefined) {
|
|
560
|
+
found.push({ path, kind: 'slug-run', ...at(node), literal: slugRun });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
ts.forEachChild(node, visit);
|
|
564
|
+
};
|
|
565
|
+
visit(file);
|
|
566
|
+
return { findings: found, replaceSites };
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* The findings alone — the shape a red proof asserts against.
|
|
570
|
+
*
|
|
571
|
+
* Kept as its own export because every proof in the companion test and in the
|
|
572
|
+
* inventory enters here, with source text, which is the anchoring issue #130
|
|
573
|
+
* asks for.
|
|
574
|
+
*/
|
|
575
|
+
export function analyzeSource(source, path) {
|
|
576
|
+
return [...analyzeFile(source, path).findings];
|
|
577
|
+
}
|
|
578
|
+
/** Every finding among the scannable files, in path order. */
|
|
579
|
+
export function findDiacriticFolds(files, roots = POPULATION_ROOTS) {
|
|
580
|
+
const found = [];
|
|
581
|
+
for (const file of files) {
|
|
582
|
+
if (!isScannablePath(file.path, roots))
|
|
583
|
+
continue;
|
|
584
|
+
found.push(...analyzeSource(file.source, file.path));
|
|
585
|
+
}
|
|
586
|
+
found.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.column - b.column);
|
|
587
|
+
return found;
|
|
588
|
+
}
|
|
589
|
+
export function checkDiacriticFolds(files, ledger, slugLedger, roots = POPULATION_ROOTS) {
|
|
590
|
+
const scannable = [...files].filter((file) => isScannablePath(file.path, roots));
|
|
591
|
+
const all = [];
|
|
592
|
+
let replaceSites = 0;
|
|
593
|
+
for (const file of scannable) {
|
|
594
|
+
const analysis = analyzeFile(file.source, file.path);
|
|
595
|
+
all.push(...analysis.findings);
|
|
596
|
+
replaceSites += analysis.replaceSites;
|
|
597
|
+
}
|
|
598
|
+
all.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.column - b.column);
|
|
599
|
+
// Counted per ledger, not per file: the two answer different questions, so a
|
|
600
|
+
// file carrying one of each is two independent entries rather than one
|
|
601
|
+
// ambiguous number.
|
|
602
|
+
const foldCounts = new Map();
|
|
603
|
+
const slugCounts = new Map();
|
|
604
|
+
for (const finding of all) {
|
|
605
|
+
const counts = EXPLICIT_FOLD_KINDS.has(finding.kind) ? foldCounts : slugCounts;
|
|
606
|
+
counts.set(finding.path, (counts.get(finding.path) ?? 0) + 1);
|
|
607
|
+
}
|
|
608
|
+
const stale = [];
|
|
609
|
+
for (const [path, entry] of Object.entries(ledger)) {
|
|
610
|
+
const actual = foldCounts.get(path) ?? 0;
|
|
611
|
+
if (actual !== entry.findings) {
|
|
612
|
+
stale.push({ path, declared: entry.findings, actual, ledger: 'folds' });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
for (const [path, entry] of Object.entries(slugLedger)) {
|
|
616
|
+
const actual = slugCounts.get(path) ?? 0;
|
|
617
|
+
if (actual !== entry.findings) {
|
|
618
|
+
stale.push({ path, declared: entry.findings, actual, ledger: 'slug-runs' });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const isLedgered = (finding) => ledgerFor(finding.kind, ledger, slugLedger)[finding.path] !== undefined;
|
|
622
|
+
return {
|
|
623
|
+
scanned: scannable.length,
|
|
624
|
+
replaceSites,
|
|
625
|
+
total: all.length,
|
|
626
|
+
violations: all.filter((finding) => !isLedgered(finding)),
|
|
627
|
+
ledgered: all.filter(isLedgered),
|
|
628
|
+
stale: stale.sort((a, b) => a.path.localeCompare(b.path) || a.ledger.localeCompare(b.ledger)),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Every file under `dir`, with the rule's own directory prunes applied.
|
|
633
|
+
*
|
|
634
|
+
* Exported because the population is part of the rule: two hosts computing
|
|
635
|
+
* "which files this check reads" two ways is the shape that lets one of them go
|
|
636
|
+
* half-blind.
|
|
637
|
+
*/
|
|
638
|
+
export function collectFoldSources(dir, out = []) {
|
|
639
|
+
if (!existsSync(dir))
|
|
640
|
+
return out;
|
|
641
|
+
for (const name of readdirSync(dir)) {
|
|
642
|
+
if (SKIPPED_DIRECTORIES[name] !== undefined)
|
|
643
|
+
continue;
|
|
644
|
+
const full = join(dir, name);
|
|
645
|
+
// A dangling symlink must not abort the walk, hence `throwIfNoEntry`.
|
|
646
|
+
const stat = statSync(full, { throwIfNoEntry: false });
|
|
647
|
+
if (stat === undefined)
|
|
648
|
+
continue;
|
|
649
|
+
if (stat.isDirectory())
|
|
650
|
+
collectFoldSources(full, out);
|
|
651
|
+
else if (stat.isFile())
|
|
652
|
+
out.push(full);
|
|
653
|
+
}
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Is the excluded helper still the owner of all three shapes?
|
|
658
|
+
*
|
|
659
|
+
* The exemption and the self-test are the same path on purpose. If the helper
|
|
660
|
+
* has moved, the exclusion exempts nothing and the real fold is now being
|
|
661
|
+
* reported at its new home; if the helper is there but the analysis finds no
|
|
662
|
+
* fold in it, the analysis has gone blind and every green it reports afterwards
|
|
663
|
+
* is vacuous. Either way the answer is exit 2, not exit 0.
|
|
664
|
+
*
|
|
665
|
+
* The `slug-run` shape joined the guard with issue #244, and it earns its place
|
|
666
|
+
* twice over. It is the third predicate, so it is a third way for the analysis
|
|
667
|
+
* to go blind unnoticed. And `slugify` is what every `slug-run` failure message
|
|
668
|
+
* tells the author to import: a run in which nothing in the helper parses as a
|
|
669
|
+
* slug builder is a run whose remedy does not exist, which is worth an exit 2
|
|
670
|
+
* on its own.
|
|
671
|
+
*
|
|
672
|
+
* It was named `helperStillFolds` until then. The rename is the point of the
|
|
673
|
+
* widening: a function reporting `false` because `slugify` was deleted, under a
|
|
674
|
+
* name that says "folds", is the sort of quiet mismatch this file exists to
|
|
675
|
+
* refuse.
|
|
676
|
+
*/
|
|
677
|
+
export function helperIsStillTheOwner(source) {
|
|
678
|
+
const kinds = new Set(analyzeSource(source, SHARED_FOLD_HELPER).map((finding) => finding.kind));
|
|
679
|
+
return kinds.has('decomposition') && kinds.has('diacritic-strip') && kinds.has('slug-run');
|
|
680
|
+
}
|
|
681
|
+
//# sourceMappingURL=diacritic-folds.js.map
|