@heyramzi/lint 0.0.0-stage → 1.32.4

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 (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +277 -2
  3. package/bin/heyramzi-comment-length.js +99 -0
  4. package/bin/heyramzi-commit-msg.js +31 -0
  5. package/bin/heyramzi-file-tokens.js +66 -0
  6. package/bin/heyramzi-inline-path.js +79 -0
  7. package/bin/heyramzi-jev-code.js +107 -0
  8. package/bin/heyramzi-push-diff.js +93 -0
  9. package/bin/heyramzi-repo-rules.js +70 -0
  10. package/bin/heyramzi-slop.js +314 -0
  11. package/bin/heyramzi-svelte-markup.js +50 -0
  12. package/bin/heyramzi-ui-rules.js +56 -0
  13. package/comment-length.js +368 -0
  14. package/commit-msg.js +57 -0
  15. package/config/oxlintrc.json +87 -0
  16. package/data/invisibles.js +218 -0
  17. package/data/jev-bench.js +115 -0
  18. package/data/jev-code-bench.js +313 -0
  19. package/data/slop-words.d.ts +64 -0
  20. package/data/slop-words.js +2681 -0
  21. package/file-tokens.js +122 -0
  22. package/gate-files.js +14 -0
  23. package/index.js +116 -0
  24. package/inline-path.js +90 -0
  25. package/jev-code.js +194 -0
  26. package/jev.js +341 -0
  27. package/package.json +81 -4
  28. package/processors/markdown.js +22 -0
  29. package/push-diff.js +55 -0
  30. package/register.d.ts +43 -0
  31. package/register.js +255 -0
  32. package/repo-rules/packages.js +164 -0
  33. package/repo-rules/paths.js +101 -0
  34. package/repo-rules/toolchain.js +172 -0
  35. package/repo-rules/tree.js +51 -0
  36. package/repo-rules/workflows.js +116 -0
  37. package/repo-rules.js +54 -0
  38. package/rules/convex-ast.js +66 -0
  39. package/rules/convex-index-name.js +40 -0
  40. package/rules/convex-internal-only.js +59 -0
  41. package/rules/convex-no-date-now-in-query.js +48 -0
  42. package/rules/convex-no-deprecated-crons.js +40 -0
  43. package/rules/convex-no-filter.js +41 -0
  44. package/rules/convex-no-get-metadata.js +26 -0
  45. package/rules/convex-no-v-any.js +24 -0
  46. package/rules/convex-react-client-module-scope.js +37 -0
  47. package/rules/convex-require-args.js +39 -0
  48. package/rules/convex-require-returns.js +31 -0
  49. package/rules/convex-runtime-imports.js +48 -0
  50. package/rules/max-function-lines.js +105 -0
  51. package/rules/max-object-methods.js +86 -0
  52. package/rules/no-ai-words.js +78 -0
  53. package/rules/no-app-stores.js +25 -0
  54. package/rules/no-chained-type-assertions.js +91 -0
  55. package/rules/no-class.js +47 -0
  56. package/rules/no-conditional-empty-object-spread.js +64 -0
  57. package/rules/no-deep-phosphor-imports.js +29 -0
  58. package/rules/no-default-export.js +70 -0
  59. package/rules/no-em-dash.js +198 -0
  60. package/rules/no-emoji.js +56 -0
  61. package/rules/no-hardcoded-models.js +63 -0
  62. package/rules/no-inline-path.js +46 -0
  63. package/rules/no-inline-svg.js +67 -0
  64. package/rules/no-known-value-widening.js +289 -0
  65. package/rules/no-legacy-compat.js +35 -0
  66. package/rules/no-overkill-fallback.js +77 -0
  67. package/rules/no-raw-try-catch.js +26 -0
  68. package/rules/no-redundant-prop-fallback.js +153 -0
  69. package/rules/no-relative-imports.js +213 -0
  70. package/rules/no-relative-type-imports.js +40 -0
  71. package/rules/no-rune-outside-svelte-file.js +56 -0
  72. package/rules/no-rune-shadowing-binding.js +85 -0
  73. package/rules/no-skipped-or-focused-tests.js +74 -0
  74. package/rules/no-snake-case-props.js +45 -0
  75. package/rules/no-svelte4-syntax.js +33 -0
  76. package/rules/no-vitest-import-in-vite-plus.js +64 -0
  77. package/rules/props-interface.js +44 -0
  78. package/rules/redirect-external-needs-option.js +45 -0
  79. package/rules/require-safety-comment-for-type-assertion.js +110 -0
  80. package/rules/svelte-section-order.js +115 -0
  81. package/rules/test-file.js +8 -0
  82. package/scripts/count-hits.mjs +213 -0
  83. package/scripts/harvest-voice.mjs +161 -0
  84. package/scripts/jev-bench.mjs +41 -0
  85. package/scripts/jev-code-bench.mjs +61 -0
  86. package/scripts/measure-register.mjs +61 -0
  87. package/slop-views.js +122 -0
  88. package/slop.d.ts +53 -0
  89. package/slop.js +534 -0
  90. package/svelte-markup.js +107 -0
  91. package/svelte-template.js +258 -0
  92. package/ui-rules.js +324 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Disallow conditional empty-object spreads (`...(cond ? { a } : {})`).
3
+ */
4
+
5
+ /*
6
+ * The empty branch hides property omission inside an expression, so the object type becomes a union nobody reads.
7
+ */
8
+
9
+ /*
10
+ * Build the object in statements and add the property only when it exists.
11
+ */
12
+
13
+ /*
14
+ * Ported from dmmulroy/anti-slop (MIT).
15
+ */
16
+
17
+ function unwrapParens(node) {
18
+ let current = node;
19
+ while (current?.type === "ParenthesizedExpression") current = current.expression;
20
+ return current;
21
+ }
22
+
23
+ function isEmptyObject(node) {
24
+ return node?.type === "ObjectExpression" && node.properties.length === 0;
25
+ }
26
+
27
+ function isConditionalEmptyObject(node) {
28
+ const conditional = unwrapParens(node);
29
+
30
+ if (conditional?.type === "ConditionalExpression") {
31
+ return isEmptyObject(conditional.consequent) || isEmptyObject(conditional.alternate);
32
+ }
33
+
34
+ // `...(cond && { a })` omits by spreading `false`, same trick with a shorter spelling.
35
+ if (conditional?.type === "LogicalExpression" && conditional.operator === "&&") {
36
+ return unwrapParens(conditional.right)?.type === "ObjectExpression";
37
+ }
38
+
39
+ return false;
40
+ }
41
+
42
+ export default {
43
+ meta: {
44
+ type: "suggestion",
45
+ docs: {
46
+ description:
47
+ "Disallow object spreads that conditionally spread an empty object to omit fields.",
48
+ },
49
+ messages: {
50
+ avoid:
51
+ "This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.",
52
+ },
53
+ schema: [],
54
+ },
55
+ create(context) {
56
+ return {
57
+ SpreadElement(node) {
58
+ if (node.parent?.type !== "ObjectExpression") return;
59
+ if (!isConditionalEmptyObject(node.argument)) return;
60
+ context.report({ node, messageId: "avoid" });
61
+ },
62
+ };
63
+ },
64
+ };
@@ -0,0 +1,29 @@
1
+ export default {
2
+ meta: {
3
+ type: "suggestion",
4
+ docs: {
5
+ description:
6
+ 'Disallow deep imports from "phosphor-svelte/lib/*". Use barrel import from "phosphor-svelte" instead.',
7
+ },
8
+ messages: {
9
+ deepImport:
10
+ 'Import from "{{ source }}" is forbidden. Use \'import { {{ name }} } from "phosphor-svelte"\' instead.',
11
+ },
12
+ schema: [],
13
+ },
14
+ create(context) {
15
+ return {
16
+ ImportDeclaration(node) {
17
+ const source = node.source.value;
18
+ if (source.startsWith("phosphor-svelte/lib/")) {
19
+ const name = source.split("/").pop();
20
+ context.report({
21
+ node: node.source,
22
+ messageId: "deepImport",
23
+ data: { source, name },
24
+ });
25
+ }
26
+ },
27
+ };
28
+ },
29
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Ban default exports for grep-ability and refactoring safety.
3
+ */
4
+
5
+ /*
6
+ * Named exports are easier to find, rename, and re-export. Exceptions: Next.js route files (page, layout, route, etc.) and config files (*.config.*) which require default exports.
7
+ */
8
+
9
+ const ALLOWED_PATTERNS = [
10
+ // Next.js App Router conventions (files directly in app/ or nested)
11
+ /[/\\]app[/\\](.*[/\\])?page\.[jt]sx?$/,
12
+ /[/\\]app[/\\](.*[/\\])?layout\.[jt]sx?$/,
13
+ /[/\\]app[/\\](.*[/\\])?loading\.[jt]sx?$/,
14
+ /[/\\]app[/\\](.*[/\\])?error\.[jt]sx?$/,
15
+ /[/\\]app[/\\](.*[/\\])?not-found\.[jt]sx?$/,
16
+ /[/\\]app[/\\](.*[/\\])?template\.[jt]sx?$/,
17
+ /[/\\]app[/\\](.*[/\\])?default\.[jt]sx?$/,
18
+ /[/\\]app[/\\](.*[/\\])?route\.[jt]s$/,
19
+ // Next.js Metadata API conventions
20
+ /[/\\]app[/\\](.*[/\\])?icon\.[jt]sx?$/,
21
+ /[/\\]app[/\\](.*[/\\])?apple-icon\.[jt]sx?$/,
22
+ /[/\\]app[/\\](.*[/\\])?opengraph-image\.[jt]sx?$/,
23
+ /[/\\]app[/\\](.*[/\\])?twitter-image\.[jt]sx?$/,
24
+ /[/\\]app[/\\](.*[/\\])?manifest\.[jt]s$/,
25
+ /[/\\]app[/\\](.*[/\\])?robots\.[jt]s$/,
26
+ /[/\\]app[/\\](.*[/\\])?sitemap\.[jt]s$/,
27
+ // Root middleware
28
+ /[/\\]middleware\.[jt]s$/,
29
+ // Config files
30
+ /\.config\.[jt]sx?$/,
31
+ /\.config\.m[jt]s$/,
32
+ ];
33
+
34
+ export default {
35
+ meta: {
36
+ type: "problem",
37
+ docs: {
38
+ description:
39
+ "Disallow default exports. Use named exports for grep-ability and refactoring safety.",
40
+ },
41
+ messages: {
42
+ noDefault:
43
+ "Use named exports instead of default exports. Default exports break grep-ability and refactoring.",
44
+ },
45
+ schema: [],
46
+ },
47
+ create(context) {
48
+ const filename = context.filename || context.getFilename();
49
+
50
+ // Skip files that require default exports
51
+ if (ALLOWED_PATTERNS.some((pattern) => pattern.test(filename))) {
52
+ return {};
53
+ }
54
+
55
+ return {
56
+ ExportDefaultDeclaration(node) {
57
+ context.report({ node, messageId: "noDefault" });
58
+ },
59
+ // Catch: export { foo as default }
60
+ ExportNamedDeclaration(node) {
61
+ if (!node.specifiers) return;
62
+ for (const specifier of node.specifiers) {
63
+ if (specifier.exported && specifier.exported.name === "default") {
64
+ context.report({ node: specifier, messageId: "noDefault" });
65
+ }
66
+ }
67
+ },
68
+ };
69
+ },
70
+ };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Ban em dashes, en dashes, and spaced hyphens in string literals.
3
+ * These are a structural tell of AI-generated prose.
4
+ */
5
+
6
+ import { isTestFile } from "#lint/rules/test-file.js";
7
+
8
+ const SPACED_HYPHEN = / - /;
9
+
10
+ const EM_DASH = String.fromCharCode(0x2014);
11
+ const EN_DASH = String.fromCharCode(0x2013);
12
+ const SPACED_HYPHEN_TEXT = String.fromCharCode(0x20, 0x2d, 0x20);
13
+
14
+ const PATTERNS = [
15
+ { regex: /\u2014/, name: `em dash (${EM_DASH})` },
16
+ { regex: /\u2013/, name: `en dash (${EN_DASH})` },
17
+ { regex: SPACED_HYPHEN, name: `spaced hyphen (${SPACED_HYPHEN_TEXT}) used as dash` },
18
+ ];
19
+
20
+ /**
21
+ * CSS is not prose.
22
+ */
23
+
24
+ /*
25
+ * `calc()` arithmetic needs spaces around its minus operator, the spec requires them, so a `<style>` block reported a spaced hyphen on every Svelte component doing arithmetic and
26
+ */
27
+
28
+ /*
29
+ * no rewording could clear it. Text inside `<style>` is skipped for that reason.
30
+ */
31
+ function isInsideStyleBlock(node) {
32
+ for (let current = node.parent; current; current = current.parent) {
33
+ if (current.type === "SvelteStyleElement") return true;
34
+ if (current.type === "SvelteElement" && current.name?.name === "style") return true;
35
+ }
36
+ return false;
37
+ }
38
+
39
+ /**
40
+ * SQL is not prose either, and for the same reason CSS is not: date subtraction in a query needs spaces around its minus operator,
41
+ */
42
+
43
+ /*
44
+ * so a `sql` tagged template reported a spaced hyphen on every date subtraction in the app's query layer and no rewording could clear it without breaking the query.
45
+ */
46
+
47
+ /*
48
+ * Five files were live in that state; one of them, `queries/crm.ts`, had already had a comment bullet blindly rewritten to `*;` by someone clearing the rule.
49
+ */
50
+ const CODE_TAGS = new Set(["sql", "css", "gql", "graphql", "html", "raw"]);
51
+
52
+ function taggedWithCode(node) {
53
+ const tagged = node.parent;
54
+ if (tagged?.type !== "TaggedTemplateExpression" || tagged.quasi !== node) return false;
55
+ const tag = tagged.tag;
56
+ const name =
57
+ tag?.type === "Identifier"
58
+ ? tag.name
59
+ : tag?.type === "MemberExpression" && tag.property?.type === "Identifier"
60
+ ? tag.property.name
61
+ : null;
62
+ return Boolean(name && CODE_TAGS.has(name));
63
+ }
64
+
65
+ /**
66
+ * A markdown bullet is not punctuation. a hyphen then a space at the head of a comment or template line is a list, and rewording it deletes the list.
67
+ */
68
+
69
+ /*
70
+ * The pattern only ever meant a hyphen standing in for an em dash mid-sentence.
71
+ */
72
+ function stripBullets(value, { startsLine = true } = {}) {
73
+ return value
74
+ .split("\n")
75
+ .map((line, i) => (i === 0 && !startsLine ? line : line.replace(/^(\s*\*?\s*)-\s/, "$1")))
76
+ .join("\n");
77
+ }
78
+
79
+ /**
80
+ * A bare dash is an empty-value placeholder, like a table cell with no data, and rewording it means inventing a word for "no value".
81
+ */
82
+ const PLACEHOLDER = /^[\u2014\u2013]$/;
83
+
84
+ /**
85
+ * Code in a string is judged on content: SQL, CSS `calc()`, GLSL and JS all need a spaced hyphen as an operator.
86
+ */
87
+
88
+ /*
89
+ * The signs are narrow on purpose. `function` alone would clear prose that mentions one, so it has to open a parameter list.
90
+ */
91
+ const CODE_SIGNS = [
92
+ /\bSELECT\b[\s\S]*\bFROM\b/,
93
+ /\bUPDATE\b[\s\S]*\bSET\b/,
94
+ /\bINSERT\s+INTO\b/,
95
+ /\bDELETE\s+FROM\b/,
96
+ /\bWHERE\b/,
97
+ /\b(?:ORDER|GROUP)\s+BY\b/,
98
+ /\b(?:LEFT|RIGHT|INNER|OUTER|CROSS)\s+JOIN\b/,
99
+ /\b(?:CREATE|ALTER)\s+TABLE\b/,
100
+ /\bcalc\(/,
101
+ /\bvar\(--/,
102
+ /\bgl_\w/,
103
+ /\bvec[234]\b/,
104
+ /\bfunction\s*\w*\s*\(/,
105
+ /=>/,
106
+ /\bconst\s+\w+\s*=/,
107
+ ];
108
+
109
+ function looksLikeCode(value) {
110
+ return CODE_SIGNS.some((sign) => sign.test(value));
111
+ }
112
+
113
+ function isUserFacingString(node) {
114
+ if (node.parent?.type === "ImportDeclaration") return false;
115
+ if (node.parent?.type === "ImportExpression") return false;
116
+ if (node.parent?.type === "CallExpression" && node.parent.callee?.name === "require")
117
+ return false;
118
+ if (node.parent?.type === "Property" && node.parent.key === node) return false;
119
+ return true;
120
+ }
121
+
122
+ export default {
123
+ meta: {
124
+ type: "suggestion",
125
+ docs: {
126
+ description: "Disallow em dashes, en dashes, and spaced hyphens in strings. Reword instead.",
127
+ },
128
+ messages: {
129
+ noDash: "Avoid {{ name }} in text. Reword the sentence or use a comma/period instead.",
130
+ },
131
+ schema: [],
132
+ },
133
+ create(context) {
134
+ const filename = context.filename || context.getFilename?.() || "";
135
+ const inTestFile = isTestFile(filename);
136
+
137
+ // `spacedHyphenOk` covers places where a spaced hyphen is an operator, and it never excuses a real dash.
138
+ function checkValue(value, node, { spacedHyphenOk = false } = {}) {
139
+ if (typeof value !== "string") return;
140
+ for (const { regex, name } of PATTERNS) {
141
+ if (regex === SPACED_HYPHEN && (spacedHyphenOk || inTestFile)) continue;
142
+ if (regex.test(value)) {
143
+ context.report({ node, messageId: "noDash", data: { name } });
144
+ }
145
+ }
146
+ }
147
+
148
+ function checkString(value, node) {
149
+ if (typeof value !== "string") return;
150
+ if (PLACEHOLDER.test(value.trim())) return;
151
+ checkValue(value, node, { spacedHyphenOk: looksLikeCode(value) });
152
+ }
153
+
154
+ return {
155
+ Program() {
156
+ // WHY: also catch em/en dashes in line and block comments, since the brand rule
157
+ // forbids them in comments and documentation, not just user-facing strings.
158
+ const sourceCode = context.sourceCode || context.getSourceCode();
159
+ for (const comment of sourceCode.getAllComments()) {
160
+ checkValue(stripBullets(comment.value), comment);
161
+ }
162
+ },
163
+ Literal(node) {
164
+ if (!isUserFacingString(node)) return;
165
+ checkString(node.value, node);
166
+ },
167
+ TemplateLiteral(node) {
168
+ if (taggedWithCode(node)) return;
169
+ if (!isUserFacingString(node)) return;
170
+ const cooked = node.quasis.map((quasi) => quasi.value.cooked ?? "");
171
+ const code = looksLikeCode(cooked.join(" "));
172
+ // The first quasi opens the template; a later one opens after `${...}`, mid-line.
173
+ cooked.forEach((text, i) => {
174
+ if (PLACEHOLDER.test(text.trim()) && cooked.length === 1) return;
175
+ checkValue(stripBullets(text, { startsLine: i === 0 }), node, { spacedHyphenOk: code });
176
+ });
177
+ },
178
+ // JSON support
179
+ JSONLiteral(node) {
180
+ checkString(node.value, node);
181
+ },
182
+ // Svelte template body text, e.g. <p>Some prose with a dash</p>.
183
+ // Without this, em/en dashes inside .svelte HTML text slip through
184
+ // because the rule otherwise only inspects JS string nodes.
185
+ SvelteText(node) {
186
+ if (isInsideStyleBlock(node)) return;
187
+ if (PLACEHOLDER.test(node.value.trim())) return;
188
+ checkValue(node.value, node);
189
+ },
190
+ // Svelte attribute literal values, e.g. title="Heads up important".
191
+ SvelteLiteral(node) {
192
+ if (isInsideStyleBlock(node)) return;
193
+ if (PLACEHOLDER.test(String(node.value).trim())) return;
194
+ checkValue(node.value, node);
195
+ },
196
+ };
197
+ },
198
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Ban emoji characters in string literals.
3
+ * Use Phosphor icons instead for visual indicators in UI content.
4
+ */
5
+
6
+ import { isTestFile } from "#lint/rules/test-file.js";
7
+
8
+ // Emoji presentation, a text pictograph forced by U+FE0F, or a keycap. A dingbat range also caught text marks like U+2713.
9
+ const EMOJI_REGEX = /\p{Emoji_Presentation}|\p{Extended_Pictographic}\u{FE0F}|\u{20E3}/u;
10
+
11
+ function isUserFacingString(node) {
12
+ if (node.parent?.type === "ImportDeclaration") return false;
13
+ if (node.parent?.type === "ImportExpression") return false;
14
+ if (node.parent?.type === "CallExpression" && node.parent.callee?.name === "require")
15
+ return false;
16
+ return true;
17
+ }
18
+
19
+ export default {
20
+ meta: {
21
+ type: "suggestion",
22
+ docs: {
23
+ description: "Disallow emoji characters in strings. Use Phosphor icons instead.",
24
+ },
25
+ messages: {
26
+ noEmoji: "Avoid emoji in content strings. Use a Phosphor icon component instead.",
27
+ },
28
+ schema: [],
29
+ },
30
+ create(context) {
31
+ if (isTestFile(context.filename || context.getFilename?.())) return {};
32
+
33
+ function checkValue(value, node) {
34
+ if (typeof value !== "string") return;
35
+ if (EMOJI_REGEX.test(value)) {
36
+ context.report({ node, messageId: "noEmoji" });
37
+ }
38
+ }
39
+
40
+ return {
41
+ Literal(node) {
42
+ if (!isUserFacingString(node)) return;
43
+ checkValue(node.value, node);
44
+ },
45
+ TemplateLiteral(node) {
46
+ if (!isUserFacingString(node)) return;
47
+ for (const quasi of node.quasis) {
48
+ checkValue(quasi.value.cooked, node);
49
+ }
50
+ },
51
+ JSONLiteral(node) {
52
+ checkValue(node.value, node);
53
+ },
54
+ };
55
+ },
56
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Ban hardcoded AI model identifiers in source code.
3
+ * Model versions should come from @heyramzi/ai tier system (MODEL_IDS / MODEL_CATALOG).
4
+ */
5
+
6
+ import { isTestFile } from "#lint/rules/test-file.js";
7
+
8
+ const MODEL_PATTERNS = [
9
+ /\bgemini-\d+\.\d+[\w-]*/,
10
+ /\bclaude-(?:opus|sonnet|haiku)-[\d.-]+/,
11
+ /\bgrok-\d+\.\d+/,
12
+ /\bgpt-\d+[\w.-]*/,
13
+ ];
14
+
15
+ const COMBINED = new RegExp(MODEL_PATTERNS.map((r) => r.source).join("|"));
16
+
17
+ // WHY: files inside @heyramzi/ai define the canonical model catalog, so they must use literal IDs.
18
+ function isModelCatalogFile(filename) {
19
+ return /packages\/ai\//.test(filename);
20
+ }
21
+
22
+ // WHY: a `ladder.ts` is where an app lists its model ids, so naming them there is the job.
23
+ function isLadderFile(filename) {
24
+ return /(?:^|[\\/])ladder\.[cm]?[jt]s$/.test(filename);
25
+ }
26
+
27
+ export default {
28
+ meta: {
29
+ type: "suggestion",
30
+ docs: {
31
+ description:
32
+ "Disallow hardcoded AI model identifiers. Use @heyramzi/ai MODEL_IDS or MODEL_CATALOG instead.",
33
+ },
34
+ messages: {
35
+ noHardcodedModel:
36
+ 'Hardcoded model "{{ model }}". Import MODEL_IDS or MODEL_CATALOG from @heyramzi/ai instead.',
37
+ },
38
+ schema: [],
39
+ },
40
+ create(context) {
41
+ const filename = context.filename || context.getFilename();
42
+ if (isModelCatalogFile(filename) || isLadderFile(filename) || isTestFile(filename)) return {};
43
+
44
+ function check(value, node) {
45
+ if (typeof value !== "string") return;
46
+ const match = value.match(COMBINED);
47
+ if (match) {
48
+ context.report({ node, messageId: "noHardcodedModel", data: { model: match[0] } });
49
+ }
50
+ }
51
+
52
+ return {
53
+ Literal(node) {
54
+ check(node.value, node);
55
+ },
56
+ TemplateLiteral(node) {
57
+ for (const quasi of node.quasis) {
58
+ check(quasi.value.cooked, node);
59
+ }
60
+ },
61
+ };
62
+ },
63
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Disallow inline SVG <path> in components. SVGs must be separate .svg files
3
+ * and imported. Contract in packages/lint/README.md.
4
+ */
5
+ import { readFileSync } from "node:fs";
6
+ import { findInlinePaths } from "#lint/inline-path.js";
7
+
8
+ export default {
9
+ meta: {
10
+ type: "problem",
11
+ docs: {
12
+ description:
13
+ "Disallow inline SVG <path> elements in component templates. SVGs must be separate files and imported.",
14
+ },
15
+ messages: {
16
+ noInlinePath:
17
+ "Inline SVG <path> found at line {{ line }}. SVGs must be separate files (.svg) and imported (e.g. import icon from './icon.svg'). Do not embed inline paths.",
18
+ },
19
+ schema: [],
20
+ },
21
+ create(context) {
22
+ const filename = context.filename;
23
+
24
+ if (filename.endsWith(".svg")) return {};
25
+ if (!/\.(svelte|vue|jsx|tsx|html|astro)$/.test(filename)) return {};
26
+
27
+ return {
28
+ Program() {
29
+ let source;
30
+ try {
31
+ source = readFileSync(filename, "utf8");
32
+ } catch {
33
+ return;
34
+ }
35
+
36
+ for (const match of findInlinePaths(source)) {
37
+ context.report({
38
+ loc: { line: 1, column: 0 },
39
+ messageId: "noInlinePath",
40
+ data: { line: String(match.line) },
41
+ });
42
+ }
43
+ },
44
+ };
45
+ },
46
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Disallow inline <svg> elements in Svelte template markup. Use icon components (e.g. phosphor-svelte) instead.
3
+ */
4
+
5
+ /*
6
+ * OXLint only exposes the <script> block as sourceCode.text for .svelte files, so this rule reads the full file from disk to inspect the template markup.
7
+ */
8
+
9
+ /*
10
+ * Skips files in allowed directories (ui/, icons/, logos/, svg/, diagrams/).
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+
14
+ const ALLOW_PATH_RE =
15
+ /[\\/](?:components[\\/]ui|components[\\/]icons|logos|components[\\/]shared[\\/]svg|diagrams|svibe|slides[\\/]layouts|embed|brand-pdf|og[\\/])[\\/]/;
16
+
17
+ export default {
18
+ meta: {
19
+ type: "suggestion",
20
+ docs: {
21
+ description: "Disallow inline <svg> elements in Svelte files. Use icon components instead.",
22
+ },
23
+ messages: {
24
+ noInlineSvg:
25
+ "Inline <svg> at line {{ line }}. Use an icon component (e.g. phosphor-svelte) or extract to a dedicated SVG component file.",
26
+ },
27
+ schema: [],
28
+ },
29
+ create(context) {
30
+ const filename = context.filename;
31
+
32
+ if (!filename.endsWith(".svelte")) return {};
33
+ if (ALLOW_PATH_RE.test(filename)) return {};
34
+
35
+ return {
36
+ Program() {
37
+ let source;
38
+ try {
39
+ source = readFileSync(filename, "utf8");
40
+ } catch {
41
+ return;
42
+ }
43
+
44
+ // Only scan the template portion (after the last </script>)
45
+ const scriptCloseIdx = source.lastIndexOf("</script>");
46
+ const template = scriptCloseIdx !== -1 ? source.slice(scriptCloseIdx) : source;
47
+ const templateStartLine =
48
+ scriptCloseIdx !== -1 ? source.slice(0, scriptCloseIdx).split("\n").length : 1;
49
+
50
+ const lines = template.split("\n");
51
+
52
+ for (let i = 0; i < lines.length; i++) {
53
+ if (!lines[i].includes("<svg")) continue;
54
+
55
+ // OXLint only sees the script block, so report at line 1
56
+ // and include the actual file line number in the message
57
+ const actualLine = templateStartLine + i;
58
+ context.report({
59
+ loc: { line: 1, column: 0 },
60
+ messageId: "noInlineSvg",
61
+ data: { line: String(actualLine) },
62
+ });
63
+ }
64
+ },
65
+ };
66
+ },
67
+ };