@motion-proto/live-tokens 0.77.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/live-tokens-check-compliance/SKILL.md +60 -29
- package/.claude/skills/live-tokens-create-component/SKILL.md +23 -28
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +61 -0
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +6 -15
- package/.claude/skills/live-tokens-create-page/SKILL.md +9 -6
- package/.claude/skills/live-tokens-create-theme/references/design-directions.md +1 -1
- package/.claude/skills/live-tokens-pick-component/SKILL.md +3 -3
- package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +1 -1
- package/.claude/skills/live-tokens-set-geometry/SKILL.md +1 -1
- package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +1 -1
- package/CHANGELOG.md +284 -0
- package/README.md +9 -15
- package/bin/check-component.mjs +152 -551
- package/bin/check-page.mjs +56 -420
- package/bin/cli.mjs +83 -15
- package/bin/contractRunner.mjs +380 -120
- package/bin/lib/catalogue.mjs +161 -34
- package/bin/lib/componentSource.mjs +152 -0
- package/bin/lib/cssValues.mjs +9 -0
- package/bin/lib/dataDir.mjs +126 -0
- package/bin/lib/findings.mjs +103 -12
- package/bin/lib/fixers.mjs +64 -0
- package/bin/lib/geometry.mjs +92 -0
- package/bin/lib/pageRoutes.mjs +309 -0
- package/bin/lib/pageSource.mjs +230 -0
- package/bin/lib/report.mjs +57 -59
- package/bin/lib/tokenVocabulary.mjs +104 -34
- package/bin/rules/componentStructure.mjs +344 -0
- package/bin/rules/componentUse.mjs +313 -0
- package/bin/rules/importsAndRoutes.mjs +136 -0
- package/bin/rules/testRuns.mjs +122 -0
- package/bin/rules/tokens.mjs +363 -0
- package/bin/setup-claude.mjs +1 -2
- package/dist-plugin/{chunk-PDNL4NC5.js → chunk-D4WRIKEZ.js} +7 -2
- package/dist-plugin/{chunk-SWXRVZKT.js → chunk-REBHE3ZM.js} +414 -1
- package/dist-plugin/index.cjs +432 -15
- package/dist-plugin/index.js +8 -9
- package/dist-plugin/migrateData/index.cjs +422 -4
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/setColors/index.cjs +414 -1
- package/dist-plugin/setColors/index.d.cts +1 -1
- package/dist-plugin/setColors/index.d.ts +1 -1
- package/dist-plugin/setColors/index.js +1 -1
- package/dist-plugin/setGeometry/index.cjs +423 -13
- package/dist-plugin/setGeometry/index.d.cts +3 -3
- package/dist-plugin/setGeometry/index.d.ts +3 -3
- package/dist-plugin/setGeometry/index.js +10 -13
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/{themeTypes-BxRtuN5V.d.cts → themeTypes-B8_Idrp4.d.cts} +2 -2
- package/dist-plugin/{themeTypes-BxRtuN5V.d.ts → themeTypes-B8_Idrp4.d.ts} +2 -2
- package/package.json +6 -4
- package/src/editor/component-editor/CalloutEditor.svelte +2 -2
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +14 -15
- package/src/editor/component-editor/CornerBadgeEditor.svelte +14 -14
- package/src/editor/component-editor/DialogEditor.svelte +5 -5
- package/src/editor/component-editor/InlineEditActionsEditor.svelte +2 -2
- package/src/editor/component-editor/RadioButtonEditor.svelte +21 -21
- package/src/editor/component-editor/SectionDividerEditor.svelte +7 -7
- package/src/editor/component-editor/SegmentedControlEditor.svelte +5 -5
- package/src/editor/component-editor/SideNavigationEditor.svelte +25 -25
- package/src/editor/component-editor/TabBarEditor.svelte +6 -6
- package/src/editor/component-editor/TableEditor.svelte +6 -6
- package/src/editor/component-editor/ToggleEditor.svelte +2 -2
- package/src/editor/component-editor/index.ts +3 -0
- package/src/editor/component-editor/registry.ts +57 -1
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +14 -14
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +3 -0
- package/src/editor/component-editor/scaffolding/types.ts +15 -0
- package/src/editor/core/components/adjustAliases.ts +4 -4
- package/src/editor/core/components/aliasKinds.ts +20 -19
- package/src/editor/core/sketch/sketchLayer.ts +3 -3
- package/src/editor/core/themes/migrateComponentConfig.ts +14 -6
- package/src/editor/core/themes/migrations/2026-09-13-badge-brand.ts +42 -0
- package/src/editor/core/themes/migrations/2026-09-13-collapsiblesection-open.ts +33 -0
- package/src/editor/core/themes/migrations/2026-09-13-cornerbadge-prefix.ts +70 -0
- package/src/editor/core/themes/migrations/2026-09-13-hairline.ts +92 -0
- package/src/editor/core/themes/migrations/2026-09-13-indicator.ts +54 -0
- package/src/editor/core/themes/migrations/2026-09-13-sectiondivider-surface.ts +36 -0
- package/src/editor/core/themes/migrations/2026-09-13-selected-state.ts +124 -0
- package/src/editor/core/themes/migrations/2026-09-13-toggle-label.ts +31 -0
- package/src/editor/core/themes/migrations/index.ts +16 -0
- package/src/editor/core/themes/themeService.ts +3 -3
- package/src/editor/core/themes/themeTypes.ts +13 -15
- package/src/editor/docs/Docs.svelte +1 -1
- package/src/editor/docs/content/light-and-dark.md +3 -3
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/index.ts +1 -0
- package/src/editor/overlay/ColumnsOverlay.svelte +1 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +16 -0
- package/src/editor/overlay/LiveTokensRouter.svelte +1 -0
- package/src/editor/skill-atlas/SkillAtlas.svelte +4 -4
- package/src/editor/skill-atlas/TreeNodeCard.svelte +4 -4
- package/src/editor/skill-atlas/skillSources.generated.ts +11 -14
- package/src/editor/skill-atlas/skillTrees.ts +1 -3
- package/src/editor/skill-atlas/trees/check-compliance.ts +258 -93
- package/src/editor/skill-atlas/trees/create-component.ts +73 -62
- package/src/editor/skill-atlas/trees/create-page.ts +54 -33
- package/src/editor/skill-atlas/trees/pick-component.ts +3 -3
- package/src/editor/skill-atlas/trees/set-geometry.ts +1 -1
- package/src/editor/ui/UIPaletteSelector.svelte +11 -15
- package/src/editor/ui/variantScales.ts +8 -8
- package/src/live-tokens/data/themes/autumn.json +188 -188
- package/src/live-tokens/data/themes/halloween.json +188 -188
- package/src/live-tokens/data/themes/midnight-study.json +245 -245
- package/src/live-tokens/data/themes/ocean.json +188 -188
- package/src/live-tokens/data/themes/royal-velvet.json +188 -188
- package/src/live-tokens/data/themes/sketchy.json +188 -188
- package/src/live-tokens/data/themes/spring-meadow.json +188 -188
- package/src/live-tokens/data/themes/sunset.json +188 -188
- package/src/system/components/Badge.svelte +27 -23
- package/src/system/components/Button.svelte +13 -7
- package/src/system/components/Callout.svelte +16 -11
- package/src/system/components/Card.svelte +22 -15
- package/src/system/components/CodeSnippet.svelte +10 -6
- package/src/system/components/CollapsibleSection.svelte +68 -63
- package/src/system/components/CornerBadge.svelte +78 -72
- package/src/system/components/Dialog.svelte +21 -18
- package/src/system/components/IconButton.svelte +13 -9
- package/src/system/components/Image.svelte +14 -8
- package/src/system/components/ImageLightbox.svelte +10 -6
- package/src/system/components/InlineEditActions.svelte +17 -14
- package/src/system/components/Input.svelte +13 -7
- package/src/system/components/MenuSelect.svelte +13 -7
- package/src/system/components/Notification.svelte +17 -11
- package/src/system/components/Panel.svelte +13 -6
- package/src/system/components/ProgressBar.svelte +10 -5
- package/src/system/components/RadioButton.svelte +33 -30
- package/src/system/components/SectionDivider.svelte +28 -21
- package/src/system/components/SegmentedControl.svelte +27 -23
- package/src/system/components/SideNavigation.svelte +175 -171
- package/src/system/components/Slider.svelte +11 -7
- package/src/system/components/TabBar.svelte +53 -49
- package/src/system/components/Table.svelte +20 -15
- package/src/system/components/Toggle.svelte +14 -10
- package/src/system/components/Tooltip.svelte +10 -6
- package/src/system/styles/CONVENTIONS.md +3 -4
- package/src/testing-js/chunk-3UKGXCDL.js +48 -0
- package/src/testing-js/chunk-3UKGXCDL.js.map +1 -0
- package/src/testing-js/{chunk-ZMSX6CXR.js → chunk-NB3NZRBM.js} +13 -2
- package/src/testing-js/chunk-NB3NZRBM.js.map +1 -0
- package/src/testing-js/{chunk-FAFOAWYL.js → chunk-Q3YIAAG3.js} +17 -3
- package/src/testing-js/chunk-Q3YIAAG3.js.map +1 -0
- package/src/testing-js/{chunk-7TI7Z6Y6.js → chunk-U7OJE5DU.js} +19 -19
- package/src/testing-js/chunk-U7OJE5DU.js.map +1 -0
- package/src/testing-js/{chunk-L73N4NSO.js → chunk-WIZ6W7UT.js} +2 -2
- package/src/testing-js/{chunk-4JQX6WWL.js → chunk-ZMZQZ33J.js} +474 -166
- package/src/testing-js/chunk-ZMZQZ33J.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +2 -2
- package/src/testing-js/component-behavior.contract.js +155 -0
- package/src/testing-js/component-behavior.contract.js.map +1 -0
- package/src/testing-js/component-editor.contract.js +7 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +15 -11
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +50 -4
- package/src/testing-js/index.js +32 -8
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +867 -0
- package/src/testing-js/page-compliance.contract.js.map +1 -0
- package/src/testing-js/registry.contract.js +5 -3
- package/src/testing-js/registry.contract.js.map +1 -1
- package/src/testing-js/{vitest-BE6uGF31.d.ts → vitest-BMLIbDs2.d.ts} +29 -3
- package/src/testing-js/vitest.d.ts +1 -1
- package/src/testing-js/vitest.js +3 -2
- package/template/README.md +16 -9
- package/template/package.json +2 -2
- package/template/src/pages/Home.svelte +13 -0
- package/.claude/skills/live-tokens-create-component/SKILL copy.md +0 -196
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +0 -98
- package/src/editor/skill-atlas/trees/fix-findings.ts +0 -469
- package/src/testing-js/chunk-4JQX6WWL.js.map +0 -1
- package/src/testing-js/chunk-7TI7Z6Y6.js.map +0 -1
- package/src/testing-js/chunk-FAFOAWYL.js.map +0 -1
- package/src/testing-js/chunk-ZMSX6CXR.js.map +0 -1
- /package/src/testing-js/{chunk-L73N4NSO.js.map → chunk-WIZ6W7UT.js.map} +0 -0
package/bin/lib/findings.mjs
CHANGED
|
@@ -20,6 +20,15 @@ export function lineOf(text, index) {
|
|
|
20
20
|
return line;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** The line a `"<key>":` sits on. The quote on both sides of the key makes
|
|
24
|
+
* this exact by construction — `"--card-default-body"` cannot match inside
|
|
25
|
+
* `"--card-default-body-padding"`, unlike a bare substring search. */
|
|
26
|
+
export function findJsonKeyLine(text, key) {
|
|
27
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
const m = new RegExp(`"${escaped}"\\s*:`).exec(text);
|
|
29
|
+
return m ? lineOf(text, m.index) : 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
export function readChecksConfig(root) {
|
|
24
33
|
const path = join(root, 'live-tokens.config.json');
|
|
25
34
|
if (!existsSync(path)) return {};
|
|
@@ -52,7 +61,7 @@ export function isExcluded(relPath, root) {
|
|
|
52
61
|
* Unrecognised flags are returned in `rest` for the caller to handle.
|
|
53
62
|
*/
|
|
54
63
|
export function parseCheckFlags(argv) {
|
|
55
|
-
const opts = { off: [], warn: [], error: [], strict: false, json: false, tests: false, rest: [] };
|
|
64
|
+
const opts = { off: [], warn: [], error: [], strict: false, json: false, tests: false, noFix: false, rest: [] };
|
|
56
65
|
for (const arg of argv) {
|
|
57
66
|
const m = arg.match(/^--(off|warn|error)=(.+)$/);
|
|
58
67
|
if (m) {
|
|
@@ -60,17 +69,43 @@ export function parseCheckFlags(argv) {
|
|
|
60
69
|
} else if (arg === '--strict') opts.strict = true;
|
|
61
70
|
else if (arg === '--json') opts.json = true;
|
|
62
71
|
else if (arg === '--tests') opts.tests = true;
|
|
72
|
+
else if (arg === '--no-fix') opts.noFix = true;
|
|
63
73
|
else opts.rest.push(arg);
|
|
64
74
|
}
|
|
65
75
|
return opts;
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
/**
|
|
79
|
+
* One checker's rule table, keyed in `order`, from the modules under
|
|
80
|
+
* `bin/rules/` that define its rules. An id on one side and not the other
|
|
81
|
+
* throws when the checker loads.
|
|
82
|
+
*/
|
|
83
|
+
export function assembleRules(order, ...tables) {
|
|
84
|
+
const defined = Object.assign({}, ...tables);
|
|
85
|
+
const stray = [...order.filter((id) => !defined[id]), ...Object.keys(defined).filter((id) => !order.includes(id))];
|
|
86
|
+
if (stray.length > 0) throw new Error(`rule order and rule modules disagree on: ${stray.join(', ')}`);
|
|
87
|
+
return Object.fromEntries(order.map((id) => [id, defined[id]]));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The config entry that records a deliberate decision to keep a finding.
|
|
92
|
+
*
|
|
93
|
+
* A page or CSS file drops out of discovery, which is narrower than turning a
|
|
94
|
+
* rule off across the project. Everything else steps its rule down one notch,
|
|
95
|
+
* since a rule already resolved to `warn` is not silenced by `warn`.
|
|
96
|
+
*/
|
|
97
|
+
function exceptionFor(finding, severity, exclude) {
|
|
98
|
+
return exclude
|
|
99
|
+
? { checks: { exclude: [finding.file] } }
|
|
100
|
+
: { checks: { rules: { [finding.rule]: severity === 'error' ? 'warn' : 'off' } } };
|
|
101
|
+
}
|
|
102
|
+
|
|
68
103
|
/** The same last-wins resolution `applySeverity` applies per finding, exposed
|
|
69
104
|
* standalone so coverage (which has no findings to attach a severity to, but
|
|
70
105
|
* still has to honor `--off`) can ask the same question. */
|
|
71
106
|
export function resolveRuleSeverity(ruleId, rules, opts = {}, config = {}) {
|
|
72
107
|
const configured = config.rules ?? {};
|
|
73
|
-
let severity = rules[ruleId] ?? 'error';
|
|
108
|
+
let severity = rules[ruleId]?.severity ?? 'error';
|
|
74
109
|
if (SEVERITIES.includes(configured[ruleId])) severity = configured[ruleId];
|
|
75
110
|
if (opts.off?.includes(ruleId)) severity = 'off';
|
|
76
111
|
if (opts.warn?.includes(ruleId)) severity = 'warn';
|
|
@@ -80,16 +115,28 @@ export function resolveRuleSeverity(ruleId, rules, opts = {}, config = {}) {
|
|
|
80
115
|
}
|
|
81
116
|
|
|
82
117
|
/**
|
|
83
|
-
* Resolve each finding's severity and drop the ones turned off
|
|
84
|
-
* `
|
|
118
|
+
* Resolve each finding's severity and drop the ones turned off, then attach
|
|
119
|
+
* what a repair needs: how to make it (`guidance`), how far code can take it
|
|
120
|
+
* (`repair`), and the config entry that records a decision to keep it
|
|
121
|
+
* (`exception`).
|
|
122
|
+
*
|
|
123
|
+
* `rules` maps rule id to `{ severity, repair, guidance }`. A rule's `repair`
|
|
124
|
+
* is the ceiling: a finding that arrives carrying its own has already lowered
|
|
125
|
+
* it, because its context is more ambiguous than the rule's.
|
|
85
126
|
*/
|
|
86
|
-
export function applySeverity(findings, rules, opts = {}, config = {},
|
|
127
|
+
export function applySeverity(findings, rules, opts = {}, config = {}, { exclude = false } = {}) {
|
|
87
128
|
return findings
|
|
88
|
-
.map((f) =>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
129
|
+
.map((f) => {
|
|
130
|
+
const severity = resolveRuleSeverity(f.rule, rules, opts, config);
|
|
131
|
+
const rule = rules[f.rule];
|
|
132
|
+
return {
|
|
133
|
+
...f,
|
|
134
|
+
severity,
|
|
135
|
+
...(rule?.guidance ? { guidance: rule.guidance } : {}),
|
|
136
|
+
repair: f.repair ?? rule?.repair ?? 'authored',
|
|
137
|
+
exception: exceptionFor(f, severity, exclude),
|
|
138
|
+
};
|
|
139
|
+
})
|
|
93
140
|
.filter((f) => f.severity !== 'off');
|
|
94
141
|
}
|
|
95
142
|
|
|
@@ -144,11 +191,55 @@ export function formatFindings(findings, { label, checked = 0 } = {}) {
|
|
|
144
191
|
return lines.join('\n');
|
|
145
192
|
}
|
|
146
193
|
|
|
147
|
-
export function toJson(findings, { label, checked = 0, coverage } = {}) {
|
|
194
|
+
export function toJson(findings, { label, checked = 0, coverage, fix } = {}) {
|
|
148
195
|
const { errors, warnings } = countBySeverity(findings);
|
|
149
196
|
return JSON.stringify(
|
|
150
|
-
{ check: label, checked, errors, warnings, findings, ...(coverage ? { coverage } : {}) },
|
|
197
|
+
{ check: label, checked, errors, warnings, ...(fix ? { fix } : {}), findings, ...(coverage ? { coverage } : {}) },
|
|
151
198
|
null,
|
|
152
199
|
2,
|
|
153
200
|
);
|
|
154
201
|
}
|
|
202
|
+
|
|
203
|
+
/** The unresolved property names a `contract-alias` finding lists, read off the
|
|
204
|
+
* first line: a Playwright or Vitest stack follows on the lines after it.
|
|
205
|
+
* `assertAliasesResolve` is the only obligation that lists them; every other
|
|
206
|
+
* `contract-alias` message (an empty shipped config, say) misses and is left
|
|
207
|
+
* alone. */
|
|
208
|
+
const UNRESOLVED_ALIASES = /aliases resolve to nothing at the root: (.+)/;
|
|
209
|
+
|
|
210
|
+
/** `component-configs/<id>/default.json`, the file both rules name. */
|
|
211
|
+
const CONFIG_FILE = /component-configs[/\\]([^/\\]+)[/\\]default\.json$/;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* One broken alias, one finding.
|
|
215
|
+
*
|
|
216
|
+
* A `default.json` alias naming something the vocabulary lacks is a
|
|
217
|
+
* `config-token` finding from plain Node, and the same string makes the
|
|
218
|
+
* property resolve to nothing at the root, which the browser reports as
|
|
219
|
+
* `contract-alias`. Under `--tests` both run, so the merge drops the browser's
|
|
220
|
+
* copy when every name it lists already carries a `config-token` finding for
|
|
221
|
+
* the same component. A `contract-alias` naming a token the vocabulary knows,
|
|
222
|
+
* declared, spelled correctly, and still unresolved, is a different defect and
|
|
223
|
+
* stays.
|
|
224
|
+
*/
|
|
225
|
+
export function dedupeAliasFindings(findings) {
|
|
226
|
+
const broken = new Map();
|
|
227
|
+
for (const f of findings) {
|
|
228
|
+
if (f.rule !== 'config-token') continue;
|
|
229
|
+
const id = CONFIG_FILE.exec(f.file ?? '')?.[1];
|
|
230
|
+
const property = f.details?.property;
|
|
231
|
+
if (!id || !property) continue;
|
|
232
|
+
if (!broken.has(id)) broken.set(id, new Set());
|
|
233
|
+
broken.get(id).add(property);
|
|
234
|
+
}
|
|
235
|
+
if (broken.size === 0) return findings;
|
|
236
|
+
return findings.filter((f) => {
|
|
237
|
+
if (f.rule !== 'contract-alias') return true;
|
|
238
|
+
const id = CONFIG_FILE.exec(f.file ?? '')?.[1];
|
|
239
|
+
const known = id ? broken.get(id) : undefined;
|
|
240
|
+
if (!known) return true;
|
|
241
|
+
const listed = UNRESOLVED_ALIASES.exec(String(f.message ?? '').split('\n')[0])?.[1];
|
|
242
|
+
if (!listed) return true;
|
|
243
|
+
return !listed.split(',').map((name) => name.trim()).filter(Boolean).every((name) => known.has(name));
|
|
244
|
+
});
|
|
245
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Applies the patches an `auto` finding already computed in its own
|
|
2
|
+
// `details.patch`. This module owns none of the per-rule repair logic — that
|
|
3
|
+
// lives beside each rule, in bin/rules/, pageSource.mjs, geometry.mjs, and
|
|
4
|
+
// catalogue.mjs, since only the rule that found the fault knows its shape.
|
|
5
|
+
// This module only knows how to write one down safely.
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
/** The character offset where 1-based `line` begins. The inverse of
|
|
11
|
+
* `lineOf` in findings.mjs. */
|
|
12
|
+
function offsetOfLine(text, line) {
|
|
13
|
+
let n = 1;
|
|
14
|
+
for (let i = 0; i < text.length; i++) {
|
|
15
|
+
if (n === line) return i;
|
|
16
|
+
if (text[i] === '\n') n++;
|
|
17
|
+
}
|
|
18
|
+
return text.length;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Writes every `auto` finding's patch, one file at a time.
|
|
23
|
+
*
|
|
24
|
+
* A patch applies at the first occurrence of `from` at or after the finding's
|
|
25
|
+
* own line: the file has moved since the finding was computed, or another
|
|
26
|
+
* patch in the same file shifted text, so a patch whose `from` cannot be
|
|
27
|
+
* found there is not applied and comes back in `skipped` instead of guessing
|
|
28
|
+
* at a different occurrence. Findings within one file apply in line order;
|
|
29
|
+
* none of the four fixers inserts or removes a newline, so an earlier
|
|
30
|
+
* patch's line never moves a later one's.
|
|
31
|
+
*/
|
|
32
|
+
export function applyFixes(findings, root) {
|
|
33
|
+
const byFile = new Map();
|
|
34
|
+
for (const f of findings) {
|
|
35
|
+
if (f.repair !== 'auto' || !f.details?.patch) continue;
|
|
36
|
+
if (!byFile.has(f.file)) byFile.set(f.file, []);
|
|
37
|
+
byFile.get(f.file).push(f);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const applied = [];
|
|
41
|
+
const skipped = [];
|
|
42
|
+
for (const [file, list] of byFile) {
|
|
43
|
+
const path = join(root, file);
|
|
44
|
+
if (!existsSync(path)) {
|
|
45
|
+
skipped.push(...list);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
let text = readFileSync(path, 'utf8');
|
|
49
|
+
let changed = false;
|
|
50
|
+
for (const finding of list.sort((a, b) => a.line - b.line)) {
|
|
51
|
+
const { from, to } = finding.details.patch;
|
|
52
|
+
const at = text.indexOf(from, offsetOfLine(text, finding.line));
|
|
53
|
+
if (at === -1) {
|
|
54
|
+
skipped.push(finding);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
text = text.slice(0, at) + to + text.slice(at + from.length);
|
|
58
|
+
changed = true;
|
|
59
|
+
applied.push(finding);
|
|
60
|
+
}
|
|
61
|
+
if (changed) writeFileSync(path, text);
|
|
62
|
+
}
|
|
63
|
+
return { applied, skipped };
|
|
64
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Which token a raw dimension should have been, computed rather than guessed.
|
|
2
|
+
//
|
|
3
|
+
// `dimension-literal` fires on a declaration the checkers already flagged; this
|
|
4
|
+
// module answers the follow-up a repair needs: what the literal measures, which
|
|
5
|
+
// step of its scale sits nearest, and whether that step is unique. A tie is the
|
|
6
|
+
// one case a person has to settle, so it is reported rather than resolved.
|
|
7
|
+
|
|
8
|
+
import { stripVarFallbacks } from './cssValues.mjs';
|
|
9
|
+
|
|
10
|
+
const REM_PX = 16;
|
|
11
|
+
|
|
12
|
+
// The same literals `hasDimensionLiteral` flags: px and rem, no leading sign.
|
|
13
|
+
const LITERAL = /(?<![\w.-])(\d*\.?\d+)(px|rem)\b/g;
|
|
14
|
+
|
|
15
|
+
const round = (n) => Math.round(n * 1000) / 1000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A token's value in pixels, or null when it is not a single length. A shadow
|
|
19
|
+
* step (`1px 1px 2px hsla(...)`) and `--space-full: 100%` both measure nothing
|
|
20
|
+
* a literal can be matched against, so neither becomes a candidate.
|
|
21
|
+
*/
|
|
22
|
+
function pixelsOf(value) {
|
|
23
|
+
const m = /^\s*(\d*\.?\d+)(px|rem)?\s*$/.exec(value);
|
|
24
|
+
if (!m) return null;
|
|
25
|
+
const n = parseFloat(m[1]);
|
|
26
|
+
if (!m[2]) return n === 0 ? 0 : null;
|
|
27
|
+
return m[2] === 'rem' ? n * REM_PX : n;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The token scale a CSS property draws its lengths from, for check-page. */
|
|
31
|
+
export function geometryScaleOfProperty(prop) {
|
|
32
|
+
if (/-radius$/.test(prop)) return 'radius';
|
|
33
|
+
if (/shadow$/.test(prop)) return 'shadow';
|
|
34
|
+
if (/^(border|outline)(-|$)/.test(prop)) return 'border-width';
|
|
35
|
+
if (/^(padding|margin|gap|row-gap|column-gap|inset|top|right|bottom|left)(-|$)/.test(prop)) return 'space';
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Each literal in `text` (fallback-stripped, the same text `from` names)
|
|
40
|
+
* replaced by its one candidate's token. Only called once every literal is
|
|
41
|
+
* unique, so `candidates[0]` is never a guess. A zero-valued match has no
|
|
42
|
+
* entry in `literals` (the census skips it too), so it must be recognised
|
|
43
|
+
* here and left untouched rather than consuming the next entry.
|
|
44
|
+
*/
|
|
45
|
+
function rewriteLiterals(text, literals) {
|
|
46
|
+
let i = 0;
|
|
47
|
+
return text.replace(LITERAL, (match, num, unit) => {
|
|
48
|
+
const px = round(parseFloat(num) * (unit === 'rem' ? REM_PX : 1));
|
|
49
|
+
if (px === 0) return match;
|
|
50
|
+
return `var(${literals[i++].candidates[0].token})`;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Every literal in a declaration, each with the nearest step of `scale`.
|
|
56
|
+
*
|
|
57
|
+
* A term inside `calc()` or a shorthand is measured on its own, so
|
|
58
|
+
* `padding: 8px 16px` reports two literals and `calc(100% - 20px)` reports the
|
|
59
|
+
* 20px. `auto` holds only when every one of them lands on a single step and
|
|
60
|
+
* the declaration carries no var() fallback: a tie, a scale with no
|
|
61
|
+
* comparable steps, or a fallback beside the literal leaves the choice open.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveGeometryLiteral(value, scale, tokens = []) {
|
|
64
|
+
const steps = [];
|
|
65
|
+
for (const token of tokens) {
|
|
66
|
+
const px = pixelsOf(token.value ?? '');
|
|
67
|
+
if (px !== null) steps.push({ token: token.name, px });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const stripped = stripVarFallbacks(value);
|
|
71
|
+
const literals = [];
|
|
72
|
+
for (const m of stripped.matchAll(LITERAL)) {
|
|
73
|
+
const px = round(parseFloat(m[1]) * (m[2] === 'rem' ? REM_PX : 1));
|
|
74
|
+
if (px === 0) continue;
|
|
75
|
+
const nearest = steps.reduce((best, s) => Math.min(best, Math.abs(s.px - px)), Infinity);
|
|
76
|
+
const candidates = steps
|
|
77
|
+
.filter((s) => Math.abs(Math.abs(s.px - px) - nearest) < 1e-6)
|
|
78
|
+
.map((s) => ({ token: s.token, px: s.px, shift: round(s.px - px) }));
|
|
79
|
+
literals.push({ value: m[0], px, candidates });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A var() fallback beside a flagged literal makes `stripped` text the source
|
|
83
|
+
// never contains, so no patch could find its site; the choice stays open.
|
|
84
|
+
const auto =
|
|
85
|
+
stripped === value && literals.length > 0 && literals.every((l) => l.candidates.length === 1);
|
|
86
|
+
return {
|
|
87
|
+
scale,
|
|
88
|
+
literals,
|
|
89
|
+
auto,
|
|
90
|
+
patch: auto ? { from: stripped, to: rewriteLiterals(stripped, literals) } : null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Page targets for `check-page --tests`: a page source file paired with the
|
|
2
|
+
// route that renders it.
|
|
3
|
+
//
|
|
4
|
+
// The route table is the consumer's own `pages` object, found by the file that
|
|
5
|
+
// imports `LiveTokensRouter`, and read statically the way `missing-source`
|
|
6
|
+
// already reads it. A route that object cannot express — one served by
|
|
7
|
+
// `resolve()` — is mapped by hand under `pageRoutes` in the testing settings.
|
|
8
|
+
// Parameter values are never guessed and the app entry point is never imported
|
|
9
|
+
// into Node.
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
12
|
+
import { join, relative, resolve } from 'node:path';
|
|
13
|
+
import { isExcluded } from './findings.mjs';
|
|
14
|
+
import { walk } from './tokenVocabulary.mjs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Directories that hold the system rather than pages built on it. Mirrors
|
|
18
|
+
* `NOT_PAGES` in `check-page.mjs`; duplicated so the static checker can import
|
|
19
|
+
* this module without an import cycle, and pinned to it by
|
|
20
|
+
* `pageRoutes.test.ts`.
|
|
21
|
+
*/
|
|
22
|
+
export const SYSTEM_DIRS = ['src/system', 'src/editor', 'src/lib', 'src/live-tokens'];
|
|
23
|
+
|
|
24
|
+
const SETTINGS_FILES = [
|
|
25
|
+
'live-tokens.testing.ts',
|
|
26
|
+
'live-tokens.testing.mts',
|
|
27
|
+
'live-tokens.testing.js',
|
|
28
|
+
'live-tokens.testing.mjs',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function settingsFilePath(root) {
|
|
32
|
+
for (const name of SETTINGS_FILES) {
|
|
33
|
+
const path = join(root, name);
|
|
34
|
+
if (existsSync(path)) return path;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Block comments first: a `//` inside one must not seed a second, overlapping
|
|
40
|
+
* strip. Same reasoning as `contractRunner.mjs`'s own scraper. */
|
|
41
|
+
function stripComments(text) {
|
|
42
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function skipString(text, start) {
|
|
46
|
+
const quote = text[start];
|
|
47
|
+
for (let i = start + 1; i < text.length; i++) {
|
|
48
|
+
if (text[i] === '\\') { i++; continue; }
|
|
49
|
+
if (text[i] === quote) return i;
|
|
50
|
+
}
|
|
51
|
+
return text.length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The text between the braces of the object literal opening at `open`. */
|
|
55
|
+
function objectBody(text, open) {
|
|
56
|
+
let depth = 0;
|
|
57
|
+
for (let i = open; i < text.length; i++) {
|
|
58
|
+
const ch = text[i];
|
|
59
|
+
if (ch === "'" || ch === '"' || ch === '`') { i = skipString(text, i); continue; }
|
|
60
|
+
if (ch === '{') depth++;
|
|
61
|
+
else if (ch === '}') {
|
|
62
|
+
depth--;
|
|
63
|
+
if (depth === 0) return text.slice(open + 1, i);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The text between the brackets of the array literal opening at `open`.
|
|
70
|
+
* Mirrors `objectBody`, bracket-keyed rather than brace-keyed. */
|
|
71
|
+
function arrayBody(text, open) {
|
|
72
|
+
let depth = 0;
|
|
73
|
+
for (let i = open; i < text.length; i++) {
|
|
74
|
+
const ch = text[i];
|
|
75
|
+
if (ch === "'" || ch === '"' || ch === '`') { i = skipString(text, i); continue; }
|
|
76
|
+
if (ch === '[') depth++;
|
|
77
|
+
else if (ch === ']') {
|
|
78
|
+
depth--;
|
|
79
|
+
if (depth === 0) return text.slice(open + 1, i);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Top-level comma-separated chunks of an object or array body, respecting
|
|
86
|
+
* strings and nested brackets. Shared by `objectEntries` (each chunk is
|
|
87
|
+
* `key: value`) and `settingsPageViewports` (each chunk is a `{ width,
|
|
88
|
+
* height }` literal). */
|
|
89
|
+
function splitTopLevel(body) {
|
|
90
|
+
const chunks = [];
|
|
91
|
+
let depth = 0;
|
|
92
|
+
let start = 0;
|
|
93
|
+
for (let i = 0; i < body.length; i++) {
|
|
94
|
+
const ch = body[i];
|
|
95
|
+
if (ch === "'" || ch === '"' || ch === '`') { i = skipString(body, i); continue; }
|
|
96
|
+
if (ch === '{' || ch === '[' || ch === '(') depth++;
|
|
97
|
+
else if (ch === '}' || ch === ']' || ch === ')') depth--;
|
|
98
|
+
else if (ch === ',' && depth === 0) {
|
|
99
|
+
chunks.push(body.slice(start, i));
|
|
100
|
+
start = i + 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
chunks.push(body.slice(start));
|
|
104
|
+
return chunks;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Top-level `key: value` pairs of an object body, values kept as source text. */
|
|
108
|
+
function objectEntries(body) {
|
|
109
|
+
const entries = [];
|
|
110
|
+
for (const chunk of splitTopLevel(body)) {
|
|
111
|
+
const match = /^\s*(?:'([^']*)'|"([^"]*)"|([A-Za-z_$][\w$]*))\s*:([\s\S]*)$/.exec(chunk);
|
|
112
|
+
if (!match) continue;
|
|
113
|
+
entries.push({ key: match[1] ?? match[2] ?? match[3], value: match[4].trim() });
|
|
114
|
+
}
|
|
115
|
+
return entries;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function namedObject(text, field) {
|
|
119
|
+
const at = new RegExp(`\\b${field}\\s*[:=]\\s*\\{`).exec(text);
|
|
120
|
+
if (!at) return null;
|
|
121
|
+
return objectBody(text, at.index + at[0].length - 1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const ROUTER_IMPORT = /import\s*\{[^}]*\bLiveTokensRouter\b[^}]*\}\s*from\s*['"]/;
|
|
125
|
+
|
|
126
|
+
/** Every file under `src/` that mounts the router, sorted so a project with
|
|
127
|
+
* more than one entry point resolves the same way on every run. */
|
|
128
|
+
export function routerFiles(root) {
|
|
129
|
+
const src = join(root, 'src');
|
|
130
|
+
if (!existsSync(src)) return [];
|
|
131
|
+
return walk(src, ['.svelte'])
|
|
132
|
+
.filter((file) => ROUTER_IMPORT.test(stripComments(readFileSync(file, 'utf8'))))
|
|
133
|
+
.sort();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Route path -> page source, from the `pages` object of every router file.
|
|
137
|
+
* The first file to claim a route keeps it. */
|
|
138
|
+
export function routeTable(root) {
|
|
139
|
+
const table = new Map();
|
|
140
|
+
for (const file of routerFiles(root)) {
|
|
141
|
+
const body = namedObject(stripComments(readFileSync(file, 'utf8')), 'pages');
|
|
142
|
+
if (body === null) continue;
|
|
143
|
+
for (const { key, value } of objectEntries(body)) {
|
|
144
|
+
const source = /\bsource\s*:\s*['"]([^'"]+)['"]/.exec(value);
|
|
145
|
+
if (!source || table.has(key)) continue;
|
|
146
|
+
table.set(key, source[1]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return table;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* `pageRoutes` from the testing settings: page source -> concrete URL. Read
|
|
154
|
+
* from the file's source text, like `contractRunner.mjs`'s `dataDir` scrape,
|
|
155
|
+
* because importing the settings file would fail on its own extensionless
|
|
156
|
+
* imports. A `pageRoutes` key present in live code that this cannot read as
|
|
157
|
+
* a plain string map throws rather than falling through to a silent default:
|
|
158
|
+
* an unread mapping reads as a page with no route.
|
|
159
|
+
*/
|
|
160
|
+
export function settingsPageRoutes(root) {
|
|
161
|
+
const settings = settingsFilePath(root);
|
|
162
|
+
if (!settings) return new Map();
|
|
163
|
+
const live = stripComments(readFileSync(settings, 'utf8'));
|
|
164
|
+
const body = namedObject(live, 'pageRoutes');
|
|
165
|
+
if (body === null) {
|
|
166
|
+
if (/\bpageRoutes\s*:/.test(live)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`"pageRoutes" in ${settings} is not a plain object literal, so --tests cannot read it statically. `
|
|
169
|
+
+ 'Write it as literal page-path to URL string pairs, or remove the key.',
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return new Map();
|
|
173
|
+
}
|
|
174
|
+
const routes = new Map();
|
|
175
|
+
for (const { key, value } of objectEntries(body)) {
|
|
176
|
+
const url = /^['"]([^'"]+)['"]$/.exec(value);
|
|
177
|
+
if (!url) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`"pageRoutes['${key}']" in ${settings} is not a plain string literal, so --tests cannot read it statically.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
routes.set(key, url[1]);
|
|
183
|
+
}
|
|
184
|
+
return routes;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The contract project's own desktop size, and the phone the text styles
|
|
188
|
+
* carry media overrides for (decision 4). Mirrors `DEFAULT_PAGE_VIEWPORTS`
|
|
189
|
+
* in `src/testing/config.ts`; duplicated for the reason `settingsPageRoutes`
|
|
190
|
+
* above reads its own settings statically rather than importing that module. */
|
|
191
|
+
export const DEFAULT_PAGE_VIEWPORTS = [
|
|
192
|
+
{ width: 1280, height: 900 },
|
|
193
|
+
{ width: 390, height: 844 },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* `pageViewports` from the testing settings, read the same way
|
|
198
|
+
* `settingsPageRoutes` reads `pageRoutes`. Falls back to
|
|
199
|
+
* `DEFAULT_PAGE_VIEWPORTS` when the key is absent, so a caller building the
|
|
200
|
+
* set of viewports a page run is expected to cover sees the same list the
|
|
201
|
+
* generated Playwright config resolves at runtime (`src/testing/
|
|
202
|
+
* playwright.ts`'s own `settings.pageViewports`), rather than a shipped
|
|
203
|
+
* default that a project replaced. A `pageViewports` present in live code
|
|
204
|
+
* that this cannot read as a plain array of `{ width, height }` literals
|
|
205
|
+
* throws rather than falling through to a silent default.
|
|
206
|
+
*/
|
|
207
|
+
export function settingsPageViewports(root) {
|
|
208
|
+
const settings = settingsFilePath(root);
|
|
209
|
+
if (!settings) return DEFAULT_PAGE_VIEWPORTS;
|
|
210
|
+
const live = stripComments(readFileSync(settings, 'utf8'));
|
|
211
|
+
const at = /\bpageViewports\s*:\s*\[/.exec(live);
|
|
212
|
+
if (!at) {
|
|
213
|
+
if (/\bpageViewports\s*:/.test(live)) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`"pageViewports" in ${settings} is not a plain array literal, so --tests cannot read it statically. `
|
|
216
|
+
+ 'Write it as literal { width, height } entries, or remove the key.',
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return DEFAULT_PAGE_VIEWPORTS;
|
|
220
|
+
}
|
|
221
|
+
const body = arrayBody(live, at.index + at[0].length - 1) ?? '';
|
|
222
|
+
const items = splitTopLevel(body).map((item) => item.trim()).filter(Boolean);
|
|
223
|
+
if (items.length === 0) return DEFAULT_PAGE_VIEWPORTS;
|
|
224
|
+
return items.map((item) => {
|
|
225
|
+
const width = /\bwidth\s*:\s*(\d+)/.exec(item);
|
|
226
|
+
const height = /\bheight\s*:\s*(\d+)/.exec(item);
|
|
227
|
+
if (!width || !height) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`"pageViewports" in ${settings} has an entry that is not a plain { width, height } literal, so --tests cannot read it statically.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return { width: Number(width[1]), height: Number(height[1]) };
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isSystemPath(rel) {
|
|
237
|
+
return SYSTEM_DIRS.some((dir) => rel === dir || rel.startsWith(`${dir}/`));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function toRelative(root, target) {
|
|
241
|
+
return relative(root, resolve(root, target)).split('\\').join('/');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const NO_ROUTE =
|
|
245
|
+
'no route renders this page. Give its entry in the pages object a `source` field, '
|
|
246
|
+
+ 'or map the file to a concrete URL under `pageRoutes` in live-tokens.testing.ts.';
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The pages `--tests` opens, each with the route that renders it.
|
|
250
|
+
*
|
|
251
|
+
* With no paths, every mapped page outside the system directories is a target.
|
|
252
|
+
* With paths, each one is looked up in the same map; a file no route names
|
|
253
|
+
* comes back with `route: null` and the `reason` the caller reports as a
|
|
254
|
+
* `tests-setup` finding.
|
|
255
|
+
*/
|
|
256
|
+
export function resolvePageTargets(paths = [], root = process.cwd()) {
|
|
257
|
+
const mapped = new Map();
|
|
258
|
+
for (const [route, source] of routeTable(root)) {
|
|
259
|
+
const rel = toRelative(root, source);
|
|
260
|
+
if (!mapped.has(rel)) mapped.set(rel, route);
|
|
261
|
+
}
|
|
262
|
+
// An explicit mapping wins: it exists because the route table could not
|
|
263
|
+
// express the route in the first place.
|
|
264
|
+
for (const [source, url] of settingsPageRoutes(root)) {
|
|
265
|
+
mapped.set(toRelative(root, source), url);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const pages = [...mapped.entries()]
|
|
269
|
+
.filter(([source]) => !isSystemPath(source))
|
|
270
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
271
|
+
.map(([source, route]) => ({ source, route }));
|
|
272
|
+
|
|
273
|
+
if (paths.length === 0) return pages;
|
|
274
|
+
|
|
275
|
+
const targets = [];
|
|
276
|
+
for (const path of paths) {
|
|
277
|
+
const full = resolve(root, path);
|
|
278
|
+
if (existsSync(full) && statSync(full).isDirectory()) {
|
|
279
|
+
const prefix = `${toRelative(root, path)}/`;
|
|
280
|
+
targets.push(...pages.filter((page) => page.source.startsWith(prefix)));
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const rel = toRelative(root, path);
|
|
284
|
+
const route = mapped.get(rel);
|
|
285
|
+
targets.push(route === undefined ? { source: rel, route: null, reason: NO_ROUTE } : { source: rel, route });
|
|
286
|
+
}
|
|
287
|
+
return targets;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* `resolvePageTargets`, filtered by `checks.exclude` the same way
|
|
292
|
+
* `checkPages` (`bin/check-page.mjs`) filters the static half of the same
|
|
293
|
+
* command: an explicit *file* argument always checks, `isExcluded`'s own
|
|
294
|
+
* contract, but a directory `checkPages` walks — an explicit directory
|
|
295
|
+
* argument, or the no-paths-given discovery — drops excluded files. The two
|
|
296
|
+
* halves of `check-page --tests` have to agree on which pages a directory or
|
|
297
|
+
* omitted target reaches, or `--tests` fails a page the static half
|
|
298
|
+
* deliberately skips.
|
|
299
|
+
*/
|
|
300
|
+
export function resolvePageTestTargets(paths = [], root = process.cwd()) {
|
|
301
|
+
const allTargets = resolvePageTargets(paths, root);
|
|
302
|
+
const explicitFiles = new Set(
|
|
303
|
+
paths
|
|
304
|
+
.map((p) => resolve(root, p))
|
|
305
|
+
.filter((full) => existsSync(full) && !statSync(full).isDirectory())
|
|
306
|
+
.map((full) => toRelative(root, full)),
|
|
307
|
+
);
|
|
308
|
+
return allTargets.filter((t) => explicitFiles.has(t.source) || !isExcluded(t.source, root));
|
|
309
|
+
}
|