@planningcenter/eslint-plugin-tapestry 0.1.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +33 -0
- package/.prettierignore +24 -0
- package/.prettierrc.js +32 -0
- package/CHANGELOG.md +49 -0
- package/README.md +28 -23
- package/docs/development.md +255 -0
- package/docs/rules/valid-href.md +41 -1
- package/index.js +11 -9
- package/jest.config.js +4 -7
- package/package.json +13 -4
- package/rules/valid-href.js +49 -65
- package/tests/utils.test.js +213 -0
- package/tests/valid-href.test.js +215 -59
- package/utils/index.js +178 -0
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
env: {
|
|
3
|
+
node: true,
|
|
4
|
+
es2022: true,
|
|
5
|
+
jest: true,
|
|
6
|
+
},
|
|
7
|
+
extends: ["eslint:recommended", "plugin:prettier/recommended"],
|
|
8
|
+
parserOptions: {
|
|
9
|
+
ecmaVersion: 2022,
|
|
10
|
+
sourceType: "module",
|
|
11
|
+
ecmaFeatures: {
|
|
12
|
+
jsx: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
rules: {
|
|
16
|
+
// Prevent common errors
|
|
17
|
+
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
18
|
+
"no-console": "warn",
|
|
19
|
+
|
|
20
|
+
// ESLint plugin specific
|
|
21
|
+
"prefer-const": "error",
|
|
22
|
+
"no-var": "error",
|
|
23
|
+
},
|
|
24
|
+
overrides: [
|
|
25
|
+
{
|
|
26
|
+
// Test files can be more lenient
|
|
27
|
+
files: ["tests/**/*.js", "**/*.test.js"],
|
|
28
|
+
rules: {
|
|
29
|
+
"no-console": "off",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}
|
package/.prettierignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build outputs
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
|
|
8
|
+
# Lock files
|
|
9
|
+
yarn.lock
|
|
10
|
+
package-lock.json
|
|
11
|
+
|
|
12
|
+
# Logs
|
|
13
|
+
*.log
|
|
14
|
+
|
|
15
|
+
# Coverage reports
|
|
16
|
+
coverage/
|
|
17
|
+
|
|
18
|
+
# IDE files
|
|
19
|
+
.vscode/
|
|
20
|
+
.idea/
|
|
21
|
+
|
|
22
|
+
# OS files
|
|
23
|
+
.DS_Store
|
|
24
|
+
Thumbs.db
|
package/.prettierrc.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
// Use double quotes to match ESLint config
|
|
3
|
+
singleQuote: false,
|
|
4
|
+
|
|
5
|
+
// No semicolons to match ESLint config
|
|
6
|
+
semi: false,
|
|
7
|
+
|
|
8
|
+
// Trailing commas where valid in ES5 (objects, arrays, etc.)
|
|
9
|
+
trailingComma: "es5",
|
|
10
|
+
|
|
11
|
+
// 2 space indentation
|
|
12
|
+
tabWidth: 2,
|
|
13
|
+
useTabs: false,
|
|
14
|
+
|
|
15
|
+
// Line length
|
|
16
|
+
printWidth: 80,
|
|
17
|
+
|
|
18
|
+
// Bracket spacing
|
|
19
|
+
bracketSpacing: true,
|
|
20
|
+
|
|
21
|
+
// Arrow function parentheses
|
|
22
|
+
arrowParens: "avoid",
|
|
23
|
+
|
|
24
|
+
// End of line
|
|
25
|
+
endOfLine: "lf",
|
|
26
|
+
|
|
27
|
+
// JSX quotes
|
|
28
|
+
jsxSingleQuote: false,
|
|
29
|
+
|
|
30
|
+
// Bracket same line for JSX
|
|
31
|
+
bracketSameLine: false,
|
|
32
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
**Changes that are visual in nature are not considered breaking and will typically only result in a minor version bump.**
|
|
5
|
+
However, when those changes are made, there will be a specific **VISUAL CHANGES** section within the version notes.
|
|
6
|
+
If you are a designer or otherwise need to be pixel-perfect, please pay special attention to those sections.
|
|
7
|
+
|
|
8
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
9
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
10
|
+
|
|
11
|
+
## Unreleased
|
|
12
|
+
|
|
13
|
+
## [v1.0.1](https://github.com/planningcenter/eslint-plugin-tapestry/releases/tag/v1.0.1) - 2025-08-11
|
|
14
|
+
|
|
15
|
+
### Dependencies
|
|
16
|
+
|
|
17
|
+
- update package.json scripts for build and formatting
|
|
18
|
+
|
|
19
|
+
## [v1.0.0](https://github.com/planningcenter/eslint-plugin-tapestry/releases/tag/v1.0.0) - 2025-08-11
|
|
20
|
+
|
|
21
|
+
<!--
|
|
22
|
+
### VISUAL CHANGES
|
|
23
|
+
|
|
24
|
+
- `Component`: Looks different
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `Broken`: description of the fix
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- List of the feature
|
|
33
|
+
|
|
34
|
+
### Security
|
|
35
|
+
|
|
36
|
+
- bump library from 1.0.0 to 1.1.1
|
|
37
|
+
|
|
38
|
+
### Dependencies
|
|
39
|
+
|
|
40
|
+
- bump @planningcenter/dep from 2.0.1 to 2.1.0
|
|
41
|
+
-->
|
|
42
|
+
|
|
43
|
+
### Dependencies
|
|
44
|
+
|
|
45
|
+
- Add, configure, and run eslint and prettier
|
|
46
|
+
|
|
47
|
+
## [v1.0.0](https://github.com/planningcenter/eslint-plugin-tapestry/releases/tag/v1.0.0) - 2025-08-11
|
|
48
|
+
|
|
49
|
+
...
|
package/README.md
CHANGED
|
@@ -15,15 +15,15 @@ yarn add @planningcenter/eslint-plugin-tapestry --dev
|
|
|
15
15
|
### Recommended Configuration
|
|
16
16
|
|
|
17
17
|
#### Flat Config
|
|
18
|
+
|
|
18
19
|
```javascript
|
|
19
|
-
import tapestry from
|
|
20
|
+
import tapestry from "@planningcenter/eslint-plugin-tapestry"
|
|
20
21
|
|
|
21
|
-
export default [
|
|
22
|
-
tapestry.configs.recommended,
|
|
23
|
-
];
|
|
22
|
+
export default [tapestry.configs.recommended]
|
|
24
23
|
```
|
|
25
24
|
|
|
26
25
|
#### Legacy Config
|
|
26
|
+
|
|
27
27
|
```json
|
|
28
28
|
"plugins": [
|
|
29
29
|
"tapestry"
|
|
@@ -37,7 +37,7 @@ export default [
|
|
|
37
37
|
### ESLint Flat Config (eslint.config.js)
|
|
38
38
|
|
|
39
39
|
```javascript
|
|
40
|
-
import tapestry from
|
|
40
|
+
import tapestry from "@planningcenter/eslint-plugin-tapestry"
|
|
41
41
|
|
|
42
42
|
export default [
|
|
43
43
|
{
|
|
@@ -45,10 +45,10 @@ export default [
|
|
|
45
45
|
tapestry,
|
|
46
46
|
},
|
|
47
47
|
rules: {
|
|
48
|
-
|
|
48
|
+
"tapestry/valid-href": "error",
|
|
49
49
|
},
|
|
50
50
|
},
|
|
51
|
-
]
|
|
51
|
+
]
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
### Legacy Config (.eslintrc.json)
|
|
@@ -64,8 +64,8 @@ export default [
|
|
|
64
64
|
|
|
65
65
|
## Rules
|
|
66
66
|
|
|
67
|
-
| Rule
|
|
68
|
-
|
|
|
67
|
+
| Rule | Category | Description |
|
|
68
|
+
| -------------------------------------- | ------------- | ----------------------------------------------------------------------------------- |
|
|
69
69
|
| [valid-href](docs/rules/valid-href.md) | Accessibility | Ensures that `BaseLink`, `Link`, and `IconLink` components have valid `href` values |
|
|
70
70
|
|
|
71
71
|
## Development
|
|
@@ -78,18 +78,21 @@ yarn test --watch # Run tests in watch mode
|
|
|
78
78
|
## Setup for initial release testing
|
|
79
79
|
|
|
80
80
|
### Tapestry
|
|
81
|
+
|
|
81
82
|
1. Open a terminal to this repo and run `yarn && yalc publish`
|
|
82
83
|
2. Open a new terminal to Tapestry and create a temp test branch from `main`
|
|
83
84
|
3. Run `yalc add @planningcenter/eslint-plugin-tapestry@1.0.0 --dev`
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
4. In `eslint.config.mjs`, add or modify
|
|
86
|
+
|
|
87
|
+
- `import tapestry from "@planningcenter/eslint-plugin-tapestry"`
|
|
88
|
+
- `plugins: {tapestry}`
|
|
89
|
+
- `rules: {"tapestry/valid-href": "error"}`
|
|
90
|
+
|
|
88
91
|
6. Restart the ESLint server (`cmd` + `shift` + `P` > `ESLint: Restart Eslint Server`)
|
|
89
92
|
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
93
|
8. Comment out the `Link` import and add a line to `import { Link } from "@planningcenter/tapestry"`
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
- You should get red squiggle lines on the `Link`
|
|
95
|
+
- Test out different variations of `href` to see the lint errors
|
|
93
96
|
9. In your terminal, run `yarn lint`, notice you'll see `tapestry/valid-href` errors
|
|
94
97
|
|
|
95
98
|
### Groups
|
|
@@ -97,14 +100,16 @@ yarn test --watch # Run tests in watch mode
|
|
|
97
100
|
1. Open a terminal to this repo and run `yarn && yalc publish`
|
|
98
101
|
2. Open a new terminal to Groups and create a temp test branch from `main`
|
|
99
102
|
3. Run `yalc add @planningcenter/eslint-plugin-tapestry@1.0.0 --dev`
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
4. In `eslint.config.mjs`, add or modify
|
|
104
|
+
|
|
105
|
+
- `import tapestry from "@planningcenter/eslint-plugin-tapestry"`
|
|
106
|
+
- `plugins: {tapestry}`
|
|
107
|
+
- `rules: {"tapestry/valid-href": "error"}`
|
|
108
|
+
- `files: ["**/*.ts", "**/*.tsx", "**/*.jsx", "**/*.js"],`
|
|
109
|
+
|
|
105
110
|
6. Restart the ESLint server (`cmd` + `shift` + `P` > `ESLint: Restart Eslint Server`)
|
|
106
111
|
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
112
|
8. Comment out the `Link` import and add a line to `import { Link } from "@planningcenter/tapestry"`
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
9. In your terminal, run `yarn lint`, notice you'll see `tapestry/valid-href` errors
|
|
113
|
+
- You should get red squiggle lines on the `Link`
|
|
114
|
+
- Test out different variations of `href` to see the lint errors
|
|
115
|
+
9. In your terminal, run `yarn lint`, notice you'll see `tapestry/valid-href` errors
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Development Guide
|
|
2
|
+
|
|
3
|
+
This guide helps you create new ESLint rules for the Tapestry plugin.
|
|
4
|
+
|
|
5
|
+
## Shared Utilities
|
|
6
|
+
|
|
7
|
+
The plugin provides shared utilities in `/utils/index.js` to maintain consistency across rules and avoid code duplication.
|
|
8
|
+
|
|
9
|
+
### Import Tracking
|
|
10
|
+
|
|
11
|
+
Use `createImportTracker` to automatically handle both scoped and relative imports:
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const { createImportTracker } = require("../utils")
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
create(context) {
|
|
18
|
+
const targetComponents = new Set(["MyComponent", "AnotherComponent"])
|
|
19
|
+
const importTracker = createImportTracker(context, targetComponents)
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
ImportDeclaration(node) {
|
|
23
|
+
importTracker.processImport(node)
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
JSXOpeningElement(node) {
|
|
27
|
+
if (node.name.type === "JSXIdentifier") {
|
|
28
|
+
const componentName = node.name.name
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
targetComponents.has(componentName) &&
|
|
32
|
+
importTracker.isTapestryComponent(componentName)
|
|
33
|
+
) {
|
|
34
|
+
// Your rule logic here
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### Import Tracker Methods
|
|
44
|
+
|
|
45
|
+
- `processImport(node)` - Process an ImportDeclaration node
|
|
46
|
+
- `isTapestryComponent(componentName)` - Check if component is tracked Tapestry component
|
|
47
|
+
- `getTrackedComponents()` - Get Set of all tracked component names
|
|
48
|
+
- `isInTapestryProject()` - Check if running in Tapestry project
|
|
49
|
+
|
|
50
|
+
### Project Detection
|
|
51
|
+
|
|
52
|
+
Use `isTapestryProject(filename)` to detect if ESLint is running within the Tapestry project:
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
const { isTapestryProject } = require("../utils")
|
|
56
|
+
|
|
57
|
+
// In your rule
|
|
58
|
+
const filename = context.getFilename()
|
|
59
|
+
const isInTapestryProject = isTapestryProject(filename)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### JSX Utilities
|
|
63
|
+
|
|
64
|
+
Use `jsxUtils` for common JSX operations:
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
const { jsxUtils } = require("../utils")
|
|
68
|
+
|
|
69
|
+
// Extract attribute value
|
|
70
|
+
const hrefProp = jsxUtils.findAttribute(node, "href")
|
|
71
|
+
const hrefValue = hrefProp ? jsxUtils.extractAttributeValue(hrefProp) : null
|
|
72
|
+
|
|
73
|
+
// Check for spread props
|
|
74
|
+
const hasSpreadProps = jsxUtils.hasSpreadProps(node)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### JSX Utils Methods
|
|
78
|
+
|
|
79
|
+
- `findAttribute(node, attributeName)` - Find JSX attribute by name
|
|
80
|
+
- `extractAttributeValue(prop)` - Extract value from JSX attribute
|
|
81
|
+
- `hasSpreadProps(node)` - Check if JSX element has spread props
|
|
82
|
+
|
|
83
|
+
## Creating a New Rule
|
|
84
|
+
|
|
85
|
+
1. **Create the rule file**: `rules/my-new-rule.js`
|
|
86
|
+
2. **Use shared utilities**: Import and use utilities for consistency
|
|
87
|
+
3. **Add tests**: Create `tests/my-new-rule.test.js`
|
|
88
|
+
4. **Register the rule**: Add to `index.js`
|
|
89
|
+
5. **Document the rule**: Create `docs/rules/my-new-rule.md`
|
|
90
|
+
|
|
91
|
+
### Example Rule Structure
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
const { createImportTracker, jsxUtils } = require("../utils")
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
meta: {
|
|
98
|
+
type: "problem", // or 'suggestion', 'layout'
|
|
99
|
+
docs: {
|
|
100
|
+
description: "Description of what the rule does",
|
|
101
|
+
category: "Best Practices", // or 'Accessibility', 'Stylistic Issues'
|
|
102
|
+
recommended: true,
|
|
103
|
+
url: "https://github.com/planningcenter/eslint-plugin-tapestry/blob/main/docs/rules/my-new-rule.md",
|
|
104
|
+
},
|
|
105
|
+
fixable: null, // or 'code', 'whitespace'
|
|
106
|
+
schema: [], // JSON schema for rule options
|
|
107
|
+
messages: {
|
|
108
|
+
messageId: "Error message with {{placeholder}}",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
create(context) {
|
|
113
|
+
const targetComponents = new Set(["ComponentName"])
|
|
114
|
+
const importTracker = createImportTracker(context, targetComponents)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
ImportDeclaration(node) {
|
|
118
|
+
importTracker.processImport(node)
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
JSXOpeningElement(node) {
|
|
122
|
+
if (node.name.type === "JSXIdentifier") {
|
|
123
|
+
const componentName = node.name.name
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
targetComponents.has(componentName) &&
|
|
127
|
+
importTracker.isTapestryComponent(componentName)
|
|
128
|
+
) {
|
|
129
|
+
// Your validation logic here
|
|
130
|
+
context.report({
|
|
131
|
+
node,
|
|
132
|
+
messageId: "messageId",
|
|
133
|
+
data: { placeholder: "value" },
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Testing Strategy
|
|
144
|
+
|
|
145
|
+
### Rule Tests
|
|
146
|
+
|
|
147
|
+
Use the existing test patterns:
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
const { RuleTester } = require("eslint")
|
|
151
|
+
const rule = require("../rules/my-new-rule")
|
|
152
|
+
|
|
153
|
+
const ruleTester = new RuleTester({
|
|
154
|
+
parserOptions: {
|
|
155
|
+
ecmaVersion: 2018,
|
|
156
|
+
sourceType: "module",
|
|
157
|
+
ecmaFeatures: { jsx: true },
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
ruleTester.run("my-new-rule", rule, {
|
|
162
|
+
valid: [
|
|
163
|
+
// External project tests
|
|
164
|
+
{
|
|
165
|
+
code: `
|
|
166
|
+
import { Component } from '@planningcenter/tapestry';
|
|
167
|
+
<Component validProp="value">Valid</Component>
|
|
168
|
+
`,
|
|
169
|
+
},
|
|
170
|
+
// Tapestry project tests with filename
|
|
171
|
+
{
|
|
172
|
+
code: `
|
|
173
|
+
import { Component } from './components/component';
|
|
174
|
+
<Component validProp="value">Valid</Component>
|
|
175
|
+
`,
|
|
176
|
+
filename: "/packages/tapestry/src/test.tsx",
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
invalid: [
|
|
180
|
+
{
|
|
181
|
+
code: `
|
|
182
|
+
import { Component } from '@planningcenter/tapestry';
|
|
183
|
+
<Component>Invalid</Component>
|
|
184
|
+
`,
|
|
185
|
+
errors: [{ messageId: "messageId" }],
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Utility Tests
|
|
192
|
+
|
|
193
|
+
Test your utilities separately if you create new ones:
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
const { myNewUtility } = require("../utils")
|
|
197
|
+
|
|
198
|
+
describe("myNewUtility", () => {
|
|
199
|
+
test("does what it should", () => {
|
|
200
|
+
expect(myNewUtility("input")).toBe("expected output")
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Rule Registration
|
|
206
|
+
|
|
207
|
+
Add your rule to `index.js`:
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
const myNewRule = require("./rules/my-new-rule")
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
rules: {
|
|
214
|
+
"my-new-rule": myNewRule,
|
|
215
|
+
// ... existing rules
|
|
216
|
+
},
|
|
217
|
+
configs: {
|
|
218
|
+
recommended: {
|
|
219
|
+
rules: {
|
|
220
|
+
"tapestry/my-new-rule": "error",
|
|
221
|
+
// ... existing rules
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Best Practices
|
|
229
|
+
|
|
230
|
+
1. **Use shared utilities** for consistency and maintainability
|
|
231
|
+
2. **Test both contexts** (external projects and Tapestry project)
|
|
232
|
+
3. **Document behavior clearly** in rule documentation
|
|
233
|
+
4. **Follow existing patterns** for rule structure and naming
|
|
234
|
+
5. **Add comprehensive tests** covering edge cases
|
|
235
|
+
6. **Use descriptive error messages** with helpful context
|
|
236
|
+
|
|
237
|
+
## File Structure
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
├── rules/
|
|
241
|
+
│ ├── valid-href.js # Example rule
|
|
242
|
+
│ └── my-new-rule.js # Your new rule
|
|
243
|
+
├── tests/
|
|
244
|
+
│ ├── valid-href.test.js # Rule tests
|
|
245
|
+
│ ├── utils.test.js # Utility tests
|
|
246
|
+
│ └── my-new-rule.test.js # Your rule tests
|
|
247
|
+
├── utils/
|
|
248
|
+
│ └── index.js # Shared utilities
|
|
249
|
+
├── docs/
|
|
250
|
+
│ ├── rules/
|
|
251
|
+
│ │ ├── valid-href.md # Rule documentation
|
|
252
|
+
│ │ └── my-new-rule.md # Your rule docs
|
|
253
|
+
│ └── development.md # This file
|
|
254
|
+
└── index.js # Main plugin file
|
|
255
|
+
```
|
package/docs/rules/valid-href.md
CHANGED
|
@@ -6,6 +6,11 @@ Ensures that `BaseLink`, `Link`, `IconLink` components have valid `href` values.
|
|
|
6
6
|
|
|
7
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
8
|
|
|
9
|
+
The rule automatically detects whether it's running within the Tapestry project itself or in an external project that consumes Tapestry:
|
|
10
|
+
|
|
11
|
+
- **In external projects**: Only checks components imported from `@planningcenter/tapestry`
|
|
12
|
+
- **In the Tapestry project**: Also checks components imported via relative paths (e.g., `./components/link`, `../link/index`)
|
|
13
|
+
|
|
9
14
|
### Requirements
|
|
10
15
|
|
|
11
16
|
- ✅ `href` must be specified as a direct prop (not in spread props)
|
|
@@ -15,29 +20,64 @@ This rule enforces that Tapestry link components (`BaseLink`, `Link`, and `IconL
|
|
|
15
20
|
|
|
16
21
|
## Examples
|
|
17
22
|
|
|
23
|
+
### Import Context
|
|
24
|
+
|
|
25
|
+
The rule behavior depends on the import source:
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
// External projects - checks only scoped imports
|
|
29
|
+
import { Link } from "@planningcenter/tapestry"
|
|
30
|
+
;<Link href="/page">Valid</Link>
|
|
31
|
+
|
|
32
|
+
// Legacy components are ignored
|
|
33
|
+
import { Link } from "tapestry-react"
|
|
34
|
+
;<Link href="#">Ignored</Link>
|
|
35
|
+
|
|
36
|
+
// In Tapestry project - checks both scoped AND relative imports
|
|
37
|
+
import { Link } from "@planningcenter/tapestry" // Checked
|
|
38
|
+
import { BaseLink } from "./components/link" // Also checked
|
|
39
|
+
import { IconLink } from "../link/index" // Also checked
|
|
40
|
+
```
|
|
41
|
+
|
|
18
42
|
### ❌ Invalid
|
|
19
43
|
|
|
20
44
|
```jsx
|
|
21
45
|
// Missing href
|
|
46
|
+
import { BaseLink } from '@planningcenter/tapestry';
|
|
22
47
|
<BaseLink>Click me</BaseLink>
|
|
23
48
|
|
|
24
49
|
// Invalid href values
|
|
50
|
+
import { Link } from '@planningcenter/tapestry';
|
|
25
51
|
<Link href="">Empty</Link>
|
|
26
52
|
<Link href="#">Hash only</Link>
|
|
27
53
|
<Link href="javascript:void(0)">JS void</Link>
|
|
28
54
|
|
|
29
55
|
// href in spread props
|
|
56
|
+
import { IconLink } from '@planningcenter/tapestry';
|
|
30
57
|
<IconLink {...props}>Icon</IconLink>
|
|
58
|
+
|
|
59
|
+
// In Tapestry project - relative imports also checked
|
|
60
|
+
import { BaseLink } from './components/link';
|
|
61
|
+
<BaseLink href="#">Also invalid</BaseLink>
|
|
31
62
|
```
|
|
32
63
|
|
|
33
64
|
### ✅ Valid
|
|
34
65
|
|
|
35
66
|
```jsx
|
|
67
|
+
// External projects
|
|
68
|
+
import { BaseLink } from '@planningcenter/tapestry';
|
|
36
69
|
<BaseLink href="/page">Click me</BaseLink>
|
|
37
70
|
<Link href="https://example.com">External</Link>
|
|
38
71
|
<IconLink href="/dashboard">Dashboard</IconLink>
|
|
39
72
|
<Link href="mailto:test@example.com">Email</Link>
|
|
40
73
|
<IconLink href="tel:555-1234">Call</IconLink>
|
|
74
|
+
|
|
75
|
+
// In Tapestry project - relative imports work too
|
|
76
|
+
import { BaseLink } from './components/link';
|
|
77
|
+
<BaseLink href="/settings">Settings</BaseLink>
|
|
78
|
+
|
|
79
|
+
import { Link } from '../link/index';
|
|
80
|
+
<Link href="https://example.com">External</Link>
|
|
41
81
|
```
|
|
42
82
|
|
|
43
83
|
## Configuration
|
|
@@ -79,4 +119,4 @@ The `href` attribute is a critical component of HTML anchor tags for several rea
|
|
|
79
119
|
## Further Reading
|
|
80
120
|
|
|
81
121
|
- [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)
|
|
122
|
+
- [MDN: HTMLAnchorElement.href](https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement/href)
|
package/index.js
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
const validHrefRule = require(
|
|
1
|
+
const validHrefRule = require("./rules/valid-href")
|
|
2
|
+
const utils = require("./utils")
|
|
2
3
|
|
|
3
4
|
module.exports = {
|
|
4
5
|
rules: {
|
|
5
|
-
|
|
6
|
+
"valid-href": validHrefRule,
|
|
6
7
|
},
|
|
8
|
+
utils,
|
|
7
9
|
configs: {
|
|
8
10
|
recommended: {
|
|
9
|
-
plugins: [
|
|
11
|
+
plugins: ["tapestry"],
|
|
10
12
|
rules: {
|
|
11
|
-
|
|
13
|
+
"tapestry/valid-href": "error",
|
|
12
14
|
},
|
|
13
15
|
},
|
|
14
16
|
// Legacy configuration for .eslintrc.json
|
|
15
17
|
legacy: {
|
|
16
|
-
plugins: [
|
|
18
|
+
plugins: ["tapestry"],
|
|
17
19
|
rules: {
|
|
18
|
-
|
|
20
|
+
"tapestry/valid-href": "error",
|
|
19
21
|
},
|
|
20
22
|
},
|
|
21
23
|
},
|
|
22
24
|
// Flat config support for eslint.config.js
|
|
23
25
|
meta: {
|
|
24
|
-
name:
|
|
25
|
-
version:
|
|
26
|
+
name: "eslint-plugin-tapestry",
|
|
27
|
+
version: "1.0.0",
|
|
26
28
|
},
|
|
27
|
-
}
|
|
29
|
+
}
|
package/jest.config.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
testEnvironment:
|
|
3
|
-
testMatch: [
|
|
4
|
-
collectCoverageFrom: [
|
|
5
|
-
|
|
6
|
-
'index.js',
|
|
7
|
-
],
|
|
8
|
-
};
|
|
2
|
+
testEnvironment: "node",
|
|
3
|
+
testMatch: ["**/tests/**/*.test.js"],
|
|
4
|
+
collectCoverageFrom: ["rules/**/*.js", "index.js"],
|
|
5
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/eslint-plugin-tapestry",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "ESLint plugin for Tapestry React components",
|
|
5
5
|
"repository": "https://github.com/planningcenter/eslint-plugin-tapestry.git",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
|
+
"build": "echo 'No build step needed for this package'",
|
|
9
|
+
"format": "prettier --write .",
|
|
10
|
+
"format:check": "prettier --check .",
|
|
11
|
+
"lint": "eslint .",
|
|
12
|
+
"lint:fix": "eslint . --fix",
|
|
8
13
|
"test": "jest",
|
|
9
14
|
"test:watch": "jest --watch"
|
|
10
15
|
},
|
|
@@ -23,12 +28,16 @@
|
|
|
23
28
|
"@babel/preset-env": "^7.23.0",
|
|
24
29
|
"@babel/preset-react": "^7.22.0",
|
|
25
30
|
"eslint": "^8.50.0",
|
|
26
|
-
"
|
|
31
|
+
"eslint-config-prettier": "^10.1.8",
|
|
32
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
33
|
+
"jest": "^29.7.0",
|
|
34
|
+
"prettier": "^3.6.2"
|
|
27
35
|
},
|
|
28
36
|
"peerDependencies": {
|
|
29
37
|
"eslint": ">=8.0.0"
|
|
30
38
|
},
|
|
31
39
|
"engines": {
|
|
32
40
|
"node": ">=18.0.0"
|
|
33
|
-
}
|
|
34
|
-
}
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {}
|
|
43
|
+
}
|