vite-plugin-comment-attrs 0.0.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.
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +227 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thanh Dat Vo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# vite-plugin-comment-attrs
|
|
2
|
+
|
|
3
|
+
Transform JSX comments into JSX attributes.
|
|
4
|
+
|
|
5
|
+
## Why use this plugin?
|
|
6
|
+
|
|
7
|
+
This plugin is mainly useful for two workflows:
|
|
8
|
+
|
|
9
|
+
### 1. Shorten JSX syntax
|
|
10
|
+
|
|
11
|
+
Long attribute values can make JSX harder to scan, especially when using utility-first CSS frameworks.
|
|
12
|
+
Instead of writing:
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
<h1 class="rounded-lg bg-blue-500 px-4 py-2 text-white">
|
|
16
|
+
Hello Mom
|
|
17
|
+
</h1>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
You can move the long attribute value above the element:
|
|
21
|
+
```tsx
|
|
22
|
+
{/* @class rounded-lg bg-blue-500 */}
|
|
23
|
+
{/* @class px-4 py-2 text-white */}
|
|
24
|
+
<h1>Hello Mom</h1>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This keeps the JSX element itself smaller and easier to read.
|
|
28
|
+
|
|
29
|
+
### 2. Try new attribute values during development
|
|
30
|
+
You can experiment with new attribute values without directly changing the original attribute.
|
|
31
|
+
|
|
32
|
+
For example:
|
|
33
|
+
```tsx
|
|
34
|
+
{/* @class rounded-lg bg-blue-500 */}
|
|
35
|
+
<h1 class="title">Hello Mom</h1>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Output:
|
|
39
|
+
```tsx
|
|
40
|
+
<h1 class="title rounded-lg bg-blue-500">Hello Mom</h1>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This is useful when you want to test new classes, IDs, labels, or other attributes while keeping the original JSX mostly unchanged.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Merge strategies
|
|
49
|
+
|
|
50
|
+
### Append
|
|
51
|
+
Appends to the existing attribute value.
|
|
52
|
+
|
|
53
|
+
Input
|
|
54
|
+
```tsx
|
|
55
|
+
{/* @class rounded-lg */}
|
|
56
|
+
<h1 class="title">Hello</h1>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Output
|
|
60
|
+
```tsx
|
|
61
|
+
<h1 class="title rounded-lg">Hello</h1>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is useful for attributes like `class` where you want to add additional classes without removing existing ones.
|
|
65
|
+
|
|
66
|
+
### Replace
|
|
67
|
+
Replaces existing value. Last directive wins.
|
|
68
|
+
|
|
69
|
+
Input
|
|
70
|
+
```tsx
|
|
71
|
+
{/* @id first */}
|
|
72
|
+
{/* @id final */}
|
|
73
|
+
<h1 id="old">Hello</h1>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
Output:
|
|
78
|
+
```tsx
|
|
79
|
+
<h1 id="final">Hello</h1>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
### Install
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm install -D vite-plugin-comment-attrs
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
### Configuration
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
commentAttrsPlugin({
|
|
97
|
+
directives: {
|
|
98
|
+
"@class": { attr: "class", merge: "append" },
|
|
99
|
+
"@id": { attr: "id", merge: "replace" },
|
|
100
|
+
"@alt": { attr: "alt", merge: "replace" },
|
|
101
|
+
"@title": { attr: "title", merge: "replace" },
|
|
102
|
+
"@ariaLabel": { attr: "aria-label", merge: "replace" },
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Plugin order
|
|
108
|
+
|
|
109
|
+
Place `commentAttrsPlugin` before the framework plugin (e.g., `react()` or `solid()`) to
|
|
110
|
+
allows the comment attributes to be transformed before JSX being processed.
|
|
111
|
+
```tsx
|
|
112
|
+
plugins: [
|
|
113
|
+
commentAttrsPlugin(),
|
|
114
|
+
solid(),
|
|
115
|
+
]
|
|
116
|
+
```
|
|
117
|
+
```tsx
|
|
118
|
+
plugins: [
|
|
119
|
+
commentAttrsPlugin(),
|
|
120
|
+
react(),
|
|
121
|
+
]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### ReactJS example
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
|
|
128
|
+
import { defineConfig } from "vite";
|
|
129
|
+
import react from "@vitejs/plugin-react";
|
|
130
|
+
import { commentAttrsPlugin } from "vite-plugin-comment-attrs";
|
|
131
|
+
|
|
132
|
+
export default defineConfig({
|
|
133
|
+
plugins: [
|
|
134
|
+
commentAttrsPlugin({
|
|
135
|
+
directives: {
|
|
136
|
+
"@class": { attr: "className", merge: "append" },
|
|
137
|
+
"@id": { attr: "id", merge: "replace" },
|
|
138
|
+
"@alt": { attr: "alt", merge: "replace" },
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
react(),
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### SolidJS example
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { defineConfig } from "vite";
|
|
150
|
+
import solid from "vite-plugin-solid";
|
|
151
|
+
import { commentAttrsPlugin } from "vite-plugin-comment-attrs";
|
|
152
|
+
|
|
153
|
+
export default defineConfig({
|
|
154
|
+
plugins: [
|
|
155
|
+
commentAttrsPlugin({
|
|
156
|
+
directives: {
|
|
157
|
+
"@class": { attr: "class", merge: "append" },
|
|
158
|
+
"@id": { attr: "id", merge: "replace" },
|
|
159
|
+
"@alt": { attr: "alt", merge: "replace" }
|
|
160
|
+
}
|
|
161
|
+
}),
|
|
162
|
+
solid()
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Notes
|
|
169
|
+
### 1. Supported file types
|
|
170
|
+
- This plugin only processes JSX/TSX/JS/TS files.
|
|
171
|
+
- Use JSX comments:
|
|
172
|
+
```tsx
|
|
173
|
+
{/* @class rounded-lg */}
|
|
174
|
+
<h1>Hello</h1>
|
|
175
|
+
```
|
|
176
|
+
- Babel comments
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// @class rounded-lg
|
|
180
|
+
<h1>Hello</h1>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 2. Framework-specific attribute mappings
|
|
184
|
+
- SolidJS uses `class`, so map `@class` to `class`.
|
|
185
|
+
- ReactJS uses `className`, so map `@class` to `className`.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Contributing
|
|
190
|
+
Contributions are welcome!
|
|
191
|
+
If you find a bug, have an idea, or want to improve the plugin,
|
|
192
|
+
feel free to open an issue or submit a pull request.
|
|
193
|
+
|
|
194
|
+
- Clone the repository
|
|
195
|
+
```sh
|
|
196
|
+
git clone https://github.com/thanhdatvo/vite-plugin-comment-attrs.git
|
|
197
|
+
cd vite-plugin-comment-attrs
|
|
198
|
+
```
|
|
199
|
+
- Install dependencies and test
|
|
200
|
+
```sh
|
|
201
|
+
bun install
|
|
202
|
+
bun run test
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Author
|
|
208
|
+
Created by Thanh Dat Vo
|
|
209
|
+
|
|
210
|
+
## AI usage disclosure
|
|
211
|
+
AI tools was used as a part of the development process for this project.
|
|
212
|
+
It helped with implementation, documentation, and testing.
|
|
213
|
+
The final design decisions, validation, and publishing responsibility remain with the maintainer.
|
|
214
|
+
I believe the usefulness and impact of the project matter more than the specific tools used in its creation.
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
MIT © 2026 Thanh Dat Vo
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
type MergeStrategy = "append" | "replace";
|
|
4
|
+
type DirectiveRule = {
|
|
5
|
+
attr: string;
|
|
6
|
+
merge: MergeStrategy;
|
|
7
|
+
};
|
|
8
|
+
type DirectiveConfig = Record<string, DirectiveRule>;
|
|
9
|
+
type CommentAttrsPluginOptions = {
|
|
10
|
+
directives?: DirectiveConfig;
|
|
11
|
+
include?: RegExp;
|
|
12
|
+
exclude?: RegExp;
|
|
13
|
+
};
|
|
14
|
+
declare function commentAttrsPlugin(options?: CommentAttrsPluginOptions): Plugin;
|
|
15
|
+
|
|
16
|
+
export { type CommentAttrsPluginOptions, type DirectiveConfig, type DirectiveRule, type MergeStrategy, commentAttrsPlugin, commentAttrsPlugin as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { parse } from "@babel/parser";
|
|
3
|
+
import traverseModule from "@babel/traverse";
|
|
4
|
+
import generateModule from "@babel/generator";
|
|
5
|
+
import * as t from "@babel/types";
|
|
6
|
+
function interopDefault(module) {
|
|
7
|
+
return "default" in module ? module.default : module;
|
|
8
|
+
}
|
|
9
|
+
var traverse = interopDefault(traverseModule);
|
|
10
|
+
var generate = interopDefault(generateModule);
|
|
11
|
+
var DEFAULT_DIRECTIVES = {
|
|
12
|
+
"@class": { attr: "class", merge: "append" }
|
|
13
|
+
};
|
|
14
|
+
function isTargetFile(id, options) {
|
|
15
|
+
const cleanId = id.split("?")[0];
|
|
16
|
+
if (options.exclude?.test(cleanId)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (options.include) {
|
|
20
|
+
return options.include.test(cleanId);
|
|
21
|
+
}
|
|
22
|
+
return /\.(jsx|tsx|js|ts)$/.test(cleanId);
|
|
23
|
+
}
|
|
24
|
+
function escapeRegExp(value) {
|
|
25
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
+
}
|
|
27
|
+
function codeContainsAnyDirective(code, directives) {
|
|
28
|
+
return Object.keys(directives).some((directive) => code.includes(directive));
|
|
29
|
+
}
|
|
30
|
+
function isWhitespaceJsxText(node) {
|
|
31
|
+
return t.isJSXText(node) && node.value.trim() === "";
|
|
32
|
+
}
|
|
33
|
+
function getJsxAttribute(openingElement, name) {
|
|
34
|
+
return openingElement.attributes.find((attr) => {
|
|
35
|
+
return t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function getCommentTextsFromJsxNode(node) {
|
|
39
|
+
if (!t.isJSXExpressionContainer(node)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const texts = [];
|
|
43
|
+
if (t.isJSXEmptyExpression(node.expression)) {
|
|
44
|
+
texts.push(
|
|
45
|
+
...(node.expression.innerComments ?? []).map((comment) => comment.value)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
texts.push(...(node.innerComments ?? []).map((comment) => comment.value));
|
|
49
|
+
texts.push(...(node.leadingComments ?? []).map((comment) => comment.value));
|
|
50
|
+
texts.push(...(node.trailingComments ?? []).map((comment) => comment.value));
|
|
51
|
+
return texts;
|
|
52
|
+
}
|
|
53
|
+
function parseDirectiveText(text, directives) {
|
|
54
|
+
const trimmed = text.trim();
|
|
55
|
+
for (const [directive, rule] of Object.entries(directives)) {
|
|
56
|
+
const escapedDirective = escapeRegExp(directive);
|
|
57
|
+
const match = trimmed.match(new RegExp(`^${escapedDirective}\\s+(.+)$`));
|
|
58
|
+
if (!match) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
directive,
|
|
63
|
+
attrName: rule.attr,
|
|
64
|
+
merge: rule.merge,
|
|
65
|
+
value: match[1].trim()
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function getJsxDirectiveValue(node, directives) {
|
|
71
|
+
for (const text of getCommentTextsFromJsxNode(node)) {
|
|
72
|
+
const parsed = parseDirectiveText(text, directives);
|
|
73
|
+
if (parsed) {
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function addCollectedAttr(attrs, parsed) {
|
|
80
|
+
const existing = attrs.get(parsed.attrName);
|
|
81
|
+
if (!existing) {
|
|
82
|
+
attrs.set(parsed.attrName, {
|
|
83
|
+
merge: parsed.merge,
|
|
84
|
+
values: [parsed.value]
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (parsed.merge === "append") {
|
|
89
|
+
existing.merge = "append";
|
|
90
|
+
existing.values.push(parsed.value);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
existing.merge = "replace";
|
|
94
|
+
existing.values = [parsed.value];
|
|
95
|
+
}
|
|
96
|
+
function setOrMergeAttribute(openingElement, attrName, value, merge) {
|
|
97
|
+
const attr = getJsxAttribute(openingElement, attrName);
|
|
98
|
+
if (!attr) {
|
|
99
|
+
openingElement.attributes.push(
|
|
100
|
+
t.jsxAttribute(t.jsxIdentifier(attrName), t.stringLiteral(value))
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (merge === "replace") {
|
|
105
|
+
attr.value = t.stringLiteral(value);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (t.isStringLiteral(attr.value)) {
|
|
109
|
+
attr.value.value = `${attr.value.value} ${value}`.trim();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (t.isJSXExpressionContainer(attr.value) && t.isExpression(attr.value.expression)) {
|
|
113
|
+
attr.value.expression = t.templateLiteral(
|
|
114
|
+
[
|
|
115
|
+
t.templateElement({ raw: "", cooked: "" }),
|
|
116
|
+
t.templateElement({ raw: ` ${value}`, cooked: ` ${value}` }, true)
|
|
117
|
+
],
|
|
118
|
+
[attr.value.expression]
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function applyAttributes(openingElement, attrs) {
|
|
123
|
+
for (const [attrName, collected] of attrs) {
|
|
124
|
+
const value = collected.merge === "append" ? collected.values.join(" ") : collected.values[collected.values.length - 1];
|
|
125
|
+
setOrMergeAttribute(openingElement, attrName, value, collected.merge);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function processJsxChildren(children, directives) {
|
|
129
|
+
for (let i = 0; i < children.length; i++) {
|
|
130
|
+
const firstDirective = getJsxDirectiveValue(children[i], directives);
|
|
131
|
+
if (!firstDirective) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
135
|
+
const removeIndexes = [i];
|
|
136
|
+
addCollectedAttr(attrs, firstDirective);
|
|
137
|
+
let j = i + 1;
|
|
138
|
+
while (j < children.length) {
|
|
139
|
+
const child = children[j];
|
|
140
|
+
if (isWhitespaceJsxText(child)) {
|
|
141
|
+
j++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const nextDirective = getJsxDirectiveValue(child, directives);
|
|
145
|
+
if (nextDirective) {
|
|
146
|
+
addCollectedAttr(attrs, nextDirective);
|
|
147
|
+
removeIndexes.push(j);
|
|
148
|
+
j++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (t.isJSXElement(child)) {
|
|
152
|
+
applyAttributes(child.openingElement, attrs);
|
|
153
|
+
for (let k = removeIndexes.length - 1; k >= 0; k--) {
|
|
154
|
+
children.splice(removeIndexes[k], 1);
|
|
155
|
+
}
|
|
156
|
+
i--;
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function processLeadingDirectiveComments(element, directives) {
|
|
163
|
+
const comments = element.leadingComments;
|
|
164
|
+
if (!comments || comments.length === 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
168
|
+
for (const comment of comments) {
|
|
169
|
+
const parsed = parseDirectiveText(comment.value, directives);
|
|
170
|
+
if (parsed) {
|
|
171
|
+
addCollectedAttr(attrs, parsed);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (attrs.size === 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
applyAttributes(element.openingElement, attrs);
|
|
178
|
+
element.leadingComments = comments.filter((comment) => {
|
|
179
|
+
return parseDirectiveText(comment.value, directives) === null;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function commentAttrsPlugin(options = {}) {
|
|
183
|
+
const directives = options.directives ?? DEFAULT_DIRECTIVES;
|
|
184
|
+
return {
|
|
185
|
+
name: "vite-plugin-comment-attrs",
|
|
186
|
+
enforce: "pre",
|
|
187
|
+
transform(code, id) {
|
|
188
|
+
if (!isTargetFile(id, options)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (!codeContainsAnyDirective(code, directives)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const ast = parse(code, {
|
|
195
|
+
sourceType: "module",
|
|
196
|
+
plugins: ["jsx", "typescript"]
|
|
197
|
+
});
|
|
198
|
+
traverse(ast, {
|
|
199
|
+
JSXElement(path) {
|
|
200
|
+
processLeadingDirectiveComments(path.node, directives);
|
|
201
|
+
processJsxChildren(path.node.children, directives);
|
|
202
|
+
},
|
|
203
|
+
JSXFragment(path) {
|
|
204
|
+
processJsxChildren(path.node.children, directives);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const output = generate(
|
|
208
|
+
ast,
|
|
209
|
+
{
|
|
210
|
+
sourceMaps: true,
|
|
211
|
+
sourceFileName: id
|
|
212
|
+
},
|
|
213
|
+
code
|
|
214
|
+
);
|
|
215
|
+
return {
|
|
216
|
+
code: output.code,
|
|
217
|
+
map: output.map
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
var index_default = commentAttrsPlugin;
|
|
223
|
+
export {
|
|
224
|
+
commentAttrsPlugin,
|
|
225
|
+
index_default as default
|
|
226
|
+
};
|
|
227
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Plugin } from \"vite\";\nimport { parse } from \"@babel/parser\";\nimport traverseModule from \"@babel/traverse\";\nimport type { NodePath } from \"@babel/traverse\";\nimport generateModule from \"@babel/generator\";\nimport * as t from \"@babel/types\";\n\nfunction interopDefault<T extends object>(module: T | { default: T }): T {\n return \"default\" in module ? module.default : module;\n}\n\nconst traverse = interopDefault(traverseModule);\nconst generate = interopDefault(generateModule);\n\nexport type MergeStrategy = \"append\" | \"replace\";\n\nexport type DirectiveRule = {\n attr: string;\n merge: MergeStrategy;\n};\n\nexport type DirectiveConfig = Record<string, DirectiveRule>;\n\nexport type CommentAttrsPluginOptions = {\n directives?: DirectiveConfig;\n include?: RegExp;\n exclude?: RegExp;\n};\n\ntype ParsedDirective = {\n directive: string;\n attrName: string;\n merge: MergeStrategy;\n value: string;\n};\n\ntype CollectedAttr = {\n merge: MergeStrategy;\n values: string[];\n};\n\nconst DEFAULT_DIRECTIVES: DirectiveConfig = {\n \"@class\": { attr: \"class\", merge: \"append\" },\n};\n\nfunction isTargetFile(id: string, options: CommentAttrsPluginOptions) {\n const cleanId = id.split(\"?\")[0];\n\n if (options.exclude?.test(cleanId)) {\n return false;\n }\n\n if (options.include) {\n return options.include.test(cleanId);\n }\n\n return /\\.(jsx|tsx|js|ts)$/.test(cleanId);\n}\n\nfunction escapeRegExp(value: string) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction codeContainsAnyDirective(code: string, directives: DirectiveConfig) {\n return Object.keys(directives).some((directive) => code.includes(directive));\n}\n\nfunction isWhitespaceJsxText(node: t.Node): boolean {\n return t.isJSXText(node) && node.value.trim() === \"\";\n}\n\nfunction getJsxAttribute(openingElement: t.JSXOpeningElement, name: string) {\n return openingElement.attributes.find((attr) => {\n return t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name });\n }) as t.JSXAttribute | undefined;\n}\n\nfunction getCommentTextsFromJsxNode(node: t.Node): string[] {\n if (!t.isJSXExpressionContainer(node)) {\n return [];\n }\n\n const texts: string[] = [];\n\n if (t.isJSXEmptyExpression(node.expression)) {\n texts.push(\n ...(node.expression.innerComments ?? []).map((comment) => comment.value),\n );\n }\n\n texts.push(...(node.innerComments ?? []).map((comment) => comment.value));\n texts.push(...(node.leadingComments ?? []).map((comment) => comment.value));\n texts.push(...(node.trailingComments ?? []).map((comment) => comment.value));\n\n return texts;\n}\n\nfunction parseDirectiveText(\n text: string,\n directives: DirectiveConfig,\n): ParsedDirective | null {\n const trimmed = text.trim();\n\n for (const [directive, rule] of Object.entries(directives)) {\n const escapedDirective = escapeRegExp(directive);\n const match = trimmed.match(new RegExp(`^${escapedDirective}\\\\s+(.+)$`));\n\n if (!match) {\n continue;\n }\n\n return {\n directive,\n attrName: rule.attr,\n merge: rule.merge,\n value: match[1].trim(),\n };\n }\n\n return null;\n}\n\nfunction getJsxDirectiveValue(\n node: t.Node,\n directives: DirectiveConfig,\n): ParsedDirective | null {\n for (const text of getCommentTextsFromJsxNode(node)) {\n const parsed = parseDirectiveText(text, directives);\n\n if (parsed) {\n return parsed;\n }\n }\n\n return null;\n}\n\nfunction addCollectedAttr(\n attrs: Map<string, CollectedAttr>,\n parsed: ParsedDirective,\n) {\n const existing = attrs.get(parsed.attrName);\n\n if (!existing) {\n attrs.set(parsed.attrName, {\n merge: parsed.merge,\n values: [parsed.value],\n });\n return;\n }\n\n if (parsed.merge === \"append\") {\n existing.merge = \"append\";\n existing.values.push(parsed.value);\n return;\n }\n\n existing.merge = \"replace\";\n existing.values = [parsed.value];\n}\n\nfunction setOrMergeAttribute(\n openingElement: t.JSXOpeningElement,\n attrName: string,\n value: string,\n merge: MergeStrategy,\n) {\n const attr = getJsxAttribute(openingElement, attrName);\n\n if (!attr) {\n openingElement.attributes.push(\n t.jsxAttribute(t.jsxIdentifier(attrName), t.stringLiteral(value)),\n );\n return;\n }\n\n if (merge === \"replace\") {\n attr.value = t.stringLiteral(value);\n return;\n }\n\n if (t.isStringLiteral(attr.value)) {\n attr.value.value = `${attr.value.value} ${value}`.trim();\n return;\n }\n\n if (\n t.isJSXExpressionContainer(attr.value) &&\n t.isExpression(attr.value.expression)\n ) {\n attr.value.expression = t.templateLiteral(\n [\n t.templateElement({ raw: \"\", cooked: \"\" }),\n t.templateElement({ raw: ` ${value}`, cooked: ` ${value}` }, true),\n ],\n [attr.value.expression],\n );\n }\n}\n\nfunction applyAttributes(\n openingElement: t.JSXOpeningElement,\n attrs: Map<string, CollectedAttr>,\n) {\n for (const [attrName, collected] of attrs) {\n const value =\n collected.merge === \"append\"\n ? collected.values.join(\" \")\n : collected.values[collected.values.length - 1];\n\n setOrMergeAttribute(openingElement, attrName, value, collected.merge);\n }\n}\n\nfunction processJsxChildren(\n children: t.JSXElement[\"children\"],\n directives: DirectiveConfig,\n) {\n for (let i = 0; i < children.length; i++) {\n const firstDirective = getJsxDirectiveValue(children[i], directives);\n\n if (!firstDirective) {\n continue;\n }\n\n const attrs = new Map<string, CollectedAttr>();\n const removeIndexes: number[] = [i];\n\n addCollectedAttr(attrs, firstDirective);\n\n let j = i + 1;\n\n while (j < children.length) {\n const child = children[j];\n\n if (isWhitespaceJsxText(child)) {\n j++;\n continue;\n }\n\n const nextDirective = getJsxDirectiveValue(child, directives);\n\n if (nextDirective) {\n addCollectedAttr(attrs, nextDirective);\n removeIndexes.push(j);\n j++;\n continue;\n }\n\n if (t.isJSXElement(child)) {\n applyAttributes(child.openingElement, attrs);\n\n for (let k = removeIndexes.length - 1; k >= 0; k--) {\n children.splice(removeIndexes[k], 1);\n }\n\n i--;\n }\n\n break;\n }\n }\n}\n\nfunction processLeadingDirectiveComments(\n element: t.JSXElement,\n directives: DirectiveConfig,\n) {\n const comments = element.leadingComments;\n\n if (!comments || comments.length === 0) {\n return;\n }\n\n const attrs = new Map<string, CollectedAttr>();\n\n for (const comment of comments) {\n const parsed = parseDirectiveText(comment.value, directives);\n\n if (parsed) {\n addCollectedAttr(attrs, parsed);\n }\n }\n\n if (attrs.size === 0) {\n return;\n }\n\n applyAttributes(element.openingElement, attrs);\n\n element.leadingComments = comments.filter((comment) => {\n return parseDirectiveText(comment.value, directives) === null;\n });\n}\n\nexport function commentAttrsPlugin(\n options: CommentAttrsPluginOptions = {},\n): Plugin {\n const directives = options.directives ?? DEFAULT_DIRECTIVES;\n\n return {\n name: \"vite-plugin-comment-attrs\",\n enforce: \"pre\",\n\n transform(code, id) {\n if (!isTargetFile(id, options)) {\n return null;\n }\n\n if (!codeContainsAnyDirective(code, directives)) {\n return null;\n }\n\n const ast = parse(code, {\n sourceType: \"module\",\n plugins: [\"jsx\", \"typescript\"],\n });\n\n traverse(ast, {\n JSXElement(path: NodePath<t.JSXElement>) {\n processLeadingDirectiveComments(path.node, directives);\n processJsxChildren(path.node.children, directives);\n },\n\n JSXFragment(path: NodePath<t.JSXFragment>) {\n processJsxChildren(path.node.children, directives);\n },\n });\n\n const output = generate(\n ast,\n {\n sourceMaps: true,\n sourceFileName: id,\n },\n code,\n );\n\n return {\n code: output.code,\n map: output.map,\n };\n },\n };\n}\n\nexport default commentAttrsPlugin;\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-comment-attrs",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A Vite plugin that turns JSX comments into configurable JSX attributes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Thanh Dat Vo",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/thanhdatvo/vite-plugin-comment-attrs.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/thanhdatvo/vite-plugin-comment-attrs/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/thanhdatvo/vite-plugin-comment-attrs#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"vite",
|
|
18
|
+
"vite-plugin",
|
|
19
|
+
"jsx",
|
|
20
|
+
"solidjs",
|
|
21
|
+
"react",
|
|
22
|
+
"babel",
|
|
23
|
+
"tailwind"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"main": "./dist/index.js",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup src/index.ts --format esm --dts --sourcemap --clean",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"prepublishOnly": "bun run test && bun run build",
|
|
43
|
+
"pack:check": "npm pack --dry-run",
|
|
44
|
+
"dev:react": "bun --cwd examples/react dev",
|
|
45
|
+
"dev:solid": "bun --cwd examples/solid dev"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"vite": ">=5"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@babel/generator": "^7.29.1",
|
|
52
|
+
"@babel/parser": "^7.29.3",
|
|
53
|
+
"@babel/traverse": "^7.29.0",
|
|
54
|
+
"@babel/types": "^7.29.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/babel__generator": "^7.27.0",
|
|
58
|
+
"@types/babel__traverse": "^7.28.0",
|
|
59
|
+
"tsup": "^8.5.1",
|
|
60
|
+
"typescript": "^5.9.0",
|
|
61
|
+
"vite": "^8.0.11",
|
|
62
|
+
"vitest": "^4.1.5"
|
|
63
|
+
}
|
|
64
|
+
}
|