@clossys/writer 0.3.2
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/CHANGELOG.md +196 -0
- package/LICENSE +21 -0
- package/README.md +593 -0
- package/dist/addressability.d.ts +245 -0
- package/dist/addressability.d.ts.map +1 -0
- package/dist/addressability.js +567 -0
- package/dist/addressability.js.map +1 -0
- package/dist/checker.d.ts +159 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +177 -0
- package/dist/checker.js.map +1 -0
- package/dist/cli.d.ts +149 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1049 -0
- package/dist/cli.js.map +1 -0
- package/dist/copy-gate.d.ts +107 -0
- package/dist/copy-gate.d.ts.map +1 -0
- package/dist/copy-gate.js +132 -0
- package/dist/copy-gate.js.map +1 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +64 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/locale-coverage.d.ts +227 -0
- package/dist/locale-coverage.d.ts.map +1 -0
- package/dist/locale-coverage.js +414 -0
- package/dist/locale-coverage.js.map +1 -0
- package/dist/passage.d.ts +294 -0
- package/dist/passage.d.ts.map +1 -0
- package/dist/passage.js +463 -0
- package/dist/passage.js.map +1 -0
- package/dist/path-exclusions.d.ts +114 -0
- package/dist/path-exclusions.d.ts.map +1 -0
- package/dist/path-exclusions.js +213 -0
- package/dist/path-exclusions.js.map +1 -0
- package/dist/registry.d.ts +61 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +64 -0
- package/dist/registry.js.map +1 -0
- package/dist/resolve.d.ts +33 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +133 -0
- package/dist/resolve.js.map +1 -0
- package/dist/scan.d.ts +457 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +1698 -0
- package/dist/scan.js.map +1 -0
- package/dist/schema.d.ts +81 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +482 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +268 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -0
- package/dist/voice/checker.d.ts +186 -0
- package/dist/voice/checker.d.ts.map +1 -0
- package/dist/voice/checker.js +483 -0
- package/dist/voice/checker.js.map +1 -0
- package/dist/voice/derivation-coverage.d.ts +168 -0
- package/dist/voice/derivation-coverage.d.ts.map +1 -0
- package/dist/voice/derivation-coverage.js +181 -0
- package/dist/voice/derivation-coverage.js.map +1 -0
- package/dist/voice/fields.d.ts +107 -0
- package/dist/voice/fields.d.ts.map +1 -0
- package/dist/voice/fields.js +144 -0
- package/dist/voice/fields.js.map +1 -0
- package/dist/voice/index.d.ts +19 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +24 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/internal/parse-template.d.ts +27 -0
- package/dist/voice/internal/parse-template.d.ts.map +1 -0
- package/dist/voice/internal/parse-template.js +111 -0
- package/dist/voice/internal/parse-template.js.map +1 -0
- package/dist/voice/internal/pattern-safety.d.ts +111 -0
- package/dist/voice/internal/pattern-safety.d.ts.map +1 -0
- package/dist/voice/internal/pattern-safety.js +278 -0
- package/dist/voice/internal/pattern-safety.js.map +1 -0
- package/dist/voice/schema.d.ts +65 -0
- package/dist/voice/schema.d.ts.map +1 -0
- package/dist/voice/schema.js +535 -0
- package/dist/voice/schema.js.map +1 -0
- package/dist/voice/types.d.ts +282 -0
- package/dist/voice/types.d.ts.map +1 -0
- package/dist/voice/types.js +37 -0
- package/dist/voice/types.js.map +1 -0
- package/package.json +72 -0
- package/src/addressability.ts +664 -0
- package/src/checker.ts +293 -0
- package/src/cli.ts +1166 -0
- package/src/copy-gate.ts +197 -0
- package/src/fingerprint.ts +66 -0
- package/src/index.ts +163 -0
- package/src/locale-coverage.ts +512 -0
- package/src/passage.ts +644 -0
- package/src/path-exclusions.ts +265 -0
- package/src/registry.ts +103 -0
- package/src/resolve.ts +172 -0
- package/src/scan.ts +1959 -0
- package/src/schema.ts +528 -0
- package/src/types.ts +301 -0
- package/src/voice/checker.ts +590 -0
- package/src/voice/derivation-coverage.ts +217 -0
- package/src/voice/fields.ts +160 -0
- package/src/voice/index.ts +55 -0
- package/src/voice/internal/parse-template.ts +114 -0
- package/src/voice/internal/pattern-safety.ts +336 -0
- package/src/voice/schema.ts +585 -0
- package/src/voice/types.ts +329 -0
- package/templates/voice-record.template.jsonc +135 -0
package/dist/scan.d.ts
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `scanCopySourceTree` and `extractCopyCandidates` — the I/O half AND the
|
|
3
|
+
* hard-analysis half of this gate, unlike the split `@clossys/
|
|
4
|
+
* strategy` draws between its own `scan.ts` (dumb directory walk) and
|
|
5
|
+
* `facts-gate.ts` (the matchers). Here the walk is the easy part; finding
|
|
6
|
+
* which string literals in a real source file are actually user-facing
|
|
7
|
+
* copy — as opposed to a class name, an import specifier, an object key, a
|
|
8
|
+
* `data-*`/`aria-*` attribute, an enum token, or a message meant for a
|
|
9
|
+
* developer, not a user — is the genuinely hard problem, and it is a
|
|
10
|
+
* problem about SOURCE TEXT, not about a file tree. So it lives here, next
|
|
11
|
+
* to the walk that feeds it, and `copy-gate.ts` is left with the simpler
|
|
12
|
+
* job: compare what this file found against a `CopyRecord` and report.
|
|
13
|
+
*
|
|
14
|
+
* NO PARSER LIBRARY — this repository's zero-runtime-dependency default for
|
|
15
|
+
* every package here, `@example/ui` (which wraps React) being the
|
|
16
|
+
* one deliberate exception. What follows is a small hand-rolled character
|
|
17
|
+
* scanner
|
|
18
|
+
* that finds string/template literal BOUNDARIES correctly (the part that
|
|
19
|
+
* is genuinely hard to get wrong-free with a regex: escaped quotes, nested
|
|
20
|
+
* template interpolation, comments, and regex literals all look enough
|
|
21
|
+
* like strings to confuse a naive `"[^"]*"` pattern), plus a set of
|
|
22
|
+
* narrow, LOCAL, documented heuristics — never a full grammar — that look
|
|
23
|
+
* at the raw text immediately around a literal to decide whether it is a
|
|
24
|
+
* user-facing string. Every one of those heuristics is a real, stated
|
|
25
|
+
* trade-off, not a black box: see "THE BOUNDARY" below for the full
|
|
26
|
+
* reasoning, calibrated against real code in this same repository
|
|
27
|
+
* (`packages/ui/src/blocks/Pagination.tsx`, `packages/ui/src/atoms/
|
|
28
|
+
* Select.tsx`, `packages/ui/src/atoms/Table.tsx` — see each package's own
|
|
29
|
+
* test fixtures, lifted near-verbatim from those three files).
|
|
30
|
+
*
|
|
31
|
+
* ============================================================================
|
|
32
|
+
* THE BOUNDARY — what counts as a "copy candidate", and why
|
|
33
|
+
* ============================================================================
|
|
34
|
+
*
|
|
35
|
+
* DEFAULT IS INCLUDE. A bare string or template literal used as a VALUE
|
|
36
|
+
* (never a key, a specifier, or a type) is a copy candidate unless one of
|
|
37
|
+
* the exclusions below applies. This is deliberate, not an oversight: this
|
|
38
|
+
* package's own calibration example — `rangeSummary = "No results"` in
|
|
39
|
+
* `Pagination.tsx` — is a plain literal assignment with NO special
|
|
40
|
+
* surrounding syntax at all (no JSX attribute, no call argument, nothing
|
|
41
|
+
* to allowlist against). A scanner that only fires inside a hand-picked
|
|
42
|
+
* allowlist of "known copy positions" would silently miss exactly this
|
|
43
|
+
* case, which is the single most common shape real UI copy actually takes
|
|
44
|
+
* in this codebase. So the default has to be permissive, and the real work
|
|
45
|
+
* is in a precise, narrow EXCLUDE list:
|
|
46
|
+
*
|
|
47
|
+
* 1. Import/export/require specifiers (`from "x"`, `import("x")`,
|
|
48
|
+
* `require("x")`) — a module path is an address, not copy.
|
|
49
|
+
* 2. Object-literal / destructuring keys (`{ "some-key": value }`) —
|
|
50
|
+
* recognized as: the literal is immediately followed by `:`, AND its
|
|
51
|
+
* immediately preceding non-whitespace character is `{` or `,` (an
|
|
52
|
+
* object-literal position), AND that preceding character is NOT `?`
|
|
53
|
+
* (a ternary's `cond ? "a" : "b"` also has a literal immediately
|
|
54
|
+
* followed by `:`-shaped text further along, but the literal
|
|
55
|
+
* immediately BEFORE a ternary's `:` is preceded by `?`, never `{`/
|
|
56
|
+
* `,` — this distinction is exactly what keeps
|
|
57
|
+
* `totalItems === 0 ? "No results" : ...` — a real line from
|
|
58
|
+
* `Pagination.tsx` — correctly classified as copy, not a key).
|
|
59
|
+
* 3. Values inside a `type` alias or `interface` body — TypeScript's own
|
|
60
|
+
* type language, never a runtime string a user sees.
|
|
61
|
+
* 4. `aria-*` and `data-*` attribute/prop VALUES, in both JSX
|
|
62
|
+
* (`aria-label="Previous page"`) and camelCase-identifier form
|
|
63
|
+
* (`` "aria-label": ariaLabel = "Select all rows" ``, the exact shape
|
|
64
|
+
* `Table.tsx`'s `TableSelectAllCheckbox` uses). Stated explicitly,
|
|
65
|
+
* because it is the one exclusion most worth arguing with: an
|
|
66
|
+
* `aria-label` genuinely is read aloud to a screen-reader user, so it
|
|
67
|
+
* IS user-facing in the most literal sense. It is excluded anyway,
|
|
68
|
+
* for a scope reason, not a "doesn't matter" reason: this repository
|
|
69
|
+
* leans heavily on icon-only controls (`Pagination`'s prev/next
|
|
70
|
+
* buttons, `Table`'s row-selection checkboxes), each of which carries
|
|
71
|
+
* an `aria-label` as its ONLY accessible name. Registering those
|
|
72
|
+
* alongside visible copy would roughly double this gate's candidate
|
|
73
|
+
* volume on `packages/ui` alone, for a category (accessibility
|
|
74
|
+
* labels) that has a different owner, a different review cadence, and
|
|
75
|
+
* arguably needs a different check shape (e.g. "every icon-only
|
|
76
|
+
* control has SOME aria-label" is a presence check, not a
|
|
77
|
+
* traceability-to-copy check). Wiring accessibility labels into this
|
|
78
|
+
* gate is real, valid future scope — deliberately not this package's
|
|
79
|
+
* v1, the same way the icon-atom migration was deliberately excluded
|
|
80
|
+
* from its own PR. A denylisted attribute value is counted and
|
|
81
|
+
* reported (see `ExcludedLiteral`), never silently dropped.
|
|
82
|
+
* 5. A fixed denylist of attribute/prop names that are structural, not
|
|
83
|
+
* textual, even though their values are strings: `className`, `class`,
|
|
84
|
+
* `id`, `key`, `htmlFor`, `role`, `type`, `rel`, `target`, `method`,
|
|
85
|
+
* `encType`, `autoComplete`, `inputMode`, `pattern`, `name`, `slot`,
|
|
86
|
+
* `variant`, `size`, `as`, `href`, `src`, `xmlns`, `viewBox`, `fill`,
|
|
87
|
+
* `stroke`, `d`, `strokeWidth`, `strokeLinecap`, `strokeLinejoin`,
|
|
88
|
+
* `testId`, `data-testid`. `className` is the canonical example this
|
|
89
|
+
* package's design brief calls out: a Tailwind utility string is not
|
|
90
|
+
* copy at all, in any sense.
|
|
91
|
+
* 6. The literal-string argument list of a class-name BUILDER call —
|
|
92
|
+
* `cx(...)` (this repository's own helper, see
|
|
93
|
+
* `packages/ui/src/atoms/internal/cx.ts`), `clsx(...)`,
|
|
94
|
+
* `classNames(...)` — even a literal that ISN'T itself passed through
|
|
95
|
+
* a `className` prop can still be pure CSS-class text if it's an
|
|
96
|
+
* argument to one of these. Detected via a bounded backward paren-
|
|
97
|
+
* balance scan to find the nearest enclosing call's identifier — see
|
|
98
|
+
* `enclosingCallName`.
|
|
99
|
+
* 7. The literal-string argument of a call this package treats as a
|
|
100
|
+
* developer diagnostic, not user copy: `console.log/warn/error/
|
|
101
|
+
* debug/info(...)`, `throw new Error(...)`, `assert(...)`,
|
|
102
|
+
* `invariant(...)`. The task brief's own words: "error messages meant
|
|
103
|
+
* for developers... look identical to a naive scanner" to real copy;
|
|
104
|
+
* this is the targeted fix for that specific confusion.
|
|
105
|
+
* 8. Literals with no letters after trimming (`"…"`, `"‹"`, `"▾"`, a bare
|
|
106
|
+
* number or punctuation glyph) — decorative or symbolic, not
|
|
107
|
+
* language-bearing text a translator or copy reviewer would ever act
|
|
108
|
+
* on. `Pagination.tsx`'s `‹`/`›` ellipsis glyphs are exactly this
|
|
109
|
+
* case.
|
|
110
|
+
* 9. A single lowercase "token" — no whitespace, matches
|
|
111
|
+
* `/^[a-z][a-z0-9-]*$/`, length <= 24 — is excluded under a heuristic
|
|
112
|
+
* reason (`enum-or-token-shaped`), NOT the type-context rule above:
|
|
113
|
+
* values like `"primary"`, `"ghost"`, `"sm"`, `"ascending"` are
|
|
114
|
+
* extremely common as component variant/size/state tokens passed as
|
|
115
|
+
* plain prop values (`<Button variant="ghost">`), a position this
|
|
116
|
+
* scanner's local-context heuristics cannot always trace back to a
|
|
117
|
+
* `type` declaration. Real visible copy in this codebase is, without
|
|
118
|
+
* exception in every example this task cites, EITHER multi-word
|
|
119
|
+
* ("No results", "Select an option", "Select all rows") OR starts
|
|
120
|
+
* with a capital letter ("Page", "Showing") — a bare, all-lowercase
|
|
121
|
+
* single word is a real, stated risk of a false NEGATIVE (a
|
|
122
|
+
* genuine one-word lowercase copy string, e.g. a placeholder
|
|
123
|
+
* literally `"search…"`, would be missed) traded deliberately against
|
|
124
|
+
* a much larger volume of false positives from variant tokens. Not
|
|
125
|
+
* silent: every such exclusion is counted and reported by reason.
|
|
126
|
+
* 10. Files matching `*.test.ts(x)`, `*.spec.ts(x)`, or `*.check.ts(x)` —
|
|
127
|
+
* test fixtures and compile-time contract assertions are not shipped
|
|
128
|
+
* copy. This is a WHOLE-FILE skip, decided in `scanCopySourceTree`,
|
|
129
|
+
* not a per-literal exclusion — see `DEFAULT_SKIP_FILE_RE`.
|
|
130
|
+
*
|
|
131
|
+
* A DIFFERENT, SIBLING mechanism — `ScanOptions.pathExclusions` — is ALSO a
|
|
132
|
+
* whole-file skip, but is consumer-CONFIGURED (this list above is fixed,
|
|
133
|
+
* shipped by this package) and answers a different question: not "is this
|
|
134
|
+
* file's CONTENT test/build machinery" but "should this file's path be
|
|
135
|
+
* looked at for copy candidates at all" (the "mention vs. use" case — a
|
|
136
|
+
* style guide documenting this voice's own banned terms trips its own
|
|
137
|
+
* rules if scanned like any other file). See `path-exclusions.ts`'s top
|
|
138
|
+
* doc comment for the full argument for why this is a NEW, separate type
|
|
139
|
+
* (`PathExclusion`/`ExcludedPath`), not an 11th `ExclusionReason` — the two
|
|
140
|
+
* mechanisms are not the same feature wearing two names.
|
|
141
|
+
*
|
|
142
|
+
* ============================================================================
|
|
143
|
+
* JSX TEXT NODES (`<span>Hello</span>`) — closing issue #37
|
|
144
|
+
* ============================================================================
|
|
145
|
+
*
|
|
146
|
+
* String/template literals are only half of what a `.tsx`/`.jsx` file can
|
|
147
|
+
* show a user: `<span>Hello</span>`'s `Hello` is neither, it is a raw JSX
|
|
148
|
+
* TEXT NODE, and a scanner that only understands literal boundaries is
|
|
149
|
+
* blind to it — a gate whose entire job is "no untracked user-facing
|
|
150
|
+
* copy" would report a clean pass over a file that is nothing but
|
|
151
|
+
* untracked copy. This is exactly what made this scanner's own top-level
|
|
152
|
+
* risk real once a JSX-heavy consumer (`@example/ui` in this same
|
|
153
|
+
* repo) existed: see issue #37.
|
|
154
|
+
*
|
|
155
|
+
* The fix stays inside "no parser library": a second, JSX-aware pass
|
|
156
|
+
* inside `tokenize()`, triggered only in a `.tsx`/`.jsx` file (`isJsxFile`
|
|
157
|
+
* — a `.ts`/`.js` file is NEVER scanned for JSX at all, so this can never
|
|
158
|
+
* fire on plain TypeScript/JavaScript). It is mutually recursive in the
|
|
159
|
+
* same style `skipInterpolation`/`skipTemplateFrom` already use for
|
|
160
|
+
* nested template interpolations:
|
|
161
|
+
*
|
|
162
|
+
* - `tryScanJsxElement` recognizes a `<` as a JSX element start ONLY
|
|
163
|
+
* when the last significant token was in "expression start" position
|
|
164
|
+
* (the exact same `lastTokenType === "other"` heuristic already used
|
|
165
|
+
* for the regex-vs-division call above) AND the next character is a
|
|
166
|
+
* valid tag-name start or `>` (a fragment `<>`). This one condition
|
|
167
|
+
* is what keeps `Map<string, string>` (preceded by the identifier
|
|
168
|
+
* `Map`, a VALUE token) and `a < b` (preceded by the identifier `a`,
|
|
169
|
+
* also a VALUE token) from ever being mistaken for JSX — a generic
|
|
170
|
+
* type argument and a comparison are structurally "value `<` ...",
|
|
171
|
+
* never "other `<` ...". A string containing `>`/`<`
|
|
172
|
+
* (`"a > b < c"`) never reaches this logic at all: the EXISTING
|
|
173
|
+
* string-literal branch above already consumes the whole literal,
|
|
174
|
+
* `<`/`>` included, before this code ever sees those characters. A
|
|
175
|
+
* `/.../ ` regex literal is the same story via the EXISTING regex
|
|
176
|
+
* branch. Once a `<` is accepted as JSX-element-shaped, this function
|
|
177
|
+
* scans the tag name and its attributes (each attribute value —
|
|
178
|
+
* quoted string or `{expression}` — skipped correctly, reusing the
|
|
179
|
+
* same string-skip and a JSX-aware sibling of `skipInterpolation`)
|
|
180
|
+
* through to the tag's own closing `>`/`/>`. Only once that full,
|
|
181
|
+
* well-formed head is found is this function COMMITTED to "this is
|
|
182
|
+
* real JSX" — a `<` that turns out to be TSX's `<T,>(x: T) => x`
|
|
183
|
+
* generic-arrow-function syntax (which also starts as `< identifier`
|
|
184
|
+
* in "other" position) fails immediately inside the attribute list,
|
|
185
|
+
* before commitment, and is silently treated as not-JSX with no
|
|
186
|
+
* `unchecked` noise at all, precisely because that failure happens
|
|
187
|
+
* too early to be good evidence either way.
|
|
188
|
+
* - `scanJsxChildren` (only reached once committed) walks the raw text
|
|
189
|
+
* between the opening tag's `>` and the next `<`, recording each
|
|
190
|
+
* non-whitespace-only run as a text-node candidate (collapsed per
|
|
191
|
+
* JSX's own whitespace rule — see `collapseJsxWhitespace` — and
|
|
192
|
+
* entity-decoded — see `decodeJsxEntities`). A `<` found here can only
|
|
193
|
+
* be a nested element (recurses back into `tryScanJsxElement`,
|
|
194
|
+
* finding that element's OWN text nodes too — this is how
|
|
195
|
+
* `{condition && <Foo>text</Foo>}`-shaped nesting inside an
|
|
196
|
+
* expression child, and plain sibling nesting like
|
|
197
|
+
* `<p>Hello <strong>there</strong> friend</p>`, both resolve: each
|
|
198
|
+
* text run between element boundaries becomes its own candidate,
|
|
199
|
+
* never concatenated across the boundary, never dropping the tail) or
|
|
200
|
+
* a closing tag (ends this element, returns to whichever caller is
|
|
201
|
+
* scanning ITS OWN children/expression one level up). A `{` starts an
|
|
202
|
+
* expression child — `{/* a JSX comment *\/}` and `{expression}` are
|
|
203
|
+
* both skipped as CODE, not text (see "WHAT THIS STILL DELIBERATELY
|
|
204
|
+
* DOES NOT CATCH" below for the one exception), via the JSX-aware
|
|
205
|
+
* sibling of `skipInterpolation` mentioned above, which is itself
|
|
206
|
+
* mutually recursive with `tryScanJsxElement`: an expression child
|
|
207
|
+
* that itself contains nested JSX (`{items.map((i) => <li
|
|
208
|
+
* key={i}>{i}</li>)}`) is walked correctly, finding ITS nested text
|
|
209
|
+
* nodes too.
|
|
210
|
+
* - Purely whitespace text runs (indentation between sibling JSX tags —
|
|
211
|
+
* the overwhelming majority of raw text a real, formatted `.tsx` file
|
|
212
|
+
* contains) are dropped at the point of collection, not even entering
|
|
213
|
+
* the candidate/exclusion pipeline. This is the one deliberate
|
|
214
|
+
* departure from "count and report every exclusion, never silently
|
|
215
|
+
* drop": that discipline exists so a REAL, AUTHORED literal a
|
|
216
|
+
* developer chose to write is never quietly reclassified without a
|
|
217
|
+
* trace. Indentation whitespace between tags was never authored
|
|
218
|
+
* content in that sense — it is the ABSENCE of a text node, exactly
|
|
219
|
+
* as invisible to a real JSX runtime as it is to this scanner, and
|
|
220
|
+
* counting it would drown `printScanAccounting`'s exclusion tally in
|
|
221
|
+
* noise proportional to a file's formatting, not its copy. A text run
|
|
222
|
+
* that IS present but decorative/symbolic (a bare `"—"` as JSX text,
|
|
223
|
+
* the direct JSX-text analogue of exclusion #8 above) still goes
|
|
224
|
+
* through the same `no-letters` exclusion as a literal would, and is
|
|
225
|
+
* still counted. The enum/token-shaped heuristic (exclusion #9) is
|
|
226
|
+
* deliberately NOT applied to JSX text: that heuristic's entire
|
|
227
|
+
* justification is "a bare lowercase word is usually a component
|
|
228
|
+
* variant/size/state PROP VALUE" — a JSX TEXT NODE can never be a
|
|
229
|
+
* prop value by construction (children and props are different
|
|
230
|
+
* syntactic positions), so the justification does not transfer, and
|
|
231
|
+
* applying it anyway would risk silently dropping exactly the short,
|
|
232
|
+
* literal words real UI badges/labels use as visible copy (`"new"`,
|
|
233
|
+
* `"beta"`, `"live"`).
|
|
234
|
+
* - Entity decoding (`decodeJsxEntities`): the common named entities
|
|
235
|
+
* (`&`, `<`, `>`, `"`, `'`, ` `, plus a
|
|
236
|
+
* small set of typographic entities real copy actually uses —
|
|
237
|
+
* `—`, `–`, `…`, `©`, `®`, `™`,
|
|
238
|
+
* curly quotes) and every numeric entity (`—`, `—`) are
|
|
239
|
+
* decoded before matching/reporting. DECODED, not "classified as
|
|
240
|
+
* unchecked": unlike a genuinely ambiguous JSX construct (see below),
|
|
241
|
+
* a named entity is a closed, enumerable vocabulary — there is no
|
|
242
|
+
* parsing risk in getting it right, only a coverage question, so this
|
|
243
|
+
* mirrors the file's own existing precedent for escape sequences (see
|
|
244
|
+
* "Full escape-sequence decoding" below): decode the common,
|
|
245
|
+
* essentially-total-coverage-in-practice set, and for a named entity
|
|
246
|
+
* outside that set, leave it verbatim (undecoded, `&whatever;`
|
|
247
|
+
* literally in the candidate's text) rather than either guessing or
|
|
248
|
+
* inventing a whole HTML entity table — an accepted, narrow
|
|
249
|
+
* simplification, not a silent one (documented here, exactly as the
|
|
250
|
+
* escape-sequence one is).
|
|
251
|
+
*
|
|
252
|
+
* WHAT THIS STILL DELIBERATELY DOES NOT CATCH, and why it is reported via
|
|
253
|
+
* `ScanResult.unchecked` instead of silently producing nothing:
|
|
254
|
+
*
|
|
255
|
+
* - Malformed/unbalanced JSX this scanner cannot resolve once it has
|
|
256
|
+
* already committed to "this is real JSX" — an opening tag with no
|
|
257
|
+
* matching closing tag before end of file, an attribute's quoted
|
|
258
|
+
* value or `{expression}` that never terminates, an expression child
|
|
259
|
+
* whose braces never balance, or JSX nesting deep enough to trip the
|
|
260
|
+
* `MAX_JSX_DEPTH` safety bound. Each of these is recorded as one
|
|
261
|
+
* `ScanResult.unchecked` entry (`kind` names the specific situation,
|
|
262
|
+
* `detail` is a human-readable explanation) rather than silently
|
|
263
|
+
* contributing zero candidates for that construct — the same
|
|
264
|
+
* "never silently narrow coverage" discipline `ParseFailure` already
|
|
265
|
+
* holds string/template literal tokenizing to, extended to the one
|
|
266
|
+
* new category (JSX) this version adds. `checkCopyTraceability` and
|
|
267
|
+
* `writer-check` (`cli.ts`) both surface a non-empty `unchecked` list —
|
|
268
|
+
* see their own doc comments for exactly where it lands in each
|
|
269
|
+
* one's contract.
|
|
270
|
+
* - A fully general regex-vs-division disambiguation for `/`. This
|
|
271
|
+
* scanner uses the same last-significant-token heuristic every
|
|
272
|
+
* hand-rolled JS lexer uses (see `isRegexAllowedHere`), which is wrong
|
|
273
|
+
* on rare, contrived inputs (e.g. `/` immediately after a `)` that
|
|
274
|
+
* closed an `if` condition, when a regex was actually intended). If
|
|
275
|
+
* that heuristic ever desyncs the scanner badly enough that quote
|
|
276
|
+
* balance goes wrong by end of file, the file is reported as a PARSE
|
|
277
|
+
* FAILURE (see `ParseFailure`) and contributes zero candidates —
|
|
278
|
+
* never silently mis-scanned.
|
|
279
|
+
* - Full escape-sequence decoding (`\u{1F600}`, `\x41`, ...). Only the
|
|
280
|
+
* common cases (`\\`, `\'`, `\"`, `` \` ``, `\n`, `\r`, `\t`, `\$`) are
|
|
281
|
+
* unescaped for the `normalized`/`raw` text used in reporting and
|
|
282
|
+
* matching; anything else has its leading backslash stripped and the
|
|
283
|
+
* rest passed through verbatim. Reporting/matching text may therefore
|
|
284
|
+
* be very slightly off for a literal using an exotic escape — an
|
|
285
|
+
* accepted, narrow simplification, not a silent one (documented here).
|
|
286
|
+
*/
|
|
287
|
+
import { type ExcludedPath, type PathExclusion, type PathExclusionFinding } from "./path-exclusions.js";
|
|
288
|
+
export type { ExcludedPath, PathExclusion, PathExclusionFinding, PathExclusionFindingRule } from "./path-exclusions.js";
|
|
289
|
+
export interface ScanOptions {
|
|
290
|
+
/** File extensions to read, each including the leading dot. Default: `.ts`, `.tsx`, `.js`, `.jsx`. */
|
|
291
|
+
extensions?: string[];
|
|
292
|
+
/** Directory names never descended into. Default: node_modules, .git, dist, build, coverage. */
|
|
293
|
+
skipDirs?: string[];
|
|
294
|
+
/**
|
|
295
|
+
* Files to skip entirely — never tokenized, never contribute a
|
|
296
|
+
* `CopyCandidate`/`ExcludedLiteral`/`Citation`/`UncheckedItem`. A DIFFERENT
|
|
297
|
+
* feature from `ExclusionReason` — see `path-exclusions.ts`'s top doc
|
|
298
|
+
* comment for the full "mention vs. use" case this exists for and exactly
|
|
299
|
+
* why it is not a tenth `ExclusionReason`. Defaults to `[]` (no
|
|
300
|
+
* exclusions — identical to every prior release of this package).
|
|
301
|
+
*/
|
|
302
|
+
pathExclusions?: PathExclusion[];
|
|
303
|
+
}
|
|
304
|
+
export interface CopyCandidate {
|
|
305
|
+
/** Repo-relative, `/`-joined path this candidate was found in. */
|
|
306
|
+
file: string;
|
|
307
|
+
/** 1-indexed line the literal/text node starts on. */
|
|
308
|
+
line: number;
|
|
309
|
+
/** `"jsx-text"` is a raw JSX text node (`<span>Hello</span>`'s `Hello`) — see this file's top doc comment, "JSX TEXT NODES". Unlike `"string"`/`"template"`, it has no quote/backtick delimiters at all. */
|
|
310
|
+
kind: "string" | "template" | "jsx-text";
|
|
311
|
+
/**
|
|
312
|
+
* Original source text, trimmed for display. For `"string"`/`"template"`,
|
|
313
|
+
* includes the literal's own quotes/backticks. For `"jsx-text"`, there
|
|
314
|
+
* are no delimiters to include — this is the JSX-whitespace-collapsed,
|
|
315
|
+
* entity-decoded text itself (see `collapseJsxWhitespace`/
|
|
316
|
+
* `decodeJsxEntities`).
|
|
317
|
+
*/
|
|
318
|
+
raw: string;
|
|
319
|
+
/**
|
|
320
|
+
* The literal's STATIC text with every interpolation (`${...}`)
|
|
321
|
+
* collapsed to `PLACEHOLDER_SENTINEL` — identical to `raw`'s unescaped
|
|
322
|
+
* content for a plain string (zero interpolations). This is what
|
|
323
|
+
* `copy-gate.ts` compares against a `CopyEntry.text` normalized the same
|
|
324
|
+
* way (its own `{name}` placeholders collapsed to the same sentinel) —
|
|
325
|
+
* comparing SHAPE, never a specific placeholder name, since this
|
|
326
|
+
* scanner has no way to know what name a `CopyEntry` author chose for
|
|
327
|
+
* an interpolation this source expression computes some other way.
|
|
328
|
+
*/
|
|
329
|
+
normalized: string;
|
|
330
|
+
/** Number of `${...}` interpolations found (0 for a plain string). */
|
|
331
|
+
placeholderCount: number;
|
|
332
|
+
/**
|
|
333
|
+
* Every `copy:<id>` citation found on this candidate's own source line
|
|
334
|
+
* (see `CITATION_RE`) — `copy-gate.ts`'s escape hatch, mirroring
|
|
335
|
+
* `@example/strategy`'s `fact:<key>`. Whether any of these ids
|
|
336
|
+
* actually exist in a real `CopyRecord` is `copy-gate.ts`'s question to
|
|
337
|
+
* ask, not this scanner's — this scanner only reports what text is
|
|
338
|
+
* literally present on the line.
|
|
339
|
+
*/
|
|
340
|
+
citedIds: string[];
|
|
341
|
+
/** Whether a `copy-gate:ignore` marker (see `IGNORE_MARKER_RE`) is present on this candidate's own source line. */
|
|
342
|
+
hasIgnoreMarker: boolean;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* One `copy:<id>` citation found anywhere in a file, independent of
|
|
346
|
+
* whether any candidate happens to share its line — mirrors
|
|
347
|
+
* `@example/strategy`'s `facts-gate.ts` Pass 1, which checks every
|
|
348
|
+
* line for a `fact:<key>` citation regardless of whether that line also
|
|
349
|
+
* carries a claim, so a citation to a rotted/misspelled id is caught even
|
|
350
|
+
* when it sits on a line with no traceable literal at all (e.g. a stray
|
|
351
|
+
* citation left behind after the copy it once annotated was deleted).
|
|
352
|
+
*/
|
|
353
|
+
export interface Citation {
|
|
354
|
+
file: string;
|
|
355
|
+
line: number;
|
|
356
|
+
id: string;
|
|
357
|
+
}
|
|
358
|
+
export type ExclusionReason = "import-or-require-specifier" | "object-or-destructuring-key" | "type-or-interface-context" | "aria-or-data-attribute-value" | "denylisted-attribute-or-prop-value" | "classname-builder-argument" | "developer-diagnostic-argument" | "no-letters" | "enum-or-token-shaped";
|
|
359
|
+
export interface ExcludedLiteral {
|
|
360
|
+
file: string;
|
|
361
|
+
line: number;
|
|
362
|
+
/** The literal's raw source text (quotes included), for a human auditing the exclusion list. */
|
|
363
|
+
raw: string;
|
|
364
|
+
reason: ExclusionReason;
|
|
365
|
+
}
|
|
366
|
+
export interface ParseFailure {
|
|
367
|
+
file: string;
|
|
368
|
+
/** Human-readable detail — an unterminated string/template/comment/regex, or the scanner ran off the end of the file in an inconsistent state. */
|
|
369
|
+
detail: string;
|
|
370
|
+
}
|
|
371
|
+
export interface SkippedFile {
|
|
372
|
+
file: string;
|
|
373
|
+
reason: "test-or-check-file";
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* One construct this scanner recognized as JSX-shaped but could not
|
|
377
|
+
* reliably classify — an unclosed element, an unterminated attribute
|
|
378
|
+
* value or expression, a nested expression child whose braces never
|
|
379
|
+
* balanced, or JSX nesting deep enough to trip the depth safety bound.
|
|
380
|
+
* See this file's top doc comment, "WHAT THIS STILL DELIBERATELY DOES
|
|
381
|
+
* NOT CATCH". NEVER how a genuine blind spot gets to stay invisible: a
|
|
382
|
+
* consumer that only checks `candidates`/`excluded` and ignores this
|
|
383
|
+
* field would be exactly the failure mode issue #37 was about — a scan
|
|
384
|
+
* that narrowed its own coverage and reported the narrowed result as a
|
|
385
|
+
* pass. `checkCopyTraceability` (`copy-gate.ts`) and `writer-check`
|
|
386
|
+
* (`cli.ts`) both refuse to let a non-empty list here go unreported.
|
|
387
|
+
*/
|
|
388
|
+
export interface UncheckedItem {
|
|
389
|
+
file: string;
|
|
390
|
+
line: number;
|
|
391
|
+
/** A short, stable machine-readable name for the situation, e.g. `"unclosed-jsx-element"`, `"malformed-jsx-expression"`, `"malformed-jsx-tag"`, `"unrecognized-jsx-child"`, `"jsx-depth-exceeded"`. */
|
|
392
|
+
kind: string;
|
|
393
|
+
/** Human-readable explanation, for a report or a human auditing the list. */
|
|
394
|
+
detail: string;
|
|
395
|
+
}
|
|
396
|
+
export interface ScanResult {
|
|
397
|
+
/** Files whose content was successfully tokenized (whether or not it produced any candidates). This is the number `cli.ts` requires to be > 0 for a clean pass — see copy-gate.ts / cli.ts's exit-code discipline. */
|
|
398
|
+
filesScanned: number;
|
|
399
|
+
candidates: CopyCandidate[];
|
|
400
|
+
/** Every literal this scanner found and deliberately excluded, with why — never silently dropped. See exclusions #1-#9 above. */
|
|
401
|
+
excluded: ExcludedLiteral[];
|
|
402
|
+
/** Every `copy:<id>` citation found anywhere in a scanned file — see `Citation`'s own doc comment for why this is file-wide, not just per-candidate. */
|
|
403
|
+
citations: Citation[];
|
|
404
|
+
/** Files matched by extension/walk but never tokenized at all — test/check/declaration files, by design (exclusion #10). Counted, never silent. */
|
|
405
|
+
skippedByDesign: SkippedFile[];
|
|
406
|
+
/** Files that were read but could not be reliably tokenized (unbalanced quote/comment/regex state at EOF) — contribute zero candidates. Counted, never silently treated as "0 findings". */
|
|
407
|
+
parseFailures: ParseFailure[];
|
|
408
|
+
/** Constructs the scanner recognized as JSX-shaped but could not classify — see `UncheckedItem`. Empty is the ordinary, expected outcome for a well-formed file; it is never emptied to make a report look cleaner than it is. */
|
|
409
|
+
unchecked: UncheckedItem[];
|
|
410
|
+
/** Files matched by extension/walk but skipped entirely because a caller-supplied `ScanOptions.pathExclusions` entry matched their path — see `path-exclusions.ts`. Counted, never silent, and DIFFERENT from `skippedByDesign` (this package's own built-in test/check-file skip) and from `excluded` (a per-LITERAL classification inside a file that WAS scanned). */
|
|
411
|
+
excludedFiles: ExcludedPath[];
|
|
412
|
+
/** Problems with `ScanOptions.pathExclusions` itself, not with any scanned file's content — a malformed entry (`"error"`, never applied) or an entry that matched zero files this run (`"warning"`, stale but still applied as configured). See `path-exclusions.ts`'s top doc comment for the fail-closed contract. Empty is the ordinary outcome when `pathExclusions` is omitted or every entry is well-formed and matched something. */
|
|
413
|
+
pathExclusionFindings: PathExclusionFinding[];
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Walks `root` recursively, reads every matching file, and extracts copy
|
|
417
|
+
* candidates from each (see `extractCopyCandidates`). FAILS CLOSED on an
|
|
418
|
+
* unreadable directory or file — throws a plain `Error`, matching
|
|
419
|
+
* `@example/strategy`'s `scanStrategyDirectory` and this
|
|
420
|
+
* repository's own `scripts/check-contamination-classes.mjs` walker: a
|
|
421
|
+
* directory this function could not read might be hiding an unknown,
|
|
422
|
+
* unbounded amount of unregistered copy, and reporting "0 files scanned"
|
|
423
|
+
* for that case would read as a clean pass when nothing was actually
|
|
424
|
+
* verified. `cli.ts` is what turns this thrown error into exit code 2.
|
|
425
|
+
*/
|
|
426
|
+
export declare function scanCopySourceTree(root: string, options?: ScanOptions): ScanResult;
|
|
427
|
+
/**
|
|
428
|
+
* Collapses every interpolation in a template literal's static text to one
|
|
429
|
+
* comparable shape. `\uE000` is the first code point of the Unicode
|
|
430
|
+
* Private Use Area — reserved by the standard for exactly this kind of
|
|
431
|
+
* private, non-textual sentinel use, and something no legitimate JS/TS
|
|
432
|
+
* source file authoring real UI copy will ever contain, so it can be
|
|
433
|
+
* dropped in without risk of colliding with genuine copy text (unlike,
|
|
434
|
+
* say, a literal space or `{}`, which real copy could plausibly contain).
|
|
435
|
+
* Exported so `copy-gate.ts` normalizes `CopyEntry.text`'s `{name}`
|
|
436
|
+
* placeholders to this exact same sentinel before comparing shapes.
|
|
437
|
+
*/
|
|
438
|
+
export declare const PLACEHOLDER_SENTINEL = "\uE000";
|
|
439
|
+
interface ExtractResult {
|
|
440
|
+
candidates: CopyCandidate[];
|
|
441
|
+
excluded: ExcludedLiteral[];
|
|
442
|
+
citations: Citation[];
|
|
443
|
+
/** See `ScanResult.unchecked` — this is the per-file slice `scanCopySourceTree` aggregates into it. */
|
|
444
|
+
unchecked: UncheckedItem[];
|
|
445
|
+
parseFailure?: string;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Pure — no I/O. Tokenizes `content` and classifies every string/template
|
|
449
|
+
* literal AND (in a `.tsx`/`.jsx` file) every JSX text node found as
|
|
450
|
+
* either a copy candidate or an excluded literal (with a stated reason),
|
|
451
|
+
* per "THE BOUNDARY" in this file's top doc comment. Exported so a test
|
|
452
|
+
* (or a future consumer wanting extraction without a real directory
|
|
453
|
+
* walk) can exercise the classification logic directly against a
|
|
454
|
+
* source-text fixture.
|
|
455
|
+
*/
|
|
456
|
+
export declare function extractCopyCandidates(content: string, filePath: string): ExtractResult;
|
|
457
|
+
//# sourceMappingURL=scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
|