@qds.dev/tools 0.11.1 → 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.
- package/lib/linter/qds-internal.d.ts +210 -0
- package/lib/linter/qds.d.ts +59 -0
- package/{lib-types/tools → lib}/linter/rule-tester.d.ts +23 -1
- package/{lib-types/tools → lib}/playground/prop-extraction.d.ts +6 -1
- package/lib/playground/prop-extraction.qwik.mjs +68 -9
- package/lib/playground/scenario-injection.qwik.mjs +41 -8
- package/lib/rolldown/as-child.d.ts +16 -0
- package/lib/rolldown/as-child.qwik.mjs +52 -91
- package/lib/rolldown/icons.d.ts +21 -0
- package/{lib-types/tools → lib}/rolldown/index.d.ts +4 -2
- package/lib/rolldown/index.qwik.mjs +3 -3
- package/lib/rolldown/inject-component-types.d.ts +2 -0
- package/lib/rolldown/inject-component-types.qwik.mjs +138 -0
- package/lib/rolldown/inline-asset.qwik.mjs +6 -6
- package/lib/rolldown/inline-css.d.ts +2 -0
- package/lib/rolldown/inline-css.qwik.mjs +1 -1
- package/lib/rolldown/qds-types.d.ts +41 -0
- package/lib/rolldown/qds.d.ts +5 -0
- package/lib/rolldown/qds.qwik.mjs +147 -0
- package/lib/rolldown/qwik-rolldown.d.ts +6 -0
- package/lib/rolldown/ui-types.d.ts +42 -0
- package/lib/rolldown/ui.d.ts +12 -0
- package/lib/rolldown/ui.qwik.mjs +445 -0
- package/lib/utils/icons/naming.unit.d.ts +1 -0
- package/{lib-types/tools → lib}/utils/icons/transform/mdx.d.ts +3 -11
- package/lib/utils/icons/transform/mdx.qwik.mjs +14 -20
- package/{lib-types/tools → lib}/utils/icons/transform/tsx.d.ts +3 -12
- package/lib/utils/icons/transform/tsx.qwik.mjs +28 -37
- package/lib/utils/index.qwik.mjs +5 -5
- package/{lib-types/tools → lib}/utils/transform-dts.d.ts +4 -3
- package/lib/utils/transform-dts.qwik.mjs +18 -23
- package/lib/utils/transform-dts.unit.d.ts +1 -0
- package/{lib-types/tools → lib}/vite/index.d.ts +2 -2
- package/lib/vite/index.qwik.mjs +2 -3
- package/lib/vite/minify-content.qwik.mjs +1 -1
- package/lib/vite/minify-content.unit.d.ts +1 -0
- package/linter/qds-internal.ts +707 -0
- package/linter/qds-internal.unit.ts +399 -0
- package/linter/qds.ts +300 -0
- package/linter/qds.unit.ts +158 -0
- package/linter/rule-tester.ts +395 -0
- package/package.json +17 -18
- package/lib/rolldown/icons.qwik.mjs +0 -112
- package/lib-types/components/src/icons-runtime.d.ts +0 -223
- package/lib-types/tools/linter/qds-internal.d.ts +0 -7
- package/lib-types/tools/rolldown/as-child.d.ts +0 -24
- package/lib-types/tools/rolldown/icons.d.ts +0 -45
- package/lib-types/tools/rolldown/inline-css.d.ts +0 -26
- package/lib-types/tools/rolldown/qwik-rolldown.d.ts +0 -9
- /package/{lib-types/tools → lib}/linter/qds-internal.unit.d.ts +0 -0
- /package/{lib-types/tools/playground/generate-metadata.d.ts → lib/linter/qds.unit.d.ts} +0 -0
- /package/{lib-types/tools/playground/generate-metadata.unit.d.ts → lib/playground/generate-metadata.d.ts} +0 -0
- /package/{lib-types/tools/playground/prop-extraction.unit.d.ts → lib/playground/generate-metadata.unit.d.ts} +0 -0
- /package/{lib-types/tools → lib}/playground/index.d.ts +0 -0
- /package/{lib-types/tools/playground/scenario-injection.unit.d.ts → lib/playground/prop-extraction.unit.d.ts} +0 -0
- /package/{lib-types/tools → lib}/playground/scenario-injection.d.ts +0 -0
- /package/{lib-types/tools/rolldown/as-child.unit.d.ts → lib/playground/scenario-injection.unit.d.ts} +0 -0
- /package/{lib-types/tools/rolldown/icons.unit.d.ts → lib/rolldown/as-child.unit.d.ts} +0 -0
- /package/{lib-types/tools/rolldown/inline-asset.unit.d.ts → lib/rolldown/icons.unit.d.ts} +0 -0
- /package/{lib-types/tools/src/generate/icon-types.unit.d.ts → lib/rolldown/inject-component-types.unit.d.ts} +0 -0
- /package/{lib-types/tools → lib}/rolldown/inline-asset.d.ts +0 -0
- /package/{lib-types/tools/utils/icons/ast/expressions.unit.d.ts → lib/rolldown/inline-asset.unit.d.ts} +0 -0
- /package/{lib-types/tools/utils/icons/ast/jsx.unit.d.ts → lib/rolldown/qds.unit.d.ts} +0 -0
- /package/{lib-types/tools/utils/icons/naming.unit.d.ts → lib/rolldown/ui.unit.d.ts} +0 -0
- /package/{lib-types/tools → lib}/src/generate/icon-types.d.ts +0 -0
- /package/{lib-types/tools → lib}/src/index.d.ts +0 -0
- /package/{lib-types/tools → lib}/src/vite.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/ast/core.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/ast/imports.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/ast/jsx-helpers.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/ast/qwik.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/fs-mock.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/fs.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/icons/ast/expressions.d.ts +0 -0
- /package/{lib-types/tools/utils/transform-dts.unit.d.ts → lib/utils/icons/ast/expressions.unit.d.ts} +0 -0
- /package/{lib-types/tools → lib}/utils/icons/ast/jsx.d.ts +0 -0
- /package/{lib-types/tools/vite/minify-content.unit.d.ts → lib/utils/icons/ast/jsx.unit.d.ts} +0 -0
- /package/{lib-types/tools → lib}/utils/icons/collections/loader.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/icons/import-resolver.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/icons/naming.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/icons/transform/shared.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/icons/types/mdx-ast.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/index.d.ts +0 -0
- /package/{lib-types/tools → lib}/utils/package-json.d.ts +0 -0
- /package/{lib-types/tools → lib}/vite/minify-content.d.ts +0 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { describe, it } from "vitest";
|
|
2
|
+
import qdsPlugin, { requireContextProxyRule } from "./qds-internal";
|
|
3
|
+
import { createRuleTester } from "./rule-tester";
|
|
4
|
+
|
|
5
|
+
describe("qds/no-default-name", () => {
|
|
6
|
+
const { valid, invalid } = createRuleTester({
|
|
7
|
+
name: "qds/no-default-name",
|
|
8
|
+
rule: qdsPlugin.rules["no-default-name"]
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("allows value-based props", () => {
|
|
12
|
+
valid("interface Props { value?: string; checked?: boolean; }");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("allows QRL props", () => {
|
|
16
|
+
valid("interface Props { onChange$?: QRL<() => void>; }");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("flags defaultValue pattern", () => {
|
|
20
|
+
invalid("interface Props { defaultValue?: string; }");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("flags multiple default patterns", () => {
|
|
24
|
+
invalid({
|
|
25
|
+
code: "interface Props { defaultChecked?: boolean; defaultOpen?: boolean; }",
|
|
26
|
+
errors: ["defaultChecked", "defaultOpen"]
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("flags default pattern in type alias", () => {
|
|
31
|
+
invalid({
|
|
32
|
+
code: "type Props = { defaultPressed?: boolean; };",
|
|
33
|
+
errors: ["defaultPressed"]
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("flags default pattern in .ts files", () => {
|
|
38
|
+
invalid({
|
|
39
|
+
code: "interface Props { defaultValue?: string; }",
|
|
40
|
+
filename: "utils.ts",
|
|
41
|
+
errors: ["defaultValue"]
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("qds/event-handlers-array-pattern", () => {
|
|
47
|
+
const { valid, invalid } = createRuleTester({
|
|
48
|
+
name: "qds/event-handlers-array-pattern",
|
|
49
|
+
rule: qdsPlugin.rules["event-handlers-array-pattern"]
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("allows array pattern", () => {
|
|
53
|
+
valid("<button onClick$={[handleClick$, props.onClick$]} />");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("allows ternary expressions", () => {
|
|
57
|
+
valid("<button onClick$={isDisabled ? undefined : handleClick$} />");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("flags non-array event handler", () => {
|
|
61
|
+
invalid({
|
|
62
|
+
code: "<button onClick$={handleClick$} />",
|
|
63
|
+
errors: ["onClick$"]
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("flags multiple non-array handlers", () => {
|
|
68
|
+
invalid({
|
|
69
|
+
code: "<input onKeyDown$={handleKeyDown$} onChange$={handleChange$} />",
|
|
70
|
+
errors: ["onKeyDown$", "onChange$"]
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("allows events without $ suffix", () => {
|
|
75
|
+
valid("<button onClick={handleClick} />");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("qds/one-element-composition", () => {
|
|
80
|
+
const { valid, invalid } = createRuleTester({
|
|
81
|
+
name: "qds/one-element-composition",
|
|
82
|
+
rule: qdsPlugin.rules["one-element-composition"]
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("allows single element type", () => {
|
|
86
|
+
valid(`
|
|
87
|
+
const Button = component$(() => {
|
|
88
|
+
return <button>Click</button>;
|
|
89
|
+
});
|
|
90
|
+
`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("allows components with custom components", () => {
|
|
94
|
+
valid(`
|
|
95
|
+
const Wrapper = component$(() => {
|
|
96
|
+
return <div><CustomComponent /><AnotherComponent /></div>;
|
|
97
|
+
});
|
|
98
|
+
`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("flags multiple HTML element types", () => {
|
|
102
|
+
invalid({
|
|
103
|
+
code: `
|
|
104
|
+
const BadComponent = component$(() => {
|
|
105
|
+
return <div><span>Text</span></div>;
|
|
106
|
+
});
|
|
107
|
+
`,
|
|
108
|
+
errors: ["div, span"]
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("flags multiple different elements", () => {
|
|
113
|
+
invalid({
|
|
114
|
+
code: `
|
|
115
|
+
const Complex = component$(() => {
|
|
116
|
+
return (
|
|
117
|
+
<section>
|
|
118
|
+
<div>Content</div>
|
|
119
|
+
<p>More</p>
|
|
120
|
+
</section>
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
`,
|
|
124
|
+
errors: 1
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("allows single element with nested same type", () => {
|
|
129
|
+
valid(`
|
|
130
|
+
const List = component$(() => {
|
|
131
|
+
return <div><div>Nested</div></div>;
|
|
132
|
+
});
|
|
133
|
+
`);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("qds/require-use-bindings", () => {
|
|
138
|
+
const { valid, invalid } = createRuleTester({
|
|
139
|
+
name: "qds/require-use-bindings",
|
|
140
|
+
rule: qdsPlugin.rules["require-use-bindings"],
|
|
141
|
+
filename: "button-root.tsx"
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("allows root component with useBindings", () => {
|
|
145
|
+
valid({
|
|
146
|
+
code: `
|
|
147
|
+
const Button = component$(() => {
|
|
148
|
+
const { valueSig } = useBindings(props, { value: "" });
|
|
149
|
+
return <button />;
|
|
150
|
+
});
|
|
151
|
+
`,
|
|
152
|
+
filename: "button-root.tsx"
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("flags root component without useBindings", () => {
|
|
157
|
+
invalid({
|
|
158
|
+
code: `
|
|
159
|
+
const Button = component$(() => {
|
|
160
|
+
return <button />;
|
|
161
|
+
});
|
|
162
|
+
`,
|
|
163
|
+
filename: "checkbox-root.tsx",
|
|
164
|
+
errors: 1
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("allows non-root component without useBindings", () => {
|
|
169
|
+
valid({
|
|
170
|
+
code: `
|
|
171
|
+
const Helper = component$(() => {
|
|
172
|
+
return <span />;
|
|
173
|
+
});
|
|
174
|
+
`,
|
|
175
|
+
filename: "helper.tsx"
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("qds/require-destructure-bindings", () => {
|
|
181
|
+
const { valid, invalid } = createRuleTester({
|
|
182
|
+
name: "qds/require-destructure-bindings",
|
|
183
|
+
rule: qdsPlugin.rules["require-destructure-bindings"]
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("allows file with both useBindings and destructureBindings", () => {
|
|
187
|
+
valid(`
|
|
188
|
+
const { valueSig } = useBindings(props, { value: "" });
|
|
189
|
+
const { value } = destructureBindings(props, { value: "" });
|
|
190
|
+
`);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("flags file with useBindings but missing destructureBindings", () => {
|
|
194
|
+
invalid({
|
|
195
|
+
code: `
|
|
196
|
+
const { valueSig } = useBindings(props, { value: "" });
|
|
197
|
+
`,
|
|
198
|
+
errors: 1
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("allows file without useBindings", () => {
|
|
203
|
+
valid(`
|
|
204
|
+
const { value } = destructureBindings(props, { value: "" });
|
|
205
|
+
`);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("qds/require-research-file", () => {
|
|
210
|
+
const { valid, invalid } = createRuleTester({
|
|
211
|
+
name: "qds/require-research-file",
|
|
212
|
+
rule: qdsPlugin.rules["require-research-file"],
|
|
213
|
+
filename: "button-root.tsx"
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("flags root component (note: actual file check is in CI)", () => {
|
|
217
|
+
// Note: This rule can't actually check the filesystem from oxlint
|
|
218
|
+
// It serves as documentation/reminder, enforced by GitHub Actions
|
|
219
|
+
// Users can disable it per-file once they've added the research file
|
|
220
|
+
invalid({
|
|
221
|
+
code: `
|
|
222
|
+
const Button = component$(() => {
|
|
223
|
+
return <button />;
|
|
224
|
+
});
|
|
225
|
+
`,
|
|
226
|
+
filename: "checkbox-root.tsx",
|
|
227
|
+
errors: 1
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("allows non-root component", () => {
|
|
232
|
+
valid({
|
|
233
|
+
code: `
|
|
234
|
+
const Helper = component$(() => {
|
|
235
|
+
return <span />;
|
|
236
|
+
});
|
|
237
|
+
`,
|
|
238
|
+
filename: "helper.tsx"
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("qds/require-test-file", () => {
|
|
244
|
+
const { valid, invalid } = createRuleTester({
|
|
245
|
+
name: "qds/require-test-file",
|
|
246
|
+
rule: qdsPlugin.rules["require-test-file"],
|
|
247
|
+
filename: "button-root.tsx"
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("flags root component (note: actual file check is in CI)", () => {
|
|
251
|
+
// Note: This rule can't actually check the filesystem from oxlint
|
|
252
|
+
// It serves as documentation/reminder, enforced by GitHub Actions
|
|
253
|
+
// Users can disable it per-file once they've added the test file
|
|
254
|
+
invalid({
|
|
255
|
+
code: `
|
|
256
|
+
const Button = component$(() => {
|
|
257
|
+
return <button />;
|
|
258
|
+
});
|
|
259
|
+
`,
|
|
260
|
+
filename: "checkbox-root.tsx",
|
|
261
|
+
errors: 1
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("allows non-root component", () => {
|
|
266
|
+
valid({
|
|
267
|
+
code: `
|
|
268
|
+
const Helper = component$(() => {
|
|
269
|
+
return <span />;
|
|
270
|
+
});
|
|
271
|
+
`,
|
|
272
|
+
filename: "helper.tsx"
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("qds-internal/require-context-proxy", () => {
|
|
278
|
+
const { valid, invalid } = createRuleTester({
|
|
279
|
+
name: "qds-internal/require-context-proxy",
|
|
280
|
+
rule: requireContextProxyRule
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("allows correct createContextProxy usage with string literal argument", () => {
|
|
284
|
+
valid(`
|
|
285
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
286
|
+
const context = createContextProxy();
|
|
287
|
+
export const getIsOpen = context("isOpen");
|
|
288
|
+
`);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("allows file with no createContextProxy import (rule skips entirely)", () => {
|
|
292
|
+
valid(`
|
|
293
|
+
export const getIsOpen = someFunction("isOpen");
|
|
294
|
+
`);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("errors when export uses proxy variable without get prefix", () => {
|
|
298
|
+
invalid({
|
|
299
|
+
code: `
|
|
300
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
301
|
+
const context = createContextProxy();
|
|
302
|
+
export const something = context("field");
|
|
303
|
+
`,
|
|
304
|
+
errors: ["missingGetPrefix"]
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("allows multiple getter exports all using context() correctly", () => {
|
|
309
|
+
valid(`
|
|
310
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
311
|
+
const context = createContextProxy();
|
|
312
|
+
export const getIsOpen = context("isOpen");
|
|
313
|
+
export const getLabel = context("label");
|
|
314
|
+
`);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("errors when context() is called with a variable argument", () => {
|
|
318
|
+
invalid({
|
|
319
|
+
code: `
|
|
320
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
321
|
+
const context = createContextProxy();
|
|
322
|
+
export const getIsOpen = context(someVar);
|
|
323
|
+
`,
|
|
324
|
+
errors: ["nonStringArg"]
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("errors when getter export uses a function other than context() from createContextProxy", () => {
|
|
329
|
+
invalid({
|
|
330
|
+
code: `
|
|
331
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
332
|
+
const context = createContextProxy();
|
|
333
|
+
export const getIsOpen = otherFunction("isOpen");
|
|
334
|
+
`,
|
|
335
|
+
errors: ["notContextProxy"]
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("errors when export uses proxy variable but missing get prefix", () => {
|
|
340
|
+
invalid({
|
|
341
|
+
code: `
|
|
342
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
343
|
+
const context = createContextProxy();
|
|
344
|
+
export const someIsOpen = context("isOpen");
|
|
345
|
+
`,
|
|
346
|
+
errors: ["missingGetPrefix"]
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("errors when context() is called with no arguments", () => {
|
|
351
|
+
invalid({
|
|
352
|
+
code: `
|
|
353
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
354
|
+
const context = createContextProxy();
|
|
355
|
+
export const getIsOpen = context();
|
|
356
|
+
`,
|
|
357
|
+
errors: ["missingArg"]
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("allows getter name that matches context field", () => {
|
|
362
|
+
valid(`
|
|
363
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
364
|
+
const context = createContextProxy();
|
|
365
|
+
export const getIsOpen = context("isOpen");
|
|
366
|
+
export const getSelectedLabels = context("selectedLabels");
|
|
367
|
+
`);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("errors when getter name does not match context field", () => {
|
|
371
|
+
invalid({
|
|
372
|
+
code: `
|
|
373
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
374
|
+
const context = createContextProxy();
|
|
375
|
+
export const getSomeIsOpen = context("isOpen");
|
|
376
|
+
`,
|
|
377
|
+
errors: ["getterNameMismatch"]
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("errors when getter name derives wrong field", () => {
|
|
382
|
+
invalid({
|
|
383
|
+
code: `
|
|
384
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
385
|
+
const context = createContextProxy();
|
|
386
|
+
export const getOpen = context("isOpen");
|
|
387
|
+
`,
|
|
388
|
+
errors: ["getterNameMismatch"]
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("allows getHighlightedIndex matching highlightedIndex", () => {
|
|
393
|
+
valid(`
|
|
394
|
+
import { createContextProxy } from "@qds.dev/base";
|
|
395
|
+
const context = createContextProxy();
|
|
396
|
+
export const getHighlightedIndex = context("highlightedIndex");
|
|
397
|
+
`);
|
|
398
|
+
});
|
|
399
|
+
});
|
package/linter/qds.ts
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oxlint plugin for Qwik Design System (public)
|
|
3
|
+
*
|
|
4
|
+
* Consumer-facing lint plugin that QDS users install in their own projects.
|
|
5
|
+
* Contains only the no-outside-getter-usage rule.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface Ranged {
|
|
9
|
+
range: [number, number];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Node extends Ranged {
|
|
13
|
+
type: string;
|
|
14
|
+
start: number;
|
|
15
|
+
end: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Context {
|
|
19
|
+
filename: string;
|
|
20
|
+
report(descriptor: {
|
|
21
|
+
node: Ranged;
|
|
22
|
+
messageId?: string;
|
|
23
|
+
message?: string;
|
|
24
|
+
data?: Record<string, string>;
|
|
25
|
+
}): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
namespace ESTree {
|
|
29
|
+
interface BaseNode extends Ranged {
|
|
30
|
+
type: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface Identifier extends BaseNode {
|
|
34
|
+
type: "Identifier";
|
|
35
|
+
name: string;
|
|
36
|
+
}
|
|
37
|
+
export interface CallExpression extends BaseNode {
|
|
38
|
+
type: "CallExpression";
|
|
39
|
+
callee: Expression;
|
|
40
|
+
arguments: BaseNode[];
|
|
41
|
+
}
|
|
42
|
+
export interface VariableDeclarator extends BaseNode {
|
|
43
|
+
type: "VariableDeclarator";
|
|
44
|
+
id: Identifier | BaseNode;
|
|
45
|
+
init: Expression | null;
|
|
46
|
+
}
|
|
47
|
+
export type Expression = BaseNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function defineRule<const T extends Record<string, unknown>>(rule: T): T {
|
|
51
|
+
return rule;
|
|
52
|
+
}
|
|
53
|
+
function definePlugin<const T extends Record<string, unknown>>(plugin: T): T {
|
|
54
|
+
return plugin;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface DeferredGetterReport {
|
|
58
|
+
node: Ranged;
|
|
59
|
+
getter: string;
|
|
60
|
+
namespace: string;
|
|
61
|
+
depth: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const QDS_IMPORT_SOURCES = new Set(["@qds.dev/ui"]);
|
|
65
|
+
|
|
66
|
+
function isGetterProperty(propertyName: string): boolean {
|
|
67
|
+
return propertyName.startsWith("get") && propertyName.length > 3;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const noOutsideGetterUsageRule = defineRule({
|
|
71
|
+
meta: {
|
|
72
|
+
type: "problem",
|
|
73
|
+
docs: {
|
|
74
|
+
description:
|
|
75
|
+
"Getters from QDS namespaces must be used inside a component$ that renders the matching root element",
|
|
76
|
+
category: "Best Practices",
|
|
77
|
+
recommended: true,
|
|
78
|
+
url: "https://qds.dev/contributing/state/"
|
|
79
|
+
},
|
|
80
|
+
messages: {
|
|
81
|
+
moduleScope:
|
|
82
|
+
"'{{getter}}' used at module scope. Getters must be used inside a component$ that renders <{{namespace}}.root> or any of its descendants.",
|
|
83
|
+
outsideComponent:
|
|
84
|
+
"'{{getter}}' used in a function outside any component$. Move this into a component$ rendered inside <{{namespace}}.root>.",
|
|
85
|
+
noMatchingRoot:
|
|
86
|
+
"'{{getter}}' used but no <{{namespace}}.root> found in JSX return. Add <{{namespace}}.root> or move this into a component that renders inside one."
|
|
87
|
+
},
|
|
88
|
+
schema: []
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
create(context: Context) {
|
|
92
|
+
const qdsNamespaces = new Set<string>();
|
|
93
|
+
const destructuredGetters = new Map<string, string>();
|
|
94
|
+
const componentCallNodes: Node[] = [];
|
|
95
|
+
const componentRoots = new Map<number, Set<string>>();
|
|
96
|
+
const deferredReports: DeferredGetterReport[] = [];
|
|
97
|
+
const fileRoots = new Set<string>();
|
|
98
|
+
const fileDeferredReports: DeferredGetterReport[] = [];
|
|
99
|
+
let functionDepthOutsideComponent = 0;
|
|
100
|
+
|
|
101
|
+
function componentDepth(): number {
|
|
102
|
+
return componentCallNodes.length;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleGetterReference(
|
|
106
|
+
reportNode: Ranged,
|
|
107
|
+
getter: string,
|
|
108
|
+
namespace: string
|
|
109
|
+
): void {
|
|
110
|
+
if (componentDepth() === 0 && functionDepthOutsideComponent === 0) {
|
|
111
|
+
context.report({
|
|
112
|
+
node: reportNode,
|
|
113
|
+
messageId: "moduleScope",
|
|
114
|
+
data: { getter, namespace }
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (componentDepth() === 0 && functionDepthOutsideComponent > 0) {
|
|
120
|
+
context.report({
|
|
121
|
+
node: reportNode,
|
|
122
|
+
messageId: "outsideComponent",
|
|
123
|
+
data: { getter, namespace }
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
deferredReports.push({
|
|
129
|
+
node: reportNode,
|
|
130
|
+
getter,
|
|
131
|
+
namespace,
|
|
132
|
+
depth: componentDepth()
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
ImportDeclaration(node: Node) {
|
|
138
|
+
const importNode = node as unknown as Record<string, unknown>;
|
|
139
|
+
const source = importNode.source as Record<string, unknown> | undefined;
|
|
140
|
+
if (!source || typeof source.value !== "string") return;
|
|
141
|
+
if (!QDS_IMPORT_SOURCES.has(source.value)) return;
|
|
142
|
+
|
|
143
|
+
const specifiers = importNode.specifiers as
|
|
144
|
+
| Array<Record<string, unknown>>
|
|
145
|
+
| undefined;
|
|
146
|
+
if (!Array.isArray(specifiers)) return;
|
|
147
|
+
|
|
148
|
+
for (const specifier of specifiers) {
|
|
149
|
+
if (specifier.type !== "ImportSpecifier") continue;
|
|
150
|
+
const local = specifier.local as Record<string, unknown> | undefined;
|
|
151
|
+
if (local?.type === "Identifier" && typeof local.name === "string") {
|
|
152
|
+
qdsNamespaces.add(local.name);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
CallExpression(node: Node) {
|
|
158
|
+
const callNode = node as unknown as ESTree.CallExpression;
|
|
159
|
+
const callee = callNode.callee;
|
|
160
|
+
if (callee.type !== "Identifier") return;
|
|
161
|
+
if ((callee as unknown as ESTree.Identifier).name !== "component$") return;
|
|
162
|
+
|
|
163
|
+
componentCallNodes.push(node);
|
|
164
|
+
componentRoots.set(componentDepth(), new Set());
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
"CallExpression:exit"(node: Node) {
|
|
168
|
+
if (componentCallNodes.length === 0) return;
|
|
169
|
+
if (componentCallNodes[componentCallNodes.length - 1] !== node) return;
|
|
170
|
+
|
|
171
|
+
const depth = componentDepth();
|
|
172
|
+
const roots = componentRoots.get(depth) ?? new Set<string>();
|
|
173
|
+
|
|
174
|
+
const reportsAtDepth = deferredReports.filter((r) => r.depth === depth);
|
|
175
|
+
for (const report of reportsAtDepth) {
|
|
176
|
+
if (!roots.has(report.namespace)) {
|
|
177
|
+
fileDeferredReports.push(report);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const remaining = deferredReports.filter((r) => r.depth !== depth);
|
|
182
|
+
deferredReports.length = 0;
|
|
183
|
+
deferredReports.push(...remaining);
|
|
184
|
+
|
|
185
|
+
componentCallNodes.pop();
|
|
186
|
+
componentRoots.delete(depth);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
"Program:exit"() {
|
|
190
|
+
for (const report of fileDeferredReports) {
|
|
191
|
+
if (!fileRoots.has(report.namespace)) {
|
|
192
|
+
context.report({
|
|
193
|
+
node: report.node,
|
|
194
|
+
messageId: "noMatchingRoot",
|
|
195
|
+
data: { getter: report.getter, namespace: report.namespace }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
FunctionDeclaration(node: Node) {
|
|
202
|
+
if (componentDepth() === 0) {
|
|
203
|
+
functionDepthOutsideComponent++;
|
|
204
|
+
}
|
|
205
|
+
void node;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
"FunctionDeclaration:exit"(node: Node) {
|
|
209
|
+
if (componentDepth() === 0 && functionDepthOutsideComponent > 0) {
|
|
210
|
+
functionDepthOutsideComponent--;
|
|
211
|
+
}
|
|
212
|
+
void node;
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
JSXMemberExpression(node: Node) {
|
|
216
|
+
if (componentDepth() === 0) return;
|
|
217
|
+
|
|
218
|
+
const jsxMember = node as unknown as Record<string, unknown>;
|
|
219
|
+
const obj = jsxMember.object as Record<string, unknown> | undefined;
|
|
220
|
+
const prop = jsxMember.property as Record<string, unknown> | undefined;
|
|
221
|
+
|
|
222
|
+
if (!obj || !prop) return;
|
|
223
|
+
if (obj.type !== "JSXIdentifier" || prop.type !== "JSXIdentifier") return;
|
|
224
|
+
|
|
225
|
+
const namespaceName = obj.name as string;
|
|
226
|
+
const propertyName = prop.name as string;
|
|
227
|
+
|
|
228
|
+
if (!qdsNamespaces.has(namespaceName)) return;
|
|
229
|
+
if (propertyName !== "root") return;
|
|
230
|
+
|
|
231
|
+
fileRoots.add(namespaceName);
|
|
232
|
+
const roots = componentRoots.get(componentDepth());
|
|
233
|
+
if (roots) {
|
|
234
|
+
roots.add(namespaceName);
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
MemberExpression(node: Node) {
|
|
239
|
+
const memberNode = node as unknown as Record<string, unknown>;
|
|
240
|
+
const obj = memberNode.object as Record<string, unknown> | undefined;
|
|
241
|
+
const prop = memberNode.property as Record<string, unknown> | undefined;
|
|
242
|
+
|
|
243
|
+
if (!obj || !prop) return;
|
|
244
|
+
if (obj.type !== "Identifier" || prop.type !== "Identifier") return;
|
|
245
|
+
|
|
246
|
+
const namespaceName = obj.name as string;
|
|
247
|
+
const propertyName = prop.name as string;
|
|
248
|
+
|
|
249
|
+
if (!qdsNamespaces.has(namespaceName)) return;
|
|
250
|
+
if (!isGetterProperty(propertyName)) return;
|
|
251
|
+
|
|
252
|
+
handleGetterReference(node, `${namespaceName}.${propertyName}`, namespaceName);
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
VariableDeclarator(node: Node) {
|
|
256
|
+
const decl = node as unknown as ESTree.VariableDeclarator;
|
|
257
|
+
if (!decl.init) return;
|
|
258
|
+
if (decl.init.type !== "Identifier") return;
|
|
259
|
+
|
|
260
|
+
const initName = (decl.init as unknown as ESTree.Identifier).name;
|
|
261
|
+
if (!qdsNamespaces.has(initName)) return;
|
|
262
|
+
|
|
263
|
+
if (decl.id.type !== "ObjectPattern") return;
|
|
264
|
+
|
|
265
|
+
const pattern = decl.id as unknown as Record<string, unknown>;
|
|
266
|
+
const properties = pattern.properties as
|
|
267
|
+
| Array<Record<string, unknown>>
|
|
268
|
+
| undefined;
|
|
269
|
+
if (!Array.isArray(properties)) return;
|
|
270
|
+
|
|
271
|
+
for (const prop of properties) {
|
|
272
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
|
|
273
|
+
const key = prop.key as Record<string, unknown> | undefined;
|
|
274
|
+
const value = prop.value as Record<string, unknown> | undefined;
|
|
275
|
+
if (!key || key.type !== "Identifier") continue;
|
|
276
|
+
|
|
277
|
+
const keyName = key.name as string;
|
|
278
|
+
if (!isGetterProperty(keyName)) continue;
|
|
279
|
+
|
|
280
|
+
const localName =
|
|
281
|
+
value && value.type === "Identifier" ? (value.name as string) : keyName;
|
|
282
|
+
|
|
283
|
+
destructuredGetters.set(localName, initName);
|
|
284
|
+
handleGetterReference(node, keyName, initName);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const qdsPlugin = definePlugin({
|
|
292
|
+
meta: {
|
|
293
|
+
name: "qds"
|
|
294
|
+
},
|
|
295
|
+
rules: {
|
|
296
|
+
"no-outside-getter-usage": noOutsideGetterUsageRule
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
export default qdsPlugin;
|