@aaqiljamal/visual-editor-babel-plugin 0.2.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/index.js +219 -0
- package/package.json +26 -0
package/index.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stamps every JSXOpeningElement with data-oid="relpath:line:col" at build time.
|
|
3
|
+
*
|
|
4
|
+
* Plugin options:
|
|
5
|
+
* - root: absolute path that data-oid paths should be relative to.
|
|
6
|
+
* Defaults to walking up from the file looking for a workspace marker
|
|
7
|
+
* (pnpm-workspace.yaml, turbo.json, lerna.json, nx.json, or .git).
|
|
8
|
+
* Falls back to `state.cwd` if no marker is found.
|
|
9
|
+
*
|
|
10
|
+
* For monorepos, point this at the monorepo root so data-oid paths
|
|
11
|
+
* are stable across packages — `packages/ui/Card.tsx:12:4` rather
|
|
12
|
+
* than `Card.tsx:12:4`.
|
|
13
|
+
*
|
|
14
|
+
* Example:
|
|
15
|
+
* plugins: [["./babel-plugin-data-oid.js", { root: "/abs/monorepo" }]]
|
|
16
|
+
*
|
|
17
|
+
* Or omit the option entirely — the plugin will find the workspace
|
|
18
|
+
* root automatically.
|
|
19
|
+
*/
|
|
20
|
+
const fs = require("node:fs");
|
|
21
|
+
// Renamed to avoid shadowing Babel's `path` parameter inside the visitor.
|
|
22
|
+
const nodePath = require("node:path");
|
|
23
|
+
|
|
24
|
+
const WORKSPACE_MARKERS = [
|
|
25
|
+
"pnpm-workspace.yaml",
|
|
26
|
+
"turbo.json",
|
|
27
|
+
"lerna.json",
|
|
28
|
+
"nx.json",
|
|
29
|
+
".git",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function findWorkspaceRoot(startDir) {
|
|
33
|
+
let dir = startDir;
|
|
34
|
+
while (dir && dir !== nodePath.dirname(dir)) {
|
|
35
|
+
for (const marker of WORKSPACE_MARKERS) {
|
|
36
|
+
try {
|
|
37
|
+
if (fs.existsSync(nodePath.join(dir, marker))) {
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
/* permission denied — keep walking */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
dir = nodePath.dirname(dir);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rootCache = new Map(); // startDir → resolved root (avoid repeated fs walks)
|
|
50
|
+
|
|
51
|
+
module.exports = function dataOidPlugin({ types: t }, options) {
|
|
52
|
+
const configuredRoot =
|
|
53
|
+
options && typeof options.root === "string" ? options.root : null;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
name: "data-oid",
|
|
57
|
+
visitor: {
|
|
58
|
+
// B2b/B3b: pre-collect default imports from .module.css files AND
|
|
59
|
+
// local styled.tagname definitions at the Program level so the
|
|
60
|
+
// JSXOpeningElement visitor below can stamp the right data attrs
|
|
61
|
+
// without re-scanning per element.
|
|
62
|
+
Program(programPath, state) {
|
|
63
|
+
const cssModuleImports = new Map();
|
|
64
|
+
const styledDefs = new Map(); // componentName → htmlTag
|
|
65
|
+
for (const node of programPath.node.body) {
|
|
66
|
+
if (
|
|
67
|
+
node.type === "ImportDeclaration" &&
|
|
68
|
+
typeof node.source?.value === "string" &&
|
|
69
|
+
node.source.value.endsWith(".module.css")
|
|
70
|
+
) {
|
|
71
|
+
for (const spec of node.specifiers || []) {
|
|
72
|
+
if (
|
|
73
|
+
spec.type === "ImportDefaultSpecifier" &&
|
|
74
|
+
spec.local?.name
|
|
75
|
+
) {
|
|
76
|
+
cssModuleImports.set(spec.local.name, node.source.value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else if (node.type === "VariableDeclaration") {
|
|
80
|
+
for (const decl of node.declarations || []) {
|
|
81
|
+
if (decl.type !== "VariableDeclarator") continue;
|
|
82
|
+
if (decl.id?.type !== "Identifier") continue;
|
|
83
|
+
const init = decl.init;
|
|
84
|
+
if (!init || init.type !== "TaggedTemplateExpression") continue;
|
|
85
|
+
const tag = init.tag;
|
|
86
|
+
if (
|
|
87
|
+
tag &&
|
|
88
|
+
tag.type === "MemberExpression" &&
|
|
89
|
+
tag.computed === false &&
|
|
90
|
+
tag.object?.type === "Identifier" &&
|
|
91
|
+
tag.object.name === "styled" &&
|
|
92
|
+
tag.property?.type === "Identifier" &&
|
|
93
|
+
// Only stamp for templates with NO interpolations (matches
|
|
94
|
+
// the v0.2 server-side support surface).
|
|
95
|
+
(init.quasi?.expressions?.length ?? 0) === 0
|
|
96
|
+
) {
|
|
97
|
+
styledDefs.set(decl.id.name, tag.property.name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
state.cssModuleImports = cssModuleImports;
|
|
103
|
+
state.styledDefs = styledDefs;
|
|
104
|
+
},
|
|
105
|
+
JSXOpeningElement(path, state) {
|
|
106
|
+
const node = path.node;
|
|
107
|
+
if (!node.loc) return;
|
|
108
|
+
|
|
109
|
+
const already = node.attributes.some(
|
|
110
|
+
(a) =>
|
|
111
|
+
a.type === "JSXAttribute" &&
|
|
112
|
+
a.name &&
|
|
113
|
+
a.name.type === "JSXIdentifier" &&
|
|
114
|
+
a.name.name === "data-oid",
|
|
115
|
+
);
|
|
116
|
+
if (already) return;
|
|
117
|
+
|
|
118
|
+
const filename = state.filename || "<unknown>";
|
|
119
|
+
|
|
120
|
+
// Resolve the path-prefix to strip. Priority:
|
|
121
|
+
// 1. Plugin option `root` (explicit, monorepo-friendly)
|
|
122
|
+
// 2. Cached workspace root from a parent dir walk
|
|
123
|
+
// 3. Walk up from this file's dir looking for workspace markers
|
|
124
|
+
// 4. state.cwd as fallback (v0.1 behavior)
|
|
125
|
+
let root = configuredRoot;
|
|
126
|
+
if (!root) {
|
|
127
|
+
const dir = nodePath.dirname(filename);
|
|
128
|
+
if (rootCache.has(dir)) {
|
|
129
|
+
root = rootCache.get(dir);
|
|
130
|
+
} else {
|
|
131
|
+
root = findWorkspaceRoot(dir);
|
|
132
|
+
rootCache.set(dir, root);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!root) root = state.cwd || "";
|
|
136
|
+
|
|
137
|
+
const rel =
|
|
138
|
+
root && filename.startsWith(root)
|
|
139
|
+
? filename.slice(root.length + 1)
|
|
140
|
+
: filename;
|
|
141
|
+
|
|
142
|
+
const oid = `${rel}:${node.loc.start.line}:${node.loc.start.column}`;
|
|
143
|
+
|
|
144
|
+
node.attributes.push(
|
|
145
|
+
t.jsxAttribute(t.jsxIdentifier("data-oid"), t.stringLiteral(oid)),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// B3b: if this JSX element's tag matches a same-file styled
|
|
149
|
+
// component definition (collected at Program level), stamp the
|
|
150
|
+
// styled-component data attrs.
|
|
151
|
+
const styledDefs = state.styledDefs;
|
|
152
|
+
if (styledDefs && styledDefs.size > 0) {
|
|
153
|
+
const elName = node.name;
|
|
154
|
+
if (
|
|
155
|
+
elName &&
|
|
156
|
+
elName.type === "JSXIdentifier" &&
|
|
157
|
+
styledDefs.has(elName.name)
|
|
158
|
+
) {
|
|
159
|
+
node.attributes.push(
|
|
160
|
+
t.jsxAttribute(
|
|
161
|
+
t.jsxIdentifier("data-styled-name"),
|
|
162
|
+
t.stringLiteral(elName.name),
|
|
163
|
+
),
|
|
164
|
+
t.jsxAttribute(
|
|
165
|
+
t.jsxIdentifier("data-styled-tag"),
|
|
166
|
+
t.stringLiteral(styledDefs.get(elName.name)),
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// B2b: if this element's className is `{identifier.property}` where
|
|
173
|
+
// the identifier is a default import from a .module.css file, stamp
|
|
174
|
+
// data-css-module-* so the overlay can detect it at runtime and
|
|
175
|
+
// route to the CSS-property mutation path.
|
|
176
|
+
const cssModuleImports = state.cssModuleImports;
|
|
177
|
+
if (cssModuleImports && cssModuleImports.size > 0) {
|
|
178
|
+
const classNameAttr = node.attributes.find(
|
|
179
|
+
(a) =>
|
|
180
|
+
a.type === "JSXAttribute" &&
|
|
181
|
+
a.name &&
|
|
182
|
+
a.name.type === "JSXIdentifier" &&
|
|
183
|
+
a.name.name === "className",
|
|
184
|
+
);
|
|
185
|
+
if (
|
|
186
|
+
classNameAttr &&
|
|
187
|
+
classNameAttr.value &&
|
|
188
|
+
classNameAttr.value.type === "JSXExpressionContainer"
|
|
189
|
+
) {
|
|
190
|
+
const expr = classNameAttr.value.expression;
|
|
191
|
+
if (
|
|
192
|
+
expr &&
|
|
193
|
+
expr.type === "MemberExpression" &&
|
|
194
|
+
expr.computed === false &&
|
|
195
|
+
expr.object &&
|
|
196
|
+
expr.object.type === "Identifier" &&
|
|
197
|
+
expr.property &&
|
|
198
|
+
expr.property.type === "Identifier"
|
|
199
|
+
) {
|
|
200
|
+
const importPath = cssModuleImports.get(expr.object.name);
|
|
201
|
+
if (importPath) {
|
|
202
|
+
node.attributes.push(
|
|
203
|
+
t.jsxAttribute(
|
|
204
|
+
t.jsxIdentifier("data-css-module-class"),
|
|
205
|
+
t.stringLiteral(expr.property.name),
|
|
206
|
+
),
|
|
207
|
+
t.jsxAttribute(
|
|
208
|
+
t.jsxIdentifier("data-css-module-file"),
|
|
209
|
+
t.stringLiteral(importPath),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aaqiljamal/visual-editor-babel-plugin",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Babel plugin: stamps data-oid + data-css-module-* + data-styled-* attributes on JSX elements so the visual-editor overlay can map runtime DOM back to source.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": ["index.js", "README.md"],
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "echo 'babel-plugin is plain CJS — no build needed'"
|
|
9
|
+
},
|
|
10
|
+
"keywords": ["visual-editor", "babel-plugin", "react", "next"],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Aaqil Jamal",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/The-Design-Alchemist/visual-editor.git",
|
|
16
|
+
"directory": "packages/babel-plugin"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/The-Design-Alchemist/visual-editor#readme",
|
|
19
|
+
"bugs": "https://github.com/The-Design-Alchemist/visual-editor/issues",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
}
|
|
26
|
+
}
|