@instruments/materia-ui 0.4.1 → 0.4.3
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/README.md +2 -4
- package/agent-docs.md +9 -9
- package/dist/components/accordion/accordion-root.d.ts +1 -0
- package/dist/components/accordion/accordion-root.js +1 -0
- package/dist/components/alert/alert-root.d.ts +1 -0
- package/dist/components/alert/alert-root.js +1 -0
- package/dist/components/alert-dialog/alert-dialog-root.d.ts +1 -0
- package/dist/components/alert-dialog/alert-dialog-root.js +1 -0
- package/dist/components/avatar/avatar-root.d.ts +1 -0
- package/dist/components/avatar/avatar-root.js +1 -0
- package/dist/components/badge/badge-root.d.ts +1 -0
- package/dist/components/badge/badge-root.js +1 -0
- package/dist/components/calendar/calendar-root.js +1 -0
- package/dist/components/carousel/carousel-root.d.ts +1 -0
- package/dist/components/carousel/carousel-root.js +1 -0
- package/dist/components/chart/chart-root.d.ts +1 -0
- package/dist/components/chart/chart-root.js +1 -0
- package/dist/components/collapsible/collapsible-root.d.ts +1 -0
- package/dist/components/collapsible/collapsible-root.js +1 -0
- package/dist/components/combobox/combobox-root.d.ts +1 -0
- package/dist/components/combobox/combobox-root.js +1 -0
- package/dist/components/command/command-root.d.ts +1 -0
- package/dist/components/command/command-root.js +1 -0
- package/dist/components/dialog/dialog-content.d.ts +2 -0
- package/dist/components/dialog/dialog-content.js +3 -2
- package/dist/components/dialog/dialog-footer.d.ts +2 -0
- package/dist/components/dialog/dialog-footer.js +8 -8
- package/dist/components/dialog/dialog-root.d.ts +1 -0
- package/dist/components/dialog/dialog-root.js +1 -0
- package/dist/components/dialog/dialog-title.d.ts +2 -0
- package/dist/components/dialog/dialog-title.js +3 -1
- package/dist/components/drawer/drawer-root.d.ts +1 -0
- package/dist/components/drawer/drawer-root.js +1 -0
- package/dist/components/heading/heading-root.d.ts +1 -0
- package/dist/components/heading/heading-root.js +1 -0
- package/dist/components/input/input-root.d.ts +1 -0
- package/dist/components/input/input-root.js +1 -0
- package/dist/components/kbd/kbd-root.d.ts +1 -0
- package/dist/components/kbd/kbd-root.js +1 -0
- package/dist/components/logo/logo-mark.d.ts +1 -1
- package/dist/components/logo/logo-mark.js +1 -1
- package/dist/components/logotype/logotype-paths.d.ts +1 -1
- package/dist/components/logotype/logotype-paths.js +1 -1
- package/dist/components/month-year-picker/month-year-picker-root.d.ts +1 -0
- package/dist/components/month-year-picker/month-year-picker-root.js +1 -0
- package/dist/components/native-select/native-select-root.d.ts +1 -0
- package/dist/components/native-select/native-select-root.js +1 -0
- package/dist/components/pill/pill-root.d.ts +1 -0
- package/dist/components/pill/pill-root.js +1 -0
- package/dist/components/popover/popover-root.d.ts +1 -0
- package/dist/components/popover/popover-root.js +1 -0
- package/dist/components/quantity-control/quantity-control-root.d.ts +1 -0
- package/dist/components/quantity-control/quantity-control-root.js +1 -0
- package/dist/components/select/select-root.d.ts +1 -0
- package/dist/components/select/select-root.js +1 -0
- package/dist/components/separator/separator-root.d.ts +1 -0
- package/dist/components/separator/separator-root.js +1 -0
- package/dist/components/sheet/sheet-root.d.ts +1 -0
- package/dist/components/sheet/sheet-root.js +1 -0
- package/dist/components/spinner/spinner-root.d.ts +1 -0
- package/dist/components/spinner/spinner-root.js +1 -0
- package/dist/components/stepper/stepper-root.d.ts +1 -0
- package/dist/components/stepper/stepper-root.js +1 -0
- package/dist/components/success-tick-ripple/success-tick-ripple-root.d.ts +1 -0
- package/dist/components/success-tick-ripple/success-tick-ripple-root.js +1 -0
- package/dist/components/switch/switch-root.d.ts +1 -0
- package/dist/components/switch/switch-root.js +1 -0
- package/dist/components/tag/tag-root.d.ts +1 -0
- package/dist/components/tag/tag-root.js +1 -0
- package/dist/components/textarea/textarea-root.d.ts +1 -0
- package/dist/components/textarea/textarea-root.js +1 -0
- package/dist/components/time-slot-picker/time-slot-picker-root.d.ts +1 -0
- package/dist/components/time-slot-picker/time-slot-picker-root.js +1 -0
- package/dist/components/toast/toast-root.d.ts +1 -0
- package/dist/components/toast/toast-root.js +1 -0
- package/dist/components/tooltip/tooltip-root.d.ts +1 -0
- package/dist/components/tooltip/tooltip-root.js +1 -0
- package/dist/compositions/empty-state/empty-state-root.d.ts +1 -0
- package/dist/compositions/empty-state/empty-state-root.js +1 -0
- package/dist/compositions/file-upload/file-upload-dropzone.d.ts +1 -0
- package/dist/compositions/file-upload/file-upload-dropzone.js +1 -0
- package/dist/compositions/product-card/product-card-caption.js +4 -2
- package/dist/compositions/product-card/product-card-content-types.d.ts +8 -0
- package/dist/compositions/product-card/product-card-meta.js +2 -2
- package/dist/compositions/product-card/product-card-provider-types.d.ts +3 -0
- package/dist/compositions/product-card/product-card-provider.js +4 -1
- package/dist/compositions/product-card/product-card-root.d.ts +1 -1
- package/dist/compositions/product-card/product-card-root.js +2 -1
- package/dist/compositions/product-card/product-card-skeleton.d.ts +14 -5
- package/dist/compositions/product-card/product-card-skeleton.js +6 -5
- package/dist/data/decision-trees.d.ts +1 -1
- package/dist/data/decision-trees.js +1 -1
- package/dist/data/docs-manifest.d.ts +2 -0
- package/dist/data/docs-manifest.js +257 -67
- package/dist/styles/index.css +0 -1
- package/dist/utils/cn-theme.js +1 -1
- package/lint/README.md +6 -2
- package/lint/doctrine-rules.mjs +4 -4
- package/lint/oxlint/index.d.mts +13 -5
- package/lint/oxlint/index.mjs +26 -0
- package/lint/oxlint/no-naked-element.mjs +123 -65
- package/lint/oxlint/no-render-layout-as.mjs +61 -56
- package/lint/scan-classes.d.mts +58 -5
- package/lint/scan-classes.mjs +274 -38
- package/lint/tokens.d.mts +16 -8
- package/lint/tokens.mjs +13 -3
- package/lint/type-case.d.mts +16 -8
- package/lint/type-case.mjs +17 -0
- package/package.json +27 -14
- package/lint/oxlint/fixtures/allowed.tsx +0 -12
- package/lint/oxlint/fixtures/fixture.config.mjs +0 -29
- package/lint/oxlint/fixtures/invalid.tsx +0 -24
- package/lint/oxlint/fixtures/layout-only.tsx +0 -26
- package/lint/oxlint/fixtures/render-as-invalid.tsx +0 -11
- package/lint/oxlint/fixtures/render-as-valid.tsx +0 -14
- package/lint/oxlint/fixtures/valid.tsx +0 -18
- package/lint/oxlint/no-naked-element.test.ts +0 -155
- package/lint/oxlint/no-render-layout-as.test.ts +0 -81
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// Fixture oxlint config. Plain object (oxlint's defineConfig is identity), so we
|
|
2
|
-
// avoid importing "oxlint" here. The plugin is referenced by a file:// URL
|
|
3
|
-
// resolved relative to this config so the test is location-independent.
|
|
4
|
-
const specifier = new URL("../index.mjs", import.meta.url).href;
|
|
5
|
-
|
|
6
|
-
export default {
|
|
7
|
-
// Single-purpose doctrine gate: silence oxlint's built-in rules so the JSON
|
|
8
|
-
// report contains only our diagnostics.
|
|
9
|
-
categories: { correctness: "off" },
|
|
10
|
-
jsPlugins: [{ name: "materia-ui", specifier }],
|
|
11
|
-
rules: {
|
|
12
|
-
"materia-ui/no-naked-element": "error",
|
|
13
|
-
"materia-ui/no-render-layout-as": "error",
|
|
14
|
-
},
|
|
15
|
-
overrides: [
|
|
16
|
-
{
|
|
17
|
-
files: ["**/allowed.tsx"],
|
|
18
|
-
rules: {
|
|
19
|
-
"materia-ui/no-naked-element": ["error", { allow: ["html", "body"] }],
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
files: ["**/layout-only.tsx"],
|
|
24
|
-
rules: {
|
|
25
|
-
"materia-ui/no-naked-element": ["error", { layoutOnly: true }],
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// Fixture: every raw host on its own line so the rule reports each one.
|
|
2
|
-
// Not a real module - never imported, never type-checked, never shipped as code.
|
|
3
|
-
export function Invalid() {
|
|
4
|
-
return (
|
|
5
|
-
<div>
|
|
6
|
-
<span>text</span>
|
|
7
|
-
<a href="#">link</a>
|
|
8
|
-
<main>main</main>
|
|
9
|
-
<ul>
|
|
10
|
-
<li>item</li>
|
|
11
|
-
</ul>
|
|
12
|
-
<code>code</code>
|
|
13
|
-
<pre>pre</pre>
|
|
14
|
-
<img src="x.png" alt="" />
|
|
15
|
-
<svg />
|
|
16
|
-
<button type="button">btn</button>
|
|
17
|
-
<input />
|
|
18
|
-
<label>label</label>
|
|
19
|
-
<p>paragraph</p>
|
|
20
|
-
<section>section</section>
|
|
21
|
-
<h1>heading</h1>
|
|
22
|
-
</div>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// Fixture: linted under an override granting layoutOnly:true. Only a layout host
|
|
2
|
-
// whose unconditional classes make it a flex or grid container reports: the
|
|
3
|
-
// flex div (line 10), the inline-flex span (line 11), the grid template literal
|
|
4
|
-
// (line 12) and the cn() argument (line 13). Everything after line 13 is clean.
|
|
5
|
-
const cn = (...classes: unknown[]) => classes.filter(Boolean).join(" ");
|
|
6
|
-
|
|
7
|
-
export function LayoutOnly({ open, size }: { open: boolean; size: number }) {
|
|
8
|
-
return (
|
|
9
|
-
<>
|
|
10
|
-
<div className="border-border flex items-center gap-2">flex</div>
|
|
11
|
-
<span className="inline-flex">inline</span>
|
|
12
|
-
<ul className={`grid gap-${size}`}>grid</ul>
|
|
13
|
-
<div className={cn("flex flex-1", open && "p-2")}>call</div>
|
|
14
|
-
<div className="p-4">plain</div>
|
|
15
|
-
<svg viewBox="0 0 1 1">
|
|
16
|
-
<path className="flex" d="M0 0" />
|
|
17
|
-
</svg>
|
|
18
|
-
<div className={cn("p-2", open && "flex")}>conditional</div>
|
|
19
|
-
<div className={`gap-${size}flex`}>glued</div>
|
|
20
|
-
<button className="flex" type="button">
|
|
21
|
-
not a layout host
|
|
22
|
-
</button>
|
|
23
|
-
<div className="flex-1 flex-col">no display utility</div>
|
|
24
|
-
</>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Fixture: each render={<Primitive as=... />} sits on its own line.
|
|
2
|
-
// Not a real module - never imported, never type-checked, never shipped as code.
|
|
3
|
-
export function RenderAsInvalid() {
|
|
4
|
-
return (
|
|
5
|
-
<>
|
|
6
|
-
<Stack render={<Box as="li" />} />
|
|
7
|
-
<Flex render={<Stack as="ul" />} />
|
|
8
|
-
<Grid render={<Center as="section" />} />
|
|
9
|
-
</>
|
|
10
|
-
);
|
|
11
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// Fixture: legitimate render and as usage the rule must leave alone.
|
|
2
|
-
// Not a real module - never imported, never type-checked, never shipped as code.
|
|
3
|
-
export function RenderAsValid() {
|
|
4
|
-
return (
|
|
5
|
-
<>
|
|
6
|
-
<Stack as="ul" />
|
|
7
|
-
<Stack render={<Link href="/" />} />
|
|
8
|
-
<Stack render={<Box />} />
|
|
9
|
-
<Stack render={<Link as="a" href="/" />} />
|
|
10
|
-
<Stack render={<Foo.Bar as="li" />} />
|
|
11
|
-
<TooltipTrigger render={<Box as="span" />} />
|
|
12
|
-
</>
|
|
13
|
-
);
|
|
14
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Fixture: only design-system components, a member expression, a fragment, and a
|
|
2
|
-
// self-closing component - none of these should trip the rule.
|
|
3
|
-
import { Anchor, Box, Card, Stack, Text } from "@instruments/materia-ui";
|
|
4
|
-
|
|
5
|
-
export function Valid() {
|
|
6
|
-
return (
|
|
7
|
-
<>
|
|
8
|
-
<Box>
|
|
9
|
-
<Stack>
|
|
10
|
-
<Text>hello</Text>
|
|
11
|
-
<Anchor href="#">link</Anchor>
|
|
12
|
-
</Stack>
|
|
13
|
-
<Card.Header>header</Card.Header>
|
|
14
|
-
<Box />
|
|
15
|
-
</Box>
|
|
16
|
-
</>
|
|
17
|
-
);
|
|
18
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
|
|
6
|
-
import { describe, expect, it } from "vitest";
|
|
7
|
-
|
|
8
|
-
import { LAYOUT_ELEMENTS, TEXT_ELEMENTS } from "../../src/tokens/element";
|
|
9
|
-
import { LAYOUT_HOSTS, REPLACEMENTS } from "./no-naked-element.mjs";
|
|
10
|
-
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
12
|
-
|
|
13
|
-
const here = import.meta.dirname;
|
|
14
|
-
const fixturesDir = path.join(here, "fixtures");
|
|
15
|
-
const configPath = path.join(fixturesDir, "fixture.config.mjs");
|
|
16
|
-
|
|
17
|
-
const RULE_CODE = "materia-ui(no-naked-element)";
|
|
18
|
-
const ESCAPE_HATCH = "className on a primitive, policed by tokens-only";
|
|
19
|
-
|
|
20
|
-
/** Walk up from the test dir to the workspace root's oxlint binary. */
|
|
21
|
-
const resolveOxlintBin = (): string => {
|
|
22
|
-
let dir = here;
|
|
23
|
-
for (let i = 0; i < 12; i += 1) {
|
|
24
|
-
const candidate = path.join(dir, "node_modules", ".bin", "oxlint");
|
|
25
|
-
if (existsSync(candidate)) {
|
|
26
|
-
return candidate;
|
|
27
|
-
}
|
|
28
|
-
const parent = path.dirname(dir);
|
|
29
|
-
if (parent === dir) {
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
dir = parent;
|
|
33
|
-
}
|
|
34
|
-
throw new Error("could not resolve the oxlint binary from node_modules/.bin");
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
interface Diagnostic {
|
|
38
|
-
code: string;
|
|
39
|
-
message: string;
|
|
40
|
-
filename: string;
|
|
41
|
-
labels: { span: { line: number } }[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const packageRoot = path.resolve(here, "..", "..");
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Run oxlint over the fixtures with the plugin config and return only our rule's
|
|
48
|
-
* diagnostics. oxlint exits non-zero when it reports, so read stdout off the
|
|
49
|
-
* thrown error too.
|
|
50
|
-
*/
|
|
51
|
-
const runRule = async (): Promise<Diagnostic[]> => {
|
|
52
|
-
const targets = [
|
|
53
|
-
"invalid.tsx",
|
|
54
|
-
"valid.tsx",
|
|
55
|
-
"allowed.tsx",
|
|
56
|
-
"layout-only.tsx",
|
|
57
|
-
].map((f) => path.join(fixturesDir, f));
|
|
58
|
-
let stdout = "[]";
|
|
59
|
-
try {
|
|
60
|
-
const result = await execFileAsync(
|
|
61
|
-
resolveOxlintBin(),
|
|
62
|
-
["--config", configPath, "--format", "json", ...targets],
|
|
63
|
-
{ cwd: packageRoot }
|
|
64
|
-
);
|
|
65
|
-
({ stdout } = result);
|
|
66
|
-
} catch (error) {
|
|
67
|
-
stdout = (error as { stdout?: string }).stdout ?? "";
|
|
68
|
-
}
|
|
69
|
-
const report = JSON.parse(stdout) as
|
|
70
|
-
| { diagnostics?: Diagnostic[] }
|
|
71
|
-
| Diagnostic[];
|
|
72
|
-
const diagnostics = Array.isArray(report)
|
|
73
|
-
? report
|
|
74
|
-
: (report.diagnostics ?? []);
|
|
75
|
-
return diagnostics.filter((d) => d.code === RULE_CODE);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const diagnosticsPromise = runRule();
|
|
79
|
-
|
|
80
|
-
const forFile = (diagnostics: Diagnostic[], basename: string) =>
|
|
81
|
-
diagnostics.filter((d) => path.basename(d.filename) === basename);
|
|
82
|
-
|
|
83
|
-
// Every raw host authored in invalid.tsx, paired with a distinctive substring of
|
|
84
|
-
// its REPLACEMENTS guidance that must appear in the diagnostic message.
|
|
85
|
-
const INVALID_TAGS: [tag: string, hint: string][] = [
|
|
86
|
-
["div", "Box"],
|
|
87
|
-
["span", 'Text as="span"'],
|
|
88
|
-
["a", "Anchor"],
|
|
89
|
-
["main", 'Box as="main"'],
|
|
90
|
-
["ul", 'Stack as="ul"'],
|
|
91
|
-
["li", 'as="li"'],
|
|
92
|
-
["code", "Code"],
|
|
93
|
-
["pre", "Code block"],
|
|
94
|
-
["img", "GalleryImage"],
|
|
95
|
-
["svg", "Icon"],
|
|
96
|
-
["button", "Button"],
|
|
97
|
-
["input", "Input"],
|
|
98
|
-
["label", "Label"],
|
|
99
|
-
["p", "Text"],
|
|
100
|
-
["section", "Section"],
|
|
101
|
-
["h1", "Heading level={1}"],
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
describe("no-naked-element (oxlint integration)", () => {
|
|
105
|
-
it("reports every raw host in invalid.tsx with its REPLACEMENTS hint", async () => {
|
|
106
|
-
const invalid = forFile(await diagnosticsPromise, "invalid.tsx");
|
|
107
|
-
for (const [tag, hint] of INVALID_TAGS) {
|
|
108
|
-
const hit = invalid.find((d) => d.message.startsWith(`Raw <${tag}>`));
|
|
109
|
-
expect(hit, `expected a diagnostic for <${tag}>`).toBeDefined();
|
|
110
|
-
expect(hit?.message).toContain(hint);
|
|
111
|
-
expect(hit?.message).toContain(ESCAPE_HATCH);
|
|
112
|
-
}
|
|
113
|
-
// No stray reports beyond the authored tags.
|
|
114
|
-
expect(invalid).toHaveLength(INVALID_TAGS.length);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("does not report components, member expressions, or fragments in valid.tsx", async () => {
|
|
118
|
-
expect(forFile(await diagnosticsPromise, "valid.tsx")).toHaveLength(0);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("honors the allow option - proves context.options reaches the rule", async () => {
|
|
122
|
-
const allowed = forFile(await diagnosticsPromise, "allowed.tsx");
|
|
123
|
-
// html + body are suppressed by allow:["html","body"]; only the div remains.
|
|
124
|
-
expect(allowed).toHaveLength(1);
|
|
125
|
-
expect(allowed[0]?.message).toMatch(/^Raw <div>/u);
|
|
126
|
-
expect(allowed[0]?.labels[0]?.span.line).toBe(8);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("layoutOnly reports only hosts whose unconditional classes are flex or grid", async () => {
|
|
130
|
-
const layout = forFile(await diagnosticsPromise, "layout-only.tsx");
|
|
131
|
-
// A plain div, an svg path, a conditional `flex`, a class glued to an
|
|
132
|
-
// interpolation, a flex <button> and `flex-1 flex-col` all stay quiet.
|
|
133
|
-
expect(layout.map((d) => d.labels[0]?.span.line)).toEqual([10, 11, 12, 13]);
|
|
134
|
-
expect(layout[0]?.message).toMatch(/^Raw <div> with `flex`/u);
|
|
135
|
-
expect(layout[1]?.message).toContain('as="span"');
|
|
136
|
-
expect(layout[2]?.message).toContain("Grid | SimpleGrid");
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// Parity: every member of the closed layout/text unions must have a REPLACEMENTS
|
|
141
|
-
// entry so the rule can always name a sanctioned replacement.
|
|
142
|
-
describe("REPLACEMENTS parity with element unions", () => {
|
|
143
|
-
it("has a replacement for every LAYOUT_ELEMENT and TEXT_ELEMENT", () => {
|
|
144
|
-
const members = [...new Set([...LAYOUT_ELEMENTS, ...TEXT_ELEMENTS])];
|
|
145
|
-
const missing = members.filter((tag) => !(tag in REPLACEMENTS));
|
|
146
|
-
expect(
|
|
147
|
-
missing,
|
|
148
|
-
`missing REPLACEMENTS entries for: ${missing.join(", ")}`
|
|
149
|
-
).toEqual([]);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("mirrors LAYOUT_ELEMENTS in the rule's layout hosts", () => {
|
|
153
|
-
expect(LAYOUT_HOSTS).toEqual([...LAYOUT_ELEMENTS]);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
|
|
6
|
-
import { describe, expect, it } from "vitest";
|
|
7
|
-
|
|
8
|
-
const execFileAsync = promisify(execFile);
|
|
9
|
-
|
|
10
|
-
const here = import.meta.dirname;
|
|
11
|
-
const fixturesDir = path.join(here, "fixtures");
|
|
12
|
-
const configPath = path.join(fixturesDir, "fixture.config.mjs");
|
|
13
|
-
const packageRoot = path.resolve(here, "..", "..");
|
|
14
|
-
const RULE_CODE = "materia-ui(no-render-layout-as)";
|
|
15
|
-
|
|
16
|
-
const resolveOxlintBin = (): string => {
|
|
17
|
-
let dir = here;
|
|
18
|
-
for (let i = 0; i < 12; i += 1) {
|
|
19
|
-
const candidate = path.join(dir, "node_modules", ".bin", "oxlint");
|
|
20
|
-
if (existsSync(candidate)) {
|
|
21
|
-
return candidate;
|
|
22
|
-
}
|
|
23
|
-
const parent = path.dirname(dir);
|
|
24
|
-
if (parent === dir) {
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
dir = parent;
|
|
28
|
-
}
|
|
29
|
-
throw new Error("could not resolve the oxlint binary from node_modules/.bin");
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
interface Diagnostic {
|
|
33
|
-
code: string;
|
|
34
|
-
message: string;
|
|
35
|
-
filename: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const runRule = async (): Promise<Diagnostic[]> => {
|
|
39
|
-
const targets = ["render-as-invalid.tsx", "render-as-valid.tsx"].map((f) =>
|
|
40
|
-
path.join(fixturesDir, f)
|
|
41
|
-
);
|
|
42
|
-
let stdout = "[]";
|
|
43
|
-
try {
|
|
44
|
-
({ stdout } = await execFileAsync(
|
|
45
|
-
resolveOxlintBin(),
|
|
46
|
-
["--config", configPath, "--format", "json", ...targets],
|
|
47
|
-
{ cwd: packageRoot }
|
|
48
|
-
));
|
|
49
|
-
} catch (error) {
|
|
50
|
-
stdout = (error as { stdout?: string }).stdout ?? "";
|
|
51
|
-
}
|
|
52
|
-
const report = JSON.parse(stdout) as
|
|
53
|
-
| { diagnostics?: Diagnostic[] }
|
|
54
|
-
| Diagnostic[];
|
|
55
|
-
const diagnostics = Array.isArray(report)
|
|
56
|
-
? report
|
|
57
|
-
: (report.diagnostics ?? []);
|
|
58
|
-
return diagnostics.filter((d) => d.code === RULE_CODE);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const diagnosticsPromise = runRule();
|
|
62
|
-
|
|
63
|
-
describe("no-render-layout-as (oxlint integration)", () => {
|
|
64
|
-
it("reports every render={<Primitive as=... />} in the invalid fixture", async () => {
|
|
65
|
-
const invalid = (await diagnosticsPromise).filter(
|
|
66
|
-
(d) => path.basename(d.filename) === "render-as-invalid.tsx"
|
|
67
|
-
);
|
|
68
|
-
expect(invalid.map((d) => d.message.match(/<(\w+) as/u)?.[1])).toEqual([
|
|
69
|
-
"Box",
|
|
70
|
-
"Stack",
|
|
71
|
-
"Center",
|
|
72
|
-
]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("leaves plain as, component render and non-layout elements alone", async () => {
|
|
76
|
-
const valid = (await diagnosticsPromise).filter(
|
|
77
|
-
(d) => path.basename(d.filename) === "render-as-valid.tsx"
|
|
78
|
-
);
|
|
79
|
-
expect(valid).toHaveLength(0);
|
|
80
|
-
});
|
|
81
|
-
});
|