@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.
Files changed (2) hide show
  1. package/index.js +219 -0
  2. 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
+ }