From f98af659f99035ec60f2378f275da7ec259b4cad Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Thu, 26 Mar 2026 13:28:06 -0500 Subject: [PATCH] chore(ci): setup monorepo Signed-off-by: Cory Rylan --- .claude/hooks/{task-completed.sh => stop.sh} | 2 +- .claude/settings.json | 6 +- .github/ISSUE_TEMPLATE/bug_report.md | 55 + .github/ISSUE_TEMPLATE/feature_request.md | 43 + .github/workflows/build.yml | 22 - .github/workflows/claude-code-review.yml | 43 + .github/workflows/claude.yml | 48 + .github/workflows/pull-request.yml | 13 + .github/workflows/release.yml | 28 + .github/workflows/stale.yml | 17 + .gitignore | 2 +- .prettierignore | 7 + CLAUDE.md | 88 +- README.md | 429 ++---- bun.lock | 1235 ++++++++++++++++- commitlint.config.js | 20 + package.json | 110 +- prettier.config.js | 8 + CHANGELOG.md => projects/cli/CHANGELOG.md | 0 projects/cli/CLAUDE.md | 73 + LICENSE => projects/cli/LICENSE | 0 projects/cli/README.md | 403 ++++++ .../cli/eslint.config.js | 0 install.sh => projects/cli/install.sh | 17 +- projects/cli/package.json | 200 +++ {src => projects/cli/src}/cli.ts | 0 {src => projects/cli/src}/cli/format.test.ts | 0 {src => projects/cli/src}/cli/format.ts | 0 {src => projects/cli/src}/cli/helpers.test.ts | 0 {src => projects/cli/src}/cli/helpers.ts | 0 {src => projects/cli/src}/global.d.ts | 0 {src => projects/cli/src}/index.ts | 0 .../cli/src}/internal/attributes/index.ts | 0 .../src}/internal/attributes/parser.test.ts | 0 .../cli/src}/internal/attributes/parser.ts | 0 .../src}/internal/attributes/resolver.test.ts | 0 .../cli/src}/internal/attributes/resolver.ts | 0 .../src}/internal/attributes/store.test.ts | 0 .../cli/src}/internal/attributes/store.ts | 0 .../internal/attributes/tools/get.test.ts | 0 .../cli/src}/internal/attributes/tools/get.ts | 0 .../internal/attributes/tools/list.test.ts | 0 .../src}/internal/attributes/tools/list.ts | 0 .../cli/src}/internal/attributes/types.ts | 0 .../cli/src}/internal/config/config.test.ts | 0 .../cli/src}/internal/config/config.ts | 0 .../cli/src}/internal/config/index.ts | 0 .../cli/src}/internal/dtcg/convert.test.ts | 0 .../cli/src}/internal/dtcg/convert.ts | 0 .../cli/src}/internal/dtcg/index.ts | 0 .../cli/src}/internal/dtcg/load.test.ts | 0 .../cli/src}/internal/dtcg/load.ts | 0 .../cli/src}/internal/dtcg/parser.ts | 0 .../cli/src}/internal/dtcg/types.ts | 0 .../cli/src}/internal/elements/errors.ts | 0 .../cli/src}/internal/elements/index.ts | 0 .../cli/src}/internal/elements/parser.test.ts | 0 .../cli/src}/internal/elements/parser.ts | 0 .../src}/internal/elements/resolver.test.ts | 0 .../cli/src}/internal/elements/resolver.ts | 0 .../cli/src}/internal/elements/store.test.ts | 0 .../cli/src}/internal/elements/store.ts | 0 .../elements/tools/attributes.test.ts | 0 .../internal/elements/tools/attributes.ts | 0 .../internal/elements/tools/commands.test.ts | 0 .../src}/internal/elements/tools/commands.ts | 0 .../internal/elements/tools/css-parts.test.ts | 0 .../src}/internal/elements/tools/css-parts.ts | 0 .../elements/tools/css-properties.test.ts | 0 .../internal/elements/tools/css-properties.ts | 0 .../internal/elements/tools/events.test.ts | 0 .../src}/internal/elements/tools/events.ts | 0 .../src}/internal/elements/tools/get.test.ts | 0 .../cli/src}/internal/elements/tools/get.ts | 0 .../src}/internal/elements/tools/list.test.ts | 0 .../cli/src}/internal/elements/tools/list.ts | 0 .../internal/elements/tools/methods.test.ts | 0 .../src}/internal/elements/tools/methods.ts | 0 .../elements/tools/properties.test.ts | 0 .../internal/elements/tools/properties.ts | 0 .../internal/elements/tools/slots.test.ts | 0 .../cli/src}/internal/elements/tools/slots.ts | 0 .../cli/src}/internal/elements/tools/tools.ts | 0 .../cli/src}/internal/elements/types.ts | 0 .../cli/src}/internal/mcp/index.ts | 0 .../cli/src}/internal/mcp/server.ts | 0 .../cli/src}/internal/patterns/index.ts | 0 .../cli/src}/internal/patterns/parser.test.ts | 0 .../cli/src}/internal/patterns/parser.ts | 0 .../src}/internal/patterns/resolver.test.ts | 0 .../cli/src}/internal/patterns/resolver.ts | 0 .../cli/src}/internal/patterns/store.test.ts | 0 .../cli/src}/internal/patterns/store.ts | 0 .../src}/internal/patterns/tools/get.test.ts | 0 .../cli/src}/internal/patterns/tools/get.ts | 0 .../src}/internal/patterns/tools/list.test.ts | 0 .../cli/src}/internal/patterns/tools/list.ts | 0 .../cli/src}/internal/patterns/types.ts | 0 .../cli/src}/internal/resolve/index.ts | 0 .../cli/src}/internal/resolve/resolve.test.ts | 0 .../cli/src}/internal/resolve/resolve.ts | 0 .../cli/src}/internal/styles/index.ts | 0 .../cli/src}/internal/styles/parser.test.ts | 0 .../cli/src}/internal/styles/parser.ts | 0 .../cli/src}/internal/styles/resolver.test.ts | 0 .../cli/src}/internal/styles/resolver.ts | 0 .../cli/src}/internal/styles/store.test.ts | 0 .../cli/src}/internal/styles/store.ts | 0 .../src}/internal/styles/tools/get.test.ts | 0 .../cli/src}/internal/styles/tools/get.ts | 0 .../src}/internal/styles/tools/list.test.ts | 0 .../cli/src}/internal/styles/tools/list.ts | 0 .../cli/src}/internal/styles/types.ts | 0 {src => projects/cli/src}/internal/tools.ts | 0 .../cli/src}/internal/validate/html.test.ts | 0 .../cli/src}/internal/validate/html.ts | 0 .../cli/src}/internal/validate/index.ts | 0 .../validate/rules/command-helper.test.ts | 0 .../internal/validate/rules/command-helper.ts | 0 .../validate/rules/css-helpers.test.ts | 0 .../internal/validate/rules/css-helpers.ts | 0 .../cli/src}/internal/validate/rules/index.ts | 0 .../rules/no-boolean-attr-value.test.ts | 0 .../validate/rules/no-boolean-attr-value.ts | 0 .../validate/rules/no-deprecated-attr.test.ts | 0 .../validate/rules/no-deprecated-attr.ts | 0 .../rules/no-deprecated-command.test.ts | 0 .../validate/rules/no-deprecated-command.ts | 0 .../rules/no-deprecated-element.test.ts | 0 .../validate/rules/no-deprecated-element.ts | 0 .../rules/no-deprecated-event.test.ts | 0 .../validate/rules/no-deprecated-event.ts | 0 .../validate/rules/no-deprecated-slot.test.ts | 0 .../validate/rules/no-deprecated-slot.ts | 0 .../rules/no-missing-required-child.test.ts | 0 .../rules/no-missing-required-child.ts | 0 .../rules/no-missing-sibling-binding.test.ts | 0 .../rules/no-missing-sibling-binding.ts | 0 .../rules/no-unknown-attr-value.test.ts | 0 .../validate/rules/no-unknown-attr-value.ts | 0 .../validate/rules/no-unknown-attr.test.ts | 0 .../validate/rules/no-unknown-attr.ts | 0 .../validate/rules/no-unknown-command.test.ts | 0 .../validate/rules/no-unknown-command.ts | 0 .../no-unknown-css-custom-property.test.ts | 0 .../rules/no-unknown-css-custom-property.ts | 0 .../rules/no-unknown-css-part.test.ts | 0 .../validate/rules/no-unknown-css-part.ts | 0 .../no-unknown-custom-attr-value.test.ts | 0 .../rules/no-unknown-custom-attr-value.ts | 0 .../validate/rules/no-unknown-element.test.ts | 0 .../validate/rules/no-unknown-element.ts | 0 .../validate/rules/no-unknown-event.test.ts | 0 .../validate/rules/no-unknown-event.ts | 0 .../validate/rules/no-unknown-slot.test.ts | 0 .../validate/rules/no-unknown-slot.ts | 0 .../rules/no-unknown-style-value.test.ts | 0 .../validate/rules/no-unknown-style-value.ts | 0 .../internal/validate/rules/registry.test.ts | 0 .../validate/rules/suggestion.test.ts | 0 .../internal/validate/rules/suggestion.ts | 0 .../internal/validate/rules/test-helper.ts | 0 .../cli/src}/internal/validate/schema.test.ts | 0 .../cli/src}/internal/validate/schema.ts | 0 .../validate/tools/validate-html.test.ts | 0 .../internal/validate/tools/validate-html.ts | 0 .../cli/src}/internal/validate/types.ts | 0 .../src}/internal/validate/validate.test.ts | 0 .../cli/src}/internal/validate/validate.ts | 0 .../cli/src}/internal/vscode/convert.test.ts | 0 .../cli/src}/internal/vscode/convert.ts | 0 .../cli/src}/internal/vscode/index.ts | 0 .../cli/src}/internal/vscode/load.test.ts | 0 .../cli/src}/internal/vscode/load.ts | 0 .../cli/src}/internal/vscode/parser.ts | 0 .../cli/src}/internal/vscode/resolver.test.ts | 0 .../cli/src}/internal/vscode/resolver.ts | 0 .../internal/vscode/testdata/package.json | 0 .../internal/vscode/testdata/sample-css.json | 0 .../internal/vscode/testdata/sample-html.json | 0 .../cli/src}/internal/vscode/types.ts | 0 .../cli/src}/templates/mcp-template.json | 0 .../cli/src}/templates/skill-template.md | 0 .../cli/testdata}/custom-attributes.json | 0 .../cli/testdata}/custom-elements.json | 0 .../cli/testdata}/custom-patterns.json | 0 .../cli/testdata}/custom-styles.json | 0 .../cli/testdata}/tokens.json | 0 tsconfig.json => projects/cli/tsconfig.json | 0 projects/eslint/CHANGELOG.md | 1 + projects/eslint/CLAUDE.md | 41 + projects/eslint/LICENSE | 21 + projects/eslint/README.md | 124 ++ projects/eslint/package.json | 85 ++ projects/eslint/src/index.ts | 78 ++ .../eslint/src/rules/custom-attributes.json | 31 + .../eslint/src/rules/custom-elements.json | 184 +++ .../eslint/src/rules/custom-patterns.json | 44 + projects/eslint/src/rules/custom-styles.json | 20 + .../src/rules/no-boolean-attr-value.test.ts | 39 + .../eslint/src/rules/no-boolean-attr-value.ts | 41 + .../src/rules/no-deprecated-attr.test.ts | 18 + .../eslint/src/rules/no-deprecated-attr.ts | 41 + .../src/rules/no-deprecated-command.test.ts | 34 + .../eslint/src/rules/no-deprecated-command.ts | 41 + .../src/rules/no-deprecated-element.test.ts | 17 + .../eslint/src/rules/no-deprecated-element.ts | 41 + .../src/rules/no-deprecated-event.test.ts | 40 + .../eslint/src/rules/no-deprecated-event.ts | 41 + .../src/rules/no-deprecated-slot.test.ts | 23 + .../eslint/src/rules/no-deprecated-slot.ts | 41 + .../rules/no-missing-required-child.test.ts | 21 + .../src/rules/no-missing-required-child.ts | 41 + .../rules/no-missing-sibling-binding.test.ts | 30 + .../src/rules/no-missing-sibling-binding.ts | 41 + .../src/rules/no-unknown-attr-value.test.ts | 43 + .../eslint/src/rules/no-unknown-attr-value.ts | 41 + .../eslint/src/rules/no-unknown-attr.test.ts | 39 + projects/eslint/src/rules/no-unknown-attr.ts | 41 + .../src/rules/no-unknown-command.test.ts | 56 + .../eslint/src/rules/no-unknown-command.ts | 41 + .../no-unknown-css-custom-property.test.ts | 56 + .../rules/no-unknown-css-custom-property.ts | 41 + .../src/rules/no-unknown-css-part.test.ts | 49 + .../eslint/src/rules/no-unknown-css-part.ts | 41 + .../no-unknown-custom-attr-value.test.ts | 42 + .../src/rules/no-unknown-custom-attr-value.ts | 41 + .../src/rules/no-unknown-element.test.ts | 41 + .../eslint/src/rules/no-unknown-element.ts | 66 + .../eslint/src/rules/no-unknown-event.test.ts | 65 + projects/eslint/src/rules/no-unknown-event.ts | 66 + .../eslint/src/rules/no-unknown-slot.test.ts | 29 + projects/eslint/src/rules/no-unknown-slot.ts | 41 + .../src/rules/no-unknown-style-value.test.ts | 59 + .../src/rules/no-unknown-style-value.ts | 41 + projects/eslint/src/rules/test-utils.ts | 12 + projects/eslint/src/utils/schema.ts | 15 + projects/eslint/src/utils/webq.ts | 55 + projects/eslint/tsconfig.json | 17 + projects/eslint/vitest.config.ts | 7 + .../schemas}/custom-attributes/README.md | 0 .../schemas}/custom-attributes/schema.json | 0 .../schemas}/custom-elements/README.md | 0 .../schemas}/custom-elements/schema.json | 0 .../schemas}/custom-patterns/README.md | 0 .../schemas}/custom-patterns/schema.json | 0 .../schemas}/custom-styles/README.md | 0 .../schemas}/custom-styles/schema.json | 0 {schemas => projects/schemas}/dtcg/README.md | 0 .../schemas}/dtcg/schema.json | 0 .../schemas}/vscode-css/README.md | 0 .../schemas}/vscode-css/schema.json | 0 .../schemas}/vscode-html/README.md | 0 .../schemas}/vscode-html/schema.json | 0 release.config.js | 86 ++ 255 files changed, 4667 insertions(+), 512 deletions(-) rename .claude/hooks/{task-completed.sh => stop.sh} (63%) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/claude-code-review.yml create mode 100644 .github/workflows/claude.yml create mode 100644 .github/workflows/pull-request.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/stale.yml create mode 100644 .prettierignore create mode 100644 commitlint.config.js create mode 100644 prettier.config.js rename CHANGELOG.md => projects/cli/CHANGELOG.md (100%) create mode 100644 projects/cli/CLAUDE.md rename LICENSE => projects/cli/LICENSE (100%) create mode 100644 projects/cli/README.md rename eslint.config.js => projects/cli/eslint.config.js (100%) rename install.sh => projects/cli/install.sh (81%) create mode 100644 projects/cli/package.json rename {src => projects/cli/src}/cli.ts (100%) rename {src => projects/cli/src}/cli/format.test.ts (100%) rename {src => projects/cli/src}/cli/format.ts (100%) rename {src => projects/cli/src}/cli/helpers.test.ts (100%) rename {src => projects/cli/src}/cli/helpers.ts (100%) rename {src => projects/cli/src}/global.d.ts (100%) rename {src => projects/cli/src}/index.ts (100%) rename {src => projects/cli/src}/internal/attributes/index.ts (100%) rename {src => projects/cli/src}/internal/attributes/parser.test.ts (100%) rename {src => projects/cli/src}/internal/attributes/parser.ts (100%) rename {src => projects/cli/src}/internal/attributes/resolver.test.ts (100%) rename {src => projects/cli/src}/internal/attributes/resolver.ts (100%) rename {src => projects/cli/src}/internal/attributes/store.test.ts (100%) rename {src => projects/cli/src}/internal/attributes/store.ts (100%) rename {src => projects/cli/src}/internal/attributes/tools/get.test.ts (100%) rename {src => projects/cli/src}/internal/attributes/tools/get.ts (100%) rename {src => projects/cli/src}/internal/attributes/tools/list.test.ts (100%) rename {src => projects/cli/src}/internal/attributes/tools/list.ts (100%) rename {src => projects/cli/src}/internal/attributes/types.ts (100%) rename {src => projects/cli/src}/internal/config/config.test.ts (100%) rename {src => projects/cli/src}/internal/config/config.ts (100%) rename {src => projects/cli/src}/internal/config/index.ts (100%) rename {src => projects/cli/src}/internal/dtcg/convert.test.ts (100%) rename {src => projects/cli/src}/internal/dtcg/convert.ts (100%) rename {src => projects/cli/src}/internal/dtcg/index.ts (100%) rename {src => projects/cli/src}/internal/dtcg/load.test.ts (100%) rename {src => projects/cli/src}/internal/dtcg/load.ts (100%) rename {src => projects/cli/src}/internal/dtcg/parser.ts (100%) rename {src => projects/cli/src}/internal/dtcg/types.ts (100%) rename {src => projects/cli/src}/internal/elements/errors.ts (100%) rename {src => projects/cli/src}/internal/elements/index.ts (100%) rename {src => projects/cli/src}/internal/elements/parser.test.ts (100%) rename {src => projects/cli/src}/internal/elements/parser.ts (100%) rename {src => projects/cli/src}/internal/elements/resolver.test.ts (100%) rename {src => projects/cli/src}/internal/elements/resolver.ts (100%) rename {src => projects/cli/src}/internal/elements/store.test.ts (100%) rename {src => projects/cli/src}/internal/elements/store.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/attributes.test.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/attributes.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/commands.test.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/commands.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/css-parts.test.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/css-parts.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/css-properties.test.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/css-properties.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/events.test.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/events.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/get.test.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/get.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/list.test.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/list.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/methods.test.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/methods.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/properties.test.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/properties.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/slots.test.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/slots.ts (100%) rename {src => projects/cli/src}/internal/elements/tools/tools.ts (100%) rename {src => projects/cli/src}/internal/elements/types.ts (100%) rename {src => projects/cli/src}/internal/mcp/index.ts (100%) rename {src => projects/cli/src}/internal/mcp/server.ts (100%) rename {src => projects/cli/src}/internal/patterns/index.ts (100%) rename {src => projects/cli/src}/internal/patterns/parser.test.ts (100%) rename {src => projects/cli/src}/internal/patterns/parser.ts (100%) rename {src => projects/cli/src}/internal/patterns/resolver.test.ts (100%) rename {src => projects/cli/src}/internal/patterns/resolver.ts (100%) rename {src => projects/cli/src}/internal/patterns/store.test.ts (100%) rename {src => projects/cli/src}/internal/patterns/store.ts (100%) rename {src => projects/cli/src}/internal/patterns/tools/get.test.ts (100%) rename {src => projects/cli/src}/internal/patterns/tools/get.ts (100%) rename {src => projects/cli/src}/internal/patterns/tools/list.test.ts (100%) rename {src => projects/cli/src}/internal/patterns/tools/list.ts (100%) rename {src => projects/cli/src}/internal/patterns/types.ts (100%) rename {src => projects/cli/src}/internal/resolve/index.ts (100%) rename {src => projects/cli/src}/internal/resolve/resolve.test.ts (100%) rename {src => projects/cli/src}/internal/resolve/resolve.ts (100%) rename {src => projects/cli/src}/internal/styles/index.ts (100%) rename {src => projects/cli/src}/internal/styles/parser.test.ts (100%) rename {src => projects/cli/src}/internal/styles/parser.ts (100%) rename {src => projects/cli/src}/internal/styles/resolver.test.ts (100%) rename {src => projects/cli/src}/internal/styles/resolver.ts (100%) rename {src => projects/cli/src}/internal/styles/store.test.ts (100%) rename {src => projects/cli/src}/internal/styles/store.ts (100%) rename {src => projects/cli/src}/internal/styles/tools/get.test.ts (100%) rename {src => projects/cli/src}/internal/styles/tools/get.ts (100%) rename {src => projects/cli/src}/internal/styles/tools/list.test.ts (100%) rename {src => projects/cli/src}/internal/styles/tools/list.ts (100%) rename {src => projects/cli/src}/internal/styles/types.ts (100%) rename {src => projects/cli/src}/internal/tools.ts (100%) rename {src => projects/cli/src}/internal/validate/html.test.ts (100%) rename {src => projects/cli/src}/internal/validate/html.ts (100%) rename {src => projects/cli/src}/internal/validate/index.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/command-helper.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/command-helper.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/css-helpers.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/css-helpers.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/index.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-boolean-attr-value.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-boolean-attr-value.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-deprecated-attr.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-deprecated-attr.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-deprecated-command.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-deprecated-command.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-deprecated-element.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-deprecated-element.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-deprecated-event.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-deprecated-event.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-deprecated-slot.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-deprecated-slot.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-missing-required-child.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-missing-required-child.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-missing-sibling-binding.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-missing-sibling-binding.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-attr-value.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-attr-value.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-attr.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-attr.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-command.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-command.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-css-custom-property.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-css-custom-property.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-css-part.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-css-part.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-custom-attr-value.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-custom-attr-value.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-element.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-element.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-event.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-event.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-slot.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-slot.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-style-value.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/no-unknown-style-value.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/registry.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/suggestion.test.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/suggestion.ts (100%) rename {src => projects/cli/src}/internal/validate/rules/test-helper.ts (100%) rename {src => projects/cli/src}/internal/validate/schema.test.ts (100%) rename {src => projects/cli/src}/internal/validate/schema.ts (100%) rename {src => projects/cli/src}/internal/validate/tools/validate-html.test.ts (100%) rename {src => projects/cli/src}/internal/validate/tools/validate-html.ts (100%) rename {src => projects/cli/src}/internal/validate/types.ts (100%) rename {src => projects/cli/src}/internal/validate/validate.test.ts (100%) rename {src => projects/cli/src}/internal/validate/validate.ts (100%) rename {src => projects/cli/src}/internal/vscode/convert.test.ts (100%) rename {src => projects/cli/src}/internal/vscode/convert.ts (100%) rename {src => projects/cli/src}/internal/vscode/index.ts (100%) rename {src => projects/cli/src}/internal/vscode/load.test.ts (100%) rename {src => projects/cli/src}/internal/vscode/load.ts (100%) rename {src => projects/cli/src}/internal/vscode/parser.ts (100%) rename {src => projects/cli/src}/internal/vscode/resolver.test.ts (100%) rename {src => projects/cli/src}/internal/vscode/resolver.ts (100%) rename {src => projects/cli/src}/internal/vscode/testdata/package.json (100%) rename {src => projects/cli/src}/internal/vscode/testdata/sample-css.json (100%) rename {src => projects/cli/src}/internal/vscode/testdata/sample-html.json (100%) rename {src => projects/cli/src}/internal/vscode/types.ts (100%) rename {src => projects/cli/src}/templates/mcp-template.json (100%) rename {src => projects/cli/src}/templates/skill-template.md (100%) rename {testdata => projects/cli/testdata}/custom-attributes.json (100%) rename {testdata => projects/cli/testdata}/custom-elements.json (100%) rename {testdata => projects/cli/testdata}/custom-patterns.json (100%) rename {testdata => projects/cli/testdata}/custom-styles.json (100%) rename {testdata => projects/cli/testdata}/tokens.json (100%) rename tsconfig.json => projects/cli/tsconfig.json (100%) create mode 100644 projects/eslint/CHANGELOG.md create mode 100644 projects/eslint/CLAUDE.md create mode 100644 projects/eslint/LICENSE create mode 100644 projects/eslint/README.md create mode 100644 projects/eslint/package.json create mode 100644 projects/eslint/src/index.ts create mode 100644 projects/eslint/src/rules/custom-attributes.json create mode 100644 projects/eslint/src/rules/custom-elements.json create mode 100644 projects/eslint/src/rules/custom-patterns.json create mode 100644 projects/eslint/src/rules/custom-styles.json create mode 100644 projects/eslint/src/rules/no-boolean-attr-value.test.ts create mode 100644 projects/eslint/src/rules/no-boolean-attr-value.ts create mode 100644 projects/eslint/src/rules/no-deprecated-attr.test.ts create mode 100644 projects/eslint/src/rules/no-deprecated-attr.ts create mode 100644 projects/eslint/src/rules/no-deprecated-command.test.ts create mode 100644 projects/eslint/src/rules/no-deprecated-command.ts create mode 100644 projects/eslint/src/rules/no-deprecated-element.test.ts create mode 100644 projects/eslint/src/rules/no-deprecated-element.ts create mode 100644 projects/eslint/src/rules/no-deprecated-event.test.ts create mode 100644 projects/eslint/src/rules/no-deprecated-event.ts create mode 100644 projects/eslint/src/rules/no-deprecated-slot.test.ts create mode 100644 projects/eslint/src/rules/no-deprecated-slot.ts create mode 100644 projects/eslint/src/rules/no-missing-required-child.test.ts create mode 100644 projects/eslint/src/rules/no-missing-required-child.ts create mode 100644 projects/eslint/src/rules/no-missing-sibling-binding.test.ts create mode 100644 projects/eslint/src/rules/no-missing-sibling-binding.ts create mode 100644 projects/eslint/src/rules/no-unknown-attr-value.test.ts create mode 100644 projects/eslint/src/rules/no-unknown-attr-value.ts create mode 100644 projects/eslint/src/rules/no-unknown-attr.test.ts create mode 100644 projects/eslint/src/rules/no-unknown-attr.ts create mode 100644 projects/eslint/src/rules/no-unknown-command.test.ts create mode 100644 projects/eslint/src/rules/no-unknown-command.ts create mode 100644 projects/eslint/src/rules/no-unknown-css-custom-property.test.ts create mode 100644 projects/eslint/src/rules/no-unknown-css-custom-property.ts create mode 100644 projects/eslint/src/rules/no-unknown-css-part.test.ts create mode 100644 projects/eslint/src/rules/no-unknown-css-part.ts create mode 100644 projects/eslint/src/rules/no-unknown-custom-attr-value.test.ts create mode 100644 projects/eslint/src/rules/no-unknown-custom-attr-value.ts create mode 100644 projects/eslint/src/rules/no-unknown-element.test.ts create mode 100644 projects/eslint/src/rules/no-unknown-element.ts create mode 100644 projects/eslint/src/rules/no-unknown-event.test.ts create mode 100644 projects/eslint/src/rules/no-unknown-event.ts create mode 100644 projects/eslint/src/rules/no-unknown-slot.test.ts create mode 100644 projects/eslint/src/rules/no-unknown-slot.ts create mode 100644 projects/eslint/src/rules/no-unknown-style-value.test.ts create mode 100644 projects/eslint/src/rules/no-unknown-style-value.ts create mode 100644 projects/eslint/src/rules/test-utils.ts create mode 100644 projects/eslint/src/utils/schema.ts create mode 100644 projects/eslint/src/utils/webq.ts create mode 100644 projects/eslint/tsconfig.json create mode 100644 projects/eslint/vitest.config.ts rename {schemas => projects/schemas}/custom-attributes/README.md (100%) rename {schemas => projects/schemas}/custom-attributes/schema.json (100%) rename {schemas => projects/schemas}/custom-elements/README.md (100%) rename {schemas => projects/schemas}/custom-elements/schema.json (100%) rename {schemas => projects/schemas}/custom-patterns/README.md (100%) rename {schemas => projects/schemas}/custom-patterns/schema.json (100%) rename {schemas => projects/schemas}/custom-styles/README.md (100%) rename {schemas => projects/schemas}/custom-styles/schema.json (100%) rename {schemas => projects/schemas}/dtcg/README.md (100%) rename {schemas => projects/schemas}/dtcg/schema.json (100%) rename {schemas => projects/schemas}/vscode-css/README.md (100%) rename {schemas => projects/schemas}/vscode-css/schema.json (100%) rename {schemas => projects/schemas}/vscode-html/README.md (100%) rename {schemas => projects/schemas}/vscode-html/schema.json (100%) create mode 100644 release.config.js diff --git a/.claude/hooks/task-completed.sh b/.claude/hooks/stop.sh similarity index 63% rename from .claude/hooks/task-completed.sh rename to .claude/hooks/stop.sh index 4afd3a6..bf8918f 100755 --- a/.claude/hooks/task-completed.sh +++ b/.claude/hooks/stop.sh @@ -1,4 +1,4 @@ #!/bin/bash set -e cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || cd "$(git rev-parse --show-toplevel)" 2>/dev/null || exit 0 -bun run format:fix && bun run lint && bun run test && bun run build +bun run format:fix && bun run ci diff --git a/.claude/settings.json b/.claude/settings.json index 57def29..cdcac94 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,18 +5,18 @@ "hooks": [ { "type": "command", - "command": ".claude/hooks/session-start.sh" + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh" } ] } ], - "TaskCompleted": [ + "Stop": [ { "matcher": "", "hooks": [ { "type": "command", - "command": ".claude/hooks/task-completed.sh", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop.sh", "timeout": 30 } ] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..844a724 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,55 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' +--- + +**Package** + +Which package is affected? + +- [ ] `@webq/eslint` — ESLint plugin +- [ ] `@webq/cli` — CLI / MCP server + +**Describe the bug** + +A clear and concise description of what the bug is. + +**To Reproduce** + +Steps to reproduce the behavior. Include a minimal example where possible: + +```html + +``` + +```js +// eslint.config.js (if applicable) +``` + +```bash +# CLI command (if applicable) +webq validate-html ... +``` + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Actual behavior** + +What actually happened, including any error output or incorrect rule messages. + +**Environment** + +- Package version: [e.g. 1.0.0] +- Node / Bun version: [e.g. Node 22, Bun 1.2] +- OS: [e.g. macOS 15, Ubuntu 24] +- ESLint version (if applicable): [e.g. 9.x] +- CEM file source: [e.g. generated by `@custom-elements-manifest/analyzer`] + +**Additional context** + +Add any other context about the problem here, such as your `webq.config.json` or relevant parts of your Custom Elements Manifest. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..8d67cdb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,43 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' +--- + +**Package** + +Which package would this feature apply to? + +- [ ] `@webq/eslint` — ESLint plugin +- [ ] `@webq/cli` — CLI / MCP server +- [ ] Both + +**Is your feature request related to a problem? Please describe.** + +A clear and concise description of what the problem is. For example: "I want to validate `my-attr` values but there is no rule for..." + +**Describe the solution you'd like** + +A clear and concise description of what you want to happen. If this involves a new validation rule, describe the rule behavior and what CEM field(s) it would check. + +**Example** + +If applicable, provide an example of the HTML, CEM definition, or CLI usage that would benefit from this feature: + +```html + +``` + +```json +// Relevant CEM definition +``` + +**Describe alternatives you've considered** + +A clear and concise description of any alternative solutions or workarounds you've considered. + +**Additional context** + +Add any other context or references here, such as links to related Custom Elements Manifest spec sections or similar tools. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index dbcd44d..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: CI - -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: setup bun - uses: oven-sh/setup-bun@v2 - - - name: install - run: bun install - - - name: ci - run: bun run ci - - - name: build - run: bun run build diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..25f4ad1 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,43 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..d118647 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,48 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..986d3c2 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,13 @@ +name: Build +on: pull_request + +jobs: + build: + name: 'Build' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + - uses: google/wireit@setup-github-actions-caching/v2 + - run: bun install + - run: bun run ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..72428a9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Release +on: + push: + branches: + - main + +jobs: + release: + name: Release + timeout-minutes: 20 + runs-on: ubuntu-latest + permissions: + issues: write + deployments: write + pull-requests: write + contents: write + id-token: write + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + - uses: google/wireit@setup-github-actions-caching/v2 + - run: bun install + - run: bun run ci + - name: Release + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + run: WIREIT_PARALLEL=1 WIREIT_LOGGER=metrics bun run release diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..507b834 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,17 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v10 + with: + days-before-stale: 60 + stale-pr-label: 'flag:stale' + stale-issue-label: 'flag:stale' + days-before-close: 180 + stale-issue-message: 'Hello there 👋. This is an automated message to let you know that in order to keep the repository running smoothly, we automatically close issues and pull requests that have not been active for a while. If you would like to continue discussing this topic, please look for another open issue or create a new one with updated information, and include a reference to this issue as needed.' + stale-pr-message: 'Hello there 👋. This is an automated message to let you know that in order to keep the repository running smoothly, we automatically close issues and pull requests that have not been active for a while. If you would like to continue discussing this topic, please look for another open issue or create a new one with updated information, and include a reference to this issue as needed.' diff --git a/.gitignore b/.gitignore index 20c20c2..7e08284 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules dist .DS_Store -.crush \ No newline at end of file +.wireit \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..99dbdcf --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules +dist +.wireit +.coverage +CHANGELOG.md +bun.lock +*.bun-build \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index e7be1e6..c882f50 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,72 +2,48 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Project Overview +## Repository Overview -WebQ is a Bun/TypeScript CLI tool and MCP (Model Context Protocol) server for querying and validating Custom Elements Manifest (CEM) JSON files. It is a 1:1 port of the Go `webq` tool at `../webq/`. It enables AI assistants and CLI users to explore Web Components documentation and validate HTML usage against CEM definitions. +This is a Bun/TypeScript monorepo (workspaces) containing tools for validating Web Components usage against [Custom Elements Manifest](https://github.com/webcomponents/custom-elements-manifest) (CEM) files. Uses [wireit](https://github.com/google/wireit) for incremental build caching. -## Build Commands +### Packages -```bash -bun install # Install dependencies -bun run build # Build for current platform (output: dist/webq) -bun run build:all # Cross-compile for all platforms -bun test # Run tests (vitest via bun) -bun src/index.ts # Run CLI directly via Bun -``` +- **`projects/cli/`** (`@webq/cli`) — Bun/TypeScript CLI tool and MCP server for querying and validating CEM JSON files. Includes 18 HTML validation rules. See `projects/cli/CLAUDE.md` for details. +- **`projects/eslint/`** (`@webq/eslint`) — ESLint plugin that validates custom element HTML usage via `@html-eslint/parser`. Delegates validation to the `webq` CLI. See `projects/eslint/CLAUDE.md` for details. +- **`projects/schemas/`** — JSON schemas for custom-elements, custom-attributes, custom-patterns, custom-styles, DTCG tokens, and VSCode custom data formats. + +## Quick Reference -### Running a Single Test +### Monorepo (root) ```bash -bun test src/internal/elements/store.test.ts -bun test src/internal/validate/rules/no-unknown-attr.test.ts +bun install # Install all dependencies +bun run ci # Build + test all packages (wireit, cached) +bun run format # Check formatting (prettier) +bun run format:fix # Fix formatting ``` -## Architecture - -### Package Structure - -- `src/cli.ts` - Yargs CLI commands (all commands in one file) -- `src/cli/helpers.ts` - Shared CLI helpers (store creation, config loading, path resolution) -- `src/cli/format.ts` - Markdown formatters for terminal output -- `src/internal/elements/` - Core CEM logic (types, parsing, querying, resolving) -- `src/internal/config/` - Config file loading (`webq.config.json`) -- `src/internal/mcp/` - MCP server implementation (tools and resources) -- `src/internal/validate/` - HTML validation engine (parser, rules, runner) -- `src/internal/validate/rules/` - Individual validation rules (18 rules) -- `src/internal/patterns/` - Compositional patterns support -- `src/internal/attributes/` - Custom attributes support -- `src/internal/styles/` - Custom styles support -- `src/internal/vscode/` - VSCode custom data adapter -- `src/internal/resolve/` - Shared file discovery logic -- `testdata/` - Test fixtures - -### Key Components - -**Store (`src/internal/elements/store.ts`)**: Central query engine that indexes manifests on creation. Provides fast lookups via `tagName → Declaration` and `path → Module` maps. - -**HTML Parser (`src/internal/validate/html.ts`)**: Custom HTML tokenizer with character-level attribute parsing for precise line:col position tracking. Uses `LineIndex` with binary search for byte offset → position mapping. - -**Validation Engine (`src/internal/validate/validate.ts`)**: Runs 18 rules against parsed HTML. Rules self-register via `registerRule()` in `rules/index.ts`. Supports `ConfigurableRule`, `PatternAwareRule`, `StyleAwareRule`, `CustomAttrAwareRule` interfaces. +### CLI (`projects/cli`) -**MCP Server (`src/internal/mcp/server.ts`)**: Uses `@modelcontextprotocol/sdk` with `StdioServerTransport`. Registers 17 tools and resources for element/pattern/attribute/style queries plus HTML validation. - -### Output Handling - -JSON command results go to stdout via `console.log()`. Human-readable output goes to stderr via `process.stderr.write()`. Exit code 1 if validation errors found. - -### Path Resolution - -Priority: `--path` CLI flag > `webq.config.json` `global.path` > `WEBQ_PATH` env var. - -### Validation Rules (18 total) +```bash +cd projects/cli +bun run ci # Lint + test + build +bun test # Run tests +bun run build # Build for all platforms +bun src/index.ts # Run CLI directly +bun test src/internal/elements/store.test.ts # Single test +``` -**Errors (10):** `no-unknown-element`, `no-unknown-attr`, `no-unknown-attr-value`, `no-unknown-event`, `no-unknown-slot`, `no-unknown-command`, `no-unknown-css-part`, `no-unknown-css-custom-property`, `no-missing-required-child`, `no-missing-sibling-binding` +### ESLint Plugin (`projects/eslint`) -**Warnings (8):** `no-deprecated-element`, `no-deprecated-attr`, `no-deprecated-event`, `no-deprecated-slot`, `no-deprecated-command`, `no-boolean-attr-value`, `no-unknown-style-value`, `no-unknown-custom-attr-value` +```bash +cd projects/eslint +bun run ci # Build + test (wireit, cached) +bun run build # TypeScript compilation +bun test # Run all tests (vitest) +npx vitest run src/rules/no-unknown-attr.test.ts # Single test +``` -## Code Style +## Cross-Cutting Concepts -- Functions should have at most 3 parameters, prefer 1 or 2 -- Prefer explicit, readable code over generic abstractions -- Mirror Go source structure for easy cross-reference with `../webq/` +Both packages consume CEM JSON (`custom-elements.json`) files — the standard format for documenting Web Components. The ESLint plugin delegates validation to the `webq` CLI at lint time; the CLI provides runtime querying and HTML validation via CLI or MCP protocol. Both packages implement the same 18 validation rules. diff --git a/README.md b/README.md index affebd7..6e26dc5 100644 --- a/README.md +++ b/README.md @@ -1,401 +1,132 @@ -# WebQ Node +# WebQ -A CLI tool for querying and validating Custom Elements Manifest (CEM) JSON files. It can also run as an MCP (Model Context Protocol) server for AI assistants. This is a Bun/TypeScript port of the [Go `webq` tool](../webq/). +Tools for querying and validating Web Components against [Custom Elements Manifest](https://github.com/webcomponents/custom-elements-manifest) (CEM) files. -## Installation +## Packages -Requires [Bun](https://bun.sh) 1.0+. +| Package | Description | +| ------------------------------------------- | ------------------------------------------------------------------- | +| [`@webq/cli`](projects/cli/README.md) | CLI tool and MCP server for querying and validating CEM files | +| [`@webq/eslint`](projects/eslint/README.md) | ESLint plugin for validating custom element HTML usage | +| [`schemas`](projects/schemas/) | JSON schemas for CEM, custom attributes, patterns, styles, and more | -```bash -bun install -``` - -### Pre-built Binaries - -Download the appropriate binary for your platform from the [releases page](https://github.com/blueprintui/webq/releases). - -| Platform | Architecture | Download | -| -------- | --------------------- | ---------------------- | -| macOS | Apple Silicon (arm64) | `webq-macos-arm64` | -| macOS | Intel (x64) | `webq-macos-x64` | -| Linux | arm64 | `webq-linux-arm64` | -| Linux | x86_64 (x64) | `webq-linux-x64` | -| Windows | x86_64 (x64) | `webq-windows-x64.exe` | - -## Configuration +## Quick Start -### Config File +### CLI -Create a `webq.config.json` in your project root to configure paths and per-rule settings: - -```json -{ - "global": { - "path": ["./node_modules/@org/components", "./node_modules/@org/icons"] - }, - "validate-html": { - "rules": { - "no-unknown-element": ["error", { "tags": ["my-extra-tag"] }], - "no-unknown-event": ["error", { "events": ["custom-event"] }], - "no-deprecated-element": "warn", - "no-boolean-attr-value": "off" - } - } -} -``` - -Rules use ESLint-style config: either a severity string (`"error"`, `"warn"`, `"off"`) or a tuple `["severity", { ...options }]`. - -The `no-unknown-element` rule accepts a `tags` allowlist and the `no-unknown-event` rule accepts an `events` allowlist. - -### Path Resolution Priority - -Schema paths are resolved in this order: - -1. `--path` CLI flag -2. `webq.config.json` `global.path` array -3. `WEBQ_PATH` environment variable - -### Config Flag - -Use `--config` to specify a config file path explicitly: +Download a pre-built binary from the [releases page](https://github.com/blueprintui/webq/releases), or install via npm: ```bash -webq validate-html '' --config ./path/to/webq.config.json +npm install -g @webq/cli ``` -Without `--config`, webq auto-discovers `webq.config.json` in the current directory. - -## Usage - -### CLI Commands - -The `--path` flag accepts directories (searched recursively for `custom-elements.json`). - -| Command | Parameters | Description | -| ------------------------ | ------------ | ---------------------------------------------------------- | -| `element.list` | | List all custom elements | -| `element` | `` | Get full details for a specific element | -| `element.attributes` | `` | Get attributes for an element | -| `element.properties` | `` | Get properties/fields for an element | -| `element.methods` | `` | Get methods for an element | -| `element.events` | `` | Get events for an element | -| `element.commands` | `` | Get invoker commands for an element | -| `element.slots` | `` | Get slots for an element | -| `element.css-properties` | `` | Get CSS custom properties for an element | -| `element.css-parts` | `` | Get CSS parts for an element | -| `pattern.list` | | List all compositional patterns | -| `pattern` | `` | Get full details for a specific pattern | -| `attribute.list` | | List all custom attributes | -| `attribute` | `` | Get details for a custom attribute | -| `style.property.list` | | List all CSS custom properties | -| `style.property` | `` | Get details for a CSS custom property | -| `validate-manifest` | | Validate a Custom Elements Manifest file | -| `validate-html` | `` | Validate HTML against CEM definitions | -| `mcp` | | Start the MCP server on STDIO transport | -| `setup-mcp` | | Add webq MCP server to `.mcp.json` | -| `setup-skill` | | Create Claude Code skill at `.claude/skills/webq/SKILL.md` | - ```bash # List all custom elements -webq element.list --path . - -# Search an entire node_modules directory webq element.list --path ./node_modules -# Multiple paths (comma-separated) -webq element.list --path ./lib1,./node_modules/@org - -# Get full details for a specific element +# Get details for a specific element webq element bp-button --path ./node_modules -# Get specific element information -webq element.attributes bp-button --path ./node_modules -webq element.properties bp-button --path ./node_modules -webq element.methods bp-button --path ./node_modules -webq element.events bp-button --path ./node_modules -webq element.commands bp-button --path ./node_modules -webq element.slots bp-button --path ./node_modules -webq element.css-properties bp-button --path ./node_modules -webq element.css-parts bp-button --path ./node_modules - -# List patterns, attributes, styles -webq pattern.list --path ./node_modules -webq attribute.list --path ./node_modules -webq style.property.list --path ./node_modules - -# Validate manifest structure -webq validate-manifest --path . -webq validate-manifest --path ./node_modules - -# Validate HTML against the manifest +# Validate HTML against CEM definitions webq validate-html 'Click' --path ./node_modules - -# Validate HTML with JSON output (ESLint-compatible format) -webq validate-html '' --path ./node_modules --json ``` -### MCP Server +See the [CLI README](projects/cli/README.md) for the full command reference, configuration, and MCP tool documentation. -Start the MCP server on STDIO transport: +### ESLint Plugin ```bash -webq mcp --path . +npm install @webq/eslint --save-dev +``` + +```js +// eslint.config.js +import htmlParser from '@html-eslint/parser'; +import { recommended } from '@webq/eslint'; -# Or scan all manifests in node_modules -webq mcp --path ./node_modules +export default [ + { + files: ['**/*.html'], + languageOptions: { parser: htmlParser }, + ...recommended({ cem: './path/to/custom-elements.json' }) + } +]; ``` -### Setup with Claude Code +See the [ESLint README](projects/eslint/README.md) for manual configuration and the full rule list. + +## MCP Server + +WebQ can run as an [MCP](https://modelcontextprotocol.io/) server, providing AI assistants with tools for querying element APIs and validating HTML. -Run the setup commands to configure the MCP server and Claude Code skill: +### Claude Code ```bash -# Add webq MCP server to .mcp.json webq setup-mcp - -# Create Claude Code skill at .claude/skills/webq/SKILL.md webq setup-skill ``` -Both commands are safe to run in existing projects. If the configuration already exists, they will notify you and offer a `--force` flag to update. - -### Configure with Cursor +### Cursor -Add the following to your Cursor configuration `.cursor/mcp.json`: +Add to `.cursor/mcp.json`: ```json { "mcpServers": { "webq": { "command": "webq", - "description": "Search and query for Web Components APIs via Custom Elements Manifest", "args": ["mcp", "--path", "./node_modules"] } } } ``` -## MCP Tools - -The server exposes the following tools: - -| Tool | Description | Parameters | -| ---------------------------- | -------------------------------------------- | ----------------- | -| `element_get_list` | List all custom elements with tag names | none | -| `element_get` | Get full details for a specific element | `tagName: string` | -| `element_get_attributes` | Get attributes for an element | `tagName: string` | -| `element_get_properties` | Get properties/fields for an element | `tagName: string` | -| `element_get_methods` | Get methods with parameters and return types | `tagName: string` | -| `element_get_events` | Get events for an element | `tagName: string` | -| `element_get_commands` | Get invoker commands for an element | `tagName: string` | -| `element_get_slots` | Get slots for an element | `tagName: string` | -| `element_get_css_properties` | Get CSS custom properties | `tagName: string` | -| `element_get_css_parts` | Get CSS parts | `tagName: string` | -| `pattern_get_list` | List all compositional patterns | none | -| `pattern_get` | Get full details for a specific pattern | `name: string` | -| `attribute_get_list` | List all custom attributes | none | -| `attribute_get` | Get details for a custom attribute | `name: string` | -| `style_property_list` | List all CSS custom properties | none | -| `style_property_get` | Get details for a CSS custom property | `name: string` | -| `validate_html` | Validate HTML against CEM definitions | `html: string` | - -## MCP Resources - -The server exposes the following resources: - -| Resource | URI Pattern | Description | -| -------- | ----------------- | ------------------ | -| Manifest | `webq://manifest` | Full manifest JSON | - -## HTML Validation - -The `validate-html` command and `validate_html` MCP tool check HTML against the Custom Elements Manifest, running 18 validation rules: - -| Rule | Severity | Description | -| -------------------------------- | -------- | -------------------------------------------------------------------------------------------------------- | -| `no-unknown-element` | error | Custom elements must exist in the manifest | -| `no-unknown-attr` | error | Attributes must be defined (global attributes like `class`, `id`, `data-*`, `aria-*` are always allowed) | -| `no-unknown-attr-value` | error | Values must match string literal union types (e.g., `'primary' \| 'secondary'`) | -| `no-unknown-event` | error | Event bindings (`@event`, `(event)`, `on-event`, `onevent`) must match defined events | -| `no-unknown-slot` | error | Slot attribute values must match the parent element's defined slots | -| `no-unknown-command` | error | Command/commandfor pairs must match the target element's defined commands | -| `no-unknown-css-part` | error | `::part()` selectors in `', + options: [webqOption] + }, + { + code: '', + options: [webqOption] + }, + // non-custom element selector — skip + { + code: '', + options: [webqOption] + }, + // unknown custom element — not in CEM, skip + { + code: '', + options: [webqOption] + }, + // valid inline style + { + code: '', + options: [webqOption] + }, + // inline style on non-custom element — skip + { + code: '
', + options: [webqOption] + } + ], + invalid: [ + // unknown property in style tag + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownCssProperty' }] + }, + // unknown property in inline style + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownCssProperty' }] + }, + // multiple unknown properties + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownCssProperty' }, { messageId: 'unknownCssProperty' }] + } + ] +}); diff --git a/projects/eslint/src/rules/no-unknown-css-custom-property.ts b/projects/eslint/src/rules/no-unknown-css-custom-property.ts new file mode 100644 index 0000000..3c4fc02 --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-css-custom-property.ts @@ -0,0 +1,41 @@ +import type { Rule } from 'eslint'; +import { webqOptionSchema } from '../utils/schema.js'; +import { runWebqValidation } from '../utils/webq.js'; + +const RULE_ID = 'no-unknown-css-custom-property'; + +const rule: Rule.RuleModule = { + meta: { + type: 'problem', + docs: { + description: 'Disallow CSS custom properties not defined in the Custom Elements Manifest' + }, + schema: webqOptionSchema, + messages: { + unknownCssProperty: '{{webqMessage}}' + } + }, + + create(context) { + const options = context.options[0] as { path: string } | undefined; + if (!options?.path) return {}; + + function checkDocument() { + const html = context.sourceCode.getText(); + const messages = runWebqValidation(html, options!.path); + + for (const msg of messages) { + if (msg.ruleId !== RULE_ID) continue; + context.report({ + loc: { start: { line: msg.line, column: msg.column - 1 }, end: { line: msg.line, column: msg.column } }, + messageId: 'unknownCssProperty', + data: { webqMessage: msg.message } + }); + } + } + + return { Document: checkDocument } as unknown as Rule.RuleListener; + } +}; + +export default rule; diff --git a/projects/eslint/src/rules/no-unknown-css-part.test.ts b/projects/eslint/src/rules/no-unknown-css-part.test.ts new file mode 100644 index 0000000..aa00c5f --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-css-part.test.ts @@ -0,0 +1,49 @@ +import rule from './no-unknown-css-part.js'; +import { ruleTester, webqOption } from './test-utils.js'; + +ruleTester.run('no-unknown-css-part', rule, { + valid: [ + // valid parts + { + code: '', + options: [webqOption] + }, + { + code: '', + options: [webqOption] + }, + // non-custom element — skip + { + code: '', + options: [webqOption] + }, + // unknown custom element — not in CEM, skip + { + code: '', + options: [webqOption] + }, + // no style tag + { + code: '', + options: [webqOption] + } + ], + invalid: [ + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownCssPart' }] + }, + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownCssPart' }] + }, + // multiple invalid parts + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownCssPart' }, { messageId: 'unknownCssPart' }] + } + ] +}); diff --git a/projects/eslint/src/rules/no-unknown-css-part.ts b/projects/eslint/src/rules/no-unknown-css-part.ts new file mode 100644 index 0000000..a75e4d2 --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-css-part.ts @@ -0,0 +1,41 @@ +import type { Rule } from 'eslint'; +import { webqOptionSchema } from '../utils/schema.js'; +import { runWebqValidation } from '../utils/webq.js'; + +const RULE_ID = 'no-unknown-css-part'; + +const rule: Rule.RuleModule = { + meta: { + type: 'problem', + docs: { + description: 'Disallow ::part() selectors not defined in the Custom Elements Manifest' + }, + schema: webqOptionSchema, + messages: { + unknownCssPart: '{{webqMessage}}' + } + }, + + create(context) { + const options = context.options[0] as { path: string } | undefined; + if (!options?.path) return {}; + + function checkDocument() { + const html = context.sourceCode.getText(); + const messages = runWebqValidation(html, options!.path); + + for (const msg of messages) { + if (msg.ruleId !== RULE_ID) continue; + context.report({ + loc: { start: { line: msg.line, column: msg.column - 1 }, end: { line: msg.line, column: msg.column } }, + messageId: 'unknownCssPart', + data: { webqMessage: msg.message } + }); + } + } + + return { Document: checkDocument } as unknown as Rule.RuleListener; + } +}; + +export default rule; diff --git a/projects/eslint/src/rules/no-unknown-custom-attr-value.test.ts b/projects/eslint/src/rules/no-unknown-custom-attr-value.test.ts new file mode 100644 index 0000000..e90746e --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-custom-attr-value.test.ts @@ -0,0 +1,42 @@ +import rule from './no-unknown-custom-attr-value.js'; +import { ruleTester, webqOption } from './test-utils.js'; + +ruleTester.run('no-unknown-custom-attr-value', rule, { + valid: [ + // valid token-list value + { + code: '
', + options: [webqOption] + }, + // valid enum value + { + code: '
', + options: [webqOption] + }, + // attribute not in custom-attributes — skip + { + code: '
', + options: [webqOption] + } + ], + invalid: [ + // unknown token in token-list value + { + code: '
', + options: [webqOption], + errors: [{ messageId: 'unknownCustomAttrValue' }] + }, + // unknown enum value + { + code: '
', + options: [webqOption], + errors: [{ messageId: 'unknownCustomAttrValue' }] + }, + // multiple unknown tokens + { + code: '
', + options: [webqOption], + errors: [{ messageId: 'unknownCustomAttrValue' }, { messageId: 'unknownCustomAttrValue' }] + } + ] +}); diff --git a/projects/eslint/src/rules/no-unknown-custom-attr-value.ts b/projects/eslint/src/rules/no-unknown-custom-attr-value.ts new file mode 100644 index 0000000..d9c3043 --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-custom-attr-value.ts @@ -0,0 +1,41 @@ +import type { Rule } from 'eslint'; +import { webqOptionSchema } from '../utils/schema.js'; +import { runWebqValidation } from '../utils/webq.js'; + +const RULE_ID = 'no-unknown-custom-attr-value'; + +const rule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: 'Disallow unknown token/enum values for custom attributes defined in custom-attributes.json' + }, + schema: webqOptionSchema, + messages: { + unknownCustomAttrValue: '{{webqMessage}}' + } + }, + + create(context) { + const options = context.options[0] as { path: string } | undefined; + if (!options?.path) return {}; + + function checkDocument() { + const html = context.sourceCode.getText(); + const messages = runWebqValidation(html, options!.path); + + for (const msg of messages) { + if (msg.ruleId !== RULE_ID) continue; + context.report({ + loc: { start: { line: msg.line, column: msg.column - 1 }, end: { line: msg.line, column: msg.column } }, + messageId: 'unknownCustomAttrValue', + data: { webqMessage: msg.message } + }); + } + } + + return { Document: checkDocument } as unknown as Rule.RuleListener; + } +}; + +export default rule; diff --git a/projects/eslint/src/rules/no-unknown-element.test.ts b/projects/eslint/src/rules/no-unknown-element.test.ts new file mode 100644 index 0000000..00038f8 --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-element.test.ts @@ -0,0 +1,41 @@ +import rule from './no-unknown-element.js'; +import { ruleTester, webqOption, webqPath } from './test-utils.js'; + +ruleTester.run('no-unknown-element', rule, { + valid: [ + // known elements from CEM + { code: '', options: [webqOption] }, + { code: '', options: [webqOption] }, + { code: '', options: [webqOption] }, + // standard HTML elements (no dash, not checked) + { code: '
', options: [webqOption] }, + { code: '', options: [webqOption] }, + // additional tags option + { + code: '', + options: [{ path: webqPath, tags: ['third-party-element'] }] + }, + { + code: '', + options: [{ path: webqPath, tags: ['x-widget', 'x-other'] }] + } + ], + invalid: [ + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownElement' }] + }, + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownElement' }] + }, + // additional tags don't cover this one + { + code: '', + options: [{ path: webqPath, tags: ['other-element'] }], + errors: [{ messageId: 'unknownElement' }] + } + ] +}); diff --git a/projects/eslint/src/rules/no-unknown-element.ts b/projects/eslint/src/rules/no-unknown-element.ts new file mode 100644 index 0000000..a6b7046 --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-element.ts @@ -0,0 +1,66 @@ +import type { Rule } from 'eslint'; +import { runWebqValidation } from '../utils/webq.js'; + +const RULE_ID = 'no-unknown-element'; + +const rule: Rule.RuleModule = { + meta: { + type: 'problem', + docs: { + description: 'Disallow custom elements not defined in the Custom Elements Manifest' + }, + schema: [ + { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Directory path to the webq manifest files' + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Additional custom element tag names to allow' + } + }, + required: ['path'], + additionalProperties: false + } + ], + messages: { + unknownElement: '{{webqMessage}}' + } + }, + + create(context) { + const options = context.options[0] as { path: string; tags?: string[] } | undefined; + if (!options?.path) return {}; + + const additionalTags = new Set(options.tags ?? []); + + function checkDocument() { + const html = context.sourceCode.getText(); + const messages = runWebqValidation(html, options!.path); + + for (const msg of messages) { + if (msg.ruleId !== RULE_ID) continue; + + // Post-filter: skip tags in the allowlist + if (additionalTags.size > 0) { + const tagMatch = msg.message.match(/Unknown custom element <(.+?)>/); + if (tagMatch && additionalTags.has(tagMatch[1])) continue; + } + + context.report({ + loc: { start: { line: msg.line, column: msg.column - 1 }, end: { line: msg.line, column: msg.column } }, + messageId: 'unknownElement', + data: { webqMessage: msg.message } + }); + } + } + + return { Document: checkDocument } as unknown as Rule.RuleListener; + } +}; + +export default rule; diff --git a/projects/eslint/src/rules/no-unknown-event.test.ts b/projects/eslint/src/rules/no-unknown-event.test.ts new file mode 100644 index 0000000..e167859 --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-event.test.ts @@ -0,0 +1,65 @@ +import rule from './no-unknown-event.js'; +import { ruleTester, webqOption, webqPath } from './test-utils.js'; + +ruleTester.run('no-unknown-event', rule, { + valid: [ + // on* syntax + { code: '', options: [webqOption] }, + { code: '', options: [webqOption] }, + // @event syntax (Lit) + { code: '', options: [webqOption] }, + { code: '', options: [webqOption] }, + // on-event syntax + { code: '', options: [webqOption] }, + { code: '', options: [webqOption] }, + // (event) syntax (Angular) + { code: '', options: [webqOption] }, + { code: '', options: [webqOption] }, + // non-custom elements are ignored + { code: '
', options: [webqOption] }, + // unknown custom element — not in CEM, skip + { code: '', options: [webqOption] }, + // non-event attributes are ignored + { code: '', options: [webqOption] }, + // on* with non-native events — not in the native event handlers whitelist, so not recognized as event bindings + { code: '', options: [webqOption] }, + { code: '', options: [webqOption] }, + // additional events option + { + code: '', + options: [{ path: webqPath, events: ['custom-event'] }] + } + ], + invalid: [ + // @event syntax — unknown event + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownEvent' }] + }, + // on-event syntax — unknown event + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownEvent' }] + }, + // (event) syntax — unknown event + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownEvent' }] + }, + // case sensitive — Close !== close + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownEvent' }] + }, + // additional events don't cover this one + { + code: '', + options: [{ path: webqPath, events: ['other-event'] }], + errors: [{ messageId: 'unknownEvent' }] + } + ] +}); diff --git a/projects/eslint/src/rules/no-unknown-event.ts b/projects/eslint/src/rules/no-unknown-event.ts new file mode 100644 index 0000000..c0b1fe5 --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-event.ts @@ -0,0 +1,66 @@ +import type { Rule } from 'eslint'; +import { runWebqValidation } from '../utils/webq.js'; + +const RULE_ID = 'no-unknown-event'; + +const rule: Rule.RuleModule = { + meta: { + type: 'problem', + docs: { + description: 'Disallow event bindings not defined in the Custom Elements Manifest' + }, + schema: [ + { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Directory path to the webq manifest files' + }, + events: { + type: 'array', + items: { type: 'string' }, + description: 'Additional event names to allow on any custom element' + } + }, + required: ['path'], + additionalProperties: false + } + ], + messages: { + unknownEvent: '{{webqMessage}}' + } + }, + + create(context) { + const options = context.options[0] as { path: string; events?: string[] } | undefined; + if (!options?.path) return {}; + + const additionalEvents = new Set(options.events ?? []); + + function checkDocument() { + const html = context.sourceCode.getText(); + const messages = runWebqValidation(html, options!.path); + + for (const msg of messages) { + if (msg.ruleId !== RULE_ID) continue; + + // Post-filter: skip events in the allowlist + if (additionalEvents.size > 0) { + const eventMatch = msg.message.match(/Unknown event "(.+?)"/); + if (eventMatch && additionalEvents.has(eventMatch[1])) continue; + } + + context.report({ + loc: { start: { line: msg.line, column: msg.column - 1 }, end: { line: msg.line, column: msg.column } }, + messageId: 'unknownEvent', + data: { webqMessage: msg.message } + }); + } + } + + return { Document: checkDocument } as unknown as Rule.RuleListener; + } +}; + +export default rule; diff --git a/projects/eslint/src/rules/no-unknown-slot.test.ts b/projects/eslint/src/rules/no-unknown-slot.test.ts new file mode 100644 index 0000000..a1cb45e --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-slot.test.ts @@ -0,0 +1,29 @@ +import rule from './no-unknown-slot.js'; +import { ruleTester, webqOption } from './test-utils.js'; + +ruleTester.run('no-unknown-slot', rule, { + valid: [ + { code: 'Title', options: [webqOption] }, + { code: 'OK', options: [webqOption] }, + { code: 'Default content', options: [webqOption] }, + { code: '
OK
', options: [webqOption] }, + { code: 'OK', options: [webqOption] } + ], + invalid: [ + { + code: 'Nope', + options: [webqOption], + errors: [{ messageId: 'unknownSlot' }] + }, + { + code: 'Nope', + options: [webqOption], + errors: [{ messageId: 'unknownSlot' }] + }, + { + code: 'X', + options: [webqOption], + errors: [{ messageId: 'unknownSlot' }] + } + ] +}); diff --git a/projects/eslint/src/rules/no-unknown-slot.ts b/projects/eslint/src/rules/no-unknown-slot.ts new file mode 100644 index 0000000..7ed0c51 --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-slot.ts @@ -0,0 +1,41 @@ +import type { Rule } from 'eslint'; +import { webqOptionSchema } from '../utils/schema.js'; +import { runWebqValidation } from '../utils/webq.js'; + +const RULE_ID = 'no-unknown-slot'; + +const rule: Rule.RuleModule = { + meta: { + type: 'problem', + docs: { + description: 'Disallow slot names not defined in the parent Custom Element Manifest' + }, + schema: webqOptionSchema, + messages: { + unknownSlot: '{{webqMessage}}' + } + }, + + create(context) { + const options = context.options[0] as { path: string } | undefined; + if (!options?.path) return {}; + + function checkDocument() { + const html = context.sourceCode.getText(); + const messages = runWebqValidation(html, options!.path); + + for (const msg of messages) { + if (msg.ruleId !== RULE_ID) continue; + context.report({ + loc: { start: { line: msg.line, column: msg.column - 1 }, end: { line: msg.line, column: msg.column } }, + messageId: 'unknownSlot', + data: { webqMessage: msg.message } + }); + } + } + + return { Document: checkDocument } as unknown as Rule.RuleListener; + } +}; + +export default rule; diff --git a/projects/eslint/src/rules/no-unknown-style-value.test.ts b/projects/eslint/src/rules/no-unknown-style-value.test.ts new file mode 100644 index 0000000..43bfc06 --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-style-value.test.ts @@ -0,0 +1,59 @@ +import rule from './no-unknown-style-value.js'; +import { ruleTester, webqOption } from './test-utils.js'; + +ruleTester.run('no-unknown-style-value', rule, { + valid: [ + // valid var() referencing a global custom style token + { + code: '', + options: [webqOption] + }, + // valid var() referencing an element-scoped CEM cssProperty + { + code: '', + options: [webqOption] + }, + // non-custom-element selector with valid global token + { + code: '', + options: [webqOption] + }, + // no var() references — nothing to validate + { + code: '', + options: [webqOption] + } + ], + invalid: [ + // unknown var() reference in custom element selector + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownStyleValue' }] + }, + // unknown var() reference in non-custom-element selector + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownStyleValue' }] + }, + // inline style with unknown var() + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownStyleValue' }] + }, + // var() with fallback still warns on unknown token + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownStyleValue' }] + }, + // multiple unknown var() refs + { + code: '', + options: [webqOption], + errors: [{ messageId: 'unknownStyleValue' }, { messageId: 'unknownStyleValue' }] + } + ] +}); diff --git a/projects/eslint/src/rules/no-unknown-style-value.ts b/projects/eslint/src/rules/no-unknown-style-value.ts new file mode 100644 index 0000000..1755e88 --- /dev/null +++ b/projects/eslint/src/rules/no-unknown-style-value.ts @@ -0,0 +1,41 @@ +import type { Rule } from 'eslint'; +import { webqOptionSchema } from '../utils/schema.js'; +import { runWebqValidation } from '../utils/webq.js'; + +const RULE_ID = 'no-unknown-style-value'; + +const rule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: 'Disallow var() references to CSS custom properties not defined in custom styles or CEM' + }, + schema: webqOptionSchema, + messages: { + unknownStyleValue: '{{webqMessage}}' + } + }, + + create(context) { + const options = context.options[0] as { path: string } | undefined; + if (!options?.path) return {}; + + function checkDocument() { + const html = context.sourceCode.getText(); + const messages = runWebqValidation(html, options!.path); + + for (const msg of messages) { + if (msg.ruleId !== RULE_ID) continue; + context.report({ + loc: { start: { line: msg.line, column: msg.column - 1 }, end: { line: msg.line, column: msg.column } }, + messageId: 'unknownStyleValue', + data: { webqMessage: msg.message } + }); + } + } + + return { Document: checkDocument } as unknown as Rule.RuleListener; + } +}; + +export default rule; diff --git a/projects/eslint/src/rules/test-utils.ts b/projects/eslint/src/rules/test-utils.ts new file mode 100644 index 0000000..91d302a --- /dev/null +++ b/projects/eslint/src/rules/test-utils.ts @@ -0,0 +1,12 @@ +import { RuleTester } from 'eslint'; +import * as htmlParser from '@html-eslint/parser'; +import path from 'node:path'; + +export const webqPath = path.resolve(import.meta.dirname, '.'); +export const webqOption = { path: webqPath }; + +export const ruleTester = new RuleTester({ + languageOptions: { + parser: htmlParser + } +}); diff --git a/projects/eslint/src/utils/schema.ts b/projects/eslint/src/utils/schema.ts new file mode 100644 index 0000000..6936f7e --- /dev/null +++ b/projects/eslint/src/utils/schema.ts @@ -0,0 +1,15 @@ +import type { Rule } from 'eslint'; + +export const webqOptionSchema: Rule.RuleMetaData['schema'] = [ + { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Directory path to the webq manifest files' + } + }, + required: ['path'], + additionalProperties: false + } +]; diff --git a/projects/eslint/src/utils/webq.ts b/projects/eslint/src/utils/webq.ts new file mode 100644 index 0000000..516835c --- /dev/null +++ b/projects/eslint/src/utils/webq.ts @@ -0,0 +1,55 @@ +import { execFileSync } from 'node:child_process'; + +export interface WebqMessage { + ruleId: string; + severity: number; + message: string; + line: number; + column: number; +} + +interface WebqResult { + messages: WebqMessage[]; + errorCount: number; + warningCount: number; +} + +const cache = new Map(); +let webqMissingWarned = false; + +export function runWebqValidation(html: string, path: string): WebqMessage[] { + const key = `${path}\0${html}`; + if (cache.has(key)) return cache.get(key)!; + + try { + const stdout = execFileSync('webq', ['validate-html', html, '--path', path, '--json'], { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + const result: WebqResult = JSON.parse(stdout); + cache.set(key, result.messages); + return result.messages; + } catch (e: any) { + if (e.stdout) { + try { + const result: WebqResult = JSON.parse(e.stdout); + cache.set(key, result.messages); + return result.messages; + } catch { + /* fall through */ + } + } + + if (e.code === 'ENOENT' && !webqMissingWarned) { + webqMissingWarned = true; + console.error( + '[@webq/eslint] The "webq" CLI tool is not installed or not on PATH.\n' + + 'Install it with: go install github.com/blueprintui/webq@latest\n' + + 'Or download from: https://github.com/blueprintui/webq/releases' + ); + } + + cache.set(key, []); + return []; + } +} diff --git a/projects/eslint/tsconfig.json b/projects/eslint/tsconfig.json new file mode 100644 index 0000000..8b5d375 --- /dev/null +++ b/projects/eslint/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "**/*.spec.ts"] +} diff --git a/projects/eslint/vitest.config.ts b/projects/eslint/vitest.config.ts new file mode 100644 index 0000000..dcaf7ef --- /dev/null +++ b/projects/eslint/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true + } +}); diff --git a/schemas/custom-attributes/README.md b/projects/schemas/custom-attributes/README.md similarity index 100% rename from schemas/custom-attributes/README.md rename to projects/schemas/custom-attributes/README.md diff --git a/schemas/custom-attributes/schema.json b/projects/schemas/custom-attributes/schema.json similarity index 100% rename from schemas/custom-attributes/schema.json rename to projects/schemas/custom-attributes/schema.json diff --git a/schemas/custom-elements/README.md b/projects/schemas/custom-elements/README.md similarity index 100% rename from schemas/custom-elements/README.md rename to projects/schemas/custom-elements/README.md diff --git a/schemas/custom-elements/schema.json b/projects/schemas/custom-elements/schema.json similarity index 100% rename from schemas/custom-elements/schema.json rename to projects/schemas/custom-elements/schema.json diff --git a/schemas/custom-patterns/README.md b/projects/schemas/custom-patterns/README.md similarity index 100% rename from schemas/custom-patterns/README.md rename to projects/schemas/custom-patterns/README.md diff --git a/schemas/custom-patterns/schema.json b/projects/schemas/custom-patterns/schema.json similarity index 100% rename from schemas/custom-patterns/schema.json rename to projects/schemas/custom-patterns/schema.json diff --git a/schemas/custom-styles/README.md b/projects/schemas/custom-styles/README.md similarity index 100% rename from schemas/custom-styles/README.md rename to projects/schemas/custom-styles/README.md diff --git a/schemas/custom-styles/schema.json b/projects/schemas/custom-styles/schema.json similarity index 100% rename from schemas/custom-styles/schema.json rename to projects/schemas/custom-styles/schema.json diff --git a/schemas/dtcg/README.md b/projects/schemas/dtcg/README.md similarity index 100% rename from schemas/dtcg/README.md rename to projects/schemas/dtcg/README.md diff --git a/schemas/dtcg/schema.json b/projects/schemas/dtcg/schema.json similarity index 100% rename from schemas/dtcg/schema.json rename to projects/schemas/dtcg/schema.json diff --git a/schemas/vscode-css/README.md b/projects/schemas/vscode-css/README.md similarity index 100% rename from schemas/vscode-css/README.md rename to projects/schemas/vscode-css/README.md diff --git a/schemas/vscode-css/schema.json b/projects/schemas/vscode-css/schema.json similarity index 100% rename from schemas/vscode-css/schema.json rename to projects/schemas/vscode-css/schema.json diff --git a/schemas/vscode-html/README.md b/projects/schemas/vscode-html/README.md similarity index 100% rename from schemas/vscode-html/README.md rename to projects/schemas/vscode-html/README.md diff --git a/schemas/vscode-html/schema.json b/projects/schemas/vscode-html/schema.json similarity index 100% rename from schemas/vscode-html/schema.json rename to projects/schemas/vscode-html/schema.json diff --git a/release.config.js b/release.config.js new file mode 100644 index 0000000..5af8706 --- /dev/null +++ b/release.config.js @@ -0,0 +1,86 @@ +import fs from 'node:fs'; + +const DRY_RUN = false; +const packageFilePath = `${process.cwd()}/package.json`; +const packageFile = JSON.parse(fs.readFileSync(packageFilePath)); +const [_org, scope] = packageFile.name.split('/'); + +export default { + dryRun: DRY_RUN, + tagFormat: `${packageFile.name}-v\${version}`, + branches: ['main'], + plugins: [ + [ + '@semantic-release/commit-analyzer', + { + releaseRules: [ + // catch all filter + { breaking: true, release: false }, + { type: 'feat', release: false }, + { type: 'fix', release: false }, + { type: 'chore', release: false }, + // scope only matches trigger release + { breaking: true, scope, release: 'major' }, + { type: 'feat', scope, release: 'minor' }, + { type: 'fix', scope, release: 'patch' } + ] + } + ], + [ + '@semantic-release/release-notes-generator', + { + preset: 'conventionalcommits', + presetConfig: { + ignoreCommits: `^(?![^]*\\(${scope}\\))(?![^]*\\[${scope}\\]).*$` + } + } + ], + [ + '@semantic-release/changelog', + { + changelogFile: 'CHANGELOG.md' + } + ], + [ + '@semantic-release/exec', + { + publishCmd: `bun publish --provenance --registry=https://registry.npmjs.org ${DRY_RUN ? '--dry-run' : ''} --access=public` + } + ], + [ + '@semantic-release/git', + { + assets: ['CHANGELOG.md', 'package.json', 'projects/**/package.json', 'projects/**/CHANGELOG.md'], + message: `chore(release): ${packageFile.name}` + '-v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}' + } + ], + [ + '@semantic-release/github', + { + success: '🎉 This issue has been resolved in version ${nextRelease.version} 🎉', + assets: [ + { + label: 'linux-arm64', + path: `dist/webq-linux-arm64` + }, + { + label: 'linux-x64', + path: `dist/webq-linux-x64` + }, + { + label: 'macos-arm64', + path: `dist/webq-macos-arm64` + }, + { + label: 'macos-x64', + path: `dist/webq-macos-x64` + }, + { + label: 'windows-x64', + path: `dist/webq-windows-x64.exe` + } + ] + } + ] + ] +};