@qds.dev/tools 0.11.2 → 0.13.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 (38) hide show
  1. package/lib/linter/qds-internal.d.ts +204 -1
  2. package/lib/linter/qds.d.ts +59 -0
  3. package/lib/linter/qds.unit.d.ts +1 -0
  4. package/lib/linter/rule-tester.d.ts +23 -1
  5. package/lib/playground/prop-extraction.d.ts +6 -1
  6. package/lib/playground/prop-extraction.qwik.mjs +68 -9
  7. package/lib/playground/scenario-injection.qwik.mjs +41 -8
  8. package/lib/rolldown/as-child.d.ts +6 -5
  9. package/lib/rolldown/as-child.qwik.mjs +52 -91
  10. package/lib/rolldown/index.d.ts +3 -2
  11. package/lib/rolldown/index.qwik.mjs +2 -3
  12. package/lib/rolldown/inject-component-types.qwik.mjs +1 -1
  13. package/lib/rolldown/inline-asset.qwik.mjs +6 -6
  14. package/lib/rolldown/inline-css.qwik.mjs +1 -1
  15. package/lib/rolldown/qds-types.d.ts +41 -0
  16. package/lib/rolldown/qds.d.ts +5 -0
  17. package/lib/rolldown/qds.qwik.mjs +147 -0
  18. package/lib/rolldown/qds.unit.d.ts +1 -0
  19. package/lib/rolldown/ui-types.d.ts +42 -0
  20. package/lib/rolldown/ui.d.ts +12 -0
  21. package/lib/rolldown/ui.qwik.mjs +445 -0
  22. package/lib/rolldown/ui.unit.d.ts +1 -0
  23. package/lib/utils/icons/transform/mdx.d.ts +3 -11
  24. package/lib/utils/icons/transform/mdx.qwik.mjs +14 -20
  25. package/lib/utils/icons/transform/tsx.d.ts +3 -12
  26. package/lib/utils/icons/transform/tsx.qwik.mjs +28 -37
  27. package/lib/utils/index.qwik.mjs +5 -5
  28. package/lib/utils/transform-dts.qwik.mjs +1 -1
  29. package/lib/vite/index.d.ts +2 -2
  30. package/lib/vite/index.qwik.mjs +2 -3
  31. package/lib/vite/minify-content.qwik.mjs +1 -1
  32. package/linter/qds-internal.ts +707 -0
  33. package/linter/qds-internal.unit.ts +399 -0
  34. package/linter/qds.ts +300 -0
  35. package/linter/qds.unit.ts +158 -0
  36. package/linter/rule-tester.ts +395 -0
  37. package/package.json +8 -7
  38. package/lib/rolldown/icons.qwik.mjs +0 -107
@@ -0,0 +1,158 @@
1
+ import { describe, it } from "vitest";
2
+ import qdsPlugin from "./qds";
3
+ import { createRuleTester } from "./rule-tester";
4
+
5
+ describe("qds-public/no-outside-getter-usage", () => {
6
+ const { valid, invalid } = createRuleTester({
7
+ name: "qds-public/no-outside-getter-usage",
8
+ rule: qdsPlugin.rules["no-outside-getter-usage"]
9
+ });
10
+
11
+ it("allows getter inside component$ with matching root", () => {
12
+ valid(`
13
+ import { select } from "@qds.dev/ui";
14
+ import { component$, useTask$ } from "@qwik.dev/core";
15
+ export default component$(() => {
16
+ useTask$(() => { console.log(select.getIsOpen.value); });
17
+ return <select.root><div /></select.root>;
18
+ });
19
+ `);
20
+ });
21
+
22
+ it("allows getter in nested function inside component$", () => {
23
+ valid(`
24
+ import { select } from "@qds.dev/ui";
25
+ import { component$ } from "@qwik.dev/core";
26
+ export default component$(() => {
27
+ function helper() { console.log(select.getIsOpen.value); }
28
+ return <select.root><div /></select.root>;
29
+ });
30
+ `);
31
+ });
32
+
33
+ it("allows getter in JSX expression (plugin handles it)", () => {
34
+ valid(`
35
+ import { select } from "@qds.dev/ui";
36
+ import { component$ } from "@qwik.dev/core";
37
+ export default component$(() => (
38
+ <select.root><div>{select.getIsOpen.value}</div></select.root>
39
+ ));
40
+ `);
41
+ });
42
+
43
+ it("allows destructured getter inside component$ with root", () => {
44
+ valid(`
45
+ import { select } from "@qds.dev/ui";
46
+ import { component$, useTask$ } from "@qwik.dev/core";
47
+ export default component$(() => {
48
+ const { getIsOpen } = select;
49
+ useTask$(() => { console.log(getIsOpen.value); });
50
+ return <select.root><div /></select.root>;
51
+ });
52
+ `);
53
+ });
54
+
55
+ it("errors on getter at module scope", () => {
56
+ invalid({
57
+ code: `
58
+ import { select } from "@qds.dev/ui";
59
+ const labels = select.getSelectedLabels;
60
+ `,
61
+ errors: ["moduleScope"]
62
+ });
63
+ });
64
+
65
+ it("errors on destructured getter at module scope", () => {
66
+ invalid({
67
+ code: `
68
+ import { select } from "@qds.dev/ui";
69
+ const { getSelectedLabels } = select;
70
+ console.log(getSelectedLabels.value);
71
+ `,
72
+ errors: ["moduleScope"]
73
+ });
74
+ });
75
+
76
+ it("errors on getter in function defined outside component$", () => {
77
+ invalid({
78
+ code: `
79
+ import { select } from "@qds.dev/ui";
80
+ import { component$ } from "@qwik.dev/core";
81
+ function myHelper() {
82
+ console.log(select.getIsOpen.value);
83
+ }
84
+ export default component$(() => {
85
+ myHelper();
86
+ return <select.root><div /></select.root>;
87
+ });
88
+ `,
89
+ errors: ["outsideComponent"]
90
+ });
91
+ });
92
+
93
+ it("errors on getter in component$ with no matching root", () => {
94
+ invalid({
95
+ code: `
96
+ import { select } from "@qds.dev/ui";
97
+ import { component$, useTask$ } from "@qwik.dev/core";
98
+ export default component$(() => {
99
+ useTask$(() => { console.log(select.getIsOpen.value); });
100
+ return <div>No root here</div>;
101
+ });
102
+ `,
103
+ errors: ["noMatchingRoot"]
104
+ });
105
+ });
106
+
107
+ it("allows getter in child component$ when parent component$ has root", () => {
108
+ valid(`
109
+ import { select } from "@qds.dev/ui";
110
+ import { component$ } from "@qwik.dev/core";
111
+ export default component$(() => {
112
+ return (
113
+ <select.root value="jim">
114
+ <Inner />
115
+ </select.root>
116
+ );
117
+ });
118
+ export const Inner = component$(() => {
119
+ const { getIsOpen, getSelectedLabels } = select;
120
+ return (
121
+ <>
122
+ <select.trigger>{getSelectedLabels.value}</select.trigger>
123
+ <p>Status: {getIsOpen.value ? "Open" : "Closed"}</p>
124
+ </>
125
+ );
126
+ });
127
+ `);
128
+ });
129
+
130
+ it("allows getter via member expression in child component$ when parent has root", () => {
131
+ valid(`
132
+ import { select } from "@qds.dev/ui";
133
+ import { component$ } from "@qwik.dev/core";
134
+ export default component$(() => (
135
+ <select.root><Inner /></select.root>
136
+ ));
137
+ const Inner = component$(() => (
138
+ <div>{select.getIsOpen.value ? "Open" : "Closed"}</div>
139
+ ));
140
+ `);
141
+ });
142
+
143
+ it("errors on getter in component$ when no sibling component$ has root", () => {
144
+ invalid({
145
+ code: `
146
+ import { select } from "@qds.dev/ui";
147
+ import { component$ } from "@qwik.dev/core";
148
+ export default component$(() => {
149
+ return <div>{select.getIsOpen.value}</div>;
150
+ });
151
+ export const Other = component$(() => {
152
+ return <div>No root here either</div>;
153
+ });
154
+ `,
155
+ errors: ["noMatchingRoot"]
156
+ });
157
+ });
158
+ });
@@ -0,0 +1,395 @@
1
+ /**
2
+ * RuleTester for oxlint rules
3
+ *
4
+ * Provides a clean API similar to ESLint's RuleTester but designed for oxc/oxlint.
5
+ * Works seamlessly with Vitest for testing custom lint rules.
6
+ */
7
+ import { parseSync } from "oxc-parser";
8
+ import { walk, type WalkerEnter } from "oxc-walker";
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ interface Ranged {
12
+ range: [number, number];
13
+ }
14
+
15
+ interface Node extends Ranged {
16
+ type: string;
17
+ start: number;
18
+ end: number;
19
+ }
20
+
21
+ interface Context {
22
+ filename: string;
23
+ report(descriptor: {
24
+ node: Ranged;
25
+ messageId?: string;
26
+ message?: string;
27
+ data?: Record<string, string>;
28
+ }): void;
29
+ }
30
+
31
+ type VisitorWithHooks = Record<string, unknown> & {
32
+ before?: () => boolean | void;
33
+ after?: () => void;
34
+ };
35
+
36
+ type Rule =
37
+ | { meta: Record<string, unknown>; createOnce: (context: Context) => VisitorWithHooks }
38
+ | { meta: Record<string, unknown>; create: (context: Context) => VisitorWithHooks };
39
+
40
+ export interface ValidTestCase {
41
+ /** The code that should not trigger any errors */
42
+ code: string;
43
+ /** Optional filename (defaults to test.tsx) */
44
+ filename?: string;
45
+ /** Optional description for the test case */
46
+ description?: string;
47
+ }
48
+
49
+ export interface InvalidTestCase {
50
+ /** The code that should trigger errors */
51
+ code: string;
52
+ /** Expected error messages or count */
53
+ errors:
54
+ | number // Just the count of errors
55
+ | string[] // Array of strings that should appear in error messages
56
+ | Array<
57
+ | string // Just the message
58
+ | { message: string; line?: number; column?: number } // Message with optional position
59
+ | { messageId: string; data?: Record<string, string> } // MessageId with data
60
+ >;
61
+ /** Optional filename (defaults to test.tsx) */
62
+ filename?: string;
63
+ /** Optional description for the test case */
64
+ description?: string;
65
+ }
66
+
67
+ export interface RuleTesterConfig {
68
+ /** Default filename for test cases */
69
+ filename?: string;
70
+ }
71
+
72
+ interface ReportedError {
73
+ node: Ranged;
74
+ messageId?: string;
75
+ message?: string;
76
+ data?: Record<string, string>;
77
+ }
78
+
79
+ /**
80
+ * RuleTester - Test utility for oxlint rules
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import { RuleTester } from './rule-tester';
85
+ * import myRule from './my-rule';
86
+ *
87
+ * const ruleTester = new RuleTester();
88
+ *
89
+ * ruleTester.run('my-rule', myRule, {
90
+ * valid: [
91
+ * 'const x = 1;',
92
+ * { code: 'const y = 2;', filename: 'test.ts' }
93
+ * ],
94
+ * invalid: [
95
+ * {
96
+ * code: 'var x = 1;',
97
+ * errors: ['Use const instead of var']
98
+ * }
99
+ * ]
100
+ * });
101
+ * ```
102
+ */
103
+ export class RuleTester {
104
+ private config: RuleTesterConfig;
105
+
106
+ constructor(config: RuleTesterConfig = {}) {
107
+ this.config = {
108
+ filename: "test.tsx",
109
+ ...config
110
+ };
111
+ }
112
+
113
+ run(
114
+ ruleName: string,
115
+ rule: Rule,
116
+ tests: {
117
+ valid?: Array<string | ValidTestCase>;
118
+ invalid?: Array<string | InvalidTestCase>;
119
+ }
120
+ ) {
121
+ describe(ruleName, () => {
122
+ const valid = tests.valid || [];
123
+ const invalid = tests.invalid || [];
124
+
125
+ if (valid.length > 0) {
126
+ describe("valid", () => {
127
+ for (const testCase of valid) {
128
+ const normalized = this.normalizeValidTestCase(testCase);
129
+ const title = normalized.description || normalized.code.trim().slice(0, 50);
130
+
131
+ it(title, () => {
132
+ const errors = this.runRule(rule, normalized.code, normalized.filename);
133
+ expect(errors).toHaveLength(0);
134
+ });
135
+ }
136
+ });
137
+ }
138
+
139
+ if (invalid.length > 0) {
140
+ describe("invalid", () => {
141
+ for (const testCase of invalid) {
142
+ const normalized: InvalidTestCase =
143
+ typeof testCase === "string" ? { code: testCase, errors: 1 } : testCase;
144
+
145
+ const title = normalized.description || normalized.code.trim().slice(0, 50);
146
+
147
+ it(title, () => {
148
+ const errors = this.runRule(rule, normalized.code, normalized.filename);
149
+
150
+ expect(errors.length).toBeGreaterThan(0);
151
+
152
+ if (typeof normalized.errors === "number") {
153
+ expect(errors).toHaveLength(normalized.errors);
154
+ return;
155
+ }
156
+
157
+ // If errors is a string array, check each string appears in error messages
158
+ if (
159
+ Array.isArray(normalized.errors) &&
160
+ normalized.errors.every((e) => typeof e === "string")
161
+ ) {
162
+ this.validateStringArrayErrors(errors, normalized.errors as string[]);
163
+ return;
164
+ }
165
+
166
+ expect(errors).toHaveLength(normalized.errors.length);
167
+
168
+ for (let i = 0; i < normalized.errors.length; i++) {
169
+ const expectedError = normalized.errors[i];
170
+ const actualError = errors[i];
171
+
172
+ if (typeof expectedError === "string") {
173
+ const actualMessage = this.formatErrorMessage(actualError);
174
+ expect(actualMessage).toContain(expectedError);
175
+ } else if ("message" in expectedError) {
176
+ const actualMessage = this.formatErrorMessage(actualError);
177
+ expect(actualMessage).toContain(expectedError.message);
178
+ } else if ("messageId" in expectedError) {
179
+ expect(actualError.messageId).toBe(expectedError.messageId);
180
+
181
+ if (expectedError.data) {
182
+ expect(actualError.data).toMatchObject(expectedError.data);
183
+ }
184
+ }
185
+ }
186
+ });
187
+ }
188
+ });
189
+ }
190
+ });
191
+ }
192
+
193
+ private runRule(rule: Rule, code: string, filename?: string): ReportedError[] {
194
+ const errors: ReportedError[] = [];
195
+ const actualFilename = filename || this.config.filename!;
196
+
197
+ const ext = actualFilename.split(".").pop() || "tsx";
198
+ const lang = (["js", "jsx", "ts", "tsx", "dts"].includes(ext) ? ext : "tsx") as
199
+ | "js"
200
+ | "jsx"
201
+ | "ts"
202
+ | "tsx"
203
+ | "dts";
204
+
205
+ const parseResult = parseSync(actualFilename, code, { lang });
206
+
207
+ if (parseResult.errors.length > 0) {
208
+ throw new Error(
209
+ `Parse error in test code:\n${parseResult.errors.map((e) => e.message).join("\n")}`
210
+ );
211
+ }
212
+
213
+ const context: Context = this.createMockContext(code, actualFilename, errors);
214
+
215
+ const ruleInstance: VisitorWithHooks =
216
+ "createOnce" in rule ? rule.createOnce(context) : rule.create(context);
217
+
218
+ if (ruleInstance.before) {
219
+ const result = ruleInstance.before();
220
+ if (result === false) {
221
+ return errors;
222
+ }
223
+ }
224
+
225
+ const visitEnter: WalkerEnter = function (node) {
226
+ const visitor = ruleInstance[node.type as keyof VisitorWithHooks];
227
+ if (typeof visitor === "function") {
228
+ (visitor as (node: Node) => void)(node as unknown as Node);
229
+ }
230
+ };
231
+
232
+ walk(parseResult.program, {
233
+ enter: visitEnter,
234
+ leave(node) {
235
+ const exitKey = `${node.type}:exit`;
236
+ const exitVisitor = ruleInstance[exitKey];
237
+ if (typeof exitVisitor === "function") {
238
+ (exitVisitor as (node: Node) => void)(node as unknown as Node);
239
+ }
240
+ }
241
+ });
242
+
243
+ if (ruleInstance.after) {
244
+ ruleInstance.after();
245
+ }
246
+
247
+ return errors;
248
+ }
249
+
250
+ private createMockContext(
251
+ code: string,
252
+ filename: string,
253
+ errors: ReportedError[]
254
+ ): Context {
255
+ return {
256
+ filename,
257
+ report(descriptor: {
258
+ node: Ranged;
259
+ messageId?: string;
260
+ message?: string;
261
+ data?: Record<string, string>;
262
+ }) {
263
+ errors.push({
264
+ node: descriptor.node,
265
+ messageId: descriptor.messageId,
266
+ message: descriptor.message,
267
+ data: descriptor.data
268
+ });
269
+ }
270
+ };
271
+ }
272
+
273
+ private normalizeValidTestCase(testCase: string | ValidTestCase): ValidTestCase {
274
+ if (typeof testCase === "string") {
275
+ return { code: testCase };
276
+ }
277
+ return testCase;
278
+ }
279
+
280
+ private formatErrorMessage(error: ReportedError): string {
281
+ if (error.message) {
282
+ return error.message;
283
+ }
284
+
285
+ // Include both messageId and data values for matching
286
+ const parts: string[] = [error.messageId || ""];
287
+
288
+ if (error.data) {
289
+ parts.push(...Object.values(error.data));
290
+ }
291
+
292
+ return parts.join(" ");
293
+ }
294
+
295
+ private validateStringArrayErrors(errors: ReportedError[], expected: string[]): void {
296
+ expect(errors).toHaveLength(expected.length);
297
+ for (let i = 0; i < expected.length; i++) {
298
+ const expectedFragment = expected[i];
299
+ const actualMessage = this.formatErrorMessage(errors[i]);
300
+ expect(actualMessage).toContain(expectedFragment);
301
+ }
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Creates explicit test helpers for better DX
307
+ * Inspired by eslint-vitest-rule-tester but with our intuitive error format
308
+ */
309
+ export function createRuleTester(options: {
310
+ name: string;
311
+ rule: Rule;
312
+ filename?: string;
313
+ }) {
314
+ const tester = new RuleTester({
315
+ filename: options.filename || "test.tsx"
316
+ });
317
+
318
+ /**
319
+ * Test a valid code case
320
+ */
321
+ function valid(code: string | ValidTestCase) {
322
+ const normalized = typeof code === "string" ? { code } : code;
323
+ const errors = tester["runRule"](options.rule, normalized.code, normalized.filename);
324
+ expect(errors).toHaveLength(0);
325
+ }
326
+
327
+ /**
328
+ * Test an invalid code case
329
+ * Returns errors for further assertions (e.g., snapshots)
330
+ */
331
+ function invalid(
332
+ testCase:
333
+ | string
334
+ | {
335
+ code: string;
336
+ errors:
337
+ | number
338
+ | string[]
339
+ | Array<
340
+ | string
341
+ | { message: string; line?: number; column?: number }
342
+ | { messageId: string; data?: Record<string, string> }
343
+ >;
344
+ filename?: string;
345
+ output?: string;
346
+ }
347
+ ) {
348
+ const normalized: InvalidTestCase =
349
+ typeof testCase === "string" ? { code: testCase, errors: 1 } : testCase;
350
+
351
+ const errors = tester["runRule"](options.rule, normalized.code, normalized.filename);
352
+
353
+ expect(errors.length).toBeGreaterThan(0);
354
+
355
+ // If errors is just a number, only check the count
356
+ if (typeof normalized.errors === "number") {
357
+ expect(errors).toHaveLength(normalized.errors);
358
+ return { errors };
359
+ }
360
+
361
+ // If errors is a string array, check each string appears in error messages
362
+ if (
363
+ Array.isArray(normalized.errors) &&
364
+ normalized.errors.every((e) => typeof e === "string")
365
+ ) {
366
+ tester["validateStringArrayErrors"](errors, normalized.errors as string[]);
367
+ return { errors };
368
+ }
369
+
370
+ expect(errors).toHaveLength(normalized.errors.length);
371
+
372
+ for (let i = 0; i < normalized.errors.length; i++) {
373
+ const expectedError = normalized.errors[i];
374
+ const actualError = errors[i];
375
+
376
+ if (typeof expectedError === "string") {
377
+ const actualMessage = tester["formatErrorMessage"](actualError);
378
+ expect(actualMessage).toContain(expectedError);
379
+ } else if ("message" in expectedError) {
380
+ const actualMessage = tester["formatErrorMessage"](actualError);
381
+ expect(actualMessage).toContain(expectedError.message);
382
+ } else if ("messageId" in expectedError) {
383
+ expect(actualError.messageId).toBe(expectedError.messageId);
384
+
385
+ if (expectedError.data) {
386
+ expect(actualError.data).toMatchObject(expectedError.data);
387
+ }
388
+ }
389
+ }
390
+
391
+ return { errors };
392
+ }
393
+
394
+ return { valid, invalid };
395
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qds.dev/tools",
3
- "version": "0.11.2",
3
+ "version": "0.13.0",
4
4
  "private": false,
5
5
  "description": "Tools and utilities for Qwik Design System",
6
6
  "repository": {
@@ -8,7 +8,8 @@
8
8
  "url": "https://github.com/kunai-consulting/qwik-design-system"
9
9
  },
10
10
  "files": [
11
- "lib"
11
+ "lib",
12
+ "linter"
12
13
  ],
13
14
  "type": "module",
14
15
  "sideEffects": false,
@@ -45,12 +46,12 @@
45
46
  "dependencies": {
46
47
  "@iconify/json": "^2.2.382",
47
48
  "@iconify/utils": "^3.0.1",
48
- "@oxc-project/types": "^0.111.0",
49
+ "@oxc-project/types": "^0.115.0",
49
50
  "magic-regexp": "^0.10.0",
50
- "oxc-minify": "^0.111.0",
51
- "oxc-parser": "^0.111.0",
52
- "oxc-transform": "^0.111.0",
53
- "oxc-walker": "^0.5.2",
51
+ "oxc-minify": "^0.115.0",
52
+ "oxc-parser": "^0.115.0",
53
+ "oxc-transform": "^0.115.0",
54
+ "oxc-walker": "^0.7.0",
54
55
  "remark": "^15.0.1",
55
56
  "remark-mdx": "^3.1.1"
56
57
  },
@@ -1,107 +0,0 @@
1
- import { CollectionLoader } from "../utils/icons/collections/loader.qwik.mjs";
2
- import { resolveImportAliases } from "../utils/icons/import-resolver.qwik.mjs";
3
- import { transformMDXFile } from "../utils/icons/transform/mdx.qwik.mjs";
4
- import { transformTSXFile } from "../utils/icons/transform/tsx.qwik.mjs";
5
- import { anyOf, createRegExp, exactly } from "magic-regexp";
6
- import { parseSync } from "oxc-parser";
7
-
8
- //#region rolldown/icons.ts
9
- /**
10
- * Rolldown plugin that transforms icon JSX elements to direct <svg /> calls
11
- * @param options - Plugin configuration options
12
- * @returns Rolldown-compatible plugin object
13
- */ const icons = (options = {}) => {
14
- const importSources = options.importSources ?? ["@qds.dev/ui"];
15
- const isDebugMode = !!options.debug;
16
- const collectionNames = /* @__PURE__ */ new Map();
17
- const debug = (message, ...data) => {
18
- if (!isDebugMode) return;
19
- console.log(`[icons] ${message}`, ...data);
20
- };
21
- const collectionLoader = new CollectionLoader(debug);
22
- /**
23
- * Parse and validate a file, returning the AST if valid
24
- * @param code - Source code to parse
25
- * @param id - File ID for debugging
26
- * @returns AST program if valid, null if invalid
27
- */ function parseAndValidateFile(code, id) {
28
- try {
29
- const parsed = parseSync(id, code);
30
- if (parsed.errors.length > 0) {
31
- debug(`Parse errors in ${id}:`, parsed.errors.map((e) => e.message));
32
- return null;
33
- }
34
- return parsed.program;
35
- } catch (error) {
36
- debug(`Error parsing ${id}:`, error);
37
- return null;
38
- }
39
- }
40
- const isTSXJSXOrMDX = createRegExp(exactly(".").and(anyOf("tsx", "jsx", "mdx")).at.lineEnd());
41
- const isVirtualIconsImport = createRegExp(exactly("virtual:icons/").at.lineStart());
42
- const isVirtualIconsModule = createRegExp(exactly(String.fromCharCode(0)).and("virtual:icons/"));
43
- return {
44
- name: "vite-plugin-qds-icons",
45
- enforce: "pre",
46
- transform: {
47
- filter: { id: isTSXJSXOrMDX },
48
- handler(code, id) {
49
- if (collectionLoader.getAvailableCollections().size === 0) collectionLoader.discoverCollections();
50
- if (id.endsWith(".mdx")) return transformMDXFile(code, id, importSources, collectionLoader.getAvailableCollections(), collectionNames, options.packs, debug);
51
- debug(`[TRANSFORM] Starting transformation for ${id}`);
52
- try {
53
- const ast = parseAndValidateFile(code, id);
54
- if (!ast) {
55
- debug(`[TRANSFORM] Failed to parse ${id}`);
56
- return null;
57
- }
58
- const aliasToPack = resolveImportAliases(ast, importSources, collectionLoader.getAvailableCollections(), collectionNames, options.packs, debug);
59
- if (aliasToPack.size === 0) {
60
- debug(`[TRANSFORM] No icon imports found in ${id}`);
61
- return null;
62
- }
63
- return transformTSXFile(code, id, ast, aliasToPack, collectionNames, collectionLoader.getAvailableCollections(), options.packs, debug);
64
- } catch (error) {
65
- debug(`[TRANSFORM] Error during transformation of ${id}:`, error);
66
- return null;
67
- }
68
- }
69
- },
70
- resolveId: {
71
- filter: { id: isVirtualIconsImport },
72
- handler(source) {
73
- if (source.startsWith("virtual:icons/")) return `\0${source}`;
74
- return null;
75
- }
76
- },
77
- load: {
78
- filter: { id: isVirtualIconsModule },
79
- async handler(id) {
80
- const virtualPath = id.slice(1);
81
- const parts = virtualPath.split("/");
82
- const prefix = parts[1];
83
- const name = parts[2];
84
- if (!prefix || !name) {
85
- debug(`Invalid virtual icon path: ${virtualPath}`);
86
- return null;
87
- }
88
- try {
89
- const iconData = name.includes("-") ? await collectionLoader.loadIconDataLazy(prefix, name) : await collectionLoader.loadIconDataByLowercase(prefix, name);
90
- if (!iconData) {
91
- debug(`Failed to load icon data for ${prefix}:${name}`);
92
- return { code: `export default '<path d="M12 2L2 7l10 5 10-5z"/><path d="M2 17l10 5 10-5M2 12l10 5 10-5"/>';\n` };
93
- }
94
- const code = `export default \`${iconData.body}\`;`;
95
- debug(`Generated virtual module for ${prefix}:${name}`);
96
- return { code };
97
- } catch (error) {
98
- debug(`Error loading virtual module ${virtualPath}:`, error);
99
- return { code: `export default '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>';\n` };
100
- }
101
- }
102
- }
103
- };
104
- };
105
-
106
- //#endregion
107
- export { icons };