@vltpkg/dss-breadcrumb 1.0.0-rc.23 → 1.0.0-rc.25

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,102 @@
1
+ import type { PostcssNode } from '@vltpkg/dss-parser';
2
+ import type { ModifierBreadcrumb, ModifierBreadcrumbItem, ModifierInteractiveBreadcrumb, BreadcrumbSpecificity } from './types.ts';
3
+ export * from './types.ts';
4
+ /**
5
+ * The returned pseudo selector parameters object.
6
+ */
7
+ type ParsedPseudoParameters = {
8
+ semverValue?: string;
9
+ };
10
+ /**
11
+ * Helper function to remove quotes from a string value.
12
+ */
13
+ export declare const removeQuotes: (value: string) => string;
14
+ /**
15
+ * Helper function to extract parameter value from pseudo selector nodes.
16
+ */
17
+ export declare const extractPseudoParameter: (item: any) => ParsedPseudoParameters;
18
+ /**
19
+ * Helper function to get the full text representation
20
+ * of a pseudo selector including parameters
21
+ */
22
+ export declare const getPseudoSelectorFullText: (item: PostcssNode) => string;
23
+ /**
24
+ * The Breadcrumb class is used to represent a valid breadcrumb
25
+ * path that helps you traverse a graph and find a specific node or edge.
26
+ *
27
+ * Alongside the traditional analogy, "Breadcrumb" is also being used here
28
+ * as a term used to describe the subset of the query language that uses
29
+ * only pseudo selectors, id selectors & combinators.
30
+ *
31
+ * The Breadcrumb class implements a doubly-linked list of items
32
+ * that can be used to navigate through the breadcrumb.
33
+ * The InteractiveBreadcrumb can also be used to keep track of state
34
+ * of the current breadcrumb item that should be used for checks.
35
+ *
36
+ * It also validates that each element of the provided query string is
37
+ * valid according to the previous definition of a "Breadcrumb" query
38
+ * language subset.
39
+ */
40
+ export declare class Breadcrumb implements ModifierBreadcrumb {
41
+ #private;
42
+ comment: string | undefined;
43
+ specificity: BreadcrumbSpecificity;
44
+ /**
45
+ * Initializes the interactive breadcrumb with a query string.
46
+ */
47
+ constructor(query: string);
48
+ /**
49
+ * Retrieves the first breadcrumb item.
50
+ */
51
+ get first(): ModifierBreadcrumbItem;
52
+ /**
53
+ * Retrieves the last breadcrumb item.
54
+ */
55
+ get last(): ModifierBreadcrumbItem;
56
+ /**
57
+ * Returns `true` if the breadcrumb is composed of a single item.
58
+ */
59
+ get single(): boolean;
60
+ [Symbol.iterator](): ArrayIterator<ModifierBreadcrumbItem>;
61
+ /**
62
+ * Empties the current breadcrumb list.
63
+ */
64
+ clear(): void;
65
+ /**
66
+ * Gets an {@link InteractiveBreadcrumb} instance that can be
67
+ * used to track state of the current breadcrumb item.
68
+ */
69
+ interactive(): InteractiveBreadcrumb;
70
+ }
71
+ /**
72
+ * The InteractiveBreadcrumb is used to keep track of state
73
+ * of the current breadcrumb item that should be used for checks.
74
+ */
75
+ export declare class InteractiveBreadcrumb implements ModifierInteractiveBreadcrumb {
76
+ #private;
77
+ constructor(breadcrumb: Breadcrumb);
78
+ /**
79
+ * The current breadcrumb item.
80
+ */
81
+ get current(): ModifierBreadcrumbItem | undefined;
82
+ /**
83
+ * Returns `true` if the current breadcrumb has no more items left.
84
+ */
85
+ get done(): boolean;
86
+ /**
87
+ * The next breadcrumb item.
88
+ */
89
+ next(): this;
90
+ }
91
+ /**
92
+ * Returns an {@link Breadcrumb} list of items
93
+ * for a given query string.
94
+ */
95
+ export declare const parseBreadcrumb: (query: string) => ModifierBreadcrumb;
96
+ /**
97
+ * Sorts an array of Breadcrumb objects by specificity. Objects with
98
+ * higher idCounter values come first, if idCounter values are equal,
99
+ * then objects with higher commonCounter values come first. Otherwise,
100
+ * the original order is preserved.
101
+ */
102
+ export declare const specificitySort: (breadcrumbs: ModifierBreadcrumb[]) => ModifierBreadcrumb[];
package/dist/index.js ADDED
@@ -0,0 +1,351 @@
1
+ import { isPostcssNodeWithChildren, isPseudoNode, isIdentifierNode, isCombinatorNode, isCommentNode, isStringNode, isTagNode, asStringNode, asTagNode, parse, asPostcssNodeWithChildren, } from '@vltpkg/dss-parser';
2
+ import { error } from '@vltpkg/error-cause';
3
+ import { intersects } from '@vltpkg/semver';
4
+ export * from "./types.js";
5
+ /**
6
+ * A comparator function that always returns true.
7
+ */
8
+ const passthroughComparator = () => () => true;
9
+ /**
10
+ * A comparator function for semver pseudo selectors.
11
+ */
12
+ const semverComparator = ({ range }) => ({ semver }) => {
13
+ if (range && semver) {
14
+ return intersects(semver, range);
15
+ }
16
+ return false;
17
+ };
18
+ /**
19
+ * A map of pseudo selectors to their comparator functions.
20
+ */
21
+ const pseudoSelectors = new Map([
22
+ [':semver', semverComparator],
23
+ [':v', semverComparator],
24
+ ]);
25
+ /**
26
+ * The subset of importer pseudo selectors that are supported.
27
+ */
28
+ const importerNames = new Set([':project', ':workspace', ':root']);
29
+ // Add importer pseudo selectors to the list of supported selectors
30
+ for (const importerName of importerNames) {
31
+ pseudoSelectors.set(importerName, passthroughComparator);
32
+ }
33
+ /**
34
+ * Helper function to remove quotes from a string value.
35
+ */
36
+ export const removeQuotes = (value) => value.replace(/^"(.*?)"$/, '$1');
37
+ /**
38
+ * Helper function to extract parameter value from pseudo selector nodes.
39
+ */
40
+ export const extractPseudoParameter = (item) => {
41
+ if (!isPostcssNodeWithChildren(item) || !item.nodes[0]) {
42
+ return {};
43
+ }
44
+ let first;
45
+ try {
46
+ // Try to parse as string node first (quoted values)
47
+ const firstNode = asPostcssNodeWithChildren(item.nodes[0])
48
+ .nodes[0];
49
+ if (isStringNode(firstNode)) {
50
+ first = removeQuotes(firstNode.value);
51
+ }
52
+ // Handle tag node (unquoted values)
53
+ if (isTagNode(firstNode)) {
54
+ first = asTagNode(firstNode).value;
55
+ }
56
+ }
57
+ catch { }
58
+ if (item.value === ':semver' || item.value === ':v') {
59
+ return {
60
+ semverValue: first,
61
+ };
62
+ }
63
+ return {};
64
+ };
65
+ /**
66
+ * Helper function to get the full text representation
67
+ * of a pseudo selector including parameters
68
+ */
69
+ export const getPseudoSelectorFullText = (item) => {
70
+ if (!isPostcssNodeWithChildren(item) || !item.nodes[0]) {
71
+ return item.value || '';
72
+ }
73
+ const baseValue = item.value;
74
+ const paramNode = item.nodes[0];
75
+ let paramText = '';
76
+ if (isPostcssNodeWithChildren(paramNode)) {
77
+ // reconstruct the parameter by combining all child nodes
78
+ paramText = paramNode.nodes
79
+ .map(node => {
80
+ if (isStringNode(node)) {
81
+ return asStringNode(node).value;
82
+ }
83
+ else if (isTagNode(node)) {
84
+ return asTagNode(node).value;
85
+ }
86
+ else {
87
+ return node.value;
88
+ }
89
+ })
90
+ .join('');
91
+ }
92
+ return `${baseValue}(${paramText})`;
93
+ };
94
+ /**
95
+ * The Breadcrumb class is used to represent a valid breadcrumb
96
+ * path that helps you traverse a graph and find a specific node or edge.
97
+ *
98
+ * Alongside the traditional analogy, "Breadcrumb" is also being used here
99
+ * as a term used to describe the subset of the query language that uses
100
+ * only pseudo selectors, id selectors & combinators.
101
+ *
102
+ * The Breadcrumb class implements a doubly-linked list of items
103
+ * that can be used to navigate through the breadcrumb.
104
+ * The InteractiveBreadcrumb can also be used to keep track of state
105
+ * of the current breadcrumb item that should be used for checks.
106
+ *
107
+ * It also validates that each element of the provided query string is
108
+ * valid according to the previous definition of a "Breadcrumb" query
109
+ * language subset.
110
+ */
111
+ export class Breadcrumb {
112
+ #items;
113
+ comment;
114
+ specificity;
115
+ /**
116
+ * Initializes the interactive breadcrumb with a query string.
117
+ */
118
+ constructor(query) {
119
+ this.#items = [];
120
+ this.specificity = { idCounter: 0, commonCounter: 0 };
121
+ const ast = parse(query);
122
+ // Track whether we encountered a combinator since the last item
123
+ let afterCombinator = true;
124
+ // iterates only at the first level of the AST since any
125
+ // pseudo selectors that relies on nested nodes are invalid syntax
126
+ for (const item of ast.first.nodes) {
127
+ const pseudoNode = isPseudoNode(item);
128
+ // checks for only supported pseudo selectors
129
+ if (pseudoNode && !pseudoSelectors.has(item.value)) {
130
+ throw error('Invalid pseudo selector', {
131
+ found: item.value,
132
+ });
133
+ }
134
+ const allowedTypes = isIdentifierNode(item) ||
135
+ pseudoNode ||
136
+ (isCombinatorNode(item) && item.value === '>') ||
137
+ isCommentNode(item);
138
+ const hasChildren = isPostcssNodeWithChildren(item) && item.nodes.length > 0;
139
+ const semverNode = pseudoNode &&
140
+ (item.value === ':semver' || item.value === ':v');
141
+ // validation, only pseudo selectors, id selectors
142
+ // and combinators are valid ast node items
143
+ // pseudo selectors are allowed to have children (parameters)
144
+ if ((hasChildren && !pseudoNode) || !allowedTypes) {
145
+ throw error('Invalid query', { found: query });
146
+ }
147
+ // combinators and comments are skipped
148
+ if (isCombinatorNode(item)) {
149
+ afterCombinator = true;
150
+ continue;
151
+ }
152
+ else if (isCommentNode(item)) {
153
+ const cleanComment = item.value
154
+ .replace(/^\/\*/, '')
155
+ .replace(/\*\/$/, '')
156
+ .trim();
157
+ this.comment = cleanComment;
158
+ afterCombinator = true;
159
+ }
160
+ else {
161
+ // we define the last item as we iterate through the list of
162
+ // breadcrumb items so that this value can also be used to
163
+ // update previous items when needed
164
+ const lastItem = this.#items.length > 0 ?
165
+ this.#items[this.#items.length - 1]
166
+ : undefined;
167
+ // Extract parameter before potential consolidation
168
+ const providedRange = (pseudoNode &&
169
+ (item.value === ':semver' || item.value === ':v')) ?
170
+ (extractPseudoParameter(item).semverValue ?? '')
171
+ : '';
172
+ // get the comparator function for a given pseudo selector item
173
+ const internalOptions = {
174
+ ...(semverNode ? { range: providedRange } : {}),
175
+ };
176
+ const comparator = (pseudoSelectors.get(item.value) ?? passthroughComparator)(internalOptions);
177
+ // If we have a previous item and we haven't encountered a combinator
178
+ // since then, consolidate with the previous item
179
+ if (lastItem && !afterCombinator) {
180
+ // Check for invalid chained pseudo selectors
181
+ if (pseudoNode && isPseudoNode(lastItem)) {
182
+ throw error('Invalid query', { found: query });
183
+ }
184
+ // determine how to combine the values based on selector types
185
+ let currentValue = item.value;
186
+ if (item.type === 'id') {
187
+ currentValue = `#${item.value}`;
188
+ }
189
+ else if (pseudoNode) {
190
+ currentValue = getPseudoSelectorFullText(item);
191
+ }
192
+ lastItem.value = `${lastItem.value}${currentValue}`;
193
+ // if current item is an ID, update the name property
194
+ if (isIdentifierNode(item)) {
195
+ lastItem.name = item.value;
196
+ }
197
+ // Handle comparator and importer when consolidating with pseudo node
198
+ if (pseudoNode) {
199
+ const lastItemComparator = lastItem.comparator;
200
+ lastItem.comparator = (opts) => comparator(opts) && lastItemComparator(opts);
201
+ lastItem.importer ||= importerNames.has(item.value);
202
+ }
203
+ // update specificity counters
204
+ if (isIdentifierNode(item)) {
205
+ this.specificity.idCounter++;
206
+ }
207
+ else if (pseudoNode) {
208
+ this.specificity.commonCounter++;
209
+ }
210
+ afterCombinator = false;
211
+ continue;
212
+ }
213
+ // Create a new breadcrumb item
214
+ let itemValue = item.value;
215
+ if (item.type === 'id') {
216
+ itemValue = `#${item.value}`;
217
+ }
218
+ else if (pseudoNode) {
219
+ itemValue = getPseudoSelectorFullText(item);
220
+ }
221
+ const newItem = {
222
+ comparator,
223
+ value: itemValue,
224
+ name: item.type === 'id' ? item.value : undefined,
225
+ type: item.type,
226
+ prev: lastItem,
227
+ next: undefined,
228
+ importer: pseudoNode && importerNames.has(item.value),
229
+ };
230
+ if (lastItem) {
231
+ lastItem.next = newItem;
232
+ }
233
+ this.#items.push(newItem);
234
+ // Update specificity counters
235
+ if (isIdentifierNode(item)) {
236
+ this.specificity.idCounter++;
237
+ }
238
+ else if (pseudoNode) {
239
+ this.specificity.commonCounter++;
240
+ }
241
+ afterCombinator = false;
242
+ }
243
+ }
244
+ // the parsed query should have at least one item
245
+ // that is then going to be set as the current item
246
+ if (!this.#items[0]) {
247
+ throw error('Failed to parse query', {
248
+ found: query,
249
+ });
250
+ }
251
+ }
252
+ /**
253
+ * Retrieves the first breadcrumb item.
254
+ */
255
+ get first() {
256
+ if (!this.#items[0]) {
257
+ throw error('Failed to find first breadcrumb item');
258
+ }
259
+ return this.#items[0];
260
+ }
261
+ /**
262
+ * Retrieves the last breadcrumb item.
263
+ */
264
+ get last() {
265
+ const lastItem = this.#items[this.#items.length - 1];
266
+ if (!lastItem) {
267
+ throw error('Failed to find first breadcrumb item');
268
+ }
269
+ return lastItem;
270
+ }
271
+ /**
272
+ * Returns `true` if the breadcrumb is composed of a single item.
273
+ */
274
+ get single() {
275
+ return this.#items.length === 1;
276
+ }
277
+ [Symbol.iterator]() {
278
+ return this.#items.values();
279
+ }
280
+ /**
281
+ * Empties the current breadcrumb list.
282
+ */
283
+ clear() {
284
+ for (const item of this.#items) {
285
+ item.prev = undefined;
286
+ item.next = undefined;
287
+ }
288
+ this.#items.length = 0;
289
+ }
290
+ /**
291
+ * Gets an {@link InteractiveBreadcrumb} instance that can be
292
+ * used to track state of the current breadcrumb item.
293
+ */
294
+ interactive() {
295
+ return new InteractiveBreadcrumb(this);
296
+ }
297
+ }
298
+ /**
299
+ * The InteractiveBreadcrumb is used to keep track of state
300
+ * of the current breadcrumb item that should be used for checks.
301
+ */
302
+ export class InteractiveBreadcrumb {
303
+ #current;
304
+ constructor(breadcrumb) {
305
+ this.#current = breadcrumb.first;
306
+ }
307
+ /**
308
+ * The current breadcrumb item.
309
+ */
310
+ get current() {
311
+ return this.#current;
312
+ }
313
+ /**
314
+ * Returns `true` if the current breadcrumb has no more items left.
315
+ */
316
+ get done() {
317
+ return !this.#current;
318
+ }
319
+ /**
320
+ * The next breadcrumb item.
321
+ */
322
+ next() {
323
+ this.#current = this.#current?.next;
324
+ return this;
325
+ }
326
+ }
327
+ /**
328
+ * Returns an {@link Breadcrumb} list of items
329
+ * for a given query string.
330
+ */
331
+ export const parseBreadcrumb = (query) => new Breadcrumb(query);
332
+ /**
333
+ * Sorts an array of Breadcrumb objects by specificity. Objects with
334
+ * higher idCounter values come first, if idCounter values are equal,
335
+ * then objects with higher commonCounter values come first. Otherwise,
336
+ * the original order is preserved.
337
+ */
338
+ export const specificitySort = (breadcrumbs) => {
339
+ return [...breadcrumbs].sort((a, b) => {
340
+ // First compare by idCounter (higher comes first)
341
+ if (a.specificity.idCounter !== b.specificity.idCounter) {
342
+ return b.specificity.idCounter - a.specificity.idCounter;
343
+ }
344
+ // If idCounter values are equal, compare by commonCounter
345
+ if (a.specificity.commonCounter !== b.specificity.commonCounter) {
346
+ return b.specificity.commonCounter - a.specificity.commonCounter;
347
+ }
348
+ // If both counters are equal, preserve original order
349
+ return 0;
350
+ });
351
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The specificity of a breadcrumb, used for sorting.
3
+ */
4
+ export type BreadcrumbSpecificity = {
5
+ idCounter: number;
6
+ commonCounter: number;
7
+ };
8
+ /**
9
+ * Options for the higher-level comparing breadcrumbs function.
10
+ */
11
+ export type InternalModifierComparatorOptions = {
12
+ range?: string;
13
+ };
14
+ /**
15
+ * Options for comparing breadcrumbs.
16
+ */
17
+ export type ModifierComparatorOptions = {
18
+ semver?: string;
19
+ };
20
+ /**
21
+ * A valid item of a given breadcrumb.
22
+ */
23
+ export type ModifierBreadcrumbItem = {
24
+ comparator: (opts: ModifierComparatorOptions) => boolean;
25
+ name?: string;
26
+ value: string;
27
+ type: string;
28
+ importer: boolean;
29
+ prev: ModifierBreadcrumbItem | undefined;
30
+ next: ModifierBreadcrumbItem | undefined;
31
+ };
32
+ /**
33
+ * A breadcrumb is a linked list of items, where
34
+ * each item has a value and a type.
35
+ */
36
+ export interface ModifierBreadcrumb extends Iterable<ModifierBreadcrumbItem> {
37
+ clear(): void;
38
+ comment: string | undefined;
39
+ first: ModifierBreadcrumbItem;
40
+ last: ModifierBreadcrumbItem;
41
+ single: boolean;
42
+ specificity: BreadcrumbSpecificity;
43
+ interactive: () => ModifierInteractiveBreadcrumb;
44
+ }
45
+ /**
46
+ * An interactive breadcrumb that holds state on what is the current item.
47
+ */
48
+ export type ModifierInteractiveBreadcrumb = {
49
+ current: ModifierBreadcrumbItem | undefined;
50
+ done: boolean;
51
+ next: () => ModifierInteractiveBreadcrumb;
52
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/dss-breadcrumb",
3
3
  "description": "The Dependency Selector Syntax (DSS) breadcrumb utilities",
4
- "version": "1.0.0-rc.23",
4
+ "version": "1.0.0-rc.25",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -12,8 +12,8 @@
12
12
  "email": "support@vlt.sh"
13
13
  },
14
14
  "dependencies": {
15
- "@vltpkg/dss-parser": "1.0.0-rc.23",
16
- "@vltpkg/error-cause": "1.0.0-rc.23"
15
+ "@vltpkg/dss-parser": "1.0.0-rc.25",
16
+ "@vltpkg/error-cause": "1.0.0-rc.25"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@eslint/js": "^9.39.1",