@glubean/scanner 0.2.2 → 0.2.4

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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Template-id sentinel helpers — match runtime row ids back to the static
3
+ * template id emitted by `extractFromSource` for `test.each` / `test.pick`.
4
+ *
5
+ * ## Background
6
+ *
7
+ * Static parsing produces ONE `ExportMeta` per data-driven export, with an
8
+ * id that contains `$placeholder` markers — e.g. `test.each(rows)({ id:
9
+ * "case-$id" })` is extracted as id `"case-$id"`. At runtime, the harness
10
+ * substitutes the placeholders against each row, emitting concrete ids
11
+ * like `"case-101"`, `"case-202"`. Downstream consumers (CLI run output,
12
+ * MCP discover/run, etc.) need to map the concrete event ids back to the
13
+ * static meta — that's what these helpers do.
14
+ *
15
+ * ## Placeholder syntax
16
+ *
17
+ * - Marker is `$<word>` — a `$` followed by a JS identifier-start character
18
+ * followed by zero or more identifier characters (letters, digits, `_`).
19
+ * - Each placeholder matches `.*` (greedy) at runtime — there is no
20
+ * typed/numeric variant. Multi-placeholder ids (`case-$a-$b`) match
21
+ * greedily left-to-right; ambiguous splits are not resolved.
22
+ * - Matching is **case-insensitive** (the runner's harness id substitution
23
+ * may lowercase row keys).
24
+ *
25
+ * ## Variant prefix
26
+ *
27
+ * VSCode prefixes ids with `each:` / `pick:` for routing. These helpers
28
+ * strip the prefix before matching so a `each:case-$id` template still
29
+ * matches a runtime `case-101`.
30
+ *
31
+ * ## Limitations
32
+ *
33
+ * - Two static templates whose runtime ids could collide (e.g.
34
+ * `case-$x` and `case-$y` in the same file) get first-match-wins
35
+ * semantics from `findTemplateMatch` — there's no warning.
36
+ * - The shape `case-$a-$b` matching `case-x-y-z` has multiple valid
37
+ * splits; this helper returns *some* match without disambiguating.
38
+ * Document your runtime ids to avoid placeholder ambiguity.
39
+ */
40
+ const TEMPLATE_RE = /\$[A-Za-z_]\w*/g;
41
+ const VARIANT_PREFIX_RE = /^(?:each|pick):/;
42
+ /**
43
+ * Strip the VSCode variant prefix (`each:` / `pick:`) from an id, leaving
44
+ * the bare template / concrete id. Returns the input unchanged if no
45
+ * prefix is present.
46
+ */
47
+ export function stripVariantPrefix(id) {
48
+ return id.replace(VARIANT_PREFIX_RE, "");
49
+ }
50
+ /**
51
+ * Returns `true` if the id contains at least one `$placeholder` marker
52
+ * (after stripping any variant prefix). Use this to detect "this is a
53
+ * template, not a concrete id" — concrete runtime ids never contain `$`.
54
+ */
55
+ export function hasTemplatePlaceholders(id) {
56
+ TEMPLATE_RE.lastIndex = 0;
57
+ return TEMPLATE_RE.test(stripVariantPrefix(id));
58
+ }
59
+ /**
60
+ * Build a regex that matches concrete runtime ids against a template id.
61
+ * Internal — exposed via `matchesTemplateId` and `findTemplateMatch`. If
62
+ * the input has no placeholders, returns `undefined` (caller should do an
63
+ * exact-string compare instead).
64
+ *
65
+ * Each `$word` placeholder becomes `.*` (greedy). The regex is anchored
66
+ * (`^...$`) and case-insensitive (`/i`).
67
+ */
68
+ function templateIdToRegExp(id) {
69
+ const normalized = stripVariantPrefix(id);
70
+ TEMPLATE_RE.lastIndex = 0;
71
+ if (!TEMPLATE_RE.test(normalized))
72
+ return undefined;
73
+ TEMPLATE_RE.lastIndex = 0;
74
+ let lastIndex = 0;
75
+ let pattern = "^";
76
+ let match;
77
+ while ((match = TEMPLATE_RE.exec(normalized)) !== null) {
78
+ pattern += escapeRegExp(normalized.slice(lastIndex, match.index));
79
+ pattern += ".*";
80
+ lastIndex = match.index + match[0].length;
81
+ }
82
+ pattern += escapeRegExp(normalized.slice(lastIndex));
83
+ pattern += "$";
84
+ return new RegExp(pattern, "i");
85
+ }
86
+ /**
87
+ * Returns `true` if `concreteId` is a substituted instance of `templateId`.
88
+ *
89
+ * Both inputs are normalised (variant prefix stripped, lowercased before
90
+ * exact compare). For `test.each([{id: "alpha"}])({ id: "case-$id" })`
91
+ * the runtime emits `"case-alpha"` — `matchesTemplateId("case-$id",
92
+ * "case-alpha")` returns `true`.
93
+ *
94
+ * Exact-id ties pass through unchanged: `matchesTemplateId("health",
95
+ * "health")` is `true` even though `"health"` has no placeholders.
96
+ */
97
+ export function matchesTemplateId(templateId, concreteId) {
98
+ const normalizedTemplate = stripVariantPrefix(templateId).toLowerCase();
99
+ const normalizedConcrete = stripVariantPrefix(concreteId).toLowerCase();
100
+ if (normalizedTemplate === normalizedConcrete)
101
+ return true;
102
+ return templateIdToRegExp(templateId)?.test(stripVariantPrefix(concreteId)) ?? false;
103
+ }
104
+ /**
105
+ * Permissive filter match — used by CLI / MCP `--filter <id>` so the user
106
+ * can target a row by its concrete id while only the template appears in
107
+ * static meta. Acceptance order:
108
+ * 1. Empty filter → match (caller wants everything).
109
+ * 2. Filter substring of the template (template `case-$id` matches filter
110
+ * `case`).
111
+ * 3. Filter is a substituted instance (template `case-$id`, filter
112
+ * `case-101`).
113
+ * 4. Filter starts with the template's literal prefix (template
114
+ * `case-$id`, filter `case-101-extra`).
115
+ *
116
+ * Slightly more permissive than `matchesTemplateId` — designed for human
117
+ * `--filter` input, not strict harness event matching.
118
+ */
119
+ export function matchesTemplateFilter(templateId, filter) {
120
+ const normalizedFilter = stripVariantPrefix(filter).toLowerCase().trim();
121
+ if (!normalizedFilter)
122
+ return true;
123
+ const normalizedTemplate = stripVariantPrefix(templateId).toLowerCase();
124
+ if (normalizedTemplate.includes(normalizedFilter))
125
+ return true;
126
+ if (matchesTemplateId(templateId, normalizedFilter))
127
+ return true;
128
+ const prefix = normalizedTemplate.split(TEMPLATE_RE)[0] ?? "";
129
+ return prefix.length > 0 && normalizedFilter.startsWith(prefix);
130
+ }
131
+ /**
132
+ * Find the static meta entry whose id matches `concreteId`. Prefers exact
133
+ * matches over template matches — so if both `case-$id` and `case-101`
134
+ * exist statically (rare but possible), the concrete row event for
135
+ * `case-101` resolves to the concrete entry, not the template.
136
+ *
137
+ * If multiple template entries could match (e.g. two overlapping
138
+ * placeholders), returns the first one in array order — no warning. Avoid
139
+ * collisions by namespacing template ids per file.
140
+ */
141
+ export function findTemplateMatch(items, concreteId) {
142
+ const normalizedConcrete = stripVariantPrefix(concreteId).toLowerCase();
143
+ const exact = items.find((item) => stripVariantPrefix(item.id).toLowerCase() === normalizedConcrete);
144
+ return exact ?? items.find((item) => matchesTemplateId(item.id, concreteId));
145
+ }
146
+ function escapeRegExp(value) {
147
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
148
+ }
149
+ //# sourceMappingURL=template-sentinel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-sentinel.js","sourceRoot":"","sources":["../src/template-sentinel.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glubean/scanner",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {