@halogen-ui/eslint-config 0.1.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/LICENSE.md ADDED
@@ -0,0 +1,102 @@
1
+ # Halogen License Agreement
2
+
3
+ Copyright © 2026 Nick Jacoy. All rights reserved.
4
+
5
+ Halogen is **source-available commercial software**. The source is published so
6
+ that it can be read, evaluated and audited before purchase — publication is not
7
+ a grant of the right to use it. Use in a project requires a valid license.
8
+
9
+ ## 1. Definitions
10
+
11
+ - **"Software"** — the Halogen design system, comprising the `@halogen-ui/react`,
12
+ `@halogen-ui/tokens` and `@halogen-ui/eslint-config` packages and their source,
13
+ documentation and generated assets.
14
+ - **"Licensee"** — the individual or single legal entity that has purchased a
15
+ license.
16
+ - **"Product"** — an application, website or service built by the Licensee in
17
+ which the Software is used as a component of a larger work.
18
+
19
+ ## 2. Evaluation
20
+
21
+ Without purchasing a license, you may download, read, run and evaluate the
22
+ Software, including building and running it locally. You may not use it in a
23
+ Product, whether or not that Product is distributed, sold or made public.
24
+
25
+ ## 3. Grant of license
26
+
27
+ Upon purchase, the Licensee is granted a perpetual, worldwide, non-exclusive,
28
+ non-transferable license to:
29
+
30
+ a. use the Software in an unlimited number of Products;
31
+ b. modify the Software and use the modified version in those Products;
32
+ c. distribute the Software **as part of** a Product, in compiled or bundled
33
+ form, to that Product's end users;
34
+ d. make copies as reasonably required for backup and development.
35
+
36
+ The license covers the Licensee and its employees and contractors, acting on the
37
+ Licensee's behalf.
38
+
39
+ ## 4. Restrictions
40
+
41
+ The Licensee may not:
42
+
43
+ a. redistribute, publish, sublicense, sell, rent or lend the Software on its
44
+ own, or in any form where the Software — rather than a Product built with
45
+ it — is the substance of what is conveyed;
46
+ b. use the Software to create or contribute to a competing design system,
47
+ component library, UI kit or template that is distributed to third parties;
48
+ c. remove or obscure copyright, license or attribution notices;
49
+ d. share license credentials or a purchased copy outside the Licensee.
50
+
51
+ Publishing a fork, a re-skin, or a repackaged subset of the Software to a public
52
+ registry or repository is prohibited under (a) and (b), and this remains true
53
+ however the files were obtained.
54
+
55
+ ## 5. Third-party components
56
+
57
+ The Software depends on and, in the case of fonts, redistributes third-party
58
+ material licensed separately by its authors — including Radix UI, `clsx`,
59
+ `class-variance-authority`, `tailwind-merge` and `lucide-react` (MIT), and the
60
+ Geist and Geist Mono typefaces (SIL Open Font License 1.1, whose text ships at
61
+ `fonts/LICENSE-Geist-OFL-1.1.txt`). Nothing in this Agreement limits or replaces
62
+ your rights and obligations under those licenses.
63
+
64
+ ## 6. Ownership
65
+
66
+ The Software is licensed, not sold. All title and intellectual property rights
67
+ in the Software remain with the copyright holder. Rights not expressly granted
68
+ here are reserved.
69
+
70
+ ## 7. Warranty disclaimer
71
+
72
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
73
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
74
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
75
+
76
+ Halogen makes strong accessibility and contrast claims and tests them
77
+ extensively. Those tests are evidence, not a warranty: conformance of *your*
78
+ Product is your responsibility.
79
+
80
+ ## 8. Limitation of liability
81
+
82
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
83
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
84
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
85
+ DEALINGS IN THE SOFTWARE. Total liability shall not exceed the amount paid for
86
+ the license.
87
+
88
+ ## 9. Termination
89
+
90
+ This license terminates automatically if the Licensee materially breaches it and
91
+ does not cure the breach within 30 days of notice. On termination the Licensee
92
+ must cease using the Software in new Products; Products already distributed are
93
+ unaffected.
94
+
95
+ ## 10. Governing law
96
+
97
+ This Agreement is governed by the laws of the State of California, United
98
+ States, without regard to its conflict-of-laws rules.
99
+
100
+ ---
101
+
102
+ Licensing questions: https://github.com/njacoy/halogen-design-system
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @halogen-ui/eslint-config
2
+
3
+ The four rules that make [Halogen](https://github.com/njacoy/halogen-design-system)'s
4
+ guarantees hold in *your* code, not just inside the library.
5
+
6
+ ```bash
7
+ pnpm add -D @halogen-ui/eslint-config
8
+ ```
9
+
10
+ ```js
11
+ // eslint.config.js
12
+ import { recommended } from '@halogen-ui/eslint-config';
13
+
14
+ export default [
15
+ recommended,
16
+ // ...the rest of your flat config
17
+ ];
18
+ ```
19
+
20
+ `recommended` turns all four on as errors. The raw `plugin` is also exported if
21
+ you would rather wire the rules yourself.
22
+
23
+ ## The rules
24
+
25
+ | Rule | Catches |
26
+ | --- | --- |
27
+ | `no-arbitrary-values` | `p-[13px]`, `text-[15px]` — a value invented to dodge a missing token |
28
+ | `no-primitive-colors` | reaching past the semantic layer into the raw palette |
29
+ | `no-aria-label-on-generic` | `aria-label` on a `span`, `div`, `kbd` — ARIA forbids naming a roleless element, so the name is silently dropped |
30
+ | `no-gradient-on-controls` | gradients on interactive controls, where they break the state vocabulary |
31
+
32
+ Each one exists because the mistake it catches **compiles cleanly and looks
33
+ right**. A dead utility produces no CSS and no error; a dropped `aria-label` is
34
+ valid HTML that no screen reader ever announces. A typechecker cannot see either.
35
+
36
+ ESLint `>=9` (flat config) is a peer dependency.
package/index.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Halogen — the enforcement rules, shipped.
3
+ *
4
+ * These travel with the kit rather than living in the source repo, because a
5
+ * design system's guarantees are only worth something inside the buyer's
6
+ * codebase. Without them, Halogen is a folder of components that will drift
7
+ * into a folder of one-offs within a year.
8
+ */
9
+ import { createRequire } from 'node:module';
10
+
11
+ import noAriaLabelOnGeneric from './rules/no-aria-label-on-generic.js';
12
+ import noArbitraryValues from './rules/no-arbitrary-values.js';
13
+ import noGradientOnControls from './rules/no-gradient-on-controls.js';
14
+ import noPrimitiveColors from './rules/no-primitive-colors.js';
15
+
16
+ /* Read from the manifest rather than written here twice.
17
+ *
18
+ * ESLint puts plugin meta into its cache keys and its error output, so a
19
+ * hardcoded version does not fail — it silently reports the wrong number from
20
+ * the first release onward, and stale cache entries are the kind of bug nobody
21
+ * traces back to a string literal. createRequire rather than a JSON import
22
+ * attribute, which is still gated behind a flag on some runtimes a consumer
23
+ * might be linting under. */
24
+ const { name, version } = createRequire(import.meta.url)('./package.json');
25
+
26
+ export const plugin = {
27
+ meta: { name, version },
28
+ rules: {
29
+ 'no-aria-label-on-generic': noAriaLabelOnGeneric,
30
+ 'no-arbitrary-values': noArbitraryValues,
31
+ 'no-gradient-on-controls': noGradientOnControls,
32
+ 'no-primitive-colors': noPrimitiveColors,
33
+ },
34
+ };
35
+
36
+ /** Recommended flat config. Spread into your eslint.config.js. */
37
+ export const recommended = {
38
+ plugins: { halogen: plugin },
39
+ rules: {
40
+ 'halogen/no-aria-label-on-generic': 'error',
41
+ 'halogen/no-arbitrary-values': 'error',
42
+ 'halogen/no-gradient-on-controls': 'error',
43
+ 'halogen/no-primitive-colors': 'error',
44
+ },
45
+ };
46
+
47
+ export default { plugin, recommended };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@halogen-ui/eslint-config",
3
+ "version": "0.1.0",
4
+ "description": "Halogen's enforcement rules. Ships to consumers — the system's guarantees travel with it.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE.md",
7
+ "main": "./index.js",
8
+ "exports": {
9
+ ".": "./index.js"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "rules",
14
+ "LICENSE.md",
15
+ "README.md"
16
+ ],
17
+ "peerDependencies": {
18
+ "eslint": ">=9"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/njacoy/halogen-design-system.git",
23
+ "directory": "packages/eslint-config"
24
+ },
25
+ "homepage": "https://github.com/njacoy/halogen-design-system#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/njacoy/halogen-design-system/issues"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "keywords": [
33
+ "eslint-config",
34
+ "design-system",
35
+ "tailwindcss",
36
+ "halogen"
37
+ ],
38
+ "scripts": {}
39
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Halogen — `no-arbitrary-values`.
3
+ *
4
+ * Arbitrary values are the crack that drains a design system.
5
+ *
6
+ * Replacing Tailwind's theme stops `p-7` from compiling, but `p-[28px]` still
7
+ * works — arbitrary values bypass the theme entirely. One of them is harmless.
8
+ * Two hundred of them, added one reasonable exception at a time over a year, is
9
+ * a codebase where the token layer describes nothing that is actually on screen.
10
+ *
11
+ * That is the failure this rule exists to prevent, and it is why the rule ships
12
+ * to buyers rather than staying in the repo: the guarantee is only worth
13
+ * something if it survives contact with their codebase.
14
+ */
15
+
16
+ /** `p-[28px]`, `bg-[#fff]`, `w-[calc(100%-1rem)]`, including under modifiers. */
17
+ const ARBITRARY = /(^|:)-?[a-z][a-z0-9-]*-\[[^\]]+\]/;
18
+ /**
19
+ * An arbitrary *variant* — a selector, not a value: `[&>svg]:size-4`, and also
20
+ * `aria-[invalid=true]:border-x`, where the brackets sit mid-token. Reported
21
+ * separately because the fix is different: Tailwind almost always has a named
22
+ * variant already (`aria-invalid:`), and reaching for a raw selector usually
23
+ * means a component boundary is in the wrong place.
24
+ */
25
+ const ARBITRARY_VARIANT = /\[[^\]]+\]:/;
26
+
27
+ const CLASS_CALLEES = new Set(['cn', 'cva', 'clsx', 'classNames', 'twMerge', 'tw']);
28
+ const CLASS_ATTRS = new Set(['className', 'class']);
29
+
30
+ /**
31
+ * A string that is *shaped* like a class list, wherever it happens to sit.
32
+ *
33
+ * The call-site check below is necessary and was not sufficient. A component can
34
+ * hold its classes in a plain lookup map and interpolate them into `className`
35
+ * later, and the rule saw nothing:
36
+ *
37
+ * const ASPECT = { wide: 'aspect-[21/9]' }; // <- invisible
38
+ * <div className={ASPECT[aspect]} />
39
+ *
40
+ * That is not a hypothetical. `ContentCard` shipped exactly this, and the sole
41
+ * arbitrary value in the entire library sat in it, unreported, while the rule
42
+ * that exists to forbid arbitrary values passed clean.
43
+ *
44
+ * The shape test is strict on purpose: EVERY whitespace-separated segment must
45
+ * look like a utility — lowercase, no sentence punctuation. Prose fails it on
46
+ * the first capital letter or bare word, so a `why:` field describing a token
47
+ * is not mistaken for a class list. A string only gets reported if it also
48
+ * contains an arbitrary value, so passing the shape test alone costs nothing.
49
+ */
50
+ const UTILITY_SEGMENT = /^-?(?:[a-z0-9][a-z0-9-]*(?:-\[[^\]]+\])?:)*-?[a-z0-9][a-z0-9./-]*(?:\[[^\]]+\])?[a-z0-9%./-]*!?$/;
51
+
52
+ function looksLikeClassList(raw) {
53
+ const segments = raw.trim().split(/\s+/).filter(Boolean);
54
+ if (segments.length === 0) return false;
55
+ return segments.every((s) => UTILITY_SEGMENT.test(s));
56
+ }
57
+
58
+ /** True when this string literal is somewhere that becomes a class name. */
59
+ function inClassContext(node, sourceCode) {
60
+ let current = node;
61
+ for (let i = 0; i < 12 && current; i++) {
62
+ const parent = sourceCode.getAncestors
63
+ ? current.parent
64
+ : current.parent;
65
+ if (!parent) return false;
66
+
67
+ if (parent.type === 'JSXExpressionContainer' && parent.parent?.type === 'JSXAttribute') {
68
+ return CLASS_ATTRS.has(parent.parent.name?.name);
69
+ }
70
+ if (parent.type === 'JSXAttribute') {
71
+ return CLASS_ATTRS.has(parent.name?.name);
72
+ }
73
+ if (parent.type === 'CallExpression') {
74
+ const callee = parent.callee;
75
+ const name = callee?.name ?? callee?.property?.name;
76
+ if (CLASS_CALLEES.has(name)) return true;
77
+ }
78
+ // A `*.variants.ts` file is class definitions top to bottom.
79
+ current = parent;
80
+ }
81
+ return false;
82
+ }
83
+
84
+ export default {
85
+ meta: {
86
+ type: 'problem',
87
+ docs: {
88
+ description:
89
+ 'Disallow Tailwind arbitrary values. Every value must come from a Halogen token.',
90
+ },
91
+ schema: [
92
+ {
93
+ type: 'object',
94
+ properties: {
95
+ /** Files whose every string is treated as classes, e.g. `*.variants.ts`. */
96
+ treatAllStringsAsClasses: { type: 'array', items: { type: 'string' } },
97
+ },
98
+ additionalProperties: false,
99
+ },
100
+ ],
101
+ messages: {
102
+ arbitrary:
103
+ '"{{ value }}" is an arbitrary value. Halogen is tokens-only: use a token from the scale, or add one deliberately if the scale is genuinely missing a step.',
104
+ arbitraryVariant:
105
+ '"{{ value }}" is an arbitrary variant. Express the relationship with a component or a documented variant instead of an ad-hoc selector.',
106
+ },
107
+ },
108
+
109
+ create(context) {
110
+ const options = context.options[0] ?? {};
111
+ const patterns = options.treatAllStringsAsClasses ?? ['\\.variants\\.(ts|tsx|js)$'];
112
+ const filename = context.filename ?? context.getFilename();
113
+ const alwaysClasses = patterns.some((p) => new RegExp(p).test(filename));
114
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
115
+
116
+ function check(node, raw) {
117
+ if (typeof raw !== 'string' || !raw.includes('[')) return;
118
+ if (!alwaysClasses && !inClassContext(node, sourceCode) && !looksLikeClassList(raw)) return;
119
+
120
+ for (const token of raw.split(/\s+/)) {
121
+ if (!token) continue;
122
+ if (ARBITRARY_VARIANT.test(token)) {
123
+ context.report({ node, messageId: 'arbitraryVariant', data: { value: token } });
124
+ } else if (ARBITRARY.test(token)) {
125
+ context.report({ node, messageId: 'arbitrary', data: { value: token } });
126
+ }
127
+ }
128
+ }
129
+
130
+ return {
131
+ Literal(node) {
132
+ check(node, node.value);
133
+ },
134
+ TemplateElement(node) {
135
+ check(node, node.value?.cooked);
136
+ },
137
+ };
138
+ },
139
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Halogen — `no-aria-label-on-generic`.
3
+ *
4
+ * `aria-label` is prohibited on an element with no ARIA role.
5
+ *
6
+ * A `<span>`, a `<div>`, a `<kbd>` — none of these have an implicit role, and
7
+ * ARIA forbids naming a roleless element. Assistive technology is free to ignore
8
+ * the attribute entirely, which it does. The label is simply thrown away.
9
+ *
10
+ * Nothing complains. The attribute is valid HTML, TypeScript accepts it, the
11
+ * element renders, and a reader hears nothing where a name was intended. It is
12
+ * the exact failure mode this system keeps producing: **written, plausible,
13
+ * silently discarded.**
14
+ *
15
+ * This rule exists because it happened twice in one afternoon — `Kbd` naming a
16
+ * shortcut, and `Breadcrumb` naming the gap in a collapsed trail. Both were
17
+ * caught by axe in a browser, and only because the showcase happened to render
18
+ * them. A component whose labelled state is not on the review page would have
19
+ * shipped.
20
+ *
21
+ * The fix is always the same: a visually-hidden span with the text in it, which
22
+ * works on any element. Or give the element a real role, if it has one.
23
+ */
24
+
25
+ /**
26
+ * Elements with no implicit ARIA role, or whose role prohibits naming.
27
+ *
28
+ * Deliberately not exhaustive — this is the set that actually gets reached for.
29
+ * `<p>` is included: its `paragraph` role also prohibits a name.
30
+ */
31
+ const ROLELESS = new Set([
32
+ 'span', 'div', 'kbd', 'code', 'p', 'em', 'strong', 'small',
33
+ 'b', 'i', 's', 'u', 'sub', 'sup', 'pre', 'blockquote', 'label',
34
+ ]);
35
+
36
+ const NAMING_ATTRS = new Set(['aria-label', 'aria-labelledby']);
37
+
38
+ export default {
39
+ meta: {
40
+ type: 'problem',
41
+ docs: {
42
+ description:
43
+ 'Disallow aria-label on elements with no ARIA role, where it is prohibited and ignored.',
44
+ },
45
+ schema: [],
46
+ messages: {
47
+ prohibited:
48
+ '`{{ attr }}` on <{{ element }}> is prohibited: the element has no ARIA role, so assistive technology discards the name. Use a visually-hidden <span className="sr-only"> with the text, or give the element a role.',
49
+ },
50
+ },
51
+
52
+ create(context) {
53
+ return {
54
+ JSXOpeningElement(node) {
55
+ const name = node.name?.name;
56
+ if (typeof name !== 'string' || !ROLELESS.has(name)) return;
57
+
58
+ // An explicit role makes naming legal again — `<div role="group">` is a
59
+ // real group and may be named.
60
+ const hasRole = node.attributes.some(
61
+ (attr) => attr.type === 'JSXAttribute' && attr.name?.name === 'role',
62
+ );
63
+ if (hasRole) return;
64
+
65
+ for (const attr of node.attributes) {
66
+ if (attr.type !== 'JSXAttribute') continue;
67
+ const attrName = attr.name?.name;
68
+ if (typeof attrName === 'string' && NAMING_ATTRS.has(attrName)) {
69
+ context.report({
70
+ node: attr,
71
+ messageId: 'prohibited',
72
+ data: { attr: attrName, element: name },
73
+ });
74
+ }
75
+ }
76
+ },
77
+ };
78
+ },
79
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Halogen — `no-gradient-on-controls`.
3
+ *
4
+ * Gradients belong on heroes, feature surfaces, promotional panels, progress
5
+ * fills and decorative fields. They do not belong on buttons, inputs, tables,
6
+ * navigation, badges, icons or borders.
7
+ *
8
+ * The reason is not taste. A gradient has no single color, so no contrast
9
+ * figure can be computed for text sitting on it — every assertion in the system
10
+ * would have to become "at the worst stop", which is exactly the treatment
11
+ * `GradientSurface` exists to contain. Spread across controls, the guarantee
12
+ * that any label meets a known ratio simply stops being true.
13
+ */
14
+ const GRADIENT = /(^|:)gradient-(ember|solar|dusk|glow|ground)\b/;
15
+
16
+ /** Files where a gradient is legitimate: marketing surfaces and the two components that own them. */
17
+ const DEFAULT_ALLOW = [
18
+ 'GradientSurface',
19
+ 'Progress',
20
+ '/marketing/',
21
+ '\\.stories\\.',
22
+ ];
23
+
24
+ export default {
25
+ meta: {
26
+ type: 'problem',
27
+ docs: { description: 'Disallow brand gradients on controls and product chrome.' },
28
+ schema: [
29
+ {
30
+ type: 'object',
31
+ properties: { allow: { type: 'array', items: { type: 'string' } } },
32
+ additionalProperties: false,
33
+ },
34
+ ],
35
+ messages: {
36
+ gradient:
37
+ '"{{ value }}" puts a gradient on a control. Gradients have no single color, so no contrast figure can be computed for text on them. Use <GradientSurface> for brand moments, or a solid surface token here.',
38
+ },
39
+ },
40
+
41
+ create(context) {
42
+ const allow = [...DEFAULT_ALLOW, ...(context.options[0]?.allow ?? [])].map((p) => new RegExp(p));
43
+ const filename = context.filename ?? context.getFilename();
44
+ if (allow.some((r) => r.test(filename))) return {};
45
+
46
+ function check(node, raw) {
47
+ if (typeof raw !== 'string') return;
48
+ for (const token of raw.split(/\s+/)) {
49
+ if (token && GRADIENT.test(token)) {
50
+ context.report({ node, messageId: 'gradient', data: { value: token } });
51
+ }
52
+ }
53
+ }
54
+
55
+ return {
56
+ Literal(node) { check(node, node.value); },
57
+ TemplateElement(node) { check(node, node.value?.cooked); },
58
+ };
59
+ },
60
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Halogen — `no-primitive-colors`.
3
+ *
4
+ * Primitive color utilities (`bg-orange-base`, `text-neutral-500`) exist for
5
+ * marketing surfaces, where a designer is composing a brand moment and the
6
+ * semantic layer would only get in the way.
7
+ *
8
+ * In product UI they are a theme bug waiting to happen: `text-neutral-500` is a
9
+ * fixed gray in both themes, so it reads correctly in dark and washes out in
10
+ * light. The semantic layer exists precisely so a component never has to know
11
+ * which theme it is in.
12
+ *
13
+ * Default scope is the component library itself, which must be semantic-only.
14
+ */
15
+ const PRIMITIVE_PREFIXES = [
16
+ 'neutral', 'orange', 'coral', 'amber', 'yellow', 'peach', 'pink', 'teal',
17
+ 'success', 'warning', 'error', 'heat',
18
+ ];
19
+ const COLOR_UTILITIES = ['bg', 'text', 'border', 'outline', 'ring', 'fill', 'stroke', 'decoration', 'divide'];
20
+
21
+ const PATTERN = new RegExp(
22
+ `(^|:)(${COLOR_UTILITIES.join('|')})-(${PRIMITIVE_PREFIXES.join('|')})-`,
23
+ );
24
+
25
+ export default {
26
+ meta: {
27
+ type: 'problem',
28
+ docs: { description: 'Disallow primitive color utilities in product UI.' },
29
+ schema: [
30
+ {
31
+ type: 'object',
32
+ properties: { allow: { type: 'array', items: { type: 'string' } } },
33
+ additionalProperties: false,
34
+ },
35
+ ],
36
+ messages: {
37
+ primitive:
38
+ '"{{ value }}" is a primitive color and does not change between themes. Use a semantic token (bg-surface-*, text-ink-*, border-line-*) so the component works in both.',
39
+ },
40
+ },
41
+
42
+ create(context) {
43
+ const allow = (context.options[0]?.allow ?? []).map((p) => new RegExp(p));
44
+ const filename = context.filename ?? context.getFilename();
45
+ if (allow.some((r) => r.test(filename))) return {};
46
+
47
+ function check(node, raw) {
48
+ if (typeof raw !== 'string') return;
49
+ for (const token of raw.split(/\s+/)) {
50
+ if (token && PATTERN.test(token)) {
51
+ context.report({ node, messageId: 'primitive', data: { value: token } });
52
+ }
53
+ }
54
+ }
55
+
56
+ return {
57
+ Literal(node) { check(node, node.value); },
58
+ TemplateElement(node) { check(node, node.value?.cooked); },
59
+ };
60
+ },
61
+ };