@instruments/materia-ui 0.4.0 → 0.4.2
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-dialog.d.ts +5 -1
- package/dist/components/command/command-dialog.js +1 -1
- package/dist/components/command/command-input.js +5 -2
- 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/segmented-control/segmented-control-root.js +6 -3
- package/dist/components/segmented-control/segmented-control-variants.js +4 -1
- 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/swatch-selector/swatch-selector-item.js +4 -1
- 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/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 +235 -68
- package/dist/styles/index.css +0 -1
- package/dist/tokens/segmented.d.ts +8 -0
- package/dist/tokens/segmented.js +26 -0
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instruments/materia-ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Materia design system: React components, tokens and Tailwind v4 styles.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"brand",
|
|
17
17
|
"dist",
|
|
18
18
|
"lint",
|
|
19
|
+
"!lint/**/*.test.ts",
|
|
19
20
|
"README.md",
|
|
20
21
|
"LICENSE"
|
|
21
22
|
],
|
|
@@ -793,6 +794,10 @@
|
|
|
793
794
|
"types": "./dist/tokens/motion.d.ts",
|
|
794
795
|
"default": "./dist/tokens/motion.js"
|
|
795
796
|
},
|
|
797
|
+
"./tokens/radius": {
|
|
798
|
+
"types": "./dist/tokens/radius.d.ts",
|
|
799
|
+
"default": "./dist/tokens/radius.js"
|
|
800
|
+
},
|
|
796
801
|
"./tokens/radius-tokens": {
|
|
797
802
|
"types": "./dist/tokens/radius-tokens.d.ts",
|
|
798
803
|
"default": "./dist/tokens/radius-tokens.js"
|
|
@@ -835,7 +840,6 @@
|
|
|
835
840
|
"@dnd-kit/sortable": "^10.0.0",
|
|
836
841
|
"@dnd-kit/utilities": "^3.2.2",
|
|
837
842
|
"@instruments/colorscope": "10.2.0",
|
|
838
|
-
"@tailwindcss/typography": "^0.5.20",
|
|
839
843
|
"@tanstack/react-table": "^9.2.4",
|
|
840
844
|
"@tanstack/react-virtual": "^3.14.13",
|
|
841
845
|
"canvas-confetti": "^1.9.4",
|
|
@@ -857,6 +861,8 @@
|
|
|
857
861
|
},
|
|
858
862
|
"devDependencies": {
|
|
859
863
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
864
|
+
"@figma/code-connect": "^2.0.1",
|
|
865
|
+
"@figma/plugin-typings": "^1.140.0",
|
|
860
866
|
"@howells/typescript-config": "^0.1.6",
|
|
861
867
|
"@materia-ui/typescript-api": "npm:typescript@6.0.3",
|
|
862
868
|
"@playwright/test": "^1.63.0",
|
|
@@ -890,23 +896,30 @@
|
|
|
890
896
|
"node": ">=22"
|
|
891
897
|
},
|
|
892
898
|
"scripts": {
|
|
893
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/finish-dist.
|
|
894
|
-
"
|
|
895
|
-
"
|
|
896
|
-
"
|
|
897
|
-
"generate:
|
|
898
|
-
"generate:
|
|
899
|
-
"generate:
|
|
900
|
-
"generate:
|
|
901
|
-
"generate:
|
|
902
|
-
"
|
|
903
|
-
"
|
|
899
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/finish-dist.ts",
|
|
900
|
+
"deploy:figma-connect": "tsx figma/connect-templates.ts --publish",
|
|
901
|
+
"deploy:figma-storybook-links": "tsx figma/storybook-links.ts --publish",
|
|
902
|
+
"deploy:figma-tokens": "tsx figma/deploy-tokens.ts",
|
|
903
|
+
"generate:agent-docs": "node scripts/generate-agent-docs.ts",
|
|
904
|
+
"generate:api": "node scripts/generate-api-report.ts",
|
|
905
|
+
"generate:brand-assets": "tsx scripts/generate-brand-assets.ts",
|
|
906
|
+
"generate:cn-theme": "node scripts/generate-cn-theme.ts",
|
|
907
|
+
"generate:docs-manifest": "node scripts/generate-docs-manifest.ts",
|
|
908
|
+
"generate:exports": "node scripts/generate-exports.ts",
|
|
909
|
+
"generate:figma-connect": "tsx figma/connect-templates.ts",
|
|
910
|
+
"generate:figma-storybook-links": "tsx figma/storybook-links.ts",
|
|
911
|
+
"generate:lint-tokens": "node scripts/generate-lint-tokens.ts",
|
|
912
|
+
"generate:lint-types": "node scripts/generate-lint-types.ts",
|
|
913
|
+
"generate:motion-css": "tsx scripts/generate-motion-css.ts",
|
|
914
|
+
"lint": "howells-check .",
|
|
915
|
+
"lint:fix": "howells-fix .",
|
|
904
916
|
"test": "vitest run",
|
|
905
917
|
"test:controls": "vitest run --config vitest.controls.config.ts",
|
|
918
|
+
"test:figma": "vitest run --config vitest.figma.config.ts",
|
|
906
919
|
"test:browser:dark": "STORYBOOK_FORCED_THEME=dark pnpm --filter @materia-ui/storybook test:browser",
|
|
907
920
|
"test:vr": "playwright test --project=visual-regression",
|
|
908
921
|
"test:vr:update": "playwright test --project=visual-regression --update-snapshots",
|
|
909
922
|
"test:vr:update:missing": "playwright test --project=visual-regression --update-snapshots=missing",
|
|
910
|
-
"typecheck": "mkdir -p node_modules/.cache/tsbuildinfo && tsc --noEmit --incremental --tsBuildInfoFile node_modules/.cache/tsbuildinfo/typecheck.tsbuildinfo"
|
|
923
|
+
"typecheck": "mkdir -p node_modules/.cache/tsbuildinfo && tsc --noEmit --incremental --tsBuildInfoFile node_modules/.cache/tsbuildinfo/typecheck.tsbuildinfo && tsc --noEmit -p figma/connect"
|
|
911
924
|
}
|
|
912
925
|
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// Fixture: linted under an override granting allow:["html","body"]. The html and
|
|
2
|
-
// body hosts must be suppressed; the div must still report. Exactly ONE
|
|
3
|
-
// diagnostic proves context.options reaches the rule and is honored selectively.
|
|
4
|
-
export function Allowed() {
|
|
5
|
-
return (
|
|
6
|
-
<html lang="en">
|
|
7
|
-
<body>
|
|
8
|
-
<div>still banned</div>
|
|
9
|
-
</body>
|
|
10
|
-
</html>
|
|
11
|
-
);
|
|
12
|
-
}
|
|
@@ -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
|
-
});
|