@commonplaces/oxlint-plugin 0.3.1 → 0.5.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.
@@ -0,0 +1,98 @@
1
+ import { Array, Option, Predicate } from "effect";
2
+ import { report, rule } from "../conventions/shared.js";
3
+ const stringValue = (node) => node.type === "Literal" && Predicate.isString(node.value)
4
+ ? Option.some(node.value)
5
+ : node.type === "TemplateElement"
6
+ ? Option.some(node.value.cooked ?? node.value.raw)
7
+ : Option.none();
8
+ const classContext = (node) => {
9
+ const parent = node.parent;
10
+ if (parent === null)
11
+ return false;
12
+ if (parent.type === "JSXAttribute" &&
13
+ parent.name.type === "JSXIdentifier" &&
14
+ Array.contains(["class", "className"], parent.name.name))
15
+ return true;
16
+ if (parent.type === "CallExpression" &&
17
+ parent.callee.type === "Identifier" &&
18
+ Array.contains(["cn", "clsx", "cva", "twMerge"], parent.callee.name))
19
+ return true;
20
+ return classContext(parent);
21
+ };
22
+ // Variant colons inside arbitrary selectors/values do not delimit the utility.
23
+ const utility = (text) => {
24
+ const state = Array.reduce(Array.fromIterable(text), { square: 0, paren: 0, last: -1, index: 0 }, (previous, character) => ({
25
+ square: character === "["
26
+ ? previous.square + 1
27
+ : character === "]"
28
+ ? Math.max(0, previous.square - 1)
29
+ : previous.square,
30
+ paren: character === "("
31
+ ? previous.paren + 1
32
+ : character === ")"
33
+ ? Math.max(0, previous.paren - 1)
34
+ : previous.paren,
35
+ last: character === ":" && previous.square === 0 && previous.paren === 0
36
+ ? previous.index
37
+ : previous.last,
38
+ index: previous.index + 1,
39
+ }));
40
+ return text
41
+ .slice(state.last + 1)
42
+ .replace(/^!|!$/gu, "")
43
+ .replace(/\/.*$/u, "");
44
+ };
45
+ const typography = [
46
+ "font-bold",
47
+ "font-medium",
48
+ "font-mono",
49
+ "font-normal",
50
+ "font-sans",
51
+ "font-semibold",
52
+ "leading-none",
53
+ "leading-normal",
54
+ "leading-relaxed",
55
+ "leading-snug",
56
+ "leading-tight",
57
+ "text-2xl",
58
+ "text-3xl",
59
+ "text-4xl",
60
+ "text-base",
61
+ "text-lg",
62
+ "text-md",
63
+ "text-sm",
64
+ "text-xl",
65
+ "text-xs",
66
+ "tracking-display",
67
+ "tracking-loose",
68
+ "tracking-tight",
69
+ ];
70
+ export const typographyRules = {
71
+ "semantic-typography": rule("Use shared semantic typography primitives instead of local text-size, line-height, tracking and font utility piles.", (context) => {
72
+ const check = (node) => {
73
+ if (classContext(node) &&
74
+ stringValue(node).pipe(Option.exists((value) => Array.some(value.split(/\s+/u), (word) => Array.contains(typography, utility(word))))))
75
+ report(context, node);
76
+ };
77
+ return { Literal: check, TemplateElement: check };
78
+ }),
79
+ "gap-layout": rule("Use flex/grid gap rather than sibling-margin space-x/space-y utilities.", (context) => {
80
+ const check = (node) => {
81
+ if (classContext(node) &&
82
+ stringValue(node).pipe(Option.exists((value) => Array.some(value.split(/\s+/u), (word) => /^space-[xy]-/u.test(utility(word))))))
83
+ report(context, node);
84
+ };
85
+ return { Literal: check, TemplateElement: check };
86
+ }),
87
+ "safe-type-line-height": rule("Use semantic line-height tokens; reject calculated divisions and ratios below one.", (context) => {
88
+ const check = (node) => {
89
+ if (stringValue(node).pipe(Option.exists((value) => Array.some(Array.fromIterable(value.matchAll(/--text-[a-z0-9-]+--line-height\s*:\s*(?<value>[^;"'`\n}]+)/giu)), (match) => {
90
+ const text = match.groups?.value?.trim() ?? "";
91
+ const number = Number(text);
92
+ return (/^calc\([^)]*\/[^)]*\)$/u.test(text) || (Number.isFinite(number) && number < 1));
93
+ }))))
94
+ report(context, node);
95
+ };
96
+ return { Literal: check, TemplateElement: check };
97
+ }),
98
+ };
package/dist/web.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@oxlint/plugins").Plugin;
2
+ export default _default;
package/dist/web.js ADDED
@@ -0,0 +1,120 @@
1
+ import { eslintCompatPlugin } from "@oxlint/plugins";
2
+ import { Array, Option, Predicate } from "effect";
3
+ import { propertyName } from "./syntax.js";
4
+ import { report, rule } from "./conventions/shared.js";
5
+ import { typographyRules } from "./web/typography.js";
6
+ const nameOf = (node) => node.type === "JSXIdentifier" ? Option.some(node.name) : propertyName(node);
7
+ const routes = (filename) => /(?:^|\/)src\/routes\/.+\.[jt]sx?$/u.test(filename);
8
+ const mdxKeys = [
9
+ "a",
10
+ "blockquote",
11
+ "code",
12
+ "h1",
13
+ "h2",
14
+ "h3",
15
+ "h4",
16
+ "h5",
17
+ "h6",
18
+ "hr",
19
+ "img",
20
+ "li",
21
+ "ol",
22
+ "p",
23
+ "pre",
24
+ "table",
25
+ "tbody",
26
+ "td",
27
+ "th",
28
+ "thead",
29
+ "tr",
30
+ "ul",
31
+ ];
32
+ export default eslintCompatPlugin({
33
+ meta: { name: "web" },
34
+ rules: {
35
+ ...typographyRules,
36
+ "no-context-nullish-default": rule("Use canonical Schema-owned metadata and preserve absent values rather than inventing defaults.", (context) => ({
37
+ LogicalExpression(node) {
38
+ if (node.operator === "??" &&
39
+ node.left.type === "MemberExpression" &&
40
+ Option.exists(propertyName(node.left.property), (name) => Array.contains(["description", "image", "title"], name)))
41
+ report(context, node);
42
+ },
43
+ })),
44
+ "no-page-wrapper": rule("Compose the route in its owning route component rather than delegating to a page-sized wrapper.", (context) => ({
45
+ Property(node) {
46
+ if (routes(context.filename) &&
47
+ Option.contains(propertyName(node.key), "component") &&
48
+ node.value.type === "Identifier" &&
49
+ /^[A-Z][A-Za-z]*Page$/u.test(node.value.name))
50
+ report(context, node.value);
51
+ },
52
+ })),
53
+ "no-route-local-seo": rule("Reuse the canonical checked route metadata builder.", (context) => ({
54
+ CallExpression(node) {
55
+ if (node.callee.type === "Identifier" && node.callee.name === "seo")
56
+ report(context, node);
57
+ },
58
+ })),
59
+ "no-route-local-markdown-negotiation": rule("Keep Markdown negotiation in the application's request middleware owner.", (context) => ({
60
+ ImportDeclaration(node) {
61
+ if (/[/]seo-aeo[/]markdown-negotiation$/u.test(node.source.value))
62
+ report(context, node);
63
+ },
64
+ })),
65
+ "no-unscoped-server-functions": rule("Use the application's typed runtime boundary rather than creating TanStack server-function wrappers.", (context) => ({
66
+ ImportDeclaration(node) {
67
+ if (node.source.value === "@tanstack/react-start")
68
+ Array.forEach(node.specifiers, (specifier) => {
69
+ if (specifier.type === "ImportSpecifier" &&
70
+ Option.exists(propertyName(specifier.imported), (name) => name === "createServerFn" || name === "createServerOnlyFn"))
71
+ report(context, specifier);
72
+ });
73
+ },
74
+ })),
75
+ "no-route-json-ld": rule("Let the canonical checked metadata owner encode JSON-LD scripts.", (context) => ({
76
+ Property(node) {
77
+ if (routes(context.filename) &&
78
+ Option.contains(propertyName(node.key), "type") &&
79
+ node.value.type === "Literal" &&
80
+ node.value.value === "application/ld+json")
81
+ report(context, node.value);
82
+ },
83
+ })),
84
+ "no-module-owned-public-asset-url": rule("Import module-owned image and pattern assets through the bundler's module graph.", (context) => ({
85
+ JSXAttribute(node) {
86
+ if (Option.contains(nameOf(node.name), "src") &&
87
+ node.value?.type === "Literal" &&
88
+ Predicate.isString(node.value.value) &&
89
+ /^\/(?:images|patterns)\//u.test(node.value.value))
90
+ report(context, node.value);
91
+ },
92
+ })),
93
+ "no-http-globals-in-seo": rule("Keep Web HTTP adaptation outside SEO and AEO domain modules.", (context) => ({
94
+ Identifier(node) {
95
+ if (/(?:\/(?:seo|seo-aeo)\/|\/packages\/aeo\/)/u.test(context.filename) &&
96
+ Array.contains(["Request", "Response", "Headers"], node.name))
97
+ report(context, node);
98
+ },
99
+ })),
100
+ "no-local-mdx-registry": rule("Reuse a shared MDX component registry rather than rebuilding it in a route or component.", (context) => ({
101
+ VariableDeclarator(node) {
102
+ if (node.id.type === "Identifier" &&
103
+ (node.id.name === "mdxComponents" ||
104
+ (node.id.name === "components" &&
105
+ node.init?.type === "ObjectExpression" &&
106
+ Array.some(node.init.properties, (property) => property.type === "Property" &&
107
+ Option.exists(propertyName(property.key), (name) => Array.contains(mdxKeys, name))))))
108
+ report(context, node.id);
109
+ },
110
+ JSXAttribute(node) {
111
+ if (Option.contains(nameOf(node.name), "components") &&
112
+ node.parent.type === "JSXOpeningElement" &&
113
+ Option.contains(nameOf(node.parent.name), "MDX") &&
114
+ node.value?.type === "JSXExpressionContainer" &&
115
+ node.value.expression.type === "ObjectExpression")
116
+ report(context, node);
117
+ },
118
+ })),
119
+ },
120
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonplaces/oxlint-plugin",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Shared Oxlint rules for strict Effect TypeScript, Alchemy and repository code.",
5
5
  "license": "MIT",
6
6
  "author": "Cooper Corbett",
@@ -29,6 +29,14 @@
29
29
  "types": "./dist/effect.d.ts",
30
30
  "default": "./dist/effect.js"
31
31
  },
32
+ "./conventions": {
33
+ "types": "./dist/conventions.d.ts",
34
+ "default": "./dist/conventions.js"
35
+ },
36
+ "./web": {
37
+ "types": "./dist/web.d.ts",
38
+ "default": "./dist/web.js"
39
+ },
32
40
  "./alchemy": {
33
41
  "types": "./dist/alchemy.d.ts",
34
42
  "default": "./dist/alchemy.js"
@@ -48,6 +56,10 @@
48
56
  "./anti-slop-effect": {
49
57
  "types": "./dist/anti-slop/effect/index.d.ts",
50
58
  "default": "./dist/anti-slop/effect/index.js"
59
+ },
60
+ "./style": {
61
+ "types": "./dist/style.d.ts",
62
+ "default": "./dist/style.js"
51
63
  }
52
64
  },
53
65
  "publishConfig": {
@@ -60,18 +72,19 @@
60
72
  "prepack": "bun run build"
61
73
  },
62
74
  "dependencies": {
63
- "@oxlint/plugins": "1.80.0",
64
- "effect": "4.0.2"
75
+ "@oxlint/plugins": "1.89.0",
76
+ "effect": "4.0.5",
77
+ "ultracite": "8.0.1"
65
78
  },
66
79
  "devDependencies": {
67
- "@effect/platform-node": "4.0.2",
68
- "@effect/vitest": "4.0.2",
69
- "@types/bun": "1.4.0",
80
+ "@effect/platform-node": "4.0.5",
81
+ "@effect/vitest": "4.0.5",
82
+ "@types/bun": "1.4.3",
70
83
  "typescript": "7.0.2",
71
- "vitest": "5.0.1"
84
+ "vitest": "5.0.5"
72
85
  },
73
86
  "peerDependencies": {
74
- "oxlint": "1.80.0 || 1.85.0 || 1.86.0"
87
+ "oxlint": "1.86.0 || 1.89.0"
75
88
  },
76
89
  "engines": {
77
90
  "node": ">=22.14.0"