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 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
@@ -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
+ }