@agimon-ai/vibe-lint-plugin-doom-web 0.0.1-alpha.74 → 0.0.1-alpha.76
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/dist/rules/primitives.cjs +1 -1
- package/dist/rules/primitives.cjs.map +1 -1
- package/dist/rules/primitives.mjs +1 -1
- package/dist/rules/primitives.mjs.map +1 -1
- package/dist/rules/theming.cjs +1 -1
- package/dist/rules/theming.cjs.map +1 -1
- package/dist/rules/theming.mjs +1 -1
- package/dist/rules/theming.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/configs/patterns.d.cts +0 -10
- package/dist/configs/patterns.d.cts.map +0 -1
- package/dist/configs/recommended.d.cts +0 -5
- package/dist/configs/recommended.d.cts.map +0 -1
- package/dist/index.d.cts +0 -12
- package/dist/plugin.d.cts +0 -5
- package/dist/plugin.d.cts.map +0 -1
- package/dist/rules/componentLibrary.d.cts +0 -7
- package/dist/rules/componentLibrary.d.cts.map +0 -1
- package/dist/rules/conventions.d.cts +0 -5
- package/dist/rules/conventions.d.cts.map +0 -1
- package/dist/rules/features.d.cts +0 -5
- package/dist/rules/features.d.cts.map +0 -1
- package/dist/rules/index.d.cts +0 -8
- package/dist/rules/layers.d.cts +0 -5
- package/dist/rules/layers.d.cts.map +0 -1
- package/dist/rules/primitives.d.cts +0 -5
- package/dist/rules/primitives.d.cts.map +0 -1
- package/dist/rules/registry.d.cts +0 -5
- package/dist/rules/registry.d.cts.map +0 -1
- package/dist/rules/theming.d.cts +0 -6
- package/dist/rules/theming.d.cts.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./moduleGraph.cjs"),n=require("./componentLibrary.cjs");let r=require("typescript");r=e.__toESM(r,1);let i=require("@agimon-ai/doompi-build/
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./moduleGraph.cjs"),n=require("./componentLibrary.cjs");let r=require("typescript");r=e.__toESM(r,1);let i=require("@agimon-ai/doompi-build/browserFile");const a={button:`Button`,input:`Input`,textarea:`Textarea`,select:`Select`},o=/prefer-shared-primitive:\s*ignore/;function s(e){return e.startsWith(`src/web/`)||(0,i.isBrowserFile)(e)}function c(e){return e.startsWith(`tests/`)||e.includes(`.test.`)||e.includes(`.spec.`)}function l(e){let t=e.tagName;return r.default.isIdentifier(t)?t.text:void 0}function u(e,t){return e.attributes.properties.some(e=>r.default.isJsxAttribute(e)&&r.default.isIdentifier(e.name)&&e.name.text===t)}function d(e){let t=r.default.isJsxOpeningElement(e)?e.parent.parent:e.parent;for(let e=t;e!==void 0;e=e.parent){if(r.default.isJsxElement(e))return u(e.openingElement,`asChild`);if(!r.default.isJsxFragment(e)&&!r.default.isJsxExpression(e))return!1}return!1}const f={preflight:!0,rule:`Browser code reaches for the shared primitive rather than restyling a raw element`,rationale:`The component library exists so a button looks and behaves the same everywhere: one focus ring, one disabled state, one set of tokens. A raw <button className="…"> renders something that only resembles a Button, and the resemblance decays. It is also invisible in review, which is how the same chip came to be spelled ten different ways across the packages. Radix asChild is the escape hatch when a primitive must lend its styling to another element, and the rule stands down there.`,check(e,i){let u=t.projectPath(e,i);if(u===null||!s(u)||c(u)||n.isComponentLibrary(i))return null;let f=t.readSource(e);if(!f||o.test(f.getFullText()))return null;let p=/* @__PURE__ */ new Map,m=e=>{if(r.default.isJsxOpeningElement(e)||r.default.isJsxSelfClosingElement(e)){let t=l(e),n=t===void 0?void 0:a[t];t!==void 0&&n!==void 0&&!d(e)&&p.set(t,n),t!==void 0&&!a[t]&&e.attributes.properties.some(e=>r.default.isJsxAttribute(e)&&r.default.isIdentifier(e.name)&&e.name.text===`role`&&e.initializer!==void 0&&r.default.isStringLiteral(e.initializer)&&e.initializer.text===`option`)&&p.set(`${t} role="option"`,`OptionRow`)}r.default.forEachChild(e,m)};return m(f),p.size===0?null:`${u} renders ${[...p].map(([e,t])=>`<${e}> (use ${t})`).join(`, `)} directly. Import the primitive from @agimon-ai/doompi-web-components instead, or wrap the element in the primitive with asChild when it must stay this element. A row that genuinely has no primitive can opt out with a "prefer-shared-primitive: ignore" comment naming why.`}};exports.preferSharedPrimitive=f;
|
|
2
2
|
//# sourceMappingURL=primitives.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"primitives.cjs","names":["isBrowserFile","ts","projectPath","isComponentLibrary","readSource"],"sources":["../../src/rules/primitives.ts"],"sourcesContent":["import { isBrowserFile } from '@agimon-ai/doompi-build/
|
|
1
|
+
{"version":3,"file":"primitives.cjs","names":["isBrowserFile","ts","projectPath","isComponentLibrary","readSource"],"sources":["../../src/rules/primitives.ts"],"sourcesContent":["import { isBrowserFile } from '@agimon-ai/doompi-build/browserFile';\nimport type { RuleDefinition } from '@agimon-ai/vibe-lint';\nimport ts from 'typescript';\n\nimport { isComponentLibrary } from './componentLibrary.js';\nimport { projectPath, readSource } from './moduleGraph.js';\n\n/** The interactive elements the shared library already owns, and what to reach for instead. */\nconst OWNED_ELEMENTS: Readonly<Record<string, string>> = {\n button: 'Button',\n input: 'Input',\n textarea: 'Textarea',\n select: 'Select',\n};\n\n/** A row of a listbox is an option, and OptionRow is the primitive for it. */\nconst OPTION_HINT = 'OptionRow';\n\nconst IGNORE_MARKER = /prefer-shared-primitive:\\s*ignore/;\n\n/**\n * Browser source: the cockpit's client tree, a plugin's src/web tree, or an\n * extension's colocated browser code. The last arm asks the build's own\n * predicate, so the rule follows a component when it moves next to the routed\n * file that renders it and still stands down on the terminal half of the same\n * `(frontend)` side, where a raw element is a pi-tui element and not a DOM one.\n */\nfunction isBrowserSource(relativePath: string): boolean {\n return relativePath.startsWith('src/web/') || isBrowserFile(relativePath);\n}\n\nfunction isTest(relativePath: string): boolean {\n return relativePath.startsWith('tests/') || relativePath.includes('.test.') || relativePath.includes('.spec.');\n}\n\nfunction tagNameOf(node: ts.JsxOpeningLikeElement): string | undefined {\n const name = node.tagName;\n return ts.isIdentifier(name) ? name.text : undefined;\n}\n\nfunction hasAttribute(node: ts.JsxOpeningLikeElement, attribute: string): boolean {\n return node.attributes.properties.some(\n (property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && property.name.text === attribute,\n );\n}\n\n/**\n * Radix lends a primitive's behaviour to whatever element it wraps, so the\n * element under an `asChild` parent is that primitive's rendering, not a\n * bypass of it.\n */\nfunction underAsChild(node: ts.JsxOpeningLikeElement): boolean {\n // An opening tag's parent is its own JsxElement; step past it so the search\n // starts at the element that encloses this one.\n const start = ts.isJsxOpeningElement(node) ? node.parent.parent : node.parent;\n for (let parent: ts.Node | undefined = start; parent !== undefined; parent = parent.parent) {\n if (ts.isJsxElement(parent)) return hasAttribute(parent.openingElement, 'asChild');\n // Only the immediately enclosing element lends its slot; a fragment or an\n // expression container between the two is transparent, anything else is not.\n if (!ts.isJsxFragment(parent) && !ts.isJsxExpression(parent)) return false;\n }\n return false;\n}\n\nexport const preferSharedPrimitive: RuleDefinition = {\n preflight: true,\n rule: 'Browser code reaches for the shared primitive rather than restyling a raw element',\n rationale:\n 'The component library exists so a button looks and behaves the same everywhere: one focus ring, one disabled state, one set of tokens. A raw <button className=\"…\"> renders something that only resembles a Button, and the resemblance decays. It is also invisible in review, which is how the same chip came to be spelled ten different ways across the packages. Radix asChild is the escape hatch when a primitive must lend its styling to another element, and the rule stands down there.',\n check(filePath, configRoot) {\n const relativePath = projectPath(filePath, configRoot);\n if (relativePath === null || !isBrowserSource(relativePath) || isTest(relativePath)) return null;\n // The library is where the primitives are built, so it owns the raw elements.\n if (isComponentLibrary(configRoot)) return null;\n const sourceFile = readSource(filePath);\n if (!sourceFile) return null;\n if (IGNORE_MARKER.test(sourceFile.getFullText())) return null;\n\n const offenders = new Map<string, string>();\n const visit = (node: ts.Node): void => {\n if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {\n const tag = tagNameOf(node);\n const replacement = tag === undefined ? undefined : OWNED_ELEMENTS[tag];\n if (tag !== undefined && replacement !== undefined && !underAsChild(node)) {\n offenders.set(tag, replacement);\n }\n // A hand-rolled listbox row: it claims the role without the shape.\n if (\n tag !== undefined &&\n !OWNED_ELEMENTS[tag] &&\n node.attributes.properties.some(\n (property) =>\n ts.isJsxAttribute(property) &&\n ts.isIdentifier(property.name) &&\n property.name.text === 'role' &&\n property.initializer !== undefined &&\n ts.isStringLiteral(property.initializer) &&\n property.initializer.text === 'option',\n )\n ) {\n offenders.set(`${tag} role=\"option\"`, OPTION_HINT);\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(sourceFile);\n if (offenders.size === 0) return null;\n\n const listed = [...offenders].map(([tag, replacement]) => `<${tag}> (use ${replacement})`).join(', ');\n return `${relativePath} renders ${listed} directly. Import the primitive from @agimon-ai/doompi-web-components instead, or wrap the element in the primitive with asChild when it must stay this element. A row that genuinely has no primitive can opt out with a \"prefer-shared-primitive: ignore\" comment naming why.`;\n },\n};\n"],"mappings":"0NAQA,MAAM,EAAmD,CACvD,OAAQ,SACR,MAAO,QACP,SAAU,WACV,OAAQ,QACV,EAKM,EAAgB,oCAStB,SAAS,EAAgB,EAA+B,CACtD,OAAO,EAAa,WAAW,UAAU,IAAA,EAAKA,EAAAA,cAAAA,CAAc,CAAY,CAC1E,CAEA,SAAS,EAAO,EAA+B,CAC7C,OAAO,EAAa,WAAW,QAAQ,GAAK,EAAa,SAAS,QAAQ,GAAK,EAAa,SAAS,QAAQ,CAC/G,CAEA,SAAS,EAAU,EAAoD,CACrE,IAAM,EAAO,EAAK,QAClB,OAAOC,EAAAA,QAAG,aAAa,CAAI,EAAI,EAAK,KAAO,IAAA,EAC7C,CAEA,SAAS,EAAa,EAAgC,EAA4B,CAChF,OAAO,EAAK,WAAW,WAAW,KAC/B,GAAaA,EAAAA,QAAG,eAAe,CAAQ,GAAKA,EAAAA,QAAG,aAAa,EAAS,IAAI,GAAK,EAAS,KAAK,OAAS,CACxG,CACF,CAOA,SAAS,EAAa,EAAyC,CAG7D,IAAM,EAAQA,EAAAA,QAAG,oBAAoB,CAAI,EAAI,EAAK,OAAO,OAAS,EAAK,OACvE,IAAK,IAAI,EAA8B,EAAO,IAAW,IAAA,GAAW,EAAS,EAAO,OAAQ,CAC1F,GAAIA,EAAAA,QAAG,aAAa,CAAM,EAAG,OAAO,EAAa,EAAO,eAAgB,SAAS,EAGjF,GAAI,CAACA,EAAAA,QAAG,cAAc,CAAM,GAAK,CAACA,EAAAA,QAAG,gBAAgB,CAAM,EAAG,MAAO,EACvE,CACA,MAAO,EACT,CAEA,MAAa,EAAwC,CACnD,UAAW,GACX,KAAM,oFACN,UACE,qeACF,MAAM,EAAU,EAAY,CAC1B,IAAM,EAAeC,EAAAA,YAAY,EAAU,CAAU,EAGrD,GAFI,IAAiB,MAAQ,CAAC,EAAgB,CAAY,GAAK,EAAO,CAAY,GAE9EC,EAAAA,mBAAmB,CAAU,EAAG,OAAO,KAC3C,IAAM,EAAaC,EAAAA,WAAW,CAAQ,EAEtC,GADI,CAAC,GACD,EAAc,KAAK,EAAW,YAAY,CAAC,EAAG,OAAO,KAEzD,IAAM,kBAAY,IAAI,IAChB,EAAS,GAAwB,CACrC,GAAIH,EAAAA,QAAG,oBAAoB,CAAI,GAAKA,EAAAA,QAAG,wBAAwB,CAAI,EAAG,CACpE,IAAM,EAAM,EAAU,CAAI,EACpB,EAAc,IAAQ,IAAA,GAAY,IAAA,GAAY,EAAe,GAC/D,IAAQ,IAAA,IAAa,IAAgB,IAAA,IAAa,CAAC,EAAa,CAAI,GACtE,EAAU,IAAI,EAAK,CAAW,EAI9B,IAAQ,IAAA,IACR,CAAC,EAAe,IAChB,EAAK,WAAW,WAAW,KACxB,GACCA,EAAAA,QAAG,eAAe,CAAQ,GAC1BA,EAAAA,QAAG,aAAa,EAAS,IAAI,GAC7B,EAAS,KAAK,OAAS,QACvB,EAAS,cAAgB,IAAA,IACzBA,EAAAA,QAAG,gBAAgB,EAAS,WAAW,GACvC,EAAS,YAAY,OAAS,QAClC,GAEA,EAAU,IAAI,GAAG,EAAI,gBAAiB,WAAW,CAErD,CACA,EAAA,QAAG,aAAa,EAAM,CAAK,CAC7B,EAKA,OAJA,EAAM,CAAU,EACZ,EAAU,OAAS,EAAU,KAG1B,GAAG,EAAa,WADR,CAAC,GAAG,CAAS,CAAC,CAAC,KAAK,CAAC,EAAK,KAAiB,IAAI,EAAI,SAAS,EAAY,EAAE,CAAC,CAAC,KAAK,IACzD,EAAE,gRAC3C,CACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{projectPath as e,readSource as t}from"./moduleGraph.mjs";import{isComponentLibrary as n}from"./componentLibrary.mjs";import r from"typescript";import{isBrowserFile as i}from"@agimon-ai/doompi-build/
|
|
1
|
+
import{projectPath as e,readSource as t}from"./moduleGraph.mjs";import{isComponentLibrary as n}from"./componentLibrary.mjs";import r from"typescript";import{isBrowserFile as i}from"@agimon-ai/doompi-build/browserFile";const a={button:`Button`,input:`Input`,textarea:`Textarea`,select:`Select`},o=/prefer-shared-primitive:\s*ignore/;function s(e){return e.startsWith(`src/web/`)||i(e)}function c(e){return e.startsWith(`tests/`)||e.includes(`.test.`)||e.includes(`.spec.`)}function l(e){let t=e.tagName;return r.isIdentifier(t)?t.text:void 0}function u(e,t){return e.attributes.properties.some(e=>r.isJsxAttribute(e)&&r.isIdentifier(e.name)&&e.name.text===t)}function d(e){let t=r.isJsxOpeningElement(e)?e.parent.parent:e.parent;for(let e=t;e!==void 0;e=e.parent){if(r.isJsxElement(e))return u(e.openingElement,`asChild`);if(!r.isJsxFragment(e)&&!r.isJsxExpression(e))return!1}return!1}const f={preflight:!0,rule:`Browser code reaches for the shared primitive rather than restyling a raw element`,rationale:`The component library exists so a button looks and behaves the same everywhere: one focus ring, one disabled state, one set of tokens. A raw <button className="…"> renders something that only resembles a Button, and the resemblance decays. It is also invisible in review, which is how the same chip came to be spelled ten different ways across the packages. Radix asChild is the escape hatch when a primitive must lend its styling to another element, and the rule stands down there.`,check(i,u){let f=e(i,u);if(f===null||!s(f)||c(f)||n(u))return null;let p=t(i);if(!p||o.test(p.getFullText()))return null;let m=/* @__PURE__ */ new Map,h=e=>{if(r.isJsxOpeningElement(e)||r.isJsxSelfClosingElement(e)){let t=l(e),n=t===void 0?void 0:a[t];t!==void 0&&n!==void 0&&!d(e)&&m.set(t,n),t!==void 0&&!a[t]&&e.attributes.properties.some(e=>r.isJsxAttribute(e)&&r.isIdentifier(e.name)&&e.name.text===`role`&&e.initializer!==void 0&&r.isStringLiteral(e.initializer)&&e.initializer.text===`option`)&&m.set(`${t} role="option"`,`OptionRow`)}r.forEachChild(e,h)};return h(p),m.size===0?null:`${f} renders ${[...m].map(([e,t])=>`<${e}> (use ${t})`).join(`, `)} directly. Import the primitive from @agimon-ai/doompi-web-components instead, or wrap the element in the primitive with asChild when it must stay this element. A row that genuinely has no primitive can opt out with a "prefer-shared-primitive: ignore" comment naming why.`}};export{f as preferSharedPrimitive};
|
|
2
2
|
//# sourceMappingURL=primitives.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"primitives.mjs","names":[],"sources":["../../src/rules/primitives.ts"],"sourcesContent":["import { isBrowserFile } from '@agimon-ai/doompi-build/
|
|
1
|
+
{"version":3,"file":"primitives.mjs","names":[],"sources":["../../src/rules/primitives.ts"],"sourcesContent":["import { isBrowserFile } from '@agimon-ai/doompi-build/browserFile';\nimport type { RuleDefinition } from '@agimon-ai/vibe-lint';\nimport ts from 'typescript';\n\nimport { isComponentLibrary } from './componentLibrary.js';\nimport { projectPath, readSource } from './moduleGraph.js';\n\n/** The interactive elements the shared library already owns, and what to reach for instead. */\nconst OWNED_ELEMENTS: Readonly<Record<string, string>> = {\n button: 'Button',\n input: 'Input',\n textarea: 'Textarea',\n select: 'Select',\n};\n\n/** A row of a listbox is an option, and OptionRow is the primitive for it. */\nconst OPTION_HINT = 'OptionRow';\n\nconst IGNORE_MARKER = /prefer-shared-primitive:\\s*ignore/;\n\n/**\n * Browser source: the cockpit's client tree, a plugin's src/web tree, or an\n * extension's colocated browser code. The last arm asks the build's own\n * predicate, so the rule follows a component when it moves next to the routed\n * file that renders it and still stands down on the terminal half of the same\n * `(frontend)` side, where a raw element is a pi-tui element and not a DOM one.\n */\nfunction isBrowserSource(relativePath: string): boolean {\n return relativePath.startsWith('src/web/') || isBrowserFile(relativePath);\n}\n\nfunction isTest(relativePath: string): boolean {\n return relativePath.startsWith('tests/') || relativePath.includes('.test.') || relativePath.includes('.spec.');\n}\n\nfunction tagNameOf(node: ts.JsxOpeningLikeElement): string | undefined {\n const name = node.tagName;\n return ts.isIdentifier(name) ? name.text : undefined;\n}\n\nfunction hasAttribute(node: ts.JsxOpeningLikeElement, attribute: string): boolean {\n return node.attributes.properties.some(\n (property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && property.name.text === attribute,\n );\n}\n\n/**\n * Radix lends a primitive's behaviour to whatever element it wraps, so the\n * element under an `asChild` parent is that primitive's rendering, not a\n * bypass of it.\n */\nfunction underAsChild(node: ts.JsxOpeningLikeElement): boolean {\n // An opening tag's parent is its own JsxElement; step past it so the search\n // starts at the element that encloses this one.\n const start = ts.isJsxOpeningElement(node) ? node.parent.parent : node.parent;\n for (let parent: ts.Node | undefined = start; parent !== undefined; parent = parent.parent) {\n if (ts.isJsxElement(parent)) return hasAttribute(parent.openingElement, 'asChild');\n // Only the immediately enclosing element lends its slot; a fragment or an\n // expression container between the two is transparent, anything else is not.\n if (!ts.isJsxFragment(parent) && !ts.isJsxExpression(parent)) return false;\n }\n return false;\n}\n\nexport const preferSharedPrimitive: RuleDefinition = {\n preflight: true,\n rule: 'Browser code reaches for the shared primitive rather than restyling a raw element',\n rationale:\n 'The component library exists so a button looks and behaves the same everywhere: one focus ring, one disabled state, one set of tokens. A raw <button className=\"…\"> renders something that only resembles a Button, and the resemblance decays. It is also invisible in review, which is how the same chip came to be spelled ten different ways across the packages. Radix asChild is the escape hatch when a primitive must lend its styling to another element, and the rule stands down there.',\n check(filePath, configRoot) {\n const relativePath = projectPath(filePath, configRoot);\n if (relativePath === null || !isBrowserSource(relativePath) || isTest(relativePath)) return null;\n // The library is where the primitives are built, so it owns the raw elements.\n if (isComponentLibrary(configRoot)) return null;\n const sourceFile = readSource(filePath);\n if (!sourceFile) return null;\n if (IGNORE_MARKER.test(sourceFile.getFullText())) return null;\n\n const offenders = new Map<string, string>();\n const visit = (node: ts.Node): void => {\n if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {\n const tag = tagNameOf(node);\n const replacement = tag === undefined ? undefined : OWNED_ELEMENTS[tag];\n if (tag !== undefined && replacement !== undefined && !underAsChild(node)) {\n offenders.set(tag, replacement);\n }\n // A hand-rolled listbox row: it claims the role without the shape.\n if (\n tag !== undefined &&\n !OWNED_ELEMENTS[tag] &&\n node.attributes.properties.some(\n (property) =>\n ts.isJsxAttribute(property) &&\n ts.isIdentifier(property.name) &&\n property.name.text === 'role' &&\n property.initializer !== undefined &&\n ts.isStringLiteral(property.initializer) &&\n property.initializer.text === 'option',\n )\n ) {\n offenders.set(`${tag} role=\"option\"`, OPTION_HINT);\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(sourceFile);\n if (offenders.size === 0) return null;\n\n const listed = [...offenders].map(([tag, replacement]) => `<${tag}> (use ${replacement})`).join(', ');\n return `${relativePath} renders ${listed} directly. Import the primitive from @agimon-ai/doompi-web-components instead, or wrap the element in the primitive with asChild when it must stay this element. A row that genuinely has no primitive can opt out with a \"prefer-shared-primitive: ignore\" comment naming why.`;\n },\n};\n"],"mappings":"0NAQA,MAAM,EAAmD,CACvD,OAAQ,SACR,MAAO,QACP,SAAU,WACV,OAAQ,QACV,EAKM,EAAgB,oCAStB,SAAS,EAAgB,EAA+B,CACtD,OAAO,EAAa,WAAW,UAAU,GAAK,EAAc,CAAY,CAC1E,CAEA,SAAS,EAAO,EAA+B,CAC7C,OAAO,EAAa,WAAW,QAAQ,GAAK,EAAa,SAAS,QAAQ,GAAK,EAAa,SAAS,QAAQ,CAC/G,CAEA,SAAS,EAAU,EAAoD,CACrE,IAAM,EAAO,EAAK,QAClB,OAAO,EAAG,aAAa,CAAI,EAAI,EAAK,KAAO,IAAA,EAC7C,CAEA,SAAS,EAAa,EAAgC,EAA4B,CAChF,OAAO,EAAK,WAAW,WAAW,KAC/B,GAAa,EAAG,eAAe,CAAQ,GAAK,EAAG,aAAa,EAAS,IAAI,GAAK,EAAS,KAAK,OAAS,CACxG,CACF,CAOA,SAAS,EAAa,EAAyC,CAG7D,IAAM,EAAQ,EAAG,oBAAoB,CAAI,EAAI,EAAK,OAAO,OAAS,EAAK,OACvE,IAAK,IAAI,EAA8B,EAAO,IAAW,IAAA,GAAW,EAAS,EAAO,OAAQ,CAC1F,GAAI,EAAG,aAAa,CAAM,EAAG,OAAO,EAAa,EAAO,eAAgB,SAAS,EAGjF,GAAI,CAAC,EAAG,cAAc,CAAM,GAAK,CAAC,EAAG,gBAAgB,CAAM,EAAG,MAAO,EACvE,CACA,MAAO,EACT,CAEA,MAAa,EAAwC,CACnD,UAAW,GACX,KAAM,oFACN,UACE,qeACF,MAAM,EAAU,EAAY,CAC1B,IAAM,EAAe,EAAY,EAAU,CAAU,EAGrD,GAFI,IAAiB,MAAQ,CAAC,EAAgB,CAAY,GAAK,EAAO,CAAY,GAE9E,EAAmB,CAAU,EAAG,OAAO,KAC3C,IAAM,EAAa,EAAW,CAAQ,EAEtC,GADI,CAAC,GACD,EAAc,KAAK,EAAW,YAAY,CAAC,EAAG,OAAO,KAEzD,IAAM,kBAAY,IAAI,IAChB,EAAS,GAAwB,CACrC,GAAI,EAAG,oBAAoB,CAAI,GAAK,EAAG,wBAAwB,CAAI,EAAG,CACpE,IAAM,EAAM,EAAU,CAAI,EACpB,EAAc,IAAQ,IAAA,GAAY,IAAA,GAAY,EAAe,GAC/D,IAAQ,IAAA,IAAa,IAAgB,IAAA,IAAa,CAAC,EAAa,CAAI,GACtE,EAAU,IAAI,EAAK,CAAW,EAI9B,IAAQ,IAAA,IACR,CAAC,EAAe,IAChB,EAAK,WAAW,WAAW,KACxB,GACC,EAAG,eAAe,CAAQ,GAC1B,EAAG,aAAa,EAAS,IAAI,GAC7B,EAAS,KAAK,OAAS,QACvB,EAAS,cAAgB,IAAA,IACzB,EAAG,gBAAgB,EAAS,WAAW,GACvC,EAAS,YAAY,OAAS,QAClC,GAEA,EAAU,IAAI,GAAG,EAAI,gBAAiB,WAAW,CAErD,CACA,EAAG,aAAa,EAAM,CAAK,CAC7B,EAKA,OAJA,EAAM,CAAU,EACZ,EAAU,OAAS,EAAU,KAG1B,GAAG,EAAa,WADR,CAAC,GAAG,CAAS,CAAC,CAAC,KAAK,CAAC,EAAK,KAAiB,IAAI,EAAI,SAAS,EAAY,EAAE,CAAC,CAAC,KAAK,IACzD,EAAE,gRAC3C,CACF"}
|
package/dist/rules/theming.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./moduleGraph.cjs");let n=require("typescript");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi-build/
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("./moduleGraph.cjs");let n=require("typescript");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi-build/browserFile");const i=/-\[#[0-9a-f]{3,8}\]/i,a=/^\s*(?:#[0-9a-f]{3,8}|(?:rgb|rgba|hsl|hsla)\([^)]*\))\s*$/i;function o(e){return e.startsWith(`src/theme/`)||e.startsWith(`themes/`)||e.startsWith(`tests/`)||e.includes(`.test.`)||e.includes(`.spec.`)}function s(e){return e.startsWith(`src/web/`)||e.startsWith(`src/components/`)||e.startsWith(`src/lib/`)?!0:(0,r.isBrowserFile)(e)}const c={preflight:!0,rule:`Browser code names a theme token, never a colour literal`,rationale:`A theme is only a theme if every colour on the page comes from it. One literal hex is invisible in review and then permanent: it survives a theme switch, so a dark-tuned tint sits inside a light surface and the reader assumes the theme is broken rather than that one card opted out. Naming a token also names the intent, and a reviewer can tell a warning tint from a brand blue without opening a colour picker.`,check(e,r){let c=t.projectPath(e,r);if(c===null||o(c)||!s(c))return null;let l=t.readSource(e);if(!l)return null;let u=/* @__PURE__ */ new Set,d=e=>{if(n.default.isStringLiteralLike(e)){let t=e.text;(i.test(t)||a.test(t))&&u.add(t.trim().slice(0,60))}n.default.forEachChild(e,d)};return d(l),u.size===0?null:`${c} hardcodes ${[...u].map(e=>`'${e}'`).join(`, `)}. Browser code names a theme token instead: a Tailwind class such as bg-doom-panel, text-doom-hi, border-doom-edge-red, or bg-doom-tint-yellow, or the CSS custom property var(--doom-blue). If the palette has no token for what you mean, add one to the theme contract rather than spelling the colour here.`}},l=/(?<![\w-])(text|rounded|tracking|leading)(?:-(?:[trbl]{1,2}|[a-z]+))?-\[([^\]]+)\]/g,u=/* @__PURE__ */ new Set([`inherit`]),d={text:`text-2xs (9px), text-xs (10px), text-sm (12px), text-base (13px), text-lg (15px)`,rounded:`rounded-xs (2px), rounded-sm (3px), rounded-md (5px), rounded-lg (10px), rounded-full`,tracking:`tracking-wide (0.08em), tracking-wider (0.14em), tracking-widest (0.18em)`,leading:`leading-none, leading-tight, leading-snug, leading-normal, leading-relaxed`},f={preflight:!0,rule:`Browser code sizes type, radius, tracking and leading from the scale, never an arbitrary value`,rationale:"A scale is only a scale if nothing steps outside it. One `text-[11px]` looks harmless, but it is a rung nobody else can land on: the next component copies it, and soon the same visual weight exists at 10px, 11px and 12px with no way to tell which was deliberate. Arbitrary values also survive a token change silently, so retuning the ramp moves every honest call site and leaves the opt-outs behind, drifting further each time. Naming a rung states the intent, and a reviewer can see that two things match without measuring them.",check(e,r){let i=t.projectPath(e,r);if(i===null||o(i)||!s(i))return null;let c=t.readSource(e);if(!c)return null;let f=/* @__PURE__ */ new Set,p=/* @__PURE__ */ new Set,m=e=>{if(n.default.isStringLiteralLike(e)||n.default.isTemplateHead(e)||n.default.isTemplateMiddle(e)||n.default.isTemplateTail(e))for(let t of e.text.matchAll(l)){let[e,n,r]=[t[0],t[1]??``,t[2]??``];u.has(r)||a.test(r)||(f.add(e.slice(0,60)),p.add(n))}n.default.forEachChild(e,m)};if(m(c),f.size===0)return null;let h=[...p].map(e=>`${e}: ${d[e]}`).join(`; `);return`${i} hardcodes ${[...f].map(e=>`'${e}'`).join(`, `)}. Browser code names a rung on the scale instead (${h}). If the ramp has no rung for what you mean, add one to styles/tokens.css so every caller can reach it, rather than spelling the value here.`}};exports.noArbitraryStyleValue=f,exports.noRawThemeColor=c;
|
|
2
2
|
//# sourceMappingURL=theming.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theming.cjs","names":["isBrowserFile","projectPath","readSource","ts"],"sources":["../../src/rules/theming.ts"],"sourcesContent":["import { isBrowserFile } from '@agimon-ai/doompi-build/
|
|
1
|
+
{"version":3,"file":"theming.cjs","names":["isBrowserFile","projectPath","readSource","ts"],"sources":["../../src/rules/theming.ts"],"sourcesContent":["import { isBrowserFile } from '@agimon-ai/doompi-build/browserFile';\nimport type { RuleDefinition } from '@agimon-ai/vibe-lint';\nimport ts from 'typescript';\n\nimport { projectPath, readSource } from './moduleGraph.js';\n\n/** A Tailwind arbitrary colour: the `bg-[#312A1C]` shape, in any utility. */\nconst ARBITRARY_COLOR = /-\\[#[0-9a-f]{3,8}\\]/i;\n/** A string that is nothing but a colour: '#282c34', 'rgb(1 2 3)', 'hsl(...)'. */\nconst BARE_COLOR = /^\\s*(?:#[0-9a-f]{3,8}|(?:rgb|rgba|hsl|hsla)\\([^)]*\\))\\s*$/i;\n\n/**\n * Where colours are the subject rather than a choice: the theme configs\n * themselves, the modules that parse and derive them, and the tests that\n * assert on them.\n */\nfunction declaresColors(relativePath: string): boolean {\n return (\n relativePath.startsWith('src/theme/') ||\n relativePath.startsWith('themes/') ||\n relativePath.startsWith('tests/') ||\n relativePath.includes('.test.') ||\n relativePath.includes('.spec.')\n );\n}\n\n/**\n * Browser source: the cockpit's client tree, a plugin's src/web tree, a\n * component library's src, or an extension's colocated browser code. The last\n * arm asks the build's own predicate, so the rule follows a component when it\n * moves next to the routed file that renders it and still stands down on the\n * terminal half of the same `(frontend)` side.\n */\nfunction isBrowserSource(relativePath: string): boolean {\n if (relativePath.startsWith('src/web/')) return true;\n if (relativePath.startsWith('src/components/') || relativePath.startsWith('src/lib/')) return true;\n return isBrowserFile(relativePath);\n}\n\nexport const noRawThemeColor: RuleDefinition = {\n preflight: true,\n rule: 'Browser code names a theme token, never a colour literal',\n rationale:\n 'A theme is only a theme if every colour on the page comes from it. One literal hex is invisible in review and then permanent: it survives a theme switch, so a dark-tuned tint sits inside a light surface and the reader assumes the theme is broken rather than that one card opted out. Naming a token also names the intent, and a reviewer can tell a warning tint from a brand blue without opening a colour picker.',\n check(filePath, configRoot) {\n const relativePath = projectPath(filePath, configRoot);\n if (relativePath === null || declaresColors(relativePath) || !isBrowserSource(relativePath)) return null;\n const sourceFile = readSource(filePath);\n if (!sourceFile) return null;\n\n const offenders = new Set<string>();\n const visit = (node: ts.Node): void => {\n if (ts.isStringLiteralLike(node)) {\n const text = node.text;\n if (ARBITRARY_COLOR.test(text) || BARE_COLOR.test(text)) offenders.add(text.trim().slice(0, 60));\n }\n ts.forEachChild(node, visit);\n };\n visit(sourceFile);\n if (offenders.size === 0) return null;\n\n return `${relativePath} hardcodes ${[...offenders].map((value) => `'${value}'`).join(', ')}. Browser code names a theme token instead: a Tailwind class such as bg-doom-panel, text-doom-hi, border-doom-edge-red, or bg-doom-tint-yellow, or the CSS custom property var(--doom-blue). If the palette has no token for what you mean, add one to the theme contract rather than spelling the colour here.`;\n },\n};\n\n/**\n * A Tailwind arbitrary value on one of the four scales the theme owns:\n * `text-[11px]`, `rounded-[3px]`, `tracking-[0.14em]`, `leading-[17px]`, and\n * the side-specific radius forms such as `rounded-tl-[3px]`.\n */\nconst ARBITRARY_SCALE = /(?<![\\w-])(text|rounded|tracking|leading)(?:-(?:[trbl]{1,2}|[a-z]+))?-\\[([^\\]]+)\\]/g;\n\n/**\n * `rounded-[inherit]` is not a size, it is a child agreeing to whatever radius\n * its clipping parent already chose, and no token can express that.\n */\nconst ALLOWED_ARBITRARY = new Set(['inherit']);\n\n/** The token each scale expects, quoted back at whoever tripped the rule. */\nconst SCALE_TOKENS: Record<string, string> = {\n text: 'text-2xs (9px), text-xs (10px), text-sm (12px), text-base (13px), text-lg (15px)',\n rounded: 'rounded-xs (2px), rounded-sm (3px), rounded-md (5px), rounded-lg (10px), rounded-full',\n tracking: 'tracking-wide (0.08em), tracking-wider (0.14em), tracking-widest (0.18em)',\n leading: 'leading-none, leading-tight, leading-snug, leading-normal, leading-relaxed',\n};\n\nexport const noArbitraryStyleValue: RuleDefinition = {\n preflight: true,\n rule: 'Browser code sizes type, radius, tracking and leading from the scale, never an arbitrary value',\n rationale:\n 'A scale is only a scale if nothing steps outside it. One `text-[11px]` looks harmless, but it is a rung nobody else can land on: the next component copies it, and soon the same visual weight exists at 10px, 11px and 12px with no way to tell which was deliberate. Arbitrary values also survive a token change silently, so retuning the ramp moves every honest call site and leaves the opt-outs behind, drifting further each time. Naming a rung states the intent, and a reviewer can see that two things match without measuring them.',\n check(filePath, configRoot) {\n const relativePath = projectPath(filePath, configRoot);\n if (relativePath === null || declaresColors(relativePath) || !isBrowserSource(relativePath)) return null;\n const sourceFile = readSource(filePath);\n if (!sourceFile) return null;\n\n const offenders = new Set<string>();\n const scales = new Set<string>();\n const visit = (node: ts.Node): void => {\n // Template spans matter: many className values are built with `${}`.\n if (\n ts.isStringLiteralLike(node) ||\n ts.isTemplateHead(node) ||\n ts.isTemplateMiddle(node) ||\n ts.isTemplateTail(node)\n ) {\n for (const match of node.text.matchAll(ARBITRARY_SCALE)) {\n const [utility, scale, value] = [match[0], match[1] ?? '', match[2] ?? ''];\n // A colour written into a text utility is the other rule's business.\n if (ALLOWED_ARBITRARY.has(value) || BARE_COLOR.test(value)) continue;\n offenders.add(utility.slice(0, 60));\n scales.add(scale);\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(sourceFile);\n if (offenders.size === 0) return null;\n\n const expected = [...scales].map((scale) => `${scale}: ${SCALE_TOKENS[scale]}`).join('; ');\n return `${relativePath} hardcodes ${[...offenders].map((value) => `'${value}'`).join(', ')}. Browser code names a rung on the scale instead (${expected}). If the ramp has no rung for what you mean, add one to styles/tokens.css so every caller can reach it, rather than spelling the value here.`;\n },\n};\n"],"mappings":"sLAOA,MAAM,EAAkB,uBAElB,EAAa,6DAOnB,SAAS,EAAe,EAA+B,CACrD,OACE,EAAa,WAAW,YAAY,GACpC,EAAa,WAAW,SAAS,GACjC,EAAa,WAAW,QAAQ,GAChC,EAAa,SAAS,QAAQ,GAC9B,EAAa,SAAS,QAAQ,CAElC,CASA,SAAS,EAAgB,EAA+B,CAGtD,OAFI,EAAa,WAAW,UAAU,GAClC,EAAa,WAAW,iBAAiB,GAAK,EAAa,WAAW,UAAU,EAAU,IAC9F,EAAOA,EAAAA,cAAAA,CAAc,CAAY,CACnC,CAEA,MAAa,EAAkC,CAC7C,UAAW,GACX,KAAM,2DACN,UACE,6ZACF,MAAM,EAAU,EAAY,CAC1B,IAAM,EAAeC,EAAAA,YAAY,EAAU,CAAU,EACrD,GAAI,IAAiB,MAAQ,EAAe,CAAY,GAAK,CAAC,EAAgB,CAAY,EAAG,OAAO,KACpG,IAAM,EAAaC,EAAAA,WAAW,CAAQ,EACtC,GAAI,CAAC,EAAY,OAAO,KAExB,IAAM,kBAAY,IAAI,IAChB,EAAS,GAAwB,CACrC,GAAIC,EAAAA,QAAG,oBAAoB,CAAI,EAAG,CAChC,IAAM,EAAO,EAAK,MACd,EAAgB,KAAK,CAAI,GAAK,EAAW,KAAK,CAAI,IAAG,EAAU,IAAI,EAAK,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,CAAC,CACjG,CACA,EAAA,QAAG,aAAa,EAAM,CAAK,CAC7B,EAIA,OAHA,EAAM,CAAU,EACZ,EAAU,OAAS,EAAU,KAE1B,GAAG,EAAa,aAAa,CAAC,GAAG,CAAS,CAAC,CAAC,IAAK,GAAU,IAAI,EAAM,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,gTAC7F,CACF,EAOM,EAAkB,sFAMlB,kBAAoB,IAAI,IAAI,CAAC,SAAS,CAAC,EAGvC,EAAuC,CAC3C,KAAM,mFACN,QAAS,wFACT,SAAU,4EACV,QAAS,4EACX,EAEa,EAAwC,CACnD,UAAW,GACX,KAAM,iGACN,UACE,ohBACF,MAAM,EAAU,EAAY,CAC1B,IAAM,EAAeF,EAAAA,YAAY,EAAU,CAAU,EACrD,GAAI,IAAiB,MAAQ,EAAe,CAAY,GAAK,CAAC,EAAgB,CAAY,EAAG,OAAO,KACpG,IAAM,EAAaC,EAAAA,WAAW,CAAQ,EACtC,GAAI,CAAC,EAAY,OAAO,KAExB,IAAM,kBAAY,IAAI,IAChB,kBAAS,IAAI,IACb,EAAS,GAAwB,CAErC,GACEC,EAAAA,QAAG,oBAAoB,CAAI,GAC3BA,EAAAA,QAAG,eAAe,CAAI,GACtBA,EAAAA,QAAG,iBAAiB,CAAI,GACxBA,EAAAA,QAAG,eAAe,CAAI,EAEtB,IAAK,IAAM,KAAS,EAAK,KAAK,SAAS,CAAe,EAAG,CACvD,GAAM,CAAC,EAAS,EAAO,GAAS,CAAC,EAAM,GAAI,EAAM,IAAM,GAAI,EAAM,IAAM,EAAE,EAErE,EAAkB,IAAI,CAAK,GAAK,EAAW,KAAK,CAAK,IACzD,EAAU,IAAI,EAAQ,MAAM,EAAG,EAAE,CAAC,EAClC,EAAO,IAAI,CAAK,EAClB,CAEF,EAAA,QAAG,aAAa,EAAM,CAAK,CAC7B,EAEA,GADA,EAAM,CAAU,EACZ,EAAU,OAAS,EAAG,OAAO,KAEjC,IAAM,EAAW,CAAC,GAAG,CAAM,CAAC,CAAC,IAAK,GAAU,GAAG,EAAM,IAAI,EAAa,IAAQ,CAAC,CAAC,KAAK,IAAI,EACzF,MAAO,GAAG,EAAa,aAAa,CAAC,GAAG,CAAS,CAAC,CAAC,IAAK,GAAU,IAAI,EAAM,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,oDAAoD,EAAS,8IAC1J,CACF"}
|
package/dist/rules/theming.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{projectPath as e,readSource as t}from"./moduleGraph.mjs";import n from"typescript";import{isBrowserFile as r}from"@agimon-ai/doompi-build/
|
|
1
|
+
import{projectPath as e,readSource as t}from"./moduleGraph.mjs";import n from"typescript";import{isBrowserFile as r}from"@agimon-ai/doompi-build/browserFile";const i=/-\[#[0-9a-f]{3,8}\]/i,a=/^\s*(?:#[0-9a-f]{3,8}|(?:rgb|rgba|hsl|hsla)\([^)]*\))\s*$/i;function o(e){return e.startsWith(`src/theme/`)||e.startsWith(`themes/`)||e.startsWith(`tests/`)||e.includes(`.test.`)||e.includes(`.spec.`)}function s(e){return e.startsWith(`src/web/`)||e.startsWith(`src/components/`)||e.startsWith(`src/lib/`)?!0:r(e)}const c={preflight:!0,rule:`Browser code names a theme token, never a colour literal`,rationale:`A theme is only a theme if every colour on the page comes from it. One literal hex is invisible in review and then permanent: it survives a theme switch, so a dark-tuned tint sits inside a light surface and the reader assumes the theme is broken rather than that one card opted out. Naming a token also names the intent, and a reviewer can tell a warning tint from a brand blue without opening a colour picker.`,check(r,c){let l=e(r,c);if(l===null||o(l)||!s(l))return null;let u=t(r);if(!u)return null;let d=/* @__PURE__ */ new Set,f=e=>{if(n.isStringLiteralLike(e)){let t=e.text;(i.test(t)||a.test(t))&&d.add(t.trim().slice(0,60))}n.forEachChild(e,f)};return f(u),d.size===0?null:`${l} hardcodes ${[...d].map(e=>`'${e}'`).join(`, `)}. Browser code names a theme token instead: a Tailwind class such as bg-doom-panel, text-doom-hi, border-doom-edge-red, or bg-doom-tint-yellow, or the CSS custom property var(--doom-blue). If the palette has no token for what you mean, add one to the theme contract rather than spelling the colour here.`}},l=/(?<![\w-])(text|rounded|tracking|leading)(?:-(?:[trbl]{1,2}|[a-z]+))?-\[([^\]]+)\]/g,u=/* @__PURE__ */ new Set([`inherit`]),d={text:`text-2xs (9px), text-xs (10px), text-sm (12px), text-base (13px), text-lg (15px)`,rounded:`rounded-xs (2px), rounded-sm (3px), rounded-md (5px), rounded-lg (10px), rounded-full`,tracking:`tracking-wide (0.08em), tracking-wider (0.14em), tracking-widest (0.18em)`,leading:`leading-none, leading-tight, leading-snug, leading-normal, leading-relaxed`},f={preflight:!0,rule:`Browser code sizes type, radius, tracking and leading from the scale, never an arbitrary value`,rationale:"A scale is only a scale if nothing steps outside it. One `text-[11px]` looks harmless, but it is a rung nobody else can land on: the next component copies it, and soon the same visual weight exists at 10px, 11px and 12px with no way to tell which was deliberate. Arbitrary values also survive a token change silently, so retuning the ramp moves every honest call site and leaves the opt-outs behind, drifting further each time. Naming a rung states the intent, and a reviewer can see that two things match without measuring them.",check(r,i){let c=e(r,i);if(c===null||o(c)||!s(c))return null;let f=t(r);if(!f)return null;let p=/* @__PURE__ */ new Set,m=/* @__PURE__ */ new Set,h=e=>{if(n.isStringLiteralLike(e)||n.isTemplateHead(e)||n.isTemplateMiddle(e)||n.isTemplateTail(e))for(let t of e.text.matchAll(l)){let[e,n,r]=[t[0],t[1]??``,t[2]??``];u.has(r)||a.test(r)||(p.add(e.slice(0,60)),m.add(n))}n.forEachChild(e,h)};if(h(f),p.size===0)return null;let g=[...m].map(e=>`${e}: ${d[e]}`).join(`; `);return`${c} hardcodes ${[...p].map(e=>`'${e}'`).join(`, `)}. Browser code names a rung on the scale instead (${g}). If the ramp has no rung for what you mean, add one to styles/tokens.css so every caller can reach it, rather than spelling the value here.`}};export{f as noArbitraryStyleValue,c as noRawThemeColor};
|
|
2
2
|
//# sourceMappingURL=theming.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theming.mjs","names":[],"sources":["../../src/rules/theming.ts"],"sourcesContent":["import { isBrowserFile } from '@agimon-ai/doompi-build/
|
|
1
|
+
{"version":3,"file":"theming.mjs","names":[],"sources":["../../src/rules/theming.ts"],"sourcesContent":["import { isBrowserFile } from '@agimon-ai/doompi-build/browserFile';\nimport type { RuleDefinition } from '@agimon-ai/vibe-lint';\nimport ts from 'typescript';\n\nimport { projectPath, readSource } from './moduleGraph.js';\n\n/** A Tailwind arbitrary colour: the `bg-[#312A1C]` shape, in any utility. */\nconst ARBITRARY_COLOR = /-\\[#[0-9a-f]{3,8}\\]/i;\n/** A string that is nothing but a colour: '#282c34', 'rgb(1 2 3)', 'hsl(...)'. */\nconst BARE_COLOR = /^\\s*(?:#[0-9a-f]{3,8}|(?:rgb|rgba|hsl|hsla)\\([^)]*\\))\\s*$/i;\n\n/**\n * Where colours are the subject rather than a choice: the theme configs\n * themselves, the modules that parse and derive them, and the tests that\n * assert on them.\n */\nfunction declaresColors(relativePath: string): boolean {\n return (\n relativePath.startsWith('src/theme/') ||\n relativePath.startsWith('themes/') ||\n relativePath.startsWith('tests/') ||\n relativePath.includes('.test.') ||\n relativePath.includes('.spec.')\n );\n}\n\n/**\n * Browser source: the cockpit's client tree, a plugin's src/web tree, a\n * component library's src, or an extension's colocated browser code. The last\n * arm asks the build's own predicate, so the rule follows a component when it\n * moves next to the routed file that renders it and still stands down on the\n * terminal half of the same `(frontend)` side.\n */\nfunction isBrowserSource(relativePath: string): boolean {\n if (relativePath.startsWith('src/web/')) return true;\n if (relativePath.startsWith('src/components/') || relativePath.startsWith('src/lib/')) return true;\n return isBrowserFile(relativePath);\n}\n\nexport const noRawThemeColor: RuleDefinition = {\n preflight: true,\n rule: 'Browser code names a theme token, never a colour literal',\n rationale:\n 'A theme is only a theme if every colour on the page comes from it. One literal hex is invisible in review and then permanent: it survives a theme switch, so a dark-tuned tint sits inside a light surface and the reader assumes the theme is broken rather than that one card opted out. Naming a token also names the intent, and a reviewer can tell a warning tint from a brand blue without opening a colour picker.',\n check(filePath, configRoot) {\n const relativePath = projectPath(filePath, configRoot);\n if (relativePath === null || declaresColors(relativePath) || !isBrowserSource(relativePath)) return null;\n const sourceFile = readSource(filePath);\n if (!sourceFile) return null;\n\n const offenders = new Set<string>();\n const visit = (node: ts.Node): void => {\n if (ts.isStringLiteralLike(node)) {\n const text = node.text;\n if (ARBITRARY_COLOR.test(text) || BARE_COLOR.test(text)) offenders.add(text.trim().slice(0, 60));\n }\n ts.forEachChild(node, visit);\n };\n visit(sourceFile);\n if (offenders.size === 0) return null;\n\n return `${relativePath} hardcodes ${[...offenders].map((value) => `'${value}'`).join(', ')}. Browser code names a theme token instead: a Tailwind class such as bg-doom-panel, text-doom-hi, border-doom-edge-red, or bg-doom-tint-yellow, or the CSS custom property var(--doom-blue). If the palette has no token for what you mean, add one to the theme contract rather than spelling the colour here.`;\n },\n};\n\n/**\n * A Tailwind arbitrary value on one of the four scales the theme owns:\n * `text-[11px]`, `rounded-[3px]`, `tracking-[0.14em]`, `leading-[17px]`, and\n * the side-specific radius forms such as `rounded-tl-[3px]`.\n */\nconst ARBITRARY_SCALE = /(?<![\\w-])(text|rounded|tracking|leading)(?:-(?:[trbl]{1,2}|[a-z]+))?-\\[([^\\]]+)\\]/g;\n\n/**\n * `rounded-[inherit]` is not a size, it is a child agreeing to whatever radius\n * its clipping parent already chose, and no token can express that.\n */\nconst ALLOWED_ARBITRARY = new Set(['inherit']);\n\n/** The token each scale expects, quoted back at whoever tripped the rule. */\nconst SCALE_TOKENS: Record<string, string> = {\n text: 'text-2xs (9px), text-xs (10px), text-sm (12px), text-base (13px), text-lg (15px)',\n rounded: 'rounded-xs (2px), rounded-sm (3px), rounded-md (5px), rounded-lg (10px), rounded-full',\n tracking: 'tracking-wide (0.08em), tracking-wider (0.14em), tracking-widest (0.18em)',\n leading: 'leading-none, leading-tight, leading-snug, leading-normal, leading-relaxed',\n};\n\nexport const noArbitraryStyleValue: RuleDefinition = {\n preflight: true,\n rule: 'Browser code sizes type, radius, tracking and leading from the scale, never an arbitrary value',\n rationale:\n 'A scale is only a scale if nothing steps outside it. One `text-[11px]` looks harmless, but it is a rung nobody else can land on: the next component copies it, and soon the same visual weight exists at 10px, 11px and 12px with no way to tell which was deliberate. Arbitrary values also survive a token change silently, so retuning the ramp moves every honest call site and leaves the opt-outs behind, drifting further each time. Naming a rung states the intent, and a reviewer can see that two things match without measuring them.',\n check(filePath, configRoot) {\n const relativePath = projectPath(filePath, configRoot);\n if (relativePath === null || declaresColors(relativePath) || !isBrowserSource(relativePath)) return null;\n const sourceFile = readSource(filePath);\n if (!sourceFile) return null;\n\n const offenders = new Set<string>();\n const scales = new Set<string>();\n const visit = (node: ts.Node): void => {\n // Template spans matter: many className values are built with `${}`.\n if (\n ts.isStringLiteralLike(node) ||\n ts.isTemplateHead(node) ||\n ts.isTemplateMiddle(node) ||\n ts.isTemplateTail(node)\n ) {\n for (const match of node.text.matchAll(ARBITRARY_SCALE)) {\n const [utility, scale, value] = [match[0], match[1] ?? '', match[2] ?? ''];\n // A colour written into a text utility is the other rule's business.\n if (ALLOWED_ARBITRARY.has(value) || BARE_COLOR.test(value)) continue;\n offenders.add(utility.slice(0, 60));\n scales.add(scale);\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(sourceFile);\n if (offenders.size === 0) return null;\n\n const expected = [...scales].map((scale) => `${scale}: ${SCALE_TOKENS[scale]}`).join('; ');\n return `${relativePath} hardcodes ${[...offenders].map((value) => `'${value}'`).join(', ')}. Browser code names a rung on the scale instead (${expected}). If the ramp has no rung for what you mean, add one to styles/tokens.css so every caller can reach it, rather than spelling the value here.`;\n },\n};\n"],"mappings":"8JAOA,MAAM,EAAkB,uBAElB,EAAa,6DAOnB,SAAS,EAAe,EAA+B,CACrD,OACE,EAAa,WAAW,YAAY,GACpC,EAAa,WAAW,SAAS,GACjC,EAAa,WAAW,QAAQ,GAChC,EAAa,SAAS,QAAQ,GAC9B,EAAa,SAAS,QAAQ,CAElC,CASA,SAAS,EAAgB,EAA+B,CAGtD,OAFI,EAAa,WAAW,UAAU,GAClC,EAAa,WAAW,iBAAiB,GAAK,EAAa,WAAW,UAAU,EAAU,GACvF,EAAc,CAAY,CACnC,CAEA,MAAa,EAAkC,CAC7C,UAAW,GACX,KAAM,2DACN,UACE,6ZACF,MAAM,EAAU,EAAY,CAC1B,IAAM,EAAe,EAAY,EAAU,CAAU,EACrD,GAAI,IAAiB,MAAQ,EAAe,CAAY,GAAK,CAAC,EAAgB,CAAY,EAAG,OAAO,KACpG,IAAM,EAAa,EAAW,CAAQ,EACtC,GAAI,CAAC,EAAY,OAAO,KAExB,IAAM,kBAAY,IAAI,IAChB,EAAS,GAAwB,CACrC,GAAI,EAAG,oBAAoB,CAAI,EAAG,CAChC,IAAM,EAAO,EAAK,MACd,EAAgB,KAAK,CAAI,GAAK,EAAW,KAAK,CAAI,IAAG,EAAU,IAAI,EAAK,KAAK,CAAC,CAAC,MAAM,EAAG,EAAE,CAAC,CACjG,CACA,EAAG,aAAa,EAAM,CAAK,CAC7B,EAIA,OAHA,EAAM,CAAU,EACZ,EAAU,OAAS,EAAU,KAE1B,GAAG,EAAa,aAAa,CAAC,GAAG,CAAS,CAAC,CAAC,IAAK,GAAU,IAAI,EAAM,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,gTAC7F,CACF,EAOM,EAAkB,sFAMlB,kBAAoB,IAAI,IAAI,CAAC,SAAS,CAAC,EAGvC,EAAuC,CAC3C,KAAM,mFACN,QAAS,wFACT,SAAU,4EACV,QAAS,4EACX,EAEa,EAAwC,CACnD,UAAW,GACX,KAAM,iGACN,UACE,ohBACF,MAAM,EAAU,EAAY,CAC1B,IAAM,EAAe,EAAY,EAAU,CAAU,EACrD,GAAI,IAAiB,MAAQ,EAAe,CAAY,GAAK,CAAC,EAAgB,CAAY,EAAG,OAAO,KACpG,IAAM,EAAa,EAAW,CAAQ,EACtC,GAAI,CAAC,EAAY,OAAO,KAExB,IAAM,kBAAY,IAAI,IAChB,kBAAS,IAAI,IACb,EAAS,GAAwB,CAErC,GACE,EAAG,oBAAoB,CAAI,GAC3B,EAAG,eAAe,CAAI,GACtB,EAAG,iBAAiB,CAAI,GACxB,EAAG,eAAe,CAAI,EAEtB,IAAK,IAAM,KAAS,EAAK,KAAK,SAAS,CAAe,EAAG,CACvD,GAAM,CAAC,EAAS,EAAO,GAAS,CAAC,EAAM,GAAI,EAAM,IAAM,GAAI,EAAM,IAAM,EAAE,EAErE,EAAkB,IAAI,CAAK,GAAK,EAAW,KAAK,CAAK,IACzD,EAAU,IAAI,EAAQ,MAAM,EAAG,EAAE,CAAC,EAClC,EAAO,IAAI,CAAK,EAClB,CAEF,EAAG,aAAa,EAAM,CAAK,CAC7B,EAEA,GADA,EAAM,CAAU,EACZ,EAAU,OAAS,EAAG,OAAO,KAEjC,IAAM,EAAW,CAAC,GAAG,CAAM,CAAC,CAAC,IAAK,GAAU,GAAG,EAAM,IAAI,EAAa,IAAQ,CAAC,CAAC,KAAK,IAAI,EACzF,MAAO,GAAG,EAAa,aAAa,CAAC,GAAG,CAAS,CAAC,CAAC,IAAK,GAAU,IAAI,EAAM,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,oDAAoD,EAAS,8IAC1J,CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/vibe-lint-plugin-doom-web",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.76",
|
|
4
4
|
"description": "Deterministic Vibe-Lint rules for DoomPi web packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"doompi",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@agimon-ai/doompi-build": "0.0.1-alpha.
|
|
42
|
+
"@agimon-ai/doompi-build": "0.0.1-alpha.11",
|
|
43
43
|
"typescript": "6.0.3"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { PatternDefinition } from "@agimon-ai/vibe-lint";
|
|
2
|
-
//#region src/configs/patterns.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Design-pattern context surfaced before a DoomPi web package file is edited.
|
|
5
|
-
* A web package ships a browser bundle and a server from one source tree, so
|
|
6
|
-
* each folder states which of the two it belongs to and what it may reach.
|
|
7
|
-
*/
|
|
8
|
-
export declare const patterns: Record<string, PatternDefinition>;
|
|
9
|
-
//#endregion
|
|
10
|
-
//# sourceMappingURL=patterns.d.cts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"patterns.d.cts","names":[],"sources":["../../src/configs/patterns.ts"],"mappings":";;;;;;;qBAOa,UAAU,eAAe"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"recommended.d.cts","names":[],"sources":["../../src/configs/recommended.ts"],"mappings":";;qBAYa,aAAa"}
|
package/dist/index.d.cts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { patterns } from "./configs/patterns.cjs";
|
|
2
|
-
import { recommended } from "./configs/recommended.cjs";
|
|
3
|
-
import { doomWebPlugin } from "./plugin.cjs";
|
|
4
|
-
import { doomComponentsLayerBoundary, isComponentLibrary } from "./rules/componentLibrary.cjs";
|
|
5
|
-
import { webFileNaming } from "./rules/conventions.cjs";
|
|
6
|
-
import { noCrossFeatureImport } from "./rules/features.cjs";
|
|
7
|
-
import { doomWebLayerBoundary } from "./rules/layers.cjs";
|
|
8
|
-
import { preferSharedPrimitive } from "./rules/primitives.cjs";
|
|
9
|
-
import { rules } from "./rules/registry.cjs";
|
|
10
|
-
import { noArbitraryStyleValue, noRawThemeColor } from "./rules/theming.cjs";
|
|
11
|
-
import "./rules/index.cjs";
|
|
12
|
-
export { doomWebPlugin as default, doomComponentsLayerBoundary, doomWebLayerBoundary, doomWebPlugin, isComponentLibrary, noArbitraryStyleValue, noCrossFeatureImport, noRawThemeColor, patterns, preferSharedPrimitive, recommended, rules, webFileNaming };
|
package/dist/plugin.d.cts
DELETED
package/dist/plugin.d.cts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/plugin.ts"],"mappings":";;qBAMa,eAAe"}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { RuleDefinition } from "@agimon-ai/vibe-lint";
|
|
2
|
-
//#region src/rules/componentLibrary.d.ts
|
|
3
|
-
/** A component library has no src/web: its whole tree is the browser bundle. */
|
|
4
|
-
export declare function isComponentLibrary(configRoot: string): boolean;
|
|
5
|
-
export declare const doomComponentsLayerBoundary: RuleDefinition;
|
|
6
|
-
//#endregion
|
|
7
|
-
//# sourceMappingURL=componentLibrary.d.cts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"componentLibrary.d.cts","names":[],"sources":["../../src/rules/componentLibrary.ts"],"mappings":";;;wBA6BgB,mBAAmB;qBActB,6BAA6B"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"conventions.d.cts","names":[],"sources":["../../src/rules/conventions.ts"],"mappings":";;qBAWa,eAAe"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"features.d.cts","names":[],"sources":["../../src/rules/features.ts"],"mappings":";;qBAIa,sBAAsB"}
|
package/dist/rules/index.d.cts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { doomComponentsLayerBoundary, isComponentLibrary } from "./componentLibrary.cjs";
|
|
2
|
-
import { webFileNaming } from "./conventions.cjs";
|
|
3
|
-
import { noCrossFeatureImport } from "./features.cjs";
|
|
4
|
-
import { doomWebLayerBoundary } from "./layers.cjs";
|
|
5
|
-
import { preferSharedPrimitive } from "./primitives.cjs";
|
|
6
|
-
import { rules } from "./registry.cjs";
|
|
7
|
-
import { noArbitraryStyleValue, noRawThemeColor } from "./theming.cjs";
|
|
8
|
-
export { doomComponentsLayerBoundary, doomWebLayerBoundary, isComponentLibrary, noArbitraryStyleValue, noCrossFeatureImport, noRawThemeColor, preferSharedPrimitive, rules, webFileNaming };
|
package/dist/rules/layers.d.cts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"layers.d.cts","names":[],"sources":["../../src/rules/layers.ts"],"mappings":";;qBA6Da,sBAAsB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"primitives.d.cts","names":[],"sources":["../../src/rules/primitives.ts"],"mappings":";;qBAgEa,uBAAuB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.cts","names":[],"sources":["../../src/rules/registry.ts"],"mappings":";;qBASa,OAAO,eAAe"}
|
package/dist/rules/theming.d.cts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"theming.d.cts","names":[],"sources":["../../src/rules/theming.ts"],"mappings":";;qBAuCa,iBAAiB;qBA+CjB,uBAAuB"}
|