This repository was archived by the owner on Jul 19, 2026. It is now read-only.
0.13.0 (#115) #329
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: ["*"] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| ci: | |
| name: Lint → Type Check → Test → Build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - name: Scaffold eslint-plugin-multicorn-ui | |
| run: | | |
| mkdir -p ../eslint-plugin-multicorn-ui/src | |
| cat > ../eslint-plugin-multicorn-ui/package.json << 'PKGJSON' | |
| { | |
| "name": "eslint-plugin-multicorn-ui", | |
| "version": "1.0.0", | |
| "description": "ESLint rules to enforce Multicorn UI language standards", | |
| "main": "src/index.js", | |
| "license": "MIT", | |
| "keywords": ["eslint", "eslintplugin"], | |
| "peerDependencies": { | |
| "eslint": ">=8.0.0" | |
| } | |
| } | |
| PKGJSON | |
| cat > ../eslint-plugin-multicorn-ui/src/index.js << 'INDEXJS' | |
| const noUiJargon = require("./no-ui-jargon"); | |
| module.exports = { | |
| rules: { | |
| "no-ui-jargon": noUiJargon, | |
| }, | |
| configs: { | |
| recommended: { | |
| plugins: ["multicorn-ui"], | |
| rules: { | |
| "multicorn-ui/no-ui-jargon": "warn", | |
| }, | |
| }, | |
| strict: { | |
| plugins: ["multicorn-ui"], | |
| rules: { | |
| "multicorn-ui/no-ui-jargon": "error", | |
| }, | |
| }, | |
| }, | |
| }; | |
| INDEXJS | |
| cat > ../eslint-plugin-multicorn-ui/src/no-ui-jargon.js << 'RULEJS' | |
| /** | |
| * Catches implementation jargon in user-facing UI text. | |
| * | |
| * Checks JSX text content, JSX attribute strings (aria-label, title, placeholder, alt), | |
| * and template literals inside JSX. Does NOT flag variable names, imports, comments, | |
| * or non-UI strings — only text that would be visible to a user. | |
| */ | |
| const BANNED_WORDS = new Map([ | |
| ["modal", "Don't label UI patterns. Just show the content."], | |
| ["dialog", "Don't label UI patterns. Name the content, not the container."], | |
| ["inline", "Users don't care about rendering method."], | |
| ["embedded", "Users don't care about rendering method."], | |
| ["component", "Use 'feature', 'section', or name the specific thing."], | |
| ["widget", "Use 'feature', 'section', or name the specific thing."], | |
| ["container", "Never user-facing."], | |
| ["wrapper", "Never user-facing."], | |
| ["rendered", "Use 'shown', 'displayed', or 'appears'."], | |
| ["callback", "Never user-facing."], | |
| ["handler", "Never user-facing."], | |
| ["instance", "Use 'agent', 'item', or name the specific thing."], | |
| ["payload", "Use 'details', 'data', or 'content'."], | |
| ["endpoint", "Never user-facing."], | |
| ["schema", "Use 'structure' or 'format'."], | |
| ["lifecycle", "Never user-facing."], | |
| ["hydrate", "Use 'load' or 'appear'."], | |
| ["hydration", "Use 'loading'."], | |
| ["dispatch", "Use 'send' or 'trigger'."], | |
| ["refetch", "Use 'refresh' or 'update'."], | |
| ["invalidate", "Use 'refresh' or 'update'."], | |
| ["fallback", "Use 'default' or 'backup'."], | |
| ["singleton", "Never user-facing."], | |
| ["polymorphic", "Never user-facing."], | |
| ["propagate", "Use 'spread', 'apply', or 'pass through'."], | |
| ["serialize", "Use 'save' or 'convert'."], | |
| ["deserialize", "Use 'load' or 'convert'."], | |
| ["deprecated", "Use 'no longer available' or 'replaced by'."], | |
| ["popover", "Don't label UI patterns."], | |
| ["pagination", "Use 'page', 'next', 'previous', or 'show more'."], | |
| ["stateful", "Describe the behaviour, not the implementation."], | |
| ["stateless", "Describe the behaviour, not the implementation."], | |
| ["unmount", "Use 'close', 'hide', or 'remove'."], | |
| ]); | |
| const UI_ATTRIBUTES = new Set([ | |
| "aria-label", | |
| "aria-describedby", | |
| "title", | |
| "placeholder", | |
| "alt", | |
| "label", | |
| ]); | |
| function buildPattern() { | |
| const words = [...BANNED_WORDS.keys()].join("|"); | |
| return new RegExp(`\\b(${words})\\b`, "gi"); | |
| } | |
| function checkText(context, node, text, bannedPattern) { | |
| bannedPattern.lastIndex = 0; | |
| let match; | |
| while ((match = bannedPattern.exec(text)) !== null) { | |
| const word = match[1].toLowerCase(); | |
| const suggestion = BANNED_WORDS.get(word); | |
| context.report({ | |
| node, | |
| message: `Banned UI term "{{word}}" in user-facing text. ${suggestion}`, | |
| data: { word: match[1] }, | |
| }); | |
| } | |
| } | |
| module.exports = { | |
| meta: { | |
| type: "suggestion", | |
| docs: { | |
| description: "Disallow implementation jargon in user-facing UI text", | |
| category: "Multicorn UI Standards", | |
| recommended: true, | |
| }, | |
| schema: [], | |
| messages: {}, | |
| }, | |
| create(context) { | |
| const bannedPattern = buildPattern(); | |
| return { | |
| JSXText(node) { | |
| checkText(context, node, node.value, bannedPattern); | |
| }, | |
| JSXExpressionContainer(node) { | |
| if (node.expression.type === "Literal" && typeof node.expression.value === "string") { | |
| checkText(context, node, node.expression.value, bannedPattern); | |
| } | |
| if (node.expression.type === "TemplateLiteral") { | |
| for (const quasi of node.expression.quasis) { | |
| checkText(context, node, quasi.value.raw, bannedPattern); | |
| } | |
| } | |
| }, | |
| JSXAttribute(node) { | |
| const attrName = node.name.type === "JSXNamespacedName" | |
| ? `${node.name.namespace.name}:${node.name.name.name}` | |
| : node.name.name; | |
| if (!UI_ATTRIBUTES.has(attrName)) return; | |
| if (node.value && node.value.type === "Literal" && typeof node.value.value === "string") { | |
| checkText(context, node, node.value.value, bannedPattern); | |
| } | |
| }, | |
| }; | |
| }, | |
| }; | |
| RULEJS | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Audit production dependencies | |
| run: pnpm audit --prod --audit-level=high | |
| - name: Lint | |
| run: pnpm lint | |
| - name: Type check | |
| run: pnpm typecheck | |
| - name: Run tests | |
| run: pnpm test | |
| - name: Build | |
| run: pnpm build | |
| - name: Validate MCPB extension manifest | |
| run: pnpm run validate:extension | |
| trigger-learn-rebuild: | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| needs: ci | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 2 | |
| - name: Check if CHANGELOG.md changed | |
| id: check | |
| run: | | |
| if git diff --name-only HEAD~1 HEAD | grep -q 'CHANGELOG.md'; then | |
| echo "changed=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Trigger multicorn-learn rebuild | |
| if: steps.check.outputs.changed == 'true' | |
| run: curl -X POST "${{ secrets.VERCEL_LEARN_DEPLOY_HOOK }}" |