@variance-authority/core 0.15.0 → 0.18.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 (82) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +25 -7
  3. package/dist/attribute/component-hash.d.ts +11 -3
  4. package/dist/attribute/component-hash.js +11 -3
  5. package/dist/attribute/component-hash.js.map +1 -1
  6. package/dist/attribute/movement.d.ts +1 -1
  7. package/dist/attribute/movement.js.map +1 -1
  8. package/dist/attribute/region.d.ts +47 -4
  9. package/dist/attribute/region.js +90 -17
  10. package/dist/attribute/region.js.map +1 -1
  11. package/dist/attribute/source.d.ts +1 -1
  12. package/dist/attribute/source.js +1 -1
  13. package/dist/attribute/source.js.map +1 -1
  14. package/dist/compare/band.d.ts +8 -2
  15. package/dist/compare/band.js +7 -2
  16. package/dist/compare/band.js.map +1 -1
  17. package/dist/compare/diff/attribution.js +6 -4
  18. package/dist/compare/diff/attribution.js.map +1 -1
  19. package/dist/compare/diff/compare-nodes.js +9 -1
  20. package/dist/compare/diff/compare-nodes.js.map +1 -1
  21. package/dist/compare/diff/match.d.ts +2 -1
  22. package/dist/compare/diff/match.js +52 -0
  23. package/dist/compare/diff/match.js.map +1 -1
  24. package/dist/compare/parting.js +6 -4
  25. package/dist/compare/parting.js.map +1 -1
  26. package/dist/format/document.d.ts +26 -3
  27. package/dist/format/document.js.map +1 -1
  28. package/dist/format/index.d.ts +7 -0
  29. package/dist/format/index.js +4 -0
  30. package/dist/format/index.js.map +1 -1
  31. package/dist/format/packed.d.ts +32 -0
  32. package/dist/format/packed.js +23 -0
  33. package/dist/format/packed.js.map +1 -0
  34. package/dist/format/shape.d.ts +31 -0
  35. package/dist/format/shape.js +269 -0
  36. package/dist/format/shape.js.map +1 -0
  37. package/dist/format/stabilize.d.ts +7 -1
  38. package/dist/format/stabilize.js +10 -3
  39. package/dist/format/stabilize.js.map +1 -1
  40. package/dist/format/steps.d.ts +44 -0
  41. package/dist/format/steps.js +67 -0
  42. package/dist/format/steps.js.map +1 -0
  43. package/dist/format/width.d.ts +35 -0
  44. package/dist/format/width.js +38 -0
  45. package/dist/format/width.js.map +1 -0
  46. package/dist/judge/docket.js +1 -0
  47. package/dist/judge/docket.js.map +1 -1
  48. package/dist/judge/index.d.ts +1 -0
  49. package/dist/judge/index.js +1 -0
  50. package/dist/judge/index.js.map +1 -1
  51. package/dist/judge/restamp.d.ts +20 -0
  52. package/dist/judge/restamp.js +20 -0
  53. package/dist/judge/restamp.js.map +1 -0
  54. package/dist/relate/before.js +2 -2
  55. package/dist/relate/before.js.map +1 -1
  56. package/dist/relate/carried.d.ts +1 -1
  57. package/dist/relate/carried.js +5 -5
  58. package/dist/relate/carried.js.map +1 -1
  59. package/dist/relate/graph.d.ts +14 -12
  60. package/dist/relate/graph.js +12 -11
  61. package/dist/relate/graph.js.map +1 -1
  62. package/dist/relate/index.d.ts +1 -2
  63. package/dist/relate/index.js +1 -2
  64. package/dist/relate/index.js.map +1 -1
  65. package/dist/relate/reach.d.ts +5 -5
  66. package/dist/relate/reach.js.map +1 -1
  67. package/dist/relate/records.d.ts +5 -4
  68. package/dist/relate/records.js +3 -2
  69. package/dist/relate/records.js.map +1 -1
  70. package/dist/rules/normalize/fixture.d.ts +2 -0
  71. package/dist/rules/normalize/fixture.js +2 -1
  72. package/dist/rules/normalize/fixture.js.map +1 -1
  73. package/dist/shard/placement.d.ts +10 -8
  74. package/dist/shard/placement.js +7 -21
  75. package/dist/shard/placement.js.map +1 -1
  76. package/dist/share/index.d.ts +1 -1
  77. package/dist/share/index.js +1 -1
  78. package/dist/share/index.js.map +1 -1
  79. package/package.json +2 -2
  80. package/dist/relate/dominate.d.ts +0 -19
  81. package/dist/relate/dominate.js +0 -77
  82. package/dist/relate/dominate.js.map +0 -1
@@ -0,0 +1,269 @@
1
+ import { FORMAT_VERSION } from './packed.js';
2
+ /**
3
+ * Whether a value read back from a file or a store is the format it claims.
4
+ *
5
+ * Read, not believed: a value parsed from JSON is `unknown` until one of these
6
+ * says otherwise, and each checks every field its type declares, so that there
7
+ * is one answer to what a snapshot, a document or a subject is. What a reader
8
+ * can re-derive, a digest against the bytes it names, is the reader's to check.
9
+ */
10
+ /**
11
+ * Whether a value is a `SemanticSnapshot`, every node down to the leaves.
12
+ *
13
+ * The readers walk all of it: the matcher pairs every child, and a node that is
14
+ * not one throws there instead of reading as a snapshot that is not recorded.
15
+ * A capture file and a packed snapshot are the two places one is read back.
16
+ * Whose snapshot it is, against the subject it was filed under, is the
17
+ * reader's question.
18
+ */
19
+ export function isSemanticSnapshot(value) {
20
+ const snapshot = record(value);
21
+ const profile = snapshot === null ? null : record(snapshot.profile);
22
+ const environment = snapshot === null ? null : record(snapshot.environment);
23
+ const root = snapshot === null ? null : record(snapshot.root);
24
+ return (snapshot !== null &&
25
+ snapshot.formatVersion === FORMAT_VERSION &&
26
+ isSubjectRef(snapshot.subject) &&
27
+ profile !== null &&
28
+ (profile.id === 'jsdom' || profile.id === 'chromium') &&
29
+ ['ariaTree', 'declaredStyle', 'computedStyle', 'layout', 'raster'].every((field) => typeof profile[field] === 'boolean') &&
30
+ environment !== null &&
31
+ typeof environment.digest === 'string' &&
32
+ typeof environment.semanticDigest === 'string' &&
33
+ environmentInputsShape(environment.inputs) &&
34
+ typeof snapshot.renderHash === 'string' &&
35
+ typeof snapshot.structureHash === 'string' &&
36
+ typeof snapshot.styleHash === 'string' &&
37
+ root !== null &&
38
+ semanticNodeShape(root) &&
39
+ Array.isArray(snapshot.styleProvenance) &&
40
+ snapshot.styleProvenance.every(styleProvenanceShape) &&
41
+ (snapshot.ignoreSites === undefined ||
42
+ (Array.isArray(snapshot.ignoreSites) && snapshot.ignoreSites.every(ignoreSiteShape))) &&
43
+ diagnosticArray(snapshot.diagnostics));
44
+ }
45
+ /**
46
+ * Whether a value is a `RenderDocument`, its resources' shape included.
47
+ *
48
+ * Shape only: that a resource's bytes are base64 and hash to its digest, and
49
+ * that a document is resource-closed, are what a reader that needs them checks.
50
+ */
51
+ export function isRenderDocument(value) {
52
+ const document = record(value);
53
+ const frame = document === null ? null : record(document.frame);
54
+ const viewport = document === null ? null : record(document.viewport);
55
+ return (document !== null &&
56
+ document.documentVersion === 1 &&
57
+ isSubjectRef(document.subject) &&
58
+ typeof document.html === 'string' &&
59
+ frame !== null &&
60
+ stringRecord(frame.html) &&
61
+ stringRecord(frame.body) &&
62
+ Array.isArray(frame.ancestors) &&
63
+ frame.ancestors.every(frameElementShape) &&
64
+ (frame.containerWidth === undefined || finiteNumber(frame.containerWidth)) &&
65
+ stringArray(document.css) &&
66
+ viewport !== null &&
67
+ finiteNumber(viewport.width) &&
68
+ finiteNumber(viewport.height) &&
69
+ finiteNumber(viewport.deviceScaleFactor) &&
70
+ (viewport.colorScheme === 'light' || viewport.colorScheme === 'dark') &&
71
+ stringRecord(document.inherited) &&
72
+ stringArray(document.fonts) &&
73
+ (document.assets === undefined || stringRecord(document.assets)) &&
74
+ optionalString(document.baseUrl) &&
75
+ (document.resources === undefined ||
76
+ (record(document.resources) !== null && Object.values(document.resources).every(resourceShape))) &&
77
+ diagnosticArray(document.diagnostics));
78
+ }
79
+ /** Whether a value is a `SubjectRef`: an id, one of the four kinds, and an optional title. */
80
+ export function isSubjectRef(value) {
81
+ const subject = record(value);
82
+ return subject !== null &&
83
+ typeof subject.id === 'string' &&
84
+ ['story', 'route', 'fixture', 'value'].includes(String(subject.kind)) &&
85
+ optionalString(subject.title);
86
+ }
87
+ function frameElementShape(value) {
88
+ const element = record(value);
89
+ return element !== null && typeof element.tag === 'string' && stringRecord(element.attributes);
90
+ }
91
+ function resourceShape(value) {
92
+ const resource = record(value);
93
+ return resource !== null &&
94
+ typeof resource.contentType === 'string' &&
95
+ typeof resource.bytes === 'string' &&
96
+ typeof resource.digest === 'string' &&
97
+ (resource.status === undefined || finiteNumber(resource.status));
98
+ }
99
+ function environmentInputsShape(value) {
100
+ const inputs = record(value);
101
+ const viewport = inputs === null ? null : record(inputs.viewport);
102
+ return inputs !== null &&
103
+ (inputs.profile === 'jsdom' || inputs.profile === 'chromium') &&
104
+ typeof inputs.engine === 'string' &&
105
+ typeof inputs.ruleset === 'string' &&
106
+ typeof inputs.allowlist === 'string' &&
107
+ viewport !== null &&
108
+ finiteNumber(viewport.width) &&
109
+ finiteNumber(viewport.height) &&
110
+ finiteNumber(viewport.deviceScaleFactor) &&
111
+ (viewport.colorScheme === 'light' || viewport.colorScheme === 'dark') &&
112
+ stringArray(inputs.fonts) &&
113
+ scalarRecord(inputs.conditions) &&
114
+ stringRecord(inputs.assets) &&
115
+ optionalString(inputs.stabilization) &&
116
+ optionalString(inputs.timezoneId) &&
117
+ optionalString(inputs.locale);
118
+ }
119
+ function styleProvenanceShape(value) {
120
+ const entry = record(value);
121
+ if (entry === null ||
122
+ typeof entry.path !== 'string' ||
123
+ typeof entry.property !== 'string' ||
124
+ typeof entry.sheet !== 'string' ||
125
+ typeof entry.selector !== 'string' ||
126
+ !optionalString(entry.tokenName))
127
+ return false;
128
+ if (entry.source === undefined)
129
+ return true;
130
+ const source = record(entry.source);
131
+ return source !== null && typeof source.file === 'string' && finiteNumber(source.line);
132
+ }
133
+ function semanticNodeShape(node) {
134
+ return (typeof node.path === 'string' &&
135
+ typeof node.tag === 'string' &&
136
+ optionalString(node.alias) &&
137
+ optionalString(node.role) &&
138
+ optionalString(node.name) &&
139
+ optionalString(node.description) &&
140
+ (node.state === undefined || scalarRecord(node.state)) &&
141
+ stringRecord(node.attributes) &&
142
+ stringRecord(node.style) &&
143
+ (node.tokens === undefined || stringRecord(node.tokens)) &&
144
+ (node.styleTokens === undefined || stringRecord(node.styleTokens)) &&
145
+ (node.rect === undefined || rectShape(node.rect)) &&
146
+ optionalString(node.text) &&
147
+ (node.provenance === undefined || provenanceShape(node.provenance)) &&
148
+ (node.wiring === undefined || wiringShape(node.wiring)) &&
149
+ (node.holding === undefined || holdingShape(node.holding)) &&
150
+ (node.portalled === undefined || typeof node.portalled === 'boolean') &&
151
+ (node.ignoredBy === undefined || stringArray(node.ignoredBy)) &&
152
+ optionalString(node.unread) &&
153
+ Array.isArray(node.children) &&
154
+ node.children.every((child) => {
155
+ const parsed = record(child);
156
+ return parsed !== null && semanticNodeShape(parsed);
157
+ }));
158
+ }
159
+ function ignoreSiteShape(value) {
160
+ const site = record(value);
161
+ return site !== null &&
162
+ typeof site.path === 'string' &&
163
+ typeof site.rule === 'string' &&
164
+ (site.rect === undefined || rectShape(site.rect));
165
+ }
166
+ function rectShape(value) {
167
+ const rect = record(value);
168
+ return rect !== null &&
169
+ finiteNumber(rect.x) &&
170
+ finiteNumber(rect.y) &&
171
+ finiteNumber(rect.width) &&
172
+ finiteNumber(rect.height);
173
+ }
174
+ function provenanceShape(value) {
175
+ const provenance = record(value);
176
+ return provenance !== null &&
177
+ Array.isArray(provenance.owners) &&
178
+ provenance.owners.every(ownerShape) &&
179
+ optionalString(provenance.createdBy) &&
180
+ (provenance.source === undefined || sourceLocationShape(provenance.source)) &&
181
+ (provenance.stack === undefined ||
182
+ (Array.isArray(provenance.stack) && provenance.stack.every(stackFrameShape)));
183
+ }
184
+ function ownerShape(value) {
185
+ const owner = record(value);
186
+ return owner !== null &&
187
+ typeof owner.name === 'string' &&
188
+ typeof owner.propsDigest === 'string' &&
189
+ optionalString(owner.createdBy);
190
+ }
191
+ function sourceLocationShape(value) {
192
+ const source = record(value);
193
+ return source !== null &&
194
+ typeof source.file === 'string' &&
195
+ finiteNumber(source.line) &&
196
+ finiteNumber(source.column);
197
+ }
198
+ function stackFrameShape(value) {
199
+ const frame = record(value);
200
+ return frame !== null &&
201
+ typeof frame.url === 'string' &&
202
+ finiteNumber(frame.line) &&
203
+ finiteNumber(frame.column) &&
204
+ optionalString(frame.function);
205
+ }
206
+ function wiringShape(value) {
207
+ const wiring = record(value);
208
+ return wiring !== null &&
209
+ (wiring.hooks === undefined || stringArray(wiring.hooks)) &&
210
+ (wiring.wrappers === undefined ||
211
+ (Array.isArray(wiring.wrappers) &&
212
+ wiring.wrappers.every((entry) => entry === 'memo' || entry === 'forwardRef'))) &&
213
+ (wiring.contexts === undefined || stringArray(wiring.contexts)) &&
214
+ optionalString(wiring.key);
215
+ }
216
+ function holdingShape(value) {
217
+ const holding = record(value);
218
+ return holding !== null &&
219
+ (holding.cells === undefined || (Array.isArray(holding.cells) && holding.cells.every((value) => {
220
+ const cell = record(value);
221
+ return cell !== null && finiteNumber(cell.index) && typeof cell.hook === 'string' && typeof cell.digest === 'string';
222
+ }))) &&
223
+ (holding.contexts === undefined || heldValues(holding.contexts)) &&
224
+ (holding.props === undefined || heldValues(holding.props)) &&
225
+ optionalString(holding.unread);
226
+ }
227
+ function heldValues(value) {
228
+ return Array.isArray(value) && value.every((entry) => {
229
+ const held = record(entry);
230
+ return held !== null && typeof held.name === 'string' && typeof held.digest === 'string';
231
+ });
232
+ }
233
+ function diagnosticArray(value) {
234
+ return Array.isArray(value) && value.every((entry) => {
235
+ const diagnostic = record(entry);
236
+ return diagnostic !== null &&
237
+ (diagnostic.severity === 'warn' || diagnostic.severity === 'error') &&
238
+ typeof diagnostic.code === 'string' &&
239
+ typeof diagnostic.message === 'string' &&
240
+ optionalString(diagnostic.nodePath);
241
+ });
242
+ }
243
+ // A plain object. `@variance-authority/unit-test`'s capture-file reader keeps
244
+ // the same guard privately: it is re-typed in several packages and has no
245
+ // exported owner, and exporting it from here would make it part of the format.
246
+ function record(value) {
247
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
248
+ ? value
249
+ : null;
250
+ }
251
+ function stringRecord(value) {
252
+ const parsed = record(value);
253
+ return parsed !== null && Object.values(parsed).every((entry) => typeof entry === 'string');
254
+ }
255
+ function scalarRecord(value) {
256
+ const parsed = record(value);
257
+ return parsed !== null && Object.values(parsed).every((entry) => ['string', 'boolean', 'number'].includes(typeof entry) &&
258
+ (typeof entry !== 'number' || Number.isFinite(entry)));
259
+ }
260
+ function stringArray(value) {
261
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
262
+ }
263
+ function optionalString(value) {
264
+ return value === undefined || typeof value === 'string';
265
+ }
266
+ function finiteNumber(value) {
267
+ return typeof value === 'number' && Number.isFinite(value);
268
+ }
269
+ //# sourceMappingURL=shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape.js","sourceRoot":"","sources":["../../src/format/shape.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SubjectRef } from './capture.js';\nimport type { RenderDocument } from './document.js';\nimport { FORMAT_VERSION } from './packed.js';\nimport type { SemanticSnapshot } from './snapshot.js';\n\n/**\n * Whether a value read back from a file or a store is the format it claims.\n *\n * Read, not believed: a value parsed from JSON is `unknown` until one of these\n * says otherwise, and each checks every field its type declares, so that there\n * is one answer to what a snapshot, a document or a subject is. What a reader\n * can re-derive, a digest against the bytes it names, is the reader's to check.\n */\n\n/**\n * Whether a value is a `SemanticSnapshot`, every node down to the leaves.\n *\n * The readers walk all of it: the matcher pairs every child, and a node that is\n * not one throws there instead of reading as a snapshot that is not recorded.\n * A capture file and a packed snapshot are the two places one is read back.\n * Whose snapshot it is, against the subject it was filed under, is the\n * reader's question.\n */\nexport function isSemanticSnapshot(value: unknown): value is SemanticSnapshot {\n const snapshot = record(value);\n const profile = snapshot === null ? null : record(snapshot.profile);\n const environment = snapshot === null ? null : record(snapshot.environment);\n const root = snapshot === null ? null : record(snapshot.root);\n return (\n snapshot !== null &&\n snapshot.formatVersion === FORMAT_VERSION &&\n isSubjectRef(snapshot.subject) &&\n profile !== null &&\n (profile.id === 'jsdom' || profile.id === 'chromium') &&\n ['ariaTree', 'declaredStyle', 'computedStyle', 'layout', 'raster'].every(\n (field) => typeof profile[field] === 'boolean',\n ) &&\n environment !== null &&\n typeof environment.digest === 'string' &&\n typeof environment.semanticDigest === 'string' &&\n environmentInputsShape(environment.inputs) &&\n typeof snapshot.renderHash === 'string' &&\n typeof snapshot.structureHash === 'string' &&\n typeof snapshot.styleHash === 'string' &&\n root !== null &&\n semanticNodeShape(root) &&\n Array.isArray(snapshot.styleProvenance) &&\n snapshot.styleProvenance.every(styleProvenanceShape) &&\n (snapshot.ignoreSites === undefined ||\n (Array.isArray(snapshot.ignoreSites) && snapshot.ignoreSites.every(ignoreSiteShape))) &&\n diagnosticArray(snapshot.diagnostics)\n );\n}\n\n/**\n * Whether a value is a `RenderDocument`, its resources' shape included.\n *\n * Shape only: that a resource's bytes are base64 and hash to its digest, and\n * that a document is resource-closed, are what a reader that needs them checks.\n */\nexport function isRenderDocument(value: unknown): value is RenderDocument {\n const document = record(value);\n const frame = document === null ? null : record(document.frame);\n const viewport = document === null ? null : record(document.viewport);\n return (\n document !== null &&\n document.documentVersion === 1 &&\n isSubjectRef(document.subject) &&\n typeof document.html === 'string' &&\n frame !== null &&\n stringRecord(frame.html) &&\n stringRecord(frame.body) &&\n Array.isArray(frame.ancestors) &&\n frame.ancestors.every(frameElementShape) &&\n (frame.containerWidth === undefined || finiteNumber(frame.containerWidth)) &&\n stringArray(document.css) &&\n viewport !== null &&\n finiteNumber(viewport.width) &&\n finiteNumber(viewport.height) &&\n finiteNumber(viewport.deviceScaleFactor) &&\n (viewport.colorScheme === 'light' || viewport.colorScheme === 'dark') &&\n stringRecord(document.inherited) &&\n stringArray(document.fonts) &&\n (document.assets === undefined || stringRecord(document.assets)) &&\n optionalString(document.baseUrl) &&\n (document.resources === undefined ||\n (record(document.resources) !== null && Object.values(document.resources as object).every(resourceShape))) &&\n diagnosticArray(document.diagnostics)\n );\n}\n\n/** Whether a value is a `SubjectRef`: an id, one of the four kinds, and an optional title. */\nexport function isSubjectRef(value: unknown): value is SubjectRef {\n const subject = record(value);\n return subject !== null &&\n typeof subject.id === 'string' &&\n ['story', 'route', 'fixture', 'value'].includes(String(subject.kind)) &&\n optionalString(subject.title);\n}\n\nfunction frameElementShape(value: unknown): boolean {\n const element = record(value);\n return element !== null && typeof element.tag === 'string' && stringRecord(element.attributes);\n}\n\nfunction resourceShape(value: unknown): boolean {\n const resource = record(value);\n return resource !== null &&\n typeof resource.contentType === 'string' &&\n typeof resource.bytes === 'string' &&\n typeof resource.digest === 'string' &&\n (resource.status === undefined || finiteNumber(resource.status));\n}\n\nfunction environmentInputsShape(value: unknown): boolean {\n const inputs = record(value);\n const viewport = inputs === null ? null : record(inputs.viewport);\n return inputs !== null &&\n (inputs.profile === 'jsdom' || inputs.profile === 'chromium') &&\n typeof inputs.engine === 'string' &&\n typeof inputs.ruleset === 'string' &&\n typeof inputs.allowlist === 'string' &&\n viewport !== null &&\n finiteNumber(viewport.width) &&\n finiteNumber(viewport.height) &&\n finiteNumber(viewport.deviceScaleFactor) &&\n (viewport.colorScheme === 'light' || viewport.colorScheme === 'dark') &&\n stringArray(inputs.fonts) &&\n scalarRecord(inputs.conditions) &&\n stringRecord(inputs.assets) &&\n optionalString(inputs.stabilization) &&\n optionalString(inputs.timezoneId) &&\n optionalString(inputs.locale);\n}\n\nfunction styleProvenanceShape(value: unknown): boolean {\n const entry = record(value);\n if (entry === null ||\n typeof entry.path !== 'string' ||\n typeof entry.property !== 'string' ||\n typeof entry.sheet !== 'string' ||\n typeof entry.selector !== 'string' ||\n !optionalString(entry.tokenName)) return false;\n if (entry.source === undefined) return true;\n const source = record(entry.source);\n return source !== null && typeof source.file === 'string' && finiteNumber(source.line);\n}\n\nfunction semanticNodeShape(node: Record<string, unknown>): boolean {\n return (\n typeof node.path === 'string' &&\n typeof node.tag === 'string' &&\n optionalString(node.alias) &&\n optionalString(node.role) &&\n optionalString(node.name) &&\n optionalString(node.description) &&\n (node.state === undefined || scalarRecord(node.state)) &&\n stringRecord(node.attributes) &&\n stringRecord(node.style) &&\n (node.tokens === undefined || stringRecord(node.tokens)) &&\n (node.styleTokens === undefined || stringRecord(node.styleTokens)) &&\n (node.rect === undefined || rectShape(node.rect)) &&\n optionalString(node.text) &&\n (node.provenance === undefined || provenanceShape(node.provenance)) &&\n (node.wiring === undefined || wiringShape(node.wiring)) &&\n (node.holding === undefined || holdingShape(node.holding)) &&\n (node.portalled === undefined || typeof node.portalled === 'boolean') &&\n (node.ignoredBy === undefined || stringArray(node.ignoredBy)) &&\n optionalString(node.unread) &&\n Array.isArray(node.children) &&\n node.children.every((child) => {\n const parsed = record(child);\n return parsed !== null && semanticNodeShape(parsed);\n })\n );\n}\n\nfunction ignoreSiteShape(value: unknown): boolean {\n const site = record(value);\n return site !== null &&\n typeof site.path === 'string' &&\n typeof site.rule === 'string' &&\n (site.rect === undefined || rectShape(site.rect));\n}\n\nfunction rectShape(value: unknown): boolean {\n const rect = record(value);\n return rect !== null &&\n finiteNumber(rect.x) &&\n finiteNumber(rect.y) &&\n finiteNumber(rect.width) &&\n finiteNumber(rect.height);\n}\n\nfunction provenanceShape(value: unknown): boolean {\n const provenance = record(value);\n return provenance !== null &&\n Array.isArray(provenance.owners) &&\n provenance.owners.every(ownerShape) &&\n optionalString(provenance.createdBy) &&\n (provenance.source === undefined || sourceLocationShape(provenance.source)) &&\n (provenance.stack === undefined ||\n (Array.isArray(provenance.stack) && provenance.stack.every(stackFrameShape)));\n}\n\nfunction ownerShape(value: unknown): boolean {\n const owner = record(value);\n return owner !== null &&\n typeof owner.name === 'string' &&\n typeof owner.propsDigest === 'string' &&\n optionalString(owner.createdBy);\n}\n\nfunction sourceLocationShape(value: unknown): boolean {\n const source = record(value);\n return source !== null &&\n typeof source.file === 'string' &&\n finiteNumber(source.line) &&\n finiteNumber(source.column);\n}\n\nfunction stackFrameShape(value: unknown): boolean {\n const frame = record(value);\n return frame !== null &&\n typeof frame.url === 'string' &&\n finiteNumber(frame.line) &&\n finiteNumber(frame.column) &&\n optionalString(frame.function);\n}\n\nfunction wiringShape(value: unknown): boolean {\n const wiring = record(value);\n return wiring !== null &&\n (wiring.hooks === undefined || stringArray(wiring.hooks)) &&\n (wiring.wrappers === undefined ||\n (Array.isArray(wiring.wrappers) &&\n wiring.wrappers.every((entry) => entry === 'memo' || entry === 'forwardRef'))) &&\n (wiring.contexts === undefined || stringArray(wiring.contexts)) &&\n optionalString(wiring.key);\n}\n\nfunction holdingShape(value: unknown): boolean {\n const holding = record(value);\n return holding !== null &&\n (holding.cells === undefined || (Array.isArray(holding.cells) && holding.cells.every((value) => {\n const cell = record(value);\n return cell !== null && finiteNumber(cell.index) && typeof cell.hook === 'string' && typeof cell.digest === 'string';\n }))) &&\n (holding.contexts === undefined || heldValues(holding.contexts)) &&\n (holding.props === undefined || heldValues(holding.props)) &&\n optionalString(holding.unread);\n}\n\nfunction heldValues(value: unknown): boolean {\n return Array.isArray(value) && value.every((entry) => {\n const held = record(entry);\n return held !== null && typeof held.name === 'string' && typeof held.digest === 'string';\n });\n}\n\nfunction diagnosticArray(value: unknown): boolean {\n return Array.isArray(value) && value.every((entry) => {\n const diagnostic = record(entry);\n return diagnostic !== null &&\n (diagnostic.severity === 'warn' || diagnostic.severity === 'error') &&\n typeof diagnostic.code === 'string' &&\n typeof diagnostic.message === 'string' &&\n optionalString(diagnostic.nodePath);\n });\n}\n\n// A plain object. `@variance-authority/unit-test`'s capture-file reader keeps\n// the same guard privately: it is re-typed in several packages and has no\n// exported owner, and exporting it from here would make it part of the format.\nfunction record(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction stringRecord(value: unknown): boolean {\n const parsed = record(value);\n return parsed !== null && Object.values(parsed).every((entry) => typeof entry === 'string');\n}\n\nfunction scalarRecord(value: unknown): boolean {\n const parsed = record(value);\n return parsed !== null && Object.values(parsed).every(\n (entry) => ['string', 'boolean', 'number'].includes(typeof entry) &&\n (typeof entry !== 'number' || Number.isFinite(entry)),\n );\n}\n\nfunction stringArray(value: unknown): boolean {\n return Array.isArray(value) && value.every((entry) => typeof entry === 'string');\n}\n\nfunction optionalString(value: unknown): boolean {\n return value === undefined || typeof value === 'string';\n}\n\nfunction finiteNumber(value: unknown): boolean {\n return typeof value === 'number' && Number.isFinite(value);\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { type Digest } from './hash.js';
2
2
  import { type Tier } from './tier.js';
3
3
  import type { Intervention, ScreenshotOptions, SettleTarget } from './intervention.js';
4
+ import type { StepClock } from './steps.js';
4
5
  /**
5
6
  * Interventions as separate, nameable tricks — not a fixed set of switches.
6
7
  *
@@ -213,7 +214,12 @@ export declare function conflicts(recipe: Recipe): readonly {
213
214
  }[];
214
215
  export declare function recipeCss(recipe: Recipe): string;
215
216
  export declare function recipeScreenshot(recipe: Recipe): ScreenshotOptions;
216
- export declare function settleRecipe(recipe: Recipe, target: SettleTarget): Promise<void>;
217
+ /**
218
+ * Every wait the recipe holds, in order. Each is one step on `steps`, named by
219
+ * the intervention's id, so a run can say which wait a slow subject spent its
220
+ * time in.
221
+ */
222
+ export declare function settleRecipe(recipe: Recipe, target: SettleTarget, steps?: StepClock): Promise<void>;
217
223
  /**
218
224
  * The identity of a recipe.
219
225
  *
@@ -276,10 +276,17 @@ export function recipeCss(recipe) {
276
276
  export function recipeScreenshot(recipe) {
277
277
  return recipe.reduce((options, intervention) => ({ ...options, ...intervention.screenshot }), {});
278
278
  }
279
- export async function settleRecipe(recipe, target) {
279
+ /**
280
+ * Every wait the recipe holds, in order. Each is one step on `steps`, named by
281
+ * the intervention's id, so a run can say which wait a slow subject spent its
282
+ * time in.
283
+ */
284
+ export async function settleRecipe(recipe, target, steps) {
280
285
  for (const intervention of recipe) {
281
- if (intervention.settle !== undefined)
282
- await intervention.settle(target);
286
+ const settle = intervention.settle;
287
+ if (settle === undefined)
288
+ continue;
289
+ await (steps === undefined ? settle(target) : steps.time(intervention.id, async () => settle(target)));
283
290
  }
284
291
  }
285
292
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"stabilize.js","sourceRoot":"","sources":["../../src/format/stabilize.ts"],"names":[],"mappings":"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","sourcesContent":["import { digestValue, type Digest } from './hash.js';\nimport { tierReaches, type Tier } from './tier.js';\nimport { clockById, freezeClock, freezeClockAt, noFocus, noHover, reducedMotion, UNTOUCHED } from './drive.js';\nimport type { Intervention, ScreenshotOptions, SettleTarget } from './intervention.js';\nimport { waitForFonts, waitForImages } from './waits.js';\n\n/**\n * Interventions as separate, nameable tricks — not a fixed set of switches.\n *\n * A subject that is still changing cannot be compared, so every system of this\n * kind reaches into the page: it holds animations, waits for fonts, hides a\n * caret, suppresses scrollbars. Written as one struct of booleans that becomes a\n * closed vocabulary: adding a trick edits a type every caller depends on, and a\n * project with a need nobody anticipated has to fork.\n *\n * So each is a value. It carries an id, the tier that can actually observe what\n * it fixes, what it costs, and how it is applied — and a *recipe* is any list of\n * them. Two tricks may express the same intent through different mechanisms and\n * remain separate values, because they produce different images and choosing\n * between them is the caller's business.\n *\n * ## What this buys\n *\n * A recipe can be filtered by tier, so the structure-and-style rung waits for\n * nothing; composed from another project's tricks alongside these; digested, so\n * a run under another recipe than its baseline names the recipe as the cause;\n * and checked for tricks that fight each other.\n *\n * ## Where the damage lands\n *\n * Ordered by increasing cost, earliest sufficient option first:\n *\n * - **Outside the subject** — injected CSS, browser screenshot options. Nothing\n * in the product imports it and deleting the tool deletes the intervention.\n * - **Runtime substitution** — wrapping `Promise`, replacing a suspense\n * boundary. Buys the same knowledge without touching a component and is\n * deliberately not shipped here: it moves the damage from design into\n * semantics, where a difference caused by the patch cannot be told apart from\n * a difference caused by the code. Expressible as a trick if a project decides\n * the trade is worth it — that is the point of an open set.\n * - **A contract the subject implements** — a readiness marker. Real design\n * damage, and reserved for what the outside genuinely cannot know.\n *\n * ## Why this is in `format` and not in the raster package\n *\n * It was in `@variance-authority/raster` until 2026-08-06, on the reading that\n * holding a page still is something you do before you photograph it. That\n * reading was wrong in a way that cost correctness: an animation in flight is a\n * *computed style value*, so it reaches the cheap representation too — and the\n * allowlist excludes `animation-*` and `transition-*` precisely because it\n * assumed a snapshot is taken with animation already disabled. Nothing disabled\n * it. See {@link COLLECT_RECIPE} and ADR-0028.\n *\n * So a recipe is a render input on every tier, `EnvironmentInputs` carries its\n * digest, and the vocabulary belongs beside the key it is part of.\n */\n\n// Re-exported from here because this is where a reader arrives looking for the\n// vocabulary; [`intervention.ts`](./intervention.js) holds the declaration.\nexport type { Intervention, ScreenshotOptions, SettleTarget, Trick } from './intervention.js';\nexport { waitForFonts, waitForImages } from './waits.js';\n\n/**\n * Hand animations to the browser rather than emulating the hold in CSS.\n *\n * A browser disabling animations for a screenshot fast-forwards a finite\n * animation to completion — the state a user comes to rest on — and cancels an\n * infinite one to its initial frame, replaying it afterwards. That is the\n * behaviour worth having, and CSS cannot express it.\n */\nexport const holdAnimations: Intervention = {\n id: 'hold-animations',\n trick: 'hold',\n needs: 'layout',\n governs: 'animations',\n because: 'animations settled by the browser: finite fast-forwarded, infinite reset',\n screenshot: { animations: 'disabled' },\n};\n\n/**\n * The same intent without a browser that can do it, and it is not equivalent.\n *\n * Pins every animation at its first frame, so a fade-in is captured at the\n * moment it is invisible. `animation: none` is avoided deliberately — removing\n * an animation drops whatever layout its keyframes contribute, which changes the\n * page rather than stopping it.\n */\nexport const pinAnimations: Intervention = {\n id: 'pin-animations',\n trick: 'hold',\n needs: 'layout',\n governs: 'animations',\n because: 'animations pinned at their first frame, which is not where a user sees them',\n css:\n '*,*::before,*::after{' +\n 'animation-play-state:paused !important;' +\n 'animation-delay:-0.0001s !important;' +\n 'transition-duration:0s !important;' +\n 'transition-delay:0s !important;' +\n 'scroll-behavior:auto !important}',\n};\n\n/**\n * A stylesheet rather than the driver's own caret switch, deliberately.\n *\n * Playwright and its peers hide a caret by writing `caret-color` onto every\n * focusable element and putting it back afterwards, and putting it back leaves\n * `style=\"\"` where there was no `style` attribute at all. Any capture that reads\n * the DOM on both sides of a screenshot — which is what the in-place path does\n * to prove the subject held still — then sees markup that moved, and reports the\n * driver's housekeeping as the page being unstable. A page with one text input\n * is enough.\n *\n * So this hold owns the caret the way {@link pinAnimations} owns animations: one\n * stylesheet, installed before the subject is read, removed with the rest of the\n * recipe.\n */\nexport const hideCaret: Intervention = {\n id: 'hide-caret',\n trick: 'support',\n // A caret paints and does not lay out, so a tier that never rasterizes cannot\n // see it and must not pay to hide it.\n needs: 'raster',\n governs: 'caret',\n because: 'text caret hidden, because it blinks on its own schedule',\n css: '*{caret-color:transparent !important}',\n};\n\nexport const hideScrollbars: Intervention = {\n id: 'hide-scrollbars',\n trick: 'support',\n needs: 'layout',\n governs: 'scrollbars',\n because: 'scrollbars hidden, removing a platform and preference difference — and their width',\n css: '*{scrollbar-width:none !important}\\n*::-webkit-scrollbar{display:none !important}',\n};\n\n/**\n * Drop the pixels of images the page itself called decorative, keeping their boxes.\n *\n * The other half of blanking, and it lives here because of what it needs to\n * know. A driver rewriting responses (`@variance-authority/playwright`'s\n * `blank` rules) can decide by URL and by intrinsic size, which answers \"every\n * illustration over 40,000 pixels\" and answers it before the bytes are even\n * fetched. It cannot answer `role=\"presentation\"`, because a request carries no\n * idea which element wanted it — that fact exists only in the document, so the\n * trick that uses it is a stylesheet.\n *\n * `visibility:hidden` rather than `display:none`, and the distinction is the\n * entire design: a hidden element still occupies exactly the box it would have,\n * so a page whose column height comes from an image's intrinsic size is\n * unchanged. `display:none` would collapse it and report a layout regression\n * this tool caused.\n *\n * **Opt-in, and it belongs in no default recipe.** Every other trick here\n * removes something that is not part of the assertion — a caret, a scrollbar, an\n * animation mid-flight. This one removes page content, which is a judgement\n * about what a suite is for, and a default that quietly stopped watching every\n * `alt=\"\"` image would hide real regressions under a green run.\n */\nexport const hidePresentationalImages: Intervention = {\n id: 'hide-presentational-images',\n trick: 'support',\n // `layout` rather than `raster`, and the difference is whether it runs at all.\n // A raster-tier trick is filtered out of every *collection* — `tierOfProfile`\n // never returns `raster` — so it would only reach a page through a renderer\n // option, which is not where an operator configures their suite. A collection\n // sheet is in force when the document is serialized, so hiding it here is what\n // makes the pixels absent from the render.\n needs: 'layout',\n governs: 'presentational-images',\n because:\n 'images the page marked decorative hidden, keeping their boxes — real changes inside them ' +\n 'are not reported',\n css:\n 'img[role=\"presentation\"],img[alt=\"\"],[role=\"presentation\"] img,[role=\"none\"] img' +\n '{visibility:hidden !important}',\n};\n\nexport const INTERVENTIONS: readonly Intervention[] = [\n holdAnimations,\n pinAnimations,\n hideCaret,\n hideScrollbars,\n hidePresentationalImages,\n noHover,\n noFocus,\n reducedMotion,\n freezeClock,\n waitForFonts,\n waitForImages,\n];\n\nexport type Recipe = readonly Intervention[];\n\n/**\n * Nothing at all, and correct for the tier that reads structure and declared style.\n *\n * A font that has not loaded cannot change which rules match or what they\n * declare; an image that has not decoded cannot either. Waiting for both before\n * a structure-and-style hash buys nothing and costs the wait per subject, on the\n * rung the tier ladder exists to make cheap.\n */\nexport const SEMANTIC_RECIPE: Recipe = [];\n\n// TODO: a `<script>` inside a subject root runs in the render document under the\n// real clock, which no trick in this recipe holds.\n// TODO: a document is painted under this recipe whatever recipe it was read\n// under: a `RenderDocument` carries no collection recipe to follow.\n/**\n * What a document is painted under, and of {@link UNTOUCHED} only `no-focus`: a\n * collected `autofocus` takes focus again. The other three have nothing but a\n * carried script to act on: the printed date and the animation's place are in\n * the document already, its `@media` rules were resolved when it was read, and\n * nothing moves the pointer over it.\n */\nexport const LAYOUT_RECIPE: Recipe = [holdAnimations, hideScrollbars, noFocus, waitForFonts, waitForImages];\n\nexport const RASTER_RECIPE: Recipe = [...LAYOUT_RECIPE, hideCaret];\n\n/**\n * What is applied to a live page *before a subject is observed*, as opposed to\n * before it is painted.\n *\n * The two are different recipes and it is not a preference. {@link\n * holdAnimations} is a *screenshot option* — it asks the browser to settle\n * animations for the image it is about to take — and there is no screenshot at\n * collection time, so it would be a trick that silently does nothing. The CSS\n * variant is the one that works where nobody is holding a camera, which is the\n * whole reason both exist as separate values rather than as one switch.\n *\n * ## Why the cheap tier needs this at all\n *\n * The claim in [`ruleset.ts`](../rules/ruleset.ts) that `transition-*` and\n * `animation-*` \"describe a journey the snapshot does not contain\" is only true\n * if the snapshot is taken with the journey stopped. `transform`, `opacity`,\n * `filter`, `color` and every geometric longhand *are* admitted, and an\n * animation in flight moves all of them — so an unstabilized collection turns a\n * 300ms fade into a component-attributed regression with a real file name on it,\n * which is worse than an unexplained pixel diff because it is credible.\n *\n * Filtered by {@link forTier}, so jsdom applies none of it: with no layout\n * engine and no animation clock there is nothing to hold still, and paying a\n * `fonts.ready` wait per subject on the rung that exists to be cheap is exactly\n * the trade {@link SEMANTIC_RECIPE} refuses.\n */\nexport const COLLECT_RECIPE: Recipe = [\n pinAnimations,\n hideScrollbars,\n // Here as well as in {@link RASTER_RECIPE}, because the in-place path\n // photographs the live page and applies no screenshot options of its own: the\n // only caret hold that reaches that image is one installed here. A subject\n // holding a focused input otherwise disagrees with itself between two\n // screenshots taken 600ms apart, and is refused for blinking.\n hideCaret,\n ...UNTOUCHED,\n waitForFonts,\n waitForImages,\n];\n\n/**\n * The ids of {@link COLLECT_RECIPE}, as an absent `stabilize` means them. A list\n * replaces the recipe, so one trick is turned off with this list without it:\n * `stabilize: DEFAULT_STABILIZE.filter((id) => id !== 'freeze-clock')`.\n */\nexport const DEFAULT_STABILIZE: readonly string[] = COLLECT_RECIPE.map((intervention) => intervention.id);\n\n/**\n * Resolve a trick by the name a caller wrote down.\n *\n * The boundary a collection recipe crosses is a `page.evaluate`, and an\n * `Intervention` does not survive it — `settle` is a function. Ids do survive,\n * and the page holds this same registry, so a recipe travels as the list of\n * names it is. A name nothing answers to is a caller error worth failing on\n * rather than a trick to skip quietly: under-stabilizing is how a suite gets a\n * flake it has already paid to prevent.\n */\nexport function interventionById(id: string): Intervention | undefined {\n return INTERVENTIONS.find((intervention) => intervention.id === id) ?? clockById(id);\n}\n\n/**\n * A recipe from the names it travels as.\n *\n * Throws on a name nothing answers to, and names it. The quiet alternative —\n * skip what cannot be resolved — turns a typo in a config into a suite that is\n * one trick less stable than its operator believes, discovered later as a flake\n * they have already paid to prevent. Failing here costs one run and one reading\n * of the message.\n */\nexport function recipeOf(ids: readonly string[]): Recipe {\n return ids.map((id) => {\n const intervention = interventionById(id);\n if (intervention === undefined) {\n throw new Error(\n `no stabilization trick is called \\`${id}\\`; this build knows ` +\n INTERVENTIONS.map((known) => `\\`${known.id}\\``).join(', ') +\n ', and `freeze-clock@<instant>`',\n );\n }\n return intervention;\n });\n}\n\n/**\n * The recipe a host that drives the browser applies, checked before it opens one:\n * `stabilize` by id, or {@link COLLECT_RECIPE} when it is absent, with its clock\n * fixed at `now`, which travels in the clock's id and lands in the digest. Throws\n * on an unknown id, an instant with no zone, or a `now` with no clock to fix, so\n * the operator hears about it before a page is loaded, not at the first subject.\n */\nexport function recipeToDrive(options: { readonly stabilize?: readonly string[]; readonly now?: string }): Recipe {\n const recipe = options.stabilize === undefined ? COLLECT_RECIPE : recipeOf(options.stabilize);\n if (options.now === undefined) return recipe;\n const clock = freezeClockAt(options.now);\n if (!recipe.some((intervention) => intervention.driver?.clock !== undefined)) {\n throw new Error(\n `\\`now\\` is the instant \\`freeze-clock\\` fixes, and this recipe has no \\`freeze-clock\\`: ` +\n 'add it to `stabilize`, or leave `now` out',\n );\n }\n return recipe.map((intervention) => (intervention.driver?.clock === undefined ? intervention : clock));\n}\n\n/** Only the tricks a tier can observe the effect of. */\nexport function forTier(recipe: Recipe, tier: Tier): Recipe {\n return recipe.filter((intervention) => tierReaches(tier, intervention.needs));\n}\n\n/**\n * Tricks in a recipe that claim the same property.\n *\n * Returned rather than thrown, so a caller can decide — an override is a\n * legitimate composition, and silently applying both is not. Two tricks over one\n * property means one of them wins by accident of ordering, and which one is\n * invisible in the result.\n */\nexport function conflicts(recipe: Recipe): readonly { governs: string; ids: readonly string[] }[] {\n const byProperty = new Map<string, string[]>();\n\n for (const intervention of recipe) {\n byProperty.set(intervention.governs, [\n ...(byProperty.get(intervention.governs) ?? []),\n intervention.id,\n ]);\n }\n\n return [...byProperty.entries()]\n .filter(([, ids]) => ids.length > 1)\n .map(([governs, ids]) => ({ governs, ids }));\n}\n\nexport function recipeCss(recipe: Recipe): string {\n return recipe\n .map((intervention) => intervention.css)\n .filter((css): css is string => css !== undefined)\n .join('\\n');\n}\n\nexport function recipeScreenshot(recipe: Recipe): ScreenshotOptions {\n return recipe.reduce<ScreenshotOptions>(\n (options, intervention) => ({ ...options, ...intervention.screenshot }),\n {},\n );\n}\n\nexport async function settleRecipe(recipe: Recipe, target: SettleTarget): Promise<void> {\n for (const intervention of recipe) {\n if (intervention.settle !== undefined) await intervention.settle(target);\n }\n}\n\n/**\n * The identity of a recipe.\n *\n * Sorted, so the order tricks were composed in does not change the identity —\n * only which ones are present. Folded into `RenderIdentity`, which is what makes\n * a cross-recipe comparison name the recipe as its cause instead of inventing a\n * component to blame.\n */\nexport function recipeDigest(recipe: Recipe): Digest {\n return digestValue(recipe.map((intervention) => intervention.id).sort());\n}\n\n/** The recipe as a sentence, so the gap between image and product stays visible. */\nexport function describeRecipe(recipe: Recipe): string {\n return recipe.length === 0\n ? 'the subject was observed untouched'\n : `the subject was altered to be observable: ${recipe.map((i) => i.because).join('; ')}`;\n}\n"]}
1
+ {"version":3,"file":"stabilize.js","sourceRoot":"","sources":["../../src/format/stabilize.ts"],"names":[],"mappings":"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","sourcesContent":["import { digestValue, type Digest } from './hash.js';\nimport { tierReaches, type Tier } from './tier.js';\nimport { clockById, freezeClock, freezeClockAt, noFocus, noHover, reducedMotion, UNTOUCHED } from './drive.js';\nimport type { Intervention, ScreenshotOptions, SettleTarget } from './intervention.js';\nimport type { StepClock } from './steps.js';\nimport { waitForFonts, waitForImages } from './waits.js';\n\n/**\n * Interventions as separate, nameable tricks — not a fixed set of switches.\n *\n * A subject that is still changing cannot be compared, so every system of this\n * kind reaches into the page: it holds animations, waits for fonts, hides a\n * caret, suppresses scrollbars. Written as one struct of booleans that becomes a\n * closed vocabulary: adding a trick edits a type every caller depends on, and a\n * project with a need nobody anticipated has to fork.\n *\n * So each is a value. It carries an id, the tier that can actually observe what\n * it fixes, what it costs, and how it is applied — and a *recipe* is any list of\n * them. Two tricks may express the same intent through different mechanisms and\n * remain separate values, because they produce different images and choosing\n * between them is the caller's business.\n *\n * ## What this buys\n *\n * A recipe can be filtered by tier, so the structure-and-style rung waits for\n * nothing; composed from another project's tricks alongside these; digested, so\n * a run under another recipe than its baseline names the recipe as the cause;\n * and checked for tricks that fight each other.\n *\n * ## Where the damage lands\n *\n * Ordered by increasing cost, earliest sufficient option first:\n *\n * - **Outside the subject** — injected CSS, browser screenshot options. Nothing\n * in the product imports it and deleting the tool deletes the intervention.\n * - **Runtime substitution** — wrapping `Promise`, replacing a suspense\n * boundary. Buys the same knowledge without touching a component and is\n * deliberately not shipped here: it moves the damage from design into\n * semantics, where a difference caused by the patch cannot be told apart from\n * a difference caused by the code. Expressible as a trick if a project decides\n * the trade is worth it — that is the point of an open set.\n * - **A contract the subject implements** — a readiness marker. Real design\n * damage, and reserved for what the outside genuinely cannot know.\n *\n * ## Why this is in `format` and not in the raster package\n *\n * It was in `@variance-authority/raster` until 2026-08-06, on the reading that\n * holding a page still is something you do before you photograph it. That\n * reading was wrong in a way that cost correctness: an animation in flight is a\n * *computed style value*, so it reaches the cheap representation too — and the\n * allowlist excludes `animation-*` and `transition-*` precisely because it\n * assumed a snapshot is taken with animation already disabled. Nothing disabled\n * it. See {@link COLLECT_RECIPE} and ADR-0028.\n *\n * So a recipe is a render input on every tier, `EnvironmentInputs` carries its\n * digest, and the vocabulary belongs beside the key it is part of.\n */\n\n// Re-exported from here because this is where a reader arrives looking for the\n// vocabulary; [`intervention.ts`](./intervention.js) holds the declaration.\nexport type { Intervention, ScreenshotOptions, SettleTarget, Trick } from './intervention.js';\nexport { waitForFonts, waitForImages } from './waits.js';\n\n/**\n * Hand animations to the browser rather than emulating the hold in CSS.\n *\n * A browser disabling animations for a screenshot fast-forwards a finite\n * animation to completion — the state a user comes to rest on — and cancels an\n * infinite one to its initial frame, replaying it afterwards. That is the\n * behaviour worth having, and CSS cannot express it.\n */\nexport const holdAnimations: Intervention = {\n id: 'hold-animations',\n trick: 'hold',\n needs: 'layout',\n governs: 'animations',\n because: 'animations settled by the browser: finite fast-forwarded, infinite reset',\n screenshot: { animations: 'disabled' },\n};\n\n/**\n * The same intent without a browser that can do it, and it is not equivalent.\n *\n * Pins every animation at its first frame, so a fade-in is captured at the\n * moment it is invisible. `animation: none` is avoided deliberately — removing\n * an animation drops whatever layout its keyframes contribute, which changes the\n * page rather than stopping it.\n */\nexport const pinAnimations: Intervention = {\n id: 'pin-animations',\n trick: 'hold',\n needs: 'layout',\n governs: 'animations',\n because: 'animations pinned at their first frame, which is not where a user sees them',\n css:\n '*,*::before,*::after{' +\n 'animation-play-state:paused !important;' +\n 'animation-delay:-0.0001s !important;' +\n 'transition-duration:0s !important;' +\n 'transition-delay:0s !important;' +\n 'scroll-behavior:auto !important}',\n};\n\n/**\n * A stylesheet rather than the driver's own caret switch, deliberately.\n *\n * Playwright and its peers hide a caret by writing `caret-color` onto every\n * focusable element and putting it back afterwards, and putting it back leaves\n * `style=\"\"` where there was no `style` attribute at all. Any capture that reads\n * the DOM on both sides of a screenshot — which is what the in-place path does\n * to prove the subject held still — then sees markup that moved, and reports the\n * driver's housekeeping as the page being unstable. A page with one text input\n * is enough.\n *\n * So this hold owns the caret the way {@link pinAnimations} owns animations: one\n * stylesheet, installed before the subject is read, removed with the rest of the\n * recipe.\n */\nexport const hideCaret: Intervention = {\n id: 'hide-caret',\n trick: 'support',\n // A caret paints and does not lay out, so a tier that never rasterizes cannot\n // see it and must not pay to hide it.\n needs: 'raster',\n governs: 'caret',\n because: 'text caret hidden, because it blinks on its own schedule',\n css: '*{caret-color:transparent !important}',\n};\n\nexport const hideScrollbars: Intervention = {\n id: 'hide-scrollbars',\n trick: 'support',\n needs: 'layout',\n governs: 'scrollbars',\n because: 'scrollbars hidden, removing a platform and preference difference — and their width',\n css: '*{scrollbar-width:none !important}\\n*::-webkit-scrollbar{display:none !important}',\n};\n\n/**\n * Drop the pixels of images the page itself called decorative, keeping their boxes.\n *\n * The other half of blanking, and it lives here because of what it needs to\n * know. A driver rewriting responses (`@variance-authority/playwright`'s\n * `blank` rules) can decide by URL and by intrinsic size, which answers \"every\n * illustration over 40,000 pixels\" and answers it before the bytes are even\n * fetched. It cannot answer `role=\"presentation\"`, because a request carries no\n * idea which element wanted it — that fact exists only in the document, so the\n * trick that uses it is a stylesheet.\n *\n * `visibility:hidden` rather than `display:none`, and the distinction is the\n * entire design: a hidden element still occupies exactly the box it would have,\n * so a page whose column height comes from an image's intrinsic size is\n * unchanged. `display:none` would collapse it and report a layout regression\n * this tool caused.\n *\n * **Opt-in, and it belongs in no default recipe.** Every other trick here\n * removes something that is not part of the assertion — a caret, a scrollbar, an\n * animation mid-flight. This one removes page content, which is a judgement\n * about what a suite is for, and a default that quietly stopped watching every\n * `alt=\"\"` image would hide real regressions under a green run.\n */\nexport const hidePresentationalImages: Intervention = {\n id: 'hide-presentational-images',\n trick: 'support',\n // `layout` rather than `raster`, and the difference is whether it runs at all.\n // A raster-tier trick is filtered out of every *collection* — `tierOfProfile`\n // never returns `raster` — so it would only reach a page through a renderer\n // option, which is not where an operator configures their suite. A collection\n // sheet is in force when the document is serialized, so hiding it here is what\n // makes the pixels absent from the render.\n needs: 'layout',\n governs: 'presentational-images',\n because:\n 'images the page marked decorative hidden, keeping their boxes — real changes inside them ' +\n 'are not reported',\n css:\n 'img[role=\"presentation\"],img[alt=\"\"],[role=\"presentation\"] img,[role=\"none\"] img' +\n '{visibility:hidden !important}',\n};\n\nexport const INTERVENTIONS: readonly Intervention[] = [\n holdAnimations,\n pinAnimations,\n hideCaret,\n hideScrollbars,\n hidePresentationalImages,\n noHover,\n noFocus,\n reducedMotion,\n freezeClock,\n waitForFonts,\n waitForImages,\n];\n\nexport type Recipe = readonly Intervention[];\n\n/**\n * Nothing at all, and correct for the tier that reads structure and declared style.\n *\n * A font that has not loaded cannot change which rules match or what they\n * declare; an image that has not decoded cannot either. Waiting for both before\n * a structure-and-style hash buys nothing and costs the wait per subject, on the\n * rung the tier ladder exists to make cheap.\n */\nexport const SEMANTIC_RECIPE: Recipe = [];\n\n// TODO: a `<script>` inside a subject root runs in the render document under the\n// real clock, which no trick in this recipe holds.\n// TODO: a document is painted under this recipe whatever recipe it was read\n// under: a `RenderDocument` carries no collection recipe to follow.\n/**\n * What a document is painted under, and of {@link UNTOUCHED} only `no-focus`: a\n * collected `autofocus` takes focus again. The other three have nothing but a\n * carried script to act on: the printed date and the animation's place are in\n * the document already, its `@media` rules were resolved when it was read, and\n * nothing moves the pointer over it.\n */\nexport const LAYOUT_RECIPE: Recipe = [holdAnimations, hideScrollbars, noFocus, waitForFonts, waitForImages];\n\nexport const RASTER_RECIPE: Recipe = [...LAYOUT_RECIPE, hideCaret];\n\n/**\n * What is applied to a live page *before a subject is observed*, as opposed to\n * before it is painted.\n *\n * The two are different recipes and it is not a preference. {@link\n * holdAnimations} is a *screenshot option* — it asks the browser to settle\n * animations for the image it is about to take — and there is no screenshot at\n * collection time, so it would be a trick that silently does nothing. The CSS\n * variant is the one that works where nobody is holding a camera, which is the\n * whole reason both exist as separate values rather than as one switch.\n *\n * ## Why the cheap tier needs this at all\n *\n * The claim in [`ruleset.ts`](../rules/ruleset.ts) that `transition-*` and\n * `animation-*` \"describe a journey the snapshot does not contain\" is only true\n * if the snapshot is taken with the journey stopped. `transform`, `opacity`,\n * `filter`, `color` and every geometric longhand *are* admitted, and an\n * animation in flight moves all of them — so an unstabilized collection turns a\n * 300ms fade into a component-attributed regression with a real file name on it,\n * which is worse than an unexplained pixel diff because it is credible.\n *\n * Filtered by {@link forTier}, so jsdom applies none of it: with no layout\n * engine and no animation clock there is nothing to hold still, and paying a\n * `fonts.ready` wait per subject on the rung that exists to be cheap is exactly\n * the trade {@link SEMANTIC_RECIPE} refuses.\n */\nexport const COLLECT_RECIPE: Recipe = [\n pinAnimations,\n hideScrollbars,\n // Here as well as in {@link RASTER_RECIPE}, because the in-place path\n // photographs the live page and applies no screenshot options of its own: the\n // only caret hold that reaches that image is one installed here. A subject\n // holding a focused input otherwise disagrees with itself between two\n // screenshots taken 600ms apart, and is refused for blinking.\n hideCaret,\n ...UNTOUCHED,\n waitForFonts,\n waitForImages,\n];\n\n/**\n * The ids of {@link COLLECT_RECIPE}, as an absent `stabilize` means them. A list\n * replaces the recipe, so one trick is turned off with this list without it:\n * `stabilize: DEFAULT_STABILIZE.filter((id) => id !== 'freeze-clock')`.\n */\nexport const DEFAULT_STABILIZE: readonly string[] = COLLECT_RECIPE.map((intervention) => intervention.id);\n\n/**\n * Resolve a trick by the name a caller wrote down.\n *\n * The boundary a collection recipe crosses is a `page.evaluate`, and an\n * `Intervention` does not survive it — `settle` is a function. Ids do survive,\n * and the page holds this same registry, so a recipe travels as the list of\n * names it is. A name nothing answers to is a caller error worth failing on\n * rather than a trick to skip quietly: under-stabilizing is how a suite gets a\n * flake it has already paid to prevent.\n */\nexport function interventionById(id: string): Intervention | undefined {\n return INTERVENTIONS.find((intervention) => intervention.id === id) ?? clockById(id);\n}\n\n/**\n * A recipe from the names it travels as.\n *\n * Throws on a name nothing answers to, and names it. The quiet alternative —\n * skip what cannot be resolved — turns a typo in a config into a suite that is\n * one trick less stable than its operator believes, discovered later as a flake\n * they have already paid to prevent. Failing here costs one run and one reading\n * of the message.\n */\nexport function recipeOf(ids: readonly string[]): Recipe {\n return ids.map((id) => {\n const intervention = interventionById(id);\n if (intervention === undefined) {\n throw new Error(\n `no stabilization trick is called \\`${id}\\`; this build knows ` +\n INTERVENTIONS.map((known) => `\\`${known.id}\\``).join(', ') +\n ', and `freeze-clock@<instant>`',\n );\n }\n return intervention;\n });\n}\n\n/**\n * The recipe a host that drives the browser applies, checked before it opens one:\n * `stabilize` by id, or {@link COLLECT_RECIPE} when it is absent, with its clock\n * fixed at `now`, which travels in the clock's id and lands in the digest. Throws\n * on an unknown id, an instant with no zone, or a `now` with no clock to fix, so\n * the operator hears about it before a page is loaded, not at the first subject.\n */\nexport function recipeToDrive(options: { readonly stabilize?: readonly string[]; readonly now?: string }): Recipe {\n const recipe = options.stabilize === undefined ? COLLECT_RECIPE : recipeOf(options.stabilize);\n if (options.now === undefined) return recipe;\n const clock = freezeClockAt(options.now);\n if (!recipe.some((intervention) => intervention.driver?.clock !== undefined)) {\n throw new Error(\n `\\`now\\` is the instant \\`freeze-clock\\` fixes, and this recipe has no \\`freeze-clock\\`: ` +\n 'add it to `stabilize`, or leave `now` out',\n );\n }\n return recipe.map((intervention) => (intervention.driver?.clock === undefined ? intervention : clock));\n}\n\n/** Only the tricks a tier can observe the effect of. */\nexport function forTier(recipe: Recipe, tier: Tier): Recipe {\n return recipe.filter((intervention) => tierReaches(tier, intervention.needs));\n}\n\n/**\n * Tricks in a recipe that claim the same property.\n *\n * Returned rather than thrown, so a caller can decide — an override is a\n * legitimate composition, and silently applying both is not. Two tricks over one\n * property means one of them wins by accident of ordering, and which one is\n * invisible in the result.\n */\nexport function conflicts(recipe: Recipe): readonly { governs: string; ids: readonly string[] }[] {\n const byProperty = new Map<string, string[]>();\n\n for (const intervention of recipe) {\n byProperty.set(intervention.governs, [\n ...(byProperty.get(intervention.governs) ?? []),\n intervention.id,\n ]);\n }\n\n return [...byProperty.entries()]\n .filter(([, ids]) => ids.length > 1)\n .map(([governs, ids]) => ({ governs, ids }));\n}\n\nexport function recipeCss(recipe: Recipe): string {\n return recipe\n .map((intervention) => intervention.css)\n .filter((css): css is string => css !== undefined)\n .join('\\n');\n}\n\nexport function recipeScreenshot(recipe: Recipe): ScreenshotOptions {\n return recipe.reduce<ScreenshotOptions>(\n (options, intervention) => ({ ...options, ...intervention.screenshot }),\n {},\n );\n}\n\n/**\n * Every wait the recipe holds, in order. Each is one step on `steps`, named by\n * the intervention's id, so a run can say which wait a slow subject spent its\n * time in.\n */\nexport async function settleRecipe(recipe: Recipe, target: SettleTarget, steps?: StepClock): Promise<void> {\n for (const intervention of recipe) {\n const settle = intervention.settle;\n if (settle === undefined) continue;\n await (steps === undefined ? settle(target) : steps.time(intervention.id, async () => settle(target)));\n }\n}\n\n/**\n * The identity of a recipe.\n *\n * Sorted, so the order tricks were composed in does not change the identity —\n * only which ones are present. Folded into `RenderIdentity`, which is what makes\n * a cross-recipe comparison name the recipe as its cause instead of inventing a\n * component to blame.\n */\nexport function recipeDigest(recipe: Recipe): Digest {\n return digestValue(recipe.map((intervention) => intervention.id).sort());\n}\n\n/** The recipe as a sentence, so the gap between image and product stays visible. */\nexport function describeRecipe(recipe: Recipe): string {\n return recipe.length === 0\n ? 'the subject was observed untouched'\n : `the subject was altered to be observable: ${recipe.map((i) => i.because).join('; ')}`;\n}\n"]}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * How long each step of reading one subject took.
3
+ *
4
+ * A step is a span of work the code already brackets as one phase — a
5
+ * navigation, one stabilization wait, the screenshot, the decode — named by the
6
+ * phase's own name. Each owner times only its own steps and hands the rest on
7
+ * as data, so a subject's steps never overlap and never sum to more than what
8
+ * the subject cost. A step that did not run is absent, never a zero: zero means
9
+ * it ran and finished under the clock's resolution.
10
+ *
11
+ * Durations only, on a monotonic clock, and the wall time of the span: with
12
+ * several worlds collecting side by side a step includes what the others cost
13
+ * it, which is what it cost this run. Steps describe a reading and are part of
14
+ * no digest, so a timing can never move an image, a key or a verdict.
15
+ */
16
+ /** One step of one subject: the phase's own name and how long its span took. */
17
+ export interface Step {
18
+ readonly step: string;
19
+ /** Wall time of the span, in milliseconds, to a tenth. */
20
+ readonly ms: number;
21
+ }
22
+ /** One step over a run: summed over the subjects that ran it, and how many did. */
23
+ export interface StepTotal {
24
+ readonly step: string;
25
+ readonly ms: number;
26
+ readonly subjects: number;
27
+ }
28
+ /** Where one subject's steps are taken, in the order they ran; each owner times its own and passes the clock on. */
29
+ export interface StepClock {
30
+ /** Run `job` as `step`. Taken whether it resolves or throws. */
31
+ time<T>(step: string, job: () => Promise<T> | T): Promise<T>;
32
+ /** Steps another owner timed, appended as they arrive, each name behind `prefix`. */
33
+ add(steps: readonly Step[] | undefined, prefix?: string): void;
34
+ /** The same list, with every name this view takes behind `prefix`. */
35
+ within(prefix: string): StepClock;
36
+ readonly taken: readonly Step[];
37
+ }
38
+ /** `now` is the owner's monotonic clock, in milliseconds: `performance.now` wherever it runs. */
39
+ export declare function stepClock(now: () => number): StepClock;
40
+ /** `job` as `step` on `steps`, or just `job` when nothing is timing it. */
41
+ export declare function timeOn<T>(steps: StepClock | undefined, step: string, job: () => Promise<T> | T): Promise<T>;
42
+ /** Each step's total over every subject that ran it, by name in code-unit order. */
43
+ export declare function totalsOf(subjects: readonly (readonly Step[] | undefined)[]): readonly StepTotal[];
44
+ //# sourceMappingURL=steps.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * How long each step of reading one subject took.
3
+ *
4
+ * A step is a span of work the code already brackets as one phase — a
5
+ * navigation, one stabilization wait, the screenshot, the decode — named by the
6
+ * phase's own name. Each owner times only its own steps and hands the rest on
7
+ * as data, so a subject's steps never overlap and never sum to more than what
8
+ * the subject cost. A step that did not run is absent, never a zero: zero means
9
+ * it ran and finished under the clock's resolution.
10
+ *
11
+ * Durations only, on a monotonic clock, and the wall time of the span: with
12
+ * several worlds collecting side by side a step includes what the others cost
13
+ * it, which is what it cost this run. Steps describe a reading and are part of
14
+ * no digest, so a timing can never move an image, a key or a verdict.
15
+ */
16
+ import { codeUnitOrder } from '../segment/index.js';
17
+ /** `now` is the owner's monotonic clock, in milliseconds: `performance.now` wherever it runs. */
18
+ export function stepClock(now) {
19
+ return viewOf([], now, '');
20
+ }
21
+ function viewOf(taken, now, prefix) {
22
+ return {
23
+ async time(step, job) {
24
+ const started = now();
25
+ try {
26
+ return await job();
27
+ }
28
+ finally {
29
+ taken.push({ step: prefix + step, ms: tenths(now() - started) });
30
+ }
31
+ },
32
+ add(steps, more = '') {
33
+ for (const { step, ms } of steps ?? [])
34
+ taken.push({ step: prefix + more + step, ms });
35
+ },
36
+ within(more) {
37
+ return viewOf(taken, now, prefix + more);
38
+ },
39
+ taken,
40
+ };
41
+ }
42
+ /** `job` as `step` on `steps`, or just `job` when nothing is timing it. */
43
+ export async function timeOn(steps, step, job) {
44
+ return steps === undefined ? job() : steps.time(step, job);
45
+ }
46
+ /** Each step's total over every subject that ran it, by name in code-unit order. */
47
+ export function totalsOf(subjects) {
48
+ const totals = new Map();
49
+ for (const steps of subjects) {
50
+ const seen = new Set();
51
+ for (const { step, ms } of steps ?? []) {
52
+ const total = totals.get(step) ?? { ms: 0, subjects: 0 };
53
+ total.ms += ms;
54
+ if (!seen.has(step))
55
+ total.subjects += 1;
56
+ seen.add(step);
57
+ totals.set(step, total);
58
+ }
59
+ }
60
+ return [...totals]
61
+ .sort(([left], [right]) => codeUnitOrder(left, right))
62
+ .map(([step, total]) => ({ step, ms: tenths(total.ms), subjects: total.subjects }));
63
+ }
64
+ function tenths(ms) {
65
+ return Math.round(ms * 10) / 10;
66
+ }
67
+ //# sourceMappingURL=steps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steps.js","sourceRoot":"","sources":["../../src/format/steps.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * How long each step of reading one subject took.\n *\n * A step is a span of work the code already brackets as one phase — a\n * navigation, one stabilization wait, the screenshot, the decode — named by the\n * phase's own name. Each owner times only its own steps and hands the rest on\n * as data, so a subject's steps never overlap and never sum to more than what\n * the subject cost. A step that did not run is absent, never a zero: zero means\n * it ran and finished under the clock's resolution.\n *\n * Durations only, on a monotonic clock, and the wall time of the span: with\n * several worlds collecting side by side a step includes what the others cost\n * it, which is what it cost this run. Steps describe a reading and are part of\n * no digest, so a timing can never move an image, a key or a verdict.\n */\n\nimport { codeUnitOrder } from '../segment/index.js';\n\n/** One step of one subject: the phase's own name and how long its span took. */\nexport interface Step {\n readonly step: string;\n /** Wall time of the span, in milliseconds, to a tenth. */\n readonly ms: number;\n}\n\n/** One step over a run: summed over the subjects that ran it, and how many did. */\nexport interface StepTotal {\n readonly step: string;\n readonly ms: number;\n readonly subjects: number;\n}\n\n/** Where one subject's steps are taken, in the order they ran; each owner times its own and passes the clock on. */\nexport interface StepClock {\n /** Run `job` as `step`. Taken whether it resolves or throws. */\n time<T>(step: string, job: () => Promise<T> | T): Promise<T>;\n /** Steps another owner timed, appended as they arrive, each name behind `prefix`. */\n add(steps: readonly Step[] | undefined, prefix?: string): void;\n /** The same list, with every name this view takes behind `prefix`. */\n within(prefix: string): StepClock;\n readonly taken: readonly Step[];\n}\n\n/** `now` is the owner's monotonic clock, in milliseconds: `performance.now` wherever it runs. */\nexport function stepClock(now: () => number): StepClock {\n return viewOf([], now, '');\n}\n\nfunction viewOf(taken: Step[], now: () => number, prefix: string): StepClock {\n return {\n async time(step, job) {\n const started = now();\n try {\n return await job();\n } finally {\n taken.push({ step: prefix + step, ms: tenths(now() - started) });\n }\n },\n add(steps, more = '') {\n for (const { step, ms } of steps ?? []) taken.push({ step: prefix + more + step, ms });\n },\n within(more) {\n return viewOf(taken, now, prefix + more);\n },\n taken,\n };\n}\n\n/** `job` as `step` on `steps`, or just `job` when nothing is timing it. */\nexport async function timeOn<T>(\n steps: StepClock | undefined,\n step: string,\n job: () => Promise<T> | T,\n): Promise<T> {\n return steps === undefined ? job() : steps.time(step, job);\n}\n\n/** Each step's total over every subject that ran it, by name in code-unit order. */\nexport function totalsOf(subjects: readonly (readonly Step[] | undefined)[]): readonly StepTotal[] {\n const totals = new Map<string, { ms: number; subjects: number }>();\n for (const steps of subjects) {\n const seen = new Set<string>();\n for (const { step, ms } of steps ?? []) {\n const total = totals.get(step) ?? { ms: 0, subjects: 0 };\n total.ms += ms;\n if (!seen.has(step)) total.subjects += 1;\n seen.add(step);\n totals.set(step, total);\n }\n }\n return [...totals]\n .sort(([left], [right]) => codeUnitOrder(left, right))\n .map(([step, total]) => ({ step, ms: tenths(total.ms), subjects: total.subjects }));\n}\n\nfunction tenths(ms: number): number {\n return Math.round(ms * 10) / 10;\n}\n"]}