@planningcenter/eslint-plugin-tapestry 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ * @planningcenter/design-engineering
@@ -0,0 +1,26 @@
1
+ name: Linting
2
+
3
+ on: [pull_request]
4
+
5
+ jobs:
6
+ linting:
7
+ runs-on: ubuntu-latest
8
+ permissions:
9
+ contents: read
10
+ checks: write
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: "20"
16
+ cache: "yarn"
17
+ - run: yarn install
18
+ - uses: planningcenter/balto-typescript@v1.2.0
19
+ env:
20
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21
+ with:
22
+ conclusionLevel: "failure"
23
+ - uses: reviewdog/action-eslint@v1
24
+ with:
25
+ github_token: ${{ secrets.GITHUB_TOKEN }}
26
+ reporter: github-pr-check
@@ -0,0 +1,23 @@
1
+ name: PCO-Release - Release Automation
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ release-automation:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: "20"
20
+ cache: "yarn"
21
+ - uses: planningcenter/pco-release-action/release-by-pr@v1
22
+ with:
23
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,27 @@
1
+ name: Create Pre-Release
2
+ on:
3
+ issue_comment:
4
+ types: [created]
5
+
6
+ jobs:
7
+ create-rc-release:
8
+ if: github.event.issue.pull_request && contains(github.event.comment.body, '@pco-release rc')
9
+ uses: planningcenter/pco-release-action/.github/workflows/release-candidate.yml@v1
10
+ secrets: inherit
11
+ permissions:
12
+ contents: write
13
+ pull-requests: write
14
+ packages: write
15
+ with:
16
+ only: accounts,calendar,check-ins,giving,groups,home,people,registrations,services
17
+
18
+ create-qa-release:
19
+ if: github.event.issue.pull_request && contains(github.event.comment.body, '@pco-release qa')
20
+ permissions:
21
+ contents: write
22
+ pull-requests: write
23
+ packages: write
24
+ uses: planningcenter/pco-release-action/.github/workflows/qa-release.yml@v1
25
+ secrets: inherit
26
+ with:
27
+ only: accounts,calendar,check-ins,giving,groups,home,people,registrations,services
@@ -0,0 +1,19 @@
1
+ name: PCO-Release - Create Release on Merge
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+ branches:
7
+ - main
8
+
9
+ jobs:
10
+ create-release:
11
+ if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'pco-release-pending')
12
+ permissions:
13
+ contents: write
14
+ pull-requests: write
15
+ packages: write
16
+ uses: planningcenter/pco-release-action/.github/workflows/release.yml@v1
17
+ secrets: inherit
18
+ with:
19
+ pr-number: ${{ github.event.pull_request.number }}
@@ -0,0 +1,22 @@
1
+ name: PCO-Release - Require Changelog Update
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ require-changelog-update:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ pull-requests: read
12
+ contents: read
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - id: changed-files
16
+ uses: tj-actions/changed-files@v46
17
+ with:
18
+ files: CHANGELOG.md
19
+ - if: steps.changed-files.outputs.any_changed == 'false'
20
+ run: |
21
+ echo "Pull Requests require an update to the CHANGELOG.md file."
22
+ exit 1
@@ -0,0 +1,19 @@
1
+ name: PCO-Release - Sync With Labels
2
+
3
+ on:
4
+ pull_request:
5
+ types: [labeled]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ sync-with-labels:
13
+ if: ${{ github.event.pull_request.head.ref == 'pco-release--internal' && (github.event.label.name == 'pco-release-patch' || github.event.label.name == 'pco-release-minor' || github.event.label.name == 'pco-release-major') }}
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: planningcenter/pco-release-action/sync-with-labels@v1
18
+ with:
19
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,21 @@
1
+ name: Revert
2
+ on:
3
+ workflow_dispatch:
4
+ inputs:
5
+ release-tag:
6
+ description: "Version to deploy"
7
+ required: true
8
+ pr-number:
9
+ description: "Pull Request number to post results of results"
10
+ required: true
11
+
12
+ jobs:
13
+ revert:
14
+ permissions:
15
+ contents: write
16
+ pull-requests: write
17
+ secrets: inherit
18
+ uses: planningcenter/pco-release-action/.github/workflows/revert.yml@v1
19
+ with:
20
+ release-tag: ${{ inputs.release-tag }}
21
+ pr-number: ${{ inputs.pr-number }}
@@ -0,0 +1,22 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "**"
7
+ - "!main"
8
+
9
+ jobs:
10
+ run-tests:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v4
15
+ - name: Setup Node
16
+ uses: actions/setup-node@v4
17
+ with:
18
+ node-version: "20"
19
+ cache: "yarn"
20
+ - run: yarn install
21
+ - name: Run eslint
22
+ run: yarn lint
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # eslint-plugin-tapestry
2
+
3
+ ESLint plugin for Tapestry components to encourage best practices and accessibility.
4
+
5
+ > This plugin only tests components provided by Tapestry; it will ignore native HTML elements, Tapestry-React, etc.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ yarn add @planningcenter/eslint-plugin-tapestry --dev
11
+ ```
12
+
13
+ ## Configuration
14
+
15
+ ### Recommended Configuration
16
+
17
+ #### Flat Config
18
+ ```javascript
19
+ import tapestry from '@planningcenter/eslint-plugin-tapestry';
20
+
21
+ export default [
22
+ tapestry.configs.recommended,
23
+ ];
24
+ ```
25
+
26
+ #### Legacy Config
27
+ ```json
28
+ "plugins": [
29
+ "tapestry"
30
+ ],
31
+ ...
32
+ {
33
+ "extends": ["plugin:tapestry/recommended"]
34
+ }
35
+ ```
36
+
37
+ ### ESLint Flat Config (eslint.config.js)
38
+
39
+ ```javascript
40
+ import tapestry from '@planningcenter/eslint-plugin-tapestry';
41
+
42
+ export default [
43
+ {
44
+ plugins: {
45
+ tapestry,
46
+ },
47
+ rules: {
48
+ 'tapestry/valid-href': 'error',
49
+ },
50
+ },
51
+ ];
52
+ ```
53
+
54
+ ### Legacy Config (.eslintrc.json)
55
+
56
+ ```json
57
+ {
58
+ "plugins": ["tapestry"],
59
+ "rules": {
60
+ "tapestry/valid-href": "error"
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## Rules
66
+
67
+ | Rule | Category | Description |
68
+ | ---- | -------- | ----------- |
69
+ | [valid-href](docs/rules/valid-href.md) | Accessibility | Ensures that `BaseLink`, `Link`, and `IconLink` components have valid `href` values |
70
+
71
+ ## Development
72
+
73
+ ```bash
74
+ yarn test # Run tests
75
+ yarn test --watch # Run tests in watch mode
76
+ ```
77
+
78
+ ## Setup for initial release testing
79
+
80
+ ### Tapestry
81
+ 1. Open a terminal to this repo and run `yarn && yalc publish`
82
+ 2. Open a new terminal to Tapestry and create a temp test branch from `main`
83
+ 3. Run `yalc add @planningcenter/eslint-plugin-tapestry@1.0.0 --dev`
84
+ 5. In `eslint.config.mjs`, add or modify
85
+ * `import tapestry from "@planningcenter/eslint-plugin-tapestry"`
86
+ * `plugins: {tapestry}`
87
+ * `rules: {"tapestry/valid-href": "error"}`
88
+ 6. Restart the ESLint server (`cmd` + `shift` + `P` > `ESLint: Restart Eslint Server`)
89
+ 7. Open `packages/tapestry/src/components/link/index.stories.tsx` and notice that the `tapestry-react` `Link` isn't reporting any lint errors (no red squiggle lines)
90
+ 8. Comment out the `Link` import and add a line to `import { Link } from "@planningcenter/tapestry"`
91
+ * You should get red squiggle lines on the `Link`
92
+ * Test out different variations of `href` to see the lint errors
93
+ 9. In your terminal, run `yarn lint`, notice you'll see `tapestry/valid-href` errors
94
+
95
+ ### Groups
96
+
97
+ 1. Open a terminal to this repo and run `yarn && yalc publish`
98
+ 2. Open a new terminal to Groups and create a temp test branch from `main`
99
+ 3. Run `yalc add @planningcenter/eslint-plugin-tapestry@1.0.0 --dev`
100
+ 5. In `eslint.config.mjs`, add or modify
101
+ * `import tapestry from "@planningcenter/eslint-plugin-tapestry"`
102
+ * `plugins: {tapestry}`
103
+ * `rules: {"tapestry/valid-href": "error"}`
104
+ * `files: ["**/*.ts", "**/*.tsx", "**/*.jsx", "**/*.js"],`
105
+ 6. Restart the ESLint server (`cmd` + `shift` + `P` > `ESLint: Restart Eslint Server`)
106
+ 7. Open `app/javascript/staff_only/staff_only_nav_links.tsx` and notice that the `tapestry-react` `Link` isn't reporting any lint errors (no red squiggle lines)
107
+ 8. Comment out the `Link` import and add a line to `import { Link } from "@planningcenter/tapestry"`
108
+ * You should get red squiggle lines on the `Link`
109
+ * Test out different variations of `href` to see the lint errors
110
+ 9. In your terminal, run `yarn lint`, notice you'll see `tapestry/valid-href` errors
@@ -0,0 +1,82 @@
1
+ # valid-href
2
+
3
+ Ensures that `BaseLink`, `Link`, `IconLink` components have valid `href` values.
4
+
5
+ ## Rule Details
6
+
7
+ This rule enforces that Tapestry link components (`BaseLink`, `Link`, and `IconLink`) have valid `href` values to ensure proper accessibility support for keyboard navigation and screen readers.
8
+
9
+ ### Requirements
10
+
11
+ - ✅ `href` must be specified as a direct prop (not in spread props)
12
+ - ✅ `href` cannot be empty string `""`
13
+ - ✅ `href` cannot be just a hash `"#"`
14
+ - ✅ `href` cannot contain `javascript`
15
+
16
+ ## Examples
17
+
18
+ ### ❌ Invalid
19
+
20
+ ```jsx
21
+ // Missing href
22
+ <BaseLink>Click me</BaseLink>
23
+
24
+ // Invalid href values
25
+ <Link href="">Empty</Link>
26
+ <Link href="#">Hash only</Link>
27
+ <Link href="javascript:void(0)">JS void</Link>
28
+
29
+ // href in spread props
30
+ <IconLink {...props}>Icon</IconLink>
31
+ ```
32
+
33
+ ### ✅ Valid
34
+
35
+ ```jsx
36
+ <BaseLink href="/page">Click me</BaseLink>
37
+ <Link href="https://example.com">External</Link>
38
+ <IconLink href="/dashboard">Dashboard</IconLink>
39
+ <Link href="mailto:test@example.com">Email</Link>
40
+ <IconLink href="tel:555-1234">Call</IconLink>
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ This rule is enabled by default in the recommended configuration.
46
+
47
+ ```json
48
+ {
49
+ "rules": {
50
+ "tapestry/valid-href": "error"
51
+ }
52
+ }
53
+ ```
54
+
55
+ ## Background
56
+
57
+ In HTML and React, the `href` attribute in anchor tags is defined as either a `string` or `undefined` by default. This flexibility, while convenient, can lead to accessibility and user experience issues when improper values are used. This RFC addresses why enforcing proper `href` values is crucial.
58
+
59
+ The `href` attribute is a critical component of HTML anchor tags for several reasons:
60
+
61
+ ### Accessibility Considerations
62
+
63
+ - **Screen Reader Navigation:** Screen readers announce links and their destinations to users. Without a valid `href`, screen readers might announce it as "link to nowhere" or skip it entirely.
64
+ - **Keyboard Navigation:** The `href` attribute makes links naturally focusable and interactive with keyboard navigation. Links without proper hrefs break this expected behavior.
65
+ - **Semantic Clarity:** A proper `href` communicates the purpose of the link to assistive technologies, helping users understand where they'll go when activating it.
66
+
67
+ ### User Experience & Performance
68
+
69
+ - **Expected Behavior:** Users expect clicking a link to navigate somewhere. Empty or JavaScript-based pseudo-links (`href="#"` or `href="javascript:void(0)"`) frustrate this expectation.
70
+ - **Browser Optimizations:** Browsers can pre-fetch content from valid URLs, improving page transition speed and responsiveness.
71
+ - **Default Link Behaviors:** Proper `href`s enable right-click context menus, "open in new tab," and other browser features users rely on.
72
+
73
+ ### Technical Benefits
74
+
75
+ - **Analytics:** Most analytics tools track link clicks based on href values, helping understand user navigation patterns.
76
+ - **History Management:** Real URLs properly update browser history, enabling back/forward navigation and bookmarking.
77
+ - **Testing:** Valid `href`s make automated testing more reliable, as tools can verify navigation paths.
78
+
79
+ ## Further Reading
80
+
81
+ - [Web Content Accessibility Guidelines (WCAG) on Links](https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context.html)
82
+ - [MDN: HTMLAnchorElement.href](https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement/href)
package/index.js ADDED
@@ -0,0 +1,27 @@
1
+ const validHrefRule = require('./rules/valid-href');
2
+
3
+ module.exports = {
4
+ rules: {
5
+ 'valid-href': validHrefRule,
6
+ },
7
+ configs: {
8
+ recommended: {
9
+ plugins: ['tapestry'],
10
+ rules: {
11
+ 'tapestry/valid-href': 'error',
12
+ },
13
+ },
14
+ // Legacy configuration for .eslintrc.json
15
+ legacy: {
16
+ plugins: ['tapestry'],
17
+ rules: {
18
+ 'tapestry/valid-href': 'error',
19
+ },
20
+ },
21
+ },
22
+ // Flat config support for eslint.config.js
23
+ meta: {
24
+ name: 'eslint-plugin-tapestry',
25
+ version: '1.0.0',
26
+ },
27
+ };
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ testMatch: ['**/tests/**/*.test.js'],
4
+ collectCoverageFrom: [
5
+ 'rules/**/*.js',
6
+ 'index.js',
7
+ ],
8
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@planningcenter/eslint-plugin-tapestry",
3
+ "version": "0.1.0",
4
+ "description": "ESLint plugin for Tapestry React components",
5
+ "repository": "https://github.com/planningcenter/eslint-plugin-tapestry.git",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "test": "jest",
9
+ "test:watch": "jest --watch"
10
+ },
11
+ "keywords": [
12
+ "eslint",
13
+ "eslintplugin",
14
+ "eslint-plugin",
15
+ "tapestry",
16
+ "react",
17
+ "accessibility"
18
+ ],
19
+ "author": "Planning Center",
20
+ "license": "UNLICENSED",
21
+ "devDependencies": {
22
+ "@babel/core": "^7.23.0",
23
+ "@babel/preset-env": "^7.23.0",
24
+ "@babel/preset-react": "^7.22.0",
25
+ "eslint": "^8.50.0",
26
+ "jest": "^29.7.0"
27
+ },
28
+ "peerDependencies": {
29
+ "eslint": ">=8.0.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ }
34
+ }
@@ -0,0 +1,117 @@
1
+ module.exports = {
2
+ meta: {
3
+ type: 'problem',
4
+ docs: {
5
+ description: 'Ensure BaseLink, Link, IconLink components have valid href values',
6
+ category: 'Accessibility',
7
+ recommended: true,
8
+ url: 'https://github.com/planningcenter/eslint-plugin-tapestry/blob/main/docs/rules/valid-href.md',
9
+ },
10
+ fixable: null,
11
+ schema: [],
12
+ messages: {
13
+ missingHref: 'Tapestry {{componentName}} component must have an href prop',
14
+ invalidHref: 'Tapestry {{componentName}} component href cannot be "{{value}}" - use a valid URL, path, or Button component instead.',
15
+ hrefInSpread: 'Tapestry {{componentName}} component href must be specified as a direct prop, not in spread props',
16
+ },
17
+ },
18
+
19
+ create(context) {
20
+ const targetComponents = new Set(['BaseLink', 'Link', 'IconLink']);
21
+ const invalidHrefValues = new Set(['', '#']);
22
+ const tapestryImports = new Set();
23
+
24
+ function isJavaScriptUrl(value) {
25
+ if (typeof value !== 'string') return false;
26
+ return value.toLowerCase().includes('javascript');
27
+ }
28
+
29
+ function isTapestryComponent(componentName) {
30
+ // Only check Tapestry components if imported from current Tapestry
31
+ return tapestryImports.has(componentName);
32
+ }
33
+
34
+ function checkHrefProp(node, componentName) {
35
+ let hasDirectHref = false;
36
+ let hasSpreadProps = false;
37
+ let hrefValue = null;
38
+
39
+ // Check for direct href prop
40
+ for (const prop of node.attributes) {
41
+ if (prop.type === 'JSXAttribute' && prop.name.name === 'href') {
42
+ hasDirectHref = true;
43
+
44
+ // Extract href value for validation
45
+ if (prop.value) {
46
+ if (prop.value.type === 'Literal') {
47
+ hrefValue = prop.value.value;
48
+ } else if (prop.value.type === 'JSXExpressionContainer') {
49
+ if (prop.value.expression.type === 'Literal') {
50
+ hrefValue = prop.value.expression.value;
51
+ }
52
+ }
53
+ }
54
+ } else if (prop.type === 'JSXSpreadAttribute') {
55
+ hasSpreadProps = true;
56
+ }
57
+ }
58
+
59
+ // Report missing href
60
+ if (!hasDirectHref && !hasSpreadProps) {
61
+ context.report({
62
+ node,
63
+ messageId: 'missingHref',
64
+ data: { componentName },
65
+ });
66
+ return;
67
+ }
68
+
69
+ // Report href in spread props
70
+ if (!hasDirectHref && hasSpreadProps) {
71
+ context.report({
72
+ node,
73
+ messageId: 'hrefInSpread',
74
+ data: { componentName },
75
+ });
76
+ return;
77
+ }
78
+
79
+ // Validate href value if we have one
80
+ if (hasDirectHref && hrefValue !== null) {
81
+ if (invalidHrefValues.has(hrefValue) || isJavaScriptUrl(hrefValue)) {
82
+ context.report({
83
+ node,
84
+ messageId: 'invalidHref',
85
+ data: {
86
+ componentName,
87
+ value: hrefValue
88
+ },
89
+ });
90
+ }
91
+ }
92
+ }
93
+
94
+ return {
95
+ ImportDeclaration(node) {
96
+ // Track imports from @planningcenter/tapestry
97
+ if (node.source.value === '@planningcenter/tapestry') {
98
+ node.specifiers.forEach(spec => {
99
+ if (spec.type === 'ImportSpecifier') {
100
+ tapestryImports.add(spec.imported.name);
101
+ }
102
+ });
103
+ }
104
+ },
105
+
106
+ JSXOpeningElement(node) {
107
+ if (node.name.type === 'JSXIdentifier') {
108
+ const componentName = node.name.name;
109
+
110
+ if (targetComponents.has(componentName) && isTapestryComponent(componentName)) {
111
+ checkHrefProp(node, componentName);
112
+ }
113
+ }
114
+ },
115
+ };
116
+ },
117
+ };
@@ -0,0 +1,208 @@
1
+ const { RuleTester } = require('eslint');
2
+ const rule = require('../rules/valid-href');
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 2018,
7
+ sourceType: 'module',
8
+ ecmaFeatures: {
9
+ jsx: true,
10
+ },
11
+ },
12
+ });
13
+
14
+ ruleTester.run('valid-href', rule, {
15
+ valid: [
16
+ // Valid href values - Tapestry components with imports
17
+ {
18
+ code: `
19
+ import { BaseLink } from '@planningcenter/tapestry';
20
+ <BaseLink href="/page">Click me</BaseLink>
21
+ `,
22
+ },
23
+ {
24
+ code: `
25
+ import { Link } from '@planningcenter/tapestry';
26
+ <Link href="https://example.com">External</Link>
27
+ `,
28
+ },
29
+ {
30
+ code: `
31
+ import { IconLink } from '@planningcenter/tapestry';
32
+ <IconLink href="/dashboard">Dashboard</IconLink>
33
+ `,
34
+ },
35
+
36
+ // Legacy components should be ignored (no @planningcenter/tapestry import)
37
+ {
38
+ code: `
39
+ import { Link } from 'tapestry-react';
40
+ <Link href="#">Legacy component ignored</Link>
41
+ `,
42
+ },
43
+ {
44
+ code: `
45
+ import { Link } from '@planningcenter/tapestry-react';
46
+ <Link href="#">Legacy component ignored</Link>
47
+ `,
48
+ },
49
+
50
+ // Components that aren't target components should be ignored
51
+ '<Button>Click me</Button>',
52
+ '<CustomLink href="#">Custom component</CustomLink>',
53
+
54
+ // Dynamic href values (we can't validate these statically)
55
+ {
56
+ code: `
57
+ import { BaseLink, Link } from '@planningcenter/tapestry';
58
+ <div>
59
+ <BaseLink href={dynamicHref}>Dynamic</BaseLink>
60
+ <Link href={\`/user/\${userId}\`}>Template literal</Link>
61
+ </div>
62
+ `,
63
+ },
64
+ ],
65
+
66
+ invalid: [
67
+ // Missing href - Tapestry components (with imports)
68
+ {
69
+ code: `
70
+ import { BaseLink } from '@planningcenter/tapestry';
71
+ <BaseLink>Click me</BaseLink>
72
+ `,
73
+ errors: [{
74
+ messageId: 'missingHref',
75
+ data: { componentName: 'BaseLink' },
76
+ }],
77
+ },
78
+ {
79
+ code: `
80
+ import { Link } from '@planningcenter/tapestry';
81
+ <Link>External</Link>
82
+ `,
83
+ errors: [{
84
+ messageId: 'missingHref',
85
+ data: { componentName: 'Link' },
86
+ }],
87
+ },
88
+ {
89
+ code: `
90
+ import { IconLink } from '@planningcenter/tapestry';
91
+ <IconLink>Dashboard</IconLink>
92
+ `,
93
+ errors: [{
94
+ messageId: 'missingHref',
95
+ data: { componentName: 'IconLink' },
96
+ }],
97
+ },
98
+
99
+ // Empty href - Tapestry components
100
+ {
101
+ code: `
102
+ import { BaseLink } from '@planningcenter/tapestry';
103
+ <BaseLink href="">Empty</BaseLink>
104
+ `,
105
+ errors: [{
106
+ messageId: 'invalidHref',
107
+ data: { componentName: 'BaseLink', value: '' },
108
+ }],
109
+ },
110
+ {
111
+ code: `
112
+ import { Link } from '@planningcenter/tapestry';
113
+ <Link href={""}>Empty expression</Link>
114
+ `,
115
+ errors: [{
116
+ messageId: 'invalidHref',
117
+ data: { componentName: 'Link', value: '' },
118
+ }],
119
+ },
120
+
121
+ // Hash only - Tapestry components
122
+ {
123
+ code: `
124
+ import { BaseLink } from '@planningcenter/tapestry';
125
+ <BaseLink href="#">Hash only</BaseLink>
126
+ `,
127
+ errors: [{
128
+ messageId: 'invalidHref',
129
+ data: { componentName: 'BaseLink', value: '#' },
130
+ }],
131
+ },
132
+ {
133
+ code: `
134
+ import { IconLink } from '@planningcenter/tapestry';
135
+ <IconLink href={"#"}>Hash expression</IconLink>
136
+ `,
137
+ errors: [{
138
+ messageId: 'invalidHref',
139
+ data: { componentName: 'IconLink', value: '#' },
140
+ }],
141
+ },
142
+
143
+ // JavaScript URLs - Tapestry components
144
+ {
145
+ code: `
146
+ import { BaseLink } from '@planningcenter/tapestry';
147
+ <BaseLink href="javascript:void(0)">JS void</BaseLink>
148
+ `,
149
+ errors: [{
150
+ messageId: 'invalidHref',
151
+ data: { componentName: 'BaseLink', value: 'javascript:void(0)' },
152
+ }],
153
+ },
154
+ {
155
+ code: `
156
+ import { Link } from '@planningcenter/tapestry';
157
+ <Link href="javascript:alert('hi')">JS alert</Link>
158
+ `,
159
+ errors: [{
160
+ messageId: 'invalidHref',
161
+ data: { componentName: 'Link', value: 'javascript:alert(\'hi\')' },
162
+ }],
163
+ },
164
+ {
165
+ code: `
166
+ import { IconLink } from '@planningcenter/tapestry';
167
+ <IconLink href="JavaScript:void(0)">Uppercase JS</IconLink>
168
+ `,
169
+ errors: [{
170
+ messageId: 'invalidHref',
171
+ data: { componentName: 'IconLink', value: 'JavaScript:void(0)' },
172
+ }],
173
+ },
174
+
175
+ // href in spread props - Tapestry components
176
+ {
177
+ code: `
178
+ import { BaseLink } from '@planningcenter/tapestry';
179
+ <BaseLink {...props}>Spread props</BaseLink>
180
+ `,
181
+ errors: [{
182
+ messageId: 'hrefInSpread',
183
+ data: { componentName: 'BaseLink' },
184
+ }],
185
+ },
186
+ {
187
+ code: `
188
+ import { Link } from '@planningcenter/tapestry';
189
+ <Link {...linkProps} className="link">Spread with other props</Link>
190
+ `,
191
+ errors: [{
192
+ messageId: 'hrefInSpread',
193
+ data: { componentName: 'Link' },
194
+ }],
195
+ },
196
+ {
197
+ code: `
198
+ import { IconLink } from '@planningcenter/tapestry';
199
+ <IconLink {...rest} aria-label="Icon">Icon with spread</IconLink>
200
+ `,
201
+ errors: [{
202
+ messageId: 'hrefInSpread',
203
+ data: { componentName: 'IconLink' },
204
+ }],
205
+ },
206
+
207
+ ],
208
+ });