@lariocpt/flawless-react 0.0.0-stage → 0.3.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.
Files changed (143) hide show
  1. package/.agents/skills/flawless-a11y/SKILL.md +113 -0
  2. package/.agents/skills/flawless-api/SKILL.md +159 -0
  3. package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
  4. package/.agents/skills/flawless-e2e/SKILL.md +155 -0
  5. package/.agents/skills/flawless-expo/SKILL.md +283 -0
  6. package/.agents/skills/flawless-findings/SKILL.md +116 -0
  7. package/.agents/skills/flawless-load/SKILL.md +112 -0
  8. package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
  9. package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
  10. package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
  11. package/.agents/skills/flawless-react/SKILL.md +403 -0
  12. package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
  13. package/.agents/skills/flawless-regen/SKILL.md +135 -0
  14. package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
  15. package/LICENSE +21 -0
  16. package/README.md +608 -2
  17. package/bin/flawless-react +4 -0
  18. package/package.json +67 -4
  19. package/scripts/postinstall.d.mts +46 -0
  20. package/scripts/postinstall.mjs +127 -0
  21. package/src/analyse/authShims.ts +220 -0
  22. package/src/analyse/bootability.ts +257 -0
  23. package/src/analyse/childBridge.ts +302 -0
  24. package/src/analyse/childrenIntoSlot.ts +66 -0
  25. package/src/analyse/compoundFamily.ts +100 -0
  26. package/src/analyse/dataExports.ts +200 -0
  27. package/src/analyse/extractChildAttrs.ts +112 -0
  28. package/src/analyse/extractDataDeps.ts +857 -0
  29. package/src/analyse/extractDisclosures.ts +186 -0
  30. package/src/analyse/extractFetchCalls.ts +81 -0
  31. package/src/analyse/extractFlows.ts +239 -0
  32. package/src/analyse/extractGuards.ts +270 -0
  33. package/src/analyse/extractHooks.ts +237 -0
  34. package/src/analyse/extractInteractions.ts +1421 -0
  35. package/src/analyse/extractLogging.ts +45 -0
  36. package/src/analyse/extractNativeA11y.ts +138 -0
  37. package/src/analyse/extractPredicates.ts +784 -0
  38. package/src/analyse/extractPropUsage.ts +162 -0
  39. package/src/analyse/extractProps.ts +1212 -0
  40. package/src/analyse/extractRenderMode.ts +165 -0
  41. package/src/analyse/extractRootTag.ts +117 -0
  42. package/src/analyse/extractState.ts +361 -0
  43. package/src/analyse/extractStaticText.ts +212 -0
  44. package/src/analyse/extractVariants.ts +186 -0
  45. package/src/analyse/harvestApiEndpoints.ts +284 -0
  46. package/src/analyse/hostProps.ts +185 -0
  47. package/src/analyse/hostTag.ts +72 -0
  48. package/src/analyse/htmlContentModel.ts +94 -0
  49. package/src/analyse/index.ts +744 -0
  50. package/src/analyse/mountPathCalls.ts +442 -0
  51. package/src/analyse/nativeContentModel.ts +129 -0
  52. package/src/analyse/nativeDialect.ts +83 -0
  53. package/src/analyse/nativeProps.ts +339 -0
  54. package/src/analyse/ownReturns.ts +31 -0
  55. package/src/analyse/parseFile.ts +919 -0
  56. package/src/analyse/pos.ts +8 -0
  57. package/src/analyse/providerWraps.ts +1977 -0
  58. package/src/analyse/pureModule.ts +88 -0
  59. package/src/analyse/reexportTable.ts +148 -0
  60. package/src/analyse/refs.ts +195 -0
  61. package/src/analyse/renderPath.ts +441 -0
  62. package/src/analyse/resolveDepReturns.ts +370 -0
  63. package/src/analyse/resolveIndexKeys.ts +98 -0
  64. package/src/analyse/resolveModule.ts +431 -0
  65. package/src/analyse/resolveTagRootTags.ts +103 -0
  66. package/src/analyse/serverActionReturns.ts +213 -0
  67. package/src/analyse/sourceCache.ts +263 -0
  68. package/src/analyse/spreadSiblings.ts +98 -0
  69. package/src/analyse/walkChildren.ts +98 -0
  70. package/src/analyse/zodSchema.ts +424 -0
  71. package/src/bin.ts +23 -0
  72. package/src/changed.ts +221 -0
  73. package/src/cli.ts +560 -0
  74. package/src/cliFlags.ts +281 -0
  75. package/src/core.ts +1229 -0
  76. package/src/docLine.ts +122 -0
  77. package/src/emitPaths.ts +51 -0
  78. package/src/expo/detect.ts +184 -0
  79. package/src/expo/refusal.ts +62 -0
  80. package/src/expo/routes.ts +145 -0
  81. package/src/index.ts +95 -0
  82. package/src/mcp/analysisTools.ts +720 -0
  83. package/src/mcp/infoTools.ts +135 -0
  84. package/src/mcp/protocol.ts +130 -0
  85. package/src/mcp/run.ts +269 -0
  86. package/src/mcp/runTools.ts +362 -0
  87. package/src/mcp/server.ts +279 -0
  88. package/src/mcp/spec.ts +86 -0
  89. package/src/mcp/support.ts +268 -0
  90. package/src/mcp/tools.ts +75 -0
  91. package/src/nextjs/apiRoutes.ts +116 -0
  92. package/src/nextjs/detect.ts +397 -0
  93. package/src/nextjs/emit.ts +106 -0
  94. package/src/nextjs/liveSpec.ts +79 -0
  95. package/src/nextjs/mswHandlers.ts +55 -0
  96. package/src/nextjs/providerWrapper.ts +603 -0
  97. package/src/nextjs/routes.ts +86 -0
  98. package/src/nextjs/swagger.ts +32 -0
  99. package/src/packageJson.ts +414 -0
  100. package/src/platform.ts +18 -0
  101. package/src/prompt.ts +132 -0
  102. package/src/reactCompiler.ts +165 -0
  103. package/src/reporters/findings.ts +156 -0
  104. package/src/reporters/report.ts +463 -0
  105. package/src/runner/runTests.ts +402 -0
  106. package/src/scaffold/adoptStyle.ts +181 -0
  107. package/src/scaffold/index.ts +253 -0
  108. package/src/scaffold/openapi.ts +183 -0
  109. package/src/scaffold/templates.ts +2379 -0
  110. package/src/scaffold-tests/index.ts +463 -0
  111. package/src/scaffold-tests/mockValues.ts +208 -0
  112. package/src/scaffold-tests/mocks.ts +794 -0
  113. package/src/scaffold-tests/nativeIndex.ts +52 -0
  114. package/src/scaffold-tests/nativeTemplates.ts +1528 -0
  115. package/src/scaffold-tests/postDispatch.ts +152 -0
  116. package/src/scaffold-tests/registry.ts +443 -0
  117. package/src/scaffold-tests/selectors.ts +493 -0
  118. package/src/scaffold-tests/selectorsNative.ts +273 -0
  119. package/src/scaffold-tests/stamp.ts +79 -0
  120. package/src/scaffold-tests/templates.ts +3578 -0
  121. package/src/skills/cli.ts +248 -0
  122. package/src/skills/compat.ts +166 -0
  123. package/src/skills/data.ts +192 -0
  124. package/src/skills/md.d.ts +7 -0
  125. package/src/skills/menu.ts +55 -0
  126. package/src/skills/spec.ts +227 -0
  127. package/src/suiteScripts.ts +110 -0
  128. package/src/tasks/a11yNative.ts +281 -0
  129. package/src/tasks/a11yScan.ts +67 -0
  130. package/src/tasks/biomeRunner.ts +301 -0
  131. package/src/tasks/biomeSweep.ts +166 -0
  132. package/src/tasks/e2eExpo.ts +436 -0
  133. package/src/tasks/e2eRoutes.ts +756 -0
  134. package/src/tasks/eslintReactRules.ts +67 -0
  135. package/src/tasks/index.ts +24 -0
  136. package/src/tasks/loadExpo.ts +273 -0
  137. package/src/tasks/loadPipes.ts +750 -0
  138. package/src/tasks/perfHarness.ts +145 -0
  139. package/src/tasks/staticPerfAnalysis.ts +510 -0
  140. package/src/tasks/types.ts +1 -0
  141. package/src/tasks/unitAndBrowser.ts +462 -0
  142. package/src/types.ts +1419 -0
  143. package/src/version.ts +12 -0
@@ -0,0 +1,273 @@
1
+ import { TEXTLESS_ELEMENTS } from "../analyse/nativeProps.ts";
2
+ import type { ClickHandler, LabelSource } from "../types.ts";
3
+ import { propNameRegex } from "./selectors.ts";
4
+
5
+ /**
6
+ * @testing-library/react-native locators, as a priority ladder.
7
+ *
8
+ * A separate file rather than a `flavour` parameter on selectors.ts. Every rung
9
+ * of the DOM ladder is DOM-shaped — TAG_TO_ROLE, `#id`, container.querySelector,
10
+ * SUBMIT_WITHIN_FORM, the lowercase isHostTag, awaitSelector's `as HTMLElement`
11
+ * string surgery — and threading a flag through fourteen exported functions
12
+ * would leave every one of those branches reachable from a native call site by
13
+ * accident. The ladder STRUCTURE transfers; the expressions do not.
14
+ *
15
+ * `undefined` is load-bearing here exactly as it is there: it makes the caller
16
+ * emit a test.skip that NAMES the gap, rather than a locator that matches
17
+ * nothing and passes.
18
+ */
19
+
20
+ /** Rung the target was found by — the emitter needs it to pick the dispatch. */
21
+ export type NativeRung =
22
+ | "testID"
23
+ | "label"
24
+ | "role"
25
+ | "placeholder"
26
+ | "displayValue"
27
+ | "text"
28
+ | "text-partial";
29
+
30
+ export interface NativeTarget {
31
+ /** An RNTL expression resolving to the element. */
32
+ locator: string;
33
+ rung: NativeRung;
34
+ /**
35
+ * The located element IS the pressable itself (a rung read off its own props),
36
+ * rather than a descendant `<Text>` that merely sits inside it.
37
+ *
38
+ * Decides the dispatch: RNTL's userEvent.press requires a pressable host
39
+ * element and rejects a Text that is not one, while fireEvent.press walks UP
40
+ * to the nearest press responder — which is exactly what a text rung hands it.
41
+ * So the rung the target came from is the evidence for the choice, rather than
42
+ * a preference.
43
+ */
44
+ self: boolean;
45
+ }
46
+
47
+ const j = (v: string): string => JSON.stringify(v);
48
+
49
+ /**
50
+ * A regex matching a fragment of an element's text, anchored at neither end.
51
+ *
52
+ * RNTL's getByText matches the element's WHOLE composed text content, so a
53
+ * partial label — `<Pressable><Icon/><Text>Add</Text></Pressable>`, where the
54
+ * icon contributes nothing knowable — can only be matched as a substring.
55
+ */
56
+ function partialRegex(value: string): string {
57
+ // The class includes `/`, which the DOM twin (selectors.ts) has had all along
58
+ // and this copy did not. An unescaped `/` CLOSES the regex literal, so copy
59
+ // like "24/7" or "and/or" emitted `screen.getAllByText(/Save 50/50 split/)` —
60
+ // a syntax error that takes the WHOLE SPEC FILE out of the run, not just that
61
+ // one assertion.
62
+ return `/${value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}/`;
63
+ }
64
+
65
+ /**
66
+ * The ladder. Returns undefined when nothing in the source identifies the
67
+ * element — see describeUnlocatableNative for what the caller says then.
68
+ *
69
+ * There is NO last-resort rung, and that is a decision rather than an omission.
70
+ * The DOM ladder bottoms out at `container.querySelector("button")` when the tag
71
+ * is unambiguous; the two candidates here are both unsound:
72
+ *
73
+ * - `UNSAFE_getByType(Pressable)` — the prefix is the library warning you it is
74
+ * coupled to implementation, and under NativeWind's `jsxImportSource` the
75
+ * Pressable the SOURCE rendered went through cssInterop's wrapper while the
76
+ * one a spec imports from "react-native" is the bare component. Different
77
+ * component references, so it matches zero elements, and fireEvent.press on
78
+ * nothing is a green test that touched nothing.
79
+ * - "the first pressable in the tree" — wrong for the same reason the DOM
80
+ * ladder refuses a bare-tag locator on an ambiguous tag, and wrong more
81
+ * often, because every React Native screen is a stack of Pressables.
82
+ */
83
+ export function emitNativeSelector(h: ClickHandler): NativeTarget | undefined {
84
+ if (h.hints.testid) {
85
+ return { locator: `screen.getByTestId(${j(h.hints.testid)})`, rung: "testID", self: true };
86
+ }
87
+ if (h.hints.ariaLabel) {
88
+ return {
89
+ locator: `screen.getAllByLabelText(${j(h.hints.ariaLabel)})[0]`,
90
+ rung: "label",
91
+ self: true,
92
+ };
93
+ }
94
+ // Only an EXPLICIT accessibilityRole. RNTL's getRole() reads that prop alone —
95
+ // there is no Pressable -> button mapping — so inferring one from the tag would
96
+ // emit a query that matches nothing while reporting coverage.
97
+ if (h.hints.role) {
98
+ const name = nameOption(h.label);
99
+ return {
100
+ locator: `screen.getAllByRole(${j(h.hints.role)}${name})[0]`,
101
+ rung: "role",
102
+ self: true,
103
+ };
104
+ }
105
+ // Above text, deliberately: a placeholder names THE ELEMENT, where text names a
106
+ // child. On the dogfood app that ordering is the difference between 25
107
+ // addressable TextInputs and none.
108
+ if (h.hints.placeholder) {
109
+ return {
110
+ locator: `screen.getAllByPlaceholderText(${j(h.hints.placeholder)})[0]`,
111
+ rung: "placeholder",
112
+ self: true,
113
+ };
114
+ }
115
+ if (h.hints.displayValue) {
116
+ return {
117
+ locator: `screen.getAllByDisplayValue(${j(h.hints.displayValue)})[0]`,
118
+ rung: "displayValue",
119
+ self: true,
120
+ };
121
+ }
122
+ // RN's <Button title="Save"/> renders no children, so no text rung can see it;
123
+ // its role and its name both come off its props.
124
+ if (h.hints.buttonTitle) {
125
+ return {
126
+ locator: `screen.getAllByText(${j(h.hints.buttonTitle)})[0]`,
127
+ rung: "text",
128
+ self: true,
129
+ };
130
+ }
131
+ if (h.label.kind === "text") {
132
+ return { locator: `screen.getAllByText(${j(h.label.value)})[0]`, rung: "text", self: false };
133
+ }
134
+ if (h.label.kind === "text-partial") {
135
+ return {
136
+ locator: `screen.getAllByText(${partialRegex(h.label.value)})[0]`,
137
+ rung: "text-partial",
138
+ self: false,
139
+ };
140
+ }
141
+ if (h.label.kind === "prop") {
142
+ if (h.label.before || h.label.after) {
143
+ return {
144
+ locator: `screen.getAllByText(${propNameRegex(h.label)})[0]`,
145
+ rung: "text",
146
+ self: false,
147
+ };
148
+ }
149
+ return {
150
+ locator: `screen.getAllByText(String(DEFAULTS[${j(h.label.propName)}] ?? ""))[0]`,
151
+ rung: "text",
152
+ self: false,
153
+ };
154
+ }
155
+ return undefined;
156
+ }
157
+
158
+ function nameOption(label: LabelSource): string {
159
+ if (label.kind === "text") return `, { name: ${j(label.value)} }`;
160
+ if (label.kind === "text-partial") return `, { name: ${partialRegex(label.value)} }`;
161
+ if (label.kind === "prop") {
162
+ if (label.before || label.after) return `, { name: ${propNameRegex(label, "i")} }`;
163
+ return `, { name: String(DEFAULTS[${j(label.propName)}] ?? "") }`;
164
+ }
165
+ return "";
166
+ }
167
+
168
+ /**
169
+ * Why a handler could not be addressed, in words the emitted skip prints.
170
+ *
171
+ * Three wordings, because unlike the DOM there is no "give it text" escape for an
172
+ * icon button — and because this string is also the remediation the native a11y
173
+ * task will report on the same element. One accessibilityLabel retires both.
174
+ */
175
+ export function describeUnlocatableNative(h: ClickHandler): string {
176
+ // icon-only is checked FIRST even when the tag is also ambiguous. Both are
177
+ // true of the same element, but one accessibilityLabel fixes both — it names
178
+ // the control for a screen reader AND distinguishes it from its siblings — so
179
+ // the message that leads to that fix is the more useful one.
180
+ if (h.iconOnly) {
181
+ return (
182
+ `<${h.tag}> renders only an icon, so no text names it — add an accessibilityLabel, ` +
183
+ `which makes it announceable as well as addressable`
184
+ );
185
+ }
186
+ if (h.ambiguousTag) {
187
+ return (
188
+ `more than one <${h.tag}> renders here and nothing distinguishes this one — ` +
189
+ `add a testID, or an accessibilityLabel (which also names it for a screen reader)`
190
+ );
191
+ }
192
+ return (
193
+ `nothing on the <${h.tag}> identifies it: no testID, no accessibilityLabel, ` +
194
+ `no accessibilityRole, and no literal text under it`
195
+ );
196
+ }
197
+
198
+ /**
199
+ * How to drive an event on a located element.
200
+ *
201
+ * A CLOSED switch with no permissive default, unlike the DOM emitter's
202
+ * `default: userEvent.click(...)`. A click on a React Native tree is nonsense
203
+ * that would pass silently, so an event with no honest dispatch returns
204
+ * undefined and the caller skips instead.
205
+ *
206
+ * `useUserEvent` is false for route specs: renderRouter calls jest.useFakeTimers()
207
+ * internally, and userEvent's press timing under fake timers is not something
208
+ * this emitter can vouch for. fireEvent has no timers in it.
209
+ */
210
+ export function emitNativeDispatch(
211
+ event: string,
212
+ target: NativeTarget,
213
+ el: string,
214
+ useUserEvent: boolean,
215
+ ): string | undefined {
216
+ const press = (kind: "press" | "longPress"): string =>
217
+ useUserEvent && target.self ? `await user.${kind}(${el})` : `fireEvent(${el}, ${j(kind)})`;
218
+ switch (event) {
219
+ case "onPress":
220
+ return press("press");
221
+ case "onLongPress":
222
+ return press("longPress");
223
+ case "onPressIn":
224
+ return `fireEvent(${el}, "pressIn")`;
225
+ case "onPressOut":
226
+ return `fireEvent(${el}, "pressOut")`;
227
+ case "onChangeText":
228
+ return `fireEvent.changeText(${el}, SAMPLE_TEXT)`;
229
+ case "onChange":
230
+ // RN's TextInput onChange payload is a DOCUMENTED shape, so this is a
231
+ // specification rather than an invented event object.
232
+ return `fireEvent(${el}, "change", { nativeEvent: { text: SAMPLE_TEXT } })`;
233
+ case "onSubmitEditing":
234
+ return `fireEvent(${el}, "submitEditing")`;
235
+ case "onEndEditing":
236
+ return `fireEvent(${el}, "endEditing")`;
237
+ case "onValueChange":
238
+ return `fireEvent(${el}, "valueChange", true)`;
239
+ case "onFocus":
240
+ return `fireEvent(${el}, "focus")`;
241
+ case "onBlur":
242
+ return `fireEvent(${el}, "blur")`;
243
+ default:
244
+ // onRefresh and onScroll land here: neither is reachable by any rung of the
245
+ // ladder above, so there is no element to drive them on.
246
+ return undefined;
247
+ }
248
+ }
249
+
250
+ /** Elements that render no text, re-exported so the emitter can explain a skip. */
251
+ export { TEXTLESS_ELEMENTS };
252
+
253
+ /**
254
+ * The async form of a locator: `getAllBy*` -> `findAllBy*`.
255
+ *
256
+ * A screen that loads in a focus effect renders its empty state first and its
257
+ * rows only after the awaited fetch resolves. Querying synchronously at mount
258
+ * then fails with "Unable to find an element with text: …" — which reads as a
259
+ * missing element and is really a missing await. Three of the dogfood app's
260
+ * screens failed exactly this way.
261
+ *
262
+ * Applied only where the component actually awaits something, so a screen with
263
+ * no data loading keeps the synchronous query and its faster failure.
264
+ */
265
+ export function awaitNativeSelector(locator: string): string {
266
+ // The parentheses are the whole point. `await screen.findAllByText(x)[0]`
267
+ // indexes the PROMISE, not the resolved array, so el is undefined and every
268
+ // assertion after it fails — 30 tests, from a rewrite that looked right.
269
+ const async = locator.replace(/screen\.get(All)?By/, (_m, all) => `screen.find${all ?? ""}By`);
270
+ const suffix = /\)(\[\d+\])$/.exec(async)?.[1];
271
+ if (suffix !== undefined) return `(await ${async.slice(0, -suffix.length)})${suffix}`;
272
+ return `await ${async}`;
273
+ }
@@ -0,0 +1,79 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { VERSION } from "../version.ts";
5
+
6
+ /**
7
+ * Provenance stamps for colocated specs — the files written beside the user's
8
+ * sources, where hand-hardening is the whole point. The first line records a
9
+ * hash of everything BELOW it, so `--force` can tell "still exactly what we
10
+ * generated" (safe to rewrite) from "someone worked on this" (never clobber).
11
+ *
12
+ * The hash excludes the stamp line for the obvious fixed-point reason, and is
13
+ * recomputed after the host project's formatter runs (restampGenerated below):
14
+ * a stamp of the raw render would mismatch the formatted bytes on disk, and
15
+ * every untouched spec would read as hand-edited on the next --force.
16
+ *
17
+ * Deleting the stamp line demotes a file to "pre-stamp": kept by --force and
18
+ * reported separately. That is deliberate — the escape hatch for both states
19
+ * is the same: delete the file to regenerate it.
20
+ */
21
+
22
+ /** First line only; the terminator tolerates files converted to CRLF. */
23
+ export const STAMP_RE = /^\/\/ @flawless-generated sha256:([0-9a-f]{16}) flawless-react@(\S+)\r?\n/;
24
+
25
+ /** First 16 sha256 hex chars — collision-proof enough for "did a human touch this". */
26
+ export function hashBody(body: string): string {
27
+ return createHash("sha256").update(body).digest("hex").slice(0, 16);
28
+ }
29
+
30
+ export function stamp(body: string, version: string = VERSION): string {
31
+ return `// @flawless-generated sha256:${hashBody(body)} flawless-react@${version}\n${body}`;
32
+ }
33
+
34
+ export interface SplitStamp {
35
+ hash?: string;
36
+ version?: string;
37
+ /** Everything below the stamp line; the whole text when there is none. */
38
+ body: string;
39
+ }
40
+
41
+ export function splitStamp(text: string): SplitStamp {
42
+ const m = text.match(STAMP_RE);
43
+ if (!m) return { body: text };
44
+ return { hash: m[1], version: m[2], body: text.slice(m[0].length) };
45
+ }
46
+
47
+ export function classifyStamped(text: string): "untouched" | "edited" | "stampless" {
48
+ const { hash, body } = splitStamp(text);
49
+ if (hash === undefined) return "stampless";
50
+ return hashBody(body) === hash ? "untouched" : "edited";
51
+ }
52
+
53
+ /**
54
+ * Re-align stamps with what is actually on disk, after a formatter may have
55
+ * rewritten the freshly written files. Only files whose FIRST line is a stamp
56
+ * are touched, so callers pass the whole written set unfiltered; the stamp's
57
+ * own version field is preserved because the restamp is bookkeeping, not a
58
+ * regeneration. Returns how many files were rewritten.
59
+ */
60
+ export async function restampGenerated(paths: string[]): Promise<number> {
61
+ let restamped = 0;
62
+ for (const p of paths) {
63
+ if (!existsSync(p)) continue;
64
+ const { hash, version, body } = splitStamp(await readFile(p, "utf8"));
65
+ if (hash === undefined || version === undefined) continue;
66
+ const fresh = hashBody(body);
67
+ if (fresh === hash) continue;
68
+ // The stamp line follows the body's own line-ending convention: a host
69
+ // formatter set to CRLF converted the whole file, and writing "\n" here
70
+ // would hand that formatter a mixed-EOL file to churn straight back.
71
+ const eol = body.includes("\r\n") ? "\r\n" : "\n";
72
+ await writeFile(
73
+ p,
74
+ `// @flawless-generated sha256:${fresh} flawless-react@${version}${eol}${body}`,
75
+ );
76
+ restamped++;
77
+ }
78
+ return restamped;
79
+ }