@designtools/next-plugin 0.1.2

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.
@@ -0,0 +1,452 @@
1
+ "use client";
2
+
3
+ /**
4
+ * <CodeSurface /> — Selection overlay component for the target app.
5
+ * Mounted automatically by withDesigntools() in development.
6
+ * Communicates with the editor UI via postMessage.
7
+ *
8
+ * Refactored from core/src/inject/selection.ts into a React component
9
+ * with proper lifecycle management.
10
+ */
11
+
12
+ import { useEffect, useRef } from "react";
13
+
14
+ // Overlay elements are created imperatively (not React-rendered)
15
+ // because they need to be fixed-position overlays that don't interfere
16
+ // with the app's React tree.
17
+
18
+ export function CodeSurface() {
19
+ const stateRef = useRef({
20
+ selectionMode: false,
21
+ hoveredElement: null as Element | null,
22
+ selectedElement: null as Element | null,
23
+ selectedDomPath: null as string | null,
24
+ overlayRafId: null as number | null,
25
+ inlineStyleBackups: new Map<string, string>(),
26
+ tokenValueBackups: new Map<string, string>(),
27
+ tokenPreviewValues: new Map<string, string>(),
28
+ tokenPreviewStyle: null as HTMLStyleElement | null,
29
+ highlightOverlay: null as HTMLDivElement | null,
30
+ tooltip: null as HTMLDivElement | null,
31
+ selectedOverlay: null as HTMLDivElement | null,
32
+ });
33
+
34
+ useEffect(() => {
35
+ const s = stateRef.current;
36
+
37
+ // --- Create overlay DOM elements ---
38
+ s.highlightOverlay = document.createElement("div");
39
+ s.highlightOverlay.id = "tool-highlight";
40
+ Object.assign(s.highlightOverlay.style, {
41
+ position: "fixed",
42
+ pointerEvents: "none",
43
+ border: "2px solid #3b82f6",
44
+ backgroundColor: "rgba(59, 130, 246, 0.08)",
45
+ borderRadius: "2px",
46
+ zIndex: "99999",
47
+ display: "none",
48
+ transition: "all 0.1s ease",
49
+ });
50
+ document.body.appendChild(s.highlightOverlay);
51
+
52
+ s.tooltip = document.createElement("div");
53
+ s.tooltip.id = "tool-tooltip";
54
+ Object.assign(s.tooltip.style, {
55
+ position: "fixed",
56
+ pointerEvents: "none",
57
+ backgroundColor: "#1e1e2e",
58
+ color: "#cdd6f4",
59
+ padding: "3px 8px",
60
+ borderRadius: "4px",
61
+ fontSize: "11px",
62
+ fontFamily: "ui-monospace, monospace",
63
+ zIndex: "100000",
64
+ display: "none",
65
+ whiteSpace: "nowrap",
66
+ boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
67
+ });
68
+ document.body.appendChild(s.tooltip);
69
+
70
+ s.selectedOverlay = document.createElement("div");
71
+ s.selectedOverlay.id = "tool-selected";
72
+ Object.assign(s.selectedOverlay.style, {
73
+ position: "fixed",
74
+ pointerEvents: "none",
75
+ border: "2px solid #f59e0b",
76
+ backgroundColor: "rgba(245, 158, 11, 0.06)",
77
+ borderRadius: "2px",
78
+ zIndex: "99998",
79
+ display: "none",
80
+ });
81
+ document.body.appendChild(s.selectedOverlay);
82
+
83
+ // --- Helpers ---
84
+ function getElementName(el: Element): string {
85
+ const slot = el.getAttribute("data-slot");
86
+ if (slot) return slot.charAt(0).toUpperCase() + slot.slice(1);
87
+ return `<${el.tagName.toLowerCase()}>`;
88
+ }
89
+
90
+ function getDomPath(el: Element): string {
91
+ // Build a pure structural path using nth-child at every level.
92
+ // This is stable across class changes (HMR only replaces attributes,
93
+ // not DOM structure) and guaranteed unique.
94
+ const parts: string[] = [];
95
+ let current: Element | null = el;
96
+ while (current && current !== document.body) {
97
+ const parent = current.parentElement;
98
+ if (parent) {
99
+ const idx = Array.from(parent.children).indexOf(current) + 1;
100
+ parts.unshift(`${current.tagName.toLowerCase()}:nth-child(${idx})`);
101
+ } else {
102
+ parts.unshift(current.tagName.toLowerCase());
103
+ }
104
+ current = current.parentElement;
105
+ }
106
+ return parts.join(" > ");
107
+ }
108
+
109
+ function positionOverlay(overlay: HTMLDivElement, rect: DOMRect) {
110
+ Object.assign(overlay.style, {
111
+ left: `${rect.left}px`,
112
+ top: `${rect.top}px`,
113
+ width: `${rect.width}px`,
114
+ height: `${rect.height}px`,
115
+ display: "block",
116
+ });
117
+ }
118
+
119
+ function findSelectableElement(target: Element): Element {
120
+ let el: Element | null = target;
121
+ while (el && el !== document.body) {
122
+ if (el.getAttribute("data-slot")) return el;
123
+ el = el.parentElement;
124
+ }
125
+ return target;
126
+ }
127
+
128
+ const relevantProps = [
129
+ "display", "position", "top", "right", "bottom", "left",
130
+ "z-index", "overflow", "overflow-x", "overflow-y",
131
+ "flex-direction", "flex-wrap", "justify-content", "align-items",
132
+ "align-self", "flex-grow", "flex-shrink", "flex-basis", "order",
133
+ "grid-template-columns", "grid-template-rows",
134
+ "gap", "row-gap", "column-gap",
135
+ "width", "height", "min-width", "min-height", "max-width", "max-height",
136
+ "margin-top", "margin-right", "margin-bottom", "margin-left",
137
+ "padding-top", "padding-right", "padding-bottom", "padding-left",
138
+ "font-family", "font-size", "font-weight", "line-height",
139
+ "letter-spacing", "text-align", "text-decoration", "text-transform",
140
+ "color", "white-space",
141
+ "background-color", "background-image", "background-size", "background-position",
142
+ "border-top-width", "border-right-width", "border-bottom-width", "border-left-width",
143
+ "border-style", "border-color",
144
+ "border-top-left-radius", "border-top-right-radius",
145
+ "border-bottom-right-radius", "border-bottom-left-radius",
146
+ "opacity", "box-shadow", "transform", "transition",
147
+ ];
148
+
149
+ const inheritableProps = [
150
+ "color", "font-family", "font-size", "font-weight", "line-height",
151
+ "letter-spacing", "text-align", "text-transform", "white-space",
152
+ ];
153
+
154
+ function extractElementData(el: Element) {
155
+ const computed = getComputedStyle(el);
156
+ const rect = el.getBoundingClientRect();
157
+
158
+ const computedStyles: Record<string, string> = {};
159
+ for (const prop of relevantProps) {
160
+ computedStyles[prop] = computed.getPropertyValue(prop);
161
+ }
162
+
163
+ const parentComputedStyles: Record<string, string> = {};
164
+ const parentEl = el.parentElement;
165
+ if (parentEl) {
166
+ const parentComputed = getComputedStyle(parentEl);
167
+ for (const prop of inheritableProps) {
168
+ parentComputedStyles[prop] = parentComputed.getPropertyValue(prop);
169
+ }
170
+ }
171
+
172
+ const attributes: Record<string, string> = {};
173
+ for (const attr of Array.from(el.attributes)) {
174
+ if (attr.name.startsWith("data-")) {
175
+ attributes[attr.name] = attr.value;
176
+ }
177
+ }
178
+
179
+ // Parse data-source
180
+ let sourceFile: string | null = null;
181
+ let sourceLine: number | null = null;
182
+ let sourceCol: number | null = null;
183
+
184
+ const dataSource = el.getAttribute("data-source");
185
+ if (dataSource) {
186
+ const lastColon = dataSource.lastIndexOf(":");
187
+ const secondLastColon = dataSource.lastIndexOf(":", lastColon - 1);
188
+ if (secondLastColon > 0) {
189
+ sourceFile = dataSource.slice(0, secondLastColon);
190
+ sourceLine = parseInt(dataSource.slice(secondLastColon + 1, lastColon), 10);
191
+ sourceCol = parseInt(dataSource.slice(lastColon + 1), 10);
192
+ }
193
+ }
194
+
195
+ // For component instances: read data-instance-source directly from the DOM element.
196
+ // The Babel transform adds this attribute to component JSX (<Button>, <Card>)
197
+ // and it propagates via {...props} to the rendered DOM element, carrying exact
198
+ // page-level coordinates of each component usage site.
199
+ let instanceSourceFile: string | null = null;
200
+ let instanceSourceLine: number | null = null;
201
+ let instanceSourceCol: number | null = null;
202
+ let componentName: string | null = null;
203
+
204
+ const instanceSource = el.getAttribute("data-instance-source");
205
+ if (instanceSource && el.getAttribute("data-slot")) {
206
+ const lc = instanceSource.lastIndexOf(":");
207
+ const slc = instanceSource.lastIndexOf(":", lc - 1);
208
+ if (slc > 0) {
209
+ instanceSourceFile = instanceSource.slice(0, slc);
210
+ instanceSourceLine = parseInt(instanceSource.slice(slc + 1, lc), 10);
211
+ instanceSourceCol = parseInt(instanceSource.slice(lc + 1), 10);
212
+ }
213
+
214
+ // Derive component name from data-slot (e.g. "card-title" -> "CardTitle")
215
+ const slot = el.getAttribute("data-slot") || "";
216
+ componentName = slot
217
+ .split("-")
218
+ .map((s: string) => s.charAt(0).toUpperCase() + s.slice(1))
219
+ .join("");
220
+ }
221
+
222
+ return {
223
+ tag: el.tagName.toLowerCase(),
224
+ className: (el.getAttribute("class") || "").trim(),
225
+ computedStyles,
226
+ parentComputedStyles,
227
+ boundingRect: rect,
228
+ domPath: getDomPath(el),
229
+ textContent: (el.textContent || "").trim().slice(0, 100),
230
+ attributes,
231
+ sourceFile,
232
+ sourceLine,
233
+ sourceCol,
234
+ instanceSourceFile,
235
+ instanceSourceLine,
236
+ instanceSourceCol,
237
+ componentName,
238
+ };
239
+ }
240
+
241
+ function selectElement(el: Element) {
242
+ s.selectedElement = el;
243
+ s.selectedDomPath = getDomPath(el);
244
+ const data = extractElementData(el);
245
+ if (s.selectedOverlay) {
246
+ positionOverlay(s.selectedOverlay, data.boundingRect);
247
+ }
248
+ startOverlayTracking();
249
+ window.parent.postMessage({ type: "tool:elementSelected", data }, "*");
250
+ }
251
+
252
+ function reselectCurrentElement() {
253
+ if (!s.selectedDomPath) return;
254
+ const el = document.querySelector(s.selectedDomPath);
255
+ if (el) {
256
+ s.selectedElement = el;
257
+ const data = extractElementData(el);
258
+ if (s.selectedOverlay) {
259
+ positionOverlay(s.selectedOverlay, data.boundingRect);
260
+ }
261
+ window.parent.postMessage({ type: "tool:elementSelected", data }, "*");
262
+ }
263
+ }
264
+
265
+ function startOverlayTracking() {
266
+ if (s.overlayRafId) cancelAnimationFrame(s.overlayRafId);
267
+ let lastRect = "";
268
+ function tick() {
269
+ if (s.selectedElement && s.selectedOverlay) {
270
+ if (!document.contains(s.selectedElement)) {
271
+ if (s.selectedDomPath) {
272
+ const newEl = document.querySelector(s.selectedDomPath);
273
+ if (newEl) {
274
+ s.selectedElement = newEl;
275
+ reselectCurrentElement();
276
+ }
277
+ }
278
+ }
279
+ if (s.selectedElement && document.contains(s.selectedElement)) {
280
+ const rect = s.selectedElement.getBoundingClientRect();
281
+ const key = `${rect.left},${rect.top},${rect.width},${rect.height}`;
282
+ if (key !== lastRect) {
283
+ lastRect = key;
284
+ positionOverlay(s.selectedOverlay!, rect);
285
+ }
286
+ }
287
+ }
288
+ s.overlayRafId = requestAnimationFrame(tick);
289
+ }
290
+ tick();
291
+ }
292
+
293
+ // --- Event handlers ---
294
+ function onMouseMove(e: MouseEvent) {
295
+ if (!s.selectionMode || !s.highlightOverlay || !s.tooltip) return;
296
+ const el = document.elementFromPoint(e.clientX, e.clientY);
297
+ if (!el || el === s.highlightOverlay || el === s.tooltip || el === s.selectedOverlay) return;
298
+ const selectable = findSelectableElement(el);
299
+ if (selectable === s.hoveredElement) return;
300
+ s.hoveredElement = selectable;
301
+ const rect = selectable.getBoundingClientRect();
302
+ positionOverlay(s.highlightOverlay, rect);
303
+ const name = getElementName(selectable);
304
+ s.tooltip.textContent = name;
305
+ s.tooltip.style.display = "block";
306
+ s.tooltip.style.left = `${rect.left}px`;
307
+ s.tooltip.style.top = `${Math.max(0, rect.top - 24)}px`;
308
+ }
309
+
310
+ function onMouseLeave() {
311
+ if (!s.highlightOverlay || !s.tooltip) return;
312
+ s.highlightOverlay.style.display = "none";
313
+ s.tooltip.style.display = "none";
314
+ s.hoveredElement = null;
315
+ }
316
+
317
+ function onClick(e: MouseEvent) {
318
+ if (!s.selectionMode) return;
319
+ e.preventDefault();
320
+ e.stopPropagation();
321
+ const el = document.elementFromPoint(e.clientX, e.clientY);
322
+ if (!el || el === s.highlightOverlay || el === s.tooltip || el === s.selectedOverlay) return;
323
+ const selectable = findSelectableElement(el);
324
+ selectElement(selectable);
325
+ }
326
+
327
+ function onMessage(e: MessageEvent) {
328
+ const msg = e.data;
329
+ if (!msg || !msg.type || !msg.type.startsWith("tool:")) return;
330
+
331
+ switch (msg.type) {
332
+ case "tool:enterSelectionMode":
333
+ s.selectionMode = true;
334
+ document.body.style.cursor = "crosshair";
335
+ break;
336
+ case "tool:exitSelectionMode":
337
+ s.selectionMode = false;
338
+ document.body.style.cursor = "";
339
+ if (s.highlightOverlay) s.highlightOverlay.style.display = "none";
340
+ if (s.tooltip) s.tooltip.style.display = "none";
341
+ s.hoveredElement = null;
342
+ break;
343
+ case "tool:previewInlineStyle": {
344
+ if (s.selectedElement && s.selectedElement instanceof HTMLElement) {
345
+ const prop = msg.property as string;
346
+ const value = msg.value as string;
347
+ if (!s.inlineStyleBackups.has(prop)) {
348
+ s.inlineStyleBackups.set(prop, s.selectedElement.style.getPropertyValue(prop));
349
+ }
350
+ s.selectedElement.style.setProperty(prop, value, "important");
351
+ }
352
+ break;
353
+ }
354
+ case "tool:revertInlineStyles": {
355
+ if (s.selectedElement && s.selectedElement instanceof HTMLElement) {
356
+ for (const [prop, original] of s.inlineStyleBackups) {
357
+ if (original) {
358
+ s.selectedElement.style.setProperty(prop, original);
359
+ } else {
360
+ s.selectedElement.style.removeProperty(prop);
361
+ }
362
+ }
363
+ s.inlineStyleBackups.clear();
364
+ }
365
+ break;
366
+ }
367
+ case "tool:previewTokenValue": {
368
+ const prop = msg.property as string;
369
+ const value = msg.value as string;
370
+ // Track current preview value
371
+ s.tokenPreviewValues.set(prop, value);
372
+ // Inject a <style> tag override.
373
+ // Tailwind v4 resolves @theme variables at build time and inlines
374
+ // them into utility classes, so setting CSS custom properties on
375
+ // :root has no effect. Instead we target utility classes directly.
376
+ if (!s.tokenPreviewStyle) {
377
+ s.tokenPreviewStyle = document.createElement("style");
378
+ s.tokenPreviewStyle.id = "codesurface-token-preview";
379
+ document.head.appendChild(s.tokenPreviewStyle);
380
+ }
381
+ const cssRules: string[] = [];
382
+ for (const [k, v] of s.tokenPreviewValues) {
383
+ // Derive Tailwind utility class from CSS variable name:
384
+ // --shadow-sm → .shadow-sm, --shadow → .shadow
385
+ if (k.startsWith("--shadow")) {
386
+ const cls = k.slice(2); // "--shadow-sm" → "shadow-sm"
387
+ cssRules.push(`.${cls}, [class*="${cls}"] { box-shadow: ${v} !important; }`);
388
+ } else {
389
+ // For other tokens (colors, spacing, etc.) override the custom property
390
+ cssRules.push(`*, *::before, *::after { ${k}: ${v} !important; }`);
391
+ }
392
+ }
393
+ s.tokenPreviewStyle.textContent = cssRules.join("\n");
394
+ break;
395
+ }
396
+ case "tool:revertTokenValues": {
397
+ if (s.tokenPreviewStyle) {
398
+ s.tokenPreviewStyle.remove();
399
+ s.tokenPreviewStyle = null;
400
+ }
401
+ s.tokenPreviewValues.clear();
402
+ s.tokenValueBackups.clear();
403
+ break;
404
+ }
405
+ case "tool:reselectElement":
406
+ reselectCurrentElement();
407
+ break;
408
+ case "tool:setTheme":
409
+ if (msg.theme === "dark") {
410
+ document.documentElement.classList.add("dark");
411
+ } else {
412
+ document.documentElement.classList.remove("dark");
413
+ }
414
+ break;
415
+ }
416
+ }
417
+
418
+ function notifyPathChanged() {
419
+ const fullPath = window.location.pathname + window.location.search + window.location.hash;
420
+ window.parent.postMessage({ type: "tool:pathChanged", path: fullPath }, "*");
421
+ }
422
+
423
+ // --- Init ---
424
+ document.addEventListener("mousemove", onMouseMove, true);
425
+ document.addEventListener("mouseleave", onMouseLeave);
426
+ document.addEventListener("click", onClick, true);
427
+ window.addEventListener("message", onMessage);
428
+ window.addEventListener("popstate", notifyPathChanged);
429
+
430
+ // Notify editor that we're ready
431
+ window.parent.postMessage({ type: "tool:injectedReady" }, "*");
432
+ notifyPathChanged();
433
+
434
+ // --- Cleanup ---
435
+ return () => {
436
+ document.removeEventListener("mousemove", onMouseMove, true);
437
+ document.removeEventListener("mouseleave", onMouseLeave);
438
+ document.removeEventListener("click", onClick, true);
439
+ window.removeEventListener("message", onMessage);
440
+ window.removeEventListener("popstate", notifyPathChanged);
441
+
442
+ if (s.overlayRafId) cancelAnimationFrame(s.overlayRafId);
443
+ s.tokenPreviewStyle?.remove();
444
+ s.highlightOverlay?.remove();
445
+ s.tooltip?.remove();
446
+ s.selectedOverlay?.remove();
447
+ };
448
+ }, []);
449
+
450
+ // This component renders nothing — overlays are created imperatively
451
+ return null;
452
+ }
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Next.js config wrapper that adds the designtools source annotation loader
3
+ * and auto-mounts the <CodeSurface /> selection component in development.
4
+ *
5
+ * Usage:
6
+ * import { withDesigntools } from "@designtools/next-plugin";
7
+ * export default withDesigntools({ ...yourConfig });
8
+ */
9
+
10
+ import path from "path";
11
+
12
+ export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {} as T): T {
13
+ return {
14
+ ...nextConfig,
15
+ webpack(config: any, context: any) {
16
+ // Only add the loader in development
17
+ if (context.dev) {
18
+ config.module.rules.push({
19
+ test: /\.(tsx|jsx)$/,
20
+ exclude: /node_modules/,
21
+ use: [
22
+ {
23
+ loader: path.resolve(__dirname, "loader.js"),
24
+ options: {
25
+ cwd: context.dir,
26
+ },
27
+ },
28
+ ],
29
+ });
30
+
31
+ // Add a loader for root layout files that auto-mounts <CodeSurface />
32
+ config.module.rules.push({
33
+ test: /layout\.(tsx|jsx)$/,
34
+ include: [
35
+ path.resolve(context.dir, "app"),
36
+ path.resolve(context.dir, "src/app"),
37
+ ],
38
+ use: [
39
+ {
40
+ loader: path.resolve(__dirname, "codesurface-mount-loader.js"),
41
+ },
42
+ ],
43
+ });
44
+ }
45
+
46
+ // Call the user's webpack config if provided
47
+ if (typeof nextConfig.webpack === "function") {
48
+ return nextConfig.webpack(config, context);
49
+ }
50
+
51
+ return config;
52
+ },
53
+ };
54
+ }
package/src/loader.ts ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Webpack loader that adds data-source="file:line:col" attributes to JSX elements.
3
+ * Uses Babel's parser for correct JSX identification (avoids matching TypeScript generics).
4
+ * SWC stays enabled as the primary compiler — Babel is only used here for the source annotation pass.
5
+ */
6
+
7
+ import path from "path";
8
+
9
+ interface LoaderContext {
10
+ resourcePath: string;
11
+ rootContext: string;
12
+ getOptions(): { cwd?: string };
13
+ callback(err: Error | null, content?: string, sourceMap?: any): void;
14
+ async(): (err: Error | null, content?: string, sourceMap?: any) => void;
15
+ }
16
+
17
+ export default function designtoolsLoader(this: LoaderContext, source: string): void {
18
+ const callback = this.async();
19
+ const opts = this.getOptions();
20
+ const cwd = opts.cwd || this.rootContext || process.cwd();
21
+ const relativePath = path.relative(cwd, this.resourcePath);
22
+
23
+ // Skip node_modules
24
+ if (relativePath.includes("node_modules")) {
25
+ callback(null, source);
26
+ return;
27
+ }
28
+
29
+ // Quick check: skip files with no JSX
30
+ if (!source.includes("<")) {
31
+ callback(null, source);
32
+ return;
33
+ }
34
+
35
+ try {
36
+ // @babel/core is available at runtime via Next.js — don't bundle it
37
+ const babel = require("@babel/core");
38
+
39
+ const isTsx = this.resourcePath.endsWith(".tsx");
40
+
41
+ const result = babel.transformSync(source, {
42
+ filename: this.resourcePath,
43
+ // No presets — we only want to parse and run our visitor, not compile
44
+ presets: [],
45
+ plugins: [
46
+ function designtoolsSourcePlugin() {
47
+ return {
48
+ visitor: {
49
+ JSXOpeningElement(nodePath: any) {
50
+ const t = babel.types;
51
+ const attrs = nodePath.node.attributes;
52
+ const name = nodePath.node.name;
53
+
54
+ // Skip fragments
55
+ if (t.isJSXIdentifier(name) && name.name === "Fragment") return;
56
+ if (t.isJSXMemberExpression(name) &&
57
+ t.isJSXIdentifier(name.property) &&
58
+ name.property.name === "Fragment") return;
59
+
60
+ const loc = nodePath.node.loc;
61
+ if (!loc) return;
62
+
63
+ const value = `${relativePath}:${loc.start.line}:${loc.start.column}`;
64
+
65
+ // Detect component elements (uppercase first letter or member expression like Foo.Bar)
66
+ const isComponent =
67
+ (t.isJSXIdentifier(name) && name.name[0] === name.name[0].toUpperCase() && name.name[0] !== name.name[0].toLowerCase()) ||
68
+ t.isJSXMemberExpression(name);
69
+
70
+ if (isComponent) {
71
+ // Component elements get data-instance-source — this attribute
72
+ // propagates through {...props} spread to the rendered DOM element,
73
+ // carrying the exact page-level coordinates of each component usage.
74
+ // The component's own data-source (on its root native element) won't
75
+ // collide because it uses a different attribute name.
76
+ const attrName = "data-instance-source";
77
+ if (attrs.some((a: any) =>
78
+ t.isJSXAttribute(a) &&
79
+ t.isJSXIdentifier(a.name) &&
80
+ a.name.name === attrName
81
+ )) return;
82
+ attrs.push(
83
+ t.jsxAttribute(
84
+ t.jsxIdentifier(attrName),
85
+ t.stringLiteral(value)
86
+ )
87
+ );
88
+ } else {
89
+ // Native elements get data-source as before
90
+ if (attrs.some((a: any) =>
91
+ t.isJSXAttribute(a) &&
92
+ t.isJSXIdentifier(a.name) &&
93
+ a.name.name === "data-source"
94
+ )) return;
95
+ attrs.push(
96
+ t.jsxAttribute(
97
+ t.jsxIdentifier("data-source"),
98
+ t.stringLiteral(value)
99
+ )
100
+ );
101
+ }
102
+ },
103
+ },
104
+ };
105
+ },
106
+ ],
107
+ // Tell Babel's parser to handle JSX and TypeScript syntax
108
+ parserOpts: {
109
+ plugins: ["jsx", ...(isTsx ? ["typescript" as const] : [])],
110
+ },
111
+ // Preserve original formatting as much as possible
112
+ retainLines: true,
113
+ // Don't look for user's .babelrc or babel.config — isolation
114
+ configFile: false,
115
+ babelrc: false,
116
+ });
117
+
118
+ callback(null, result?.code || source);
119
+ } catch (err: any) {
120
+ // If Babel fails, pass through the original source
121
+ // This ensures the app still works even if our plugin has issues
122
+ console.warn(`[designtools] Source annotation skipped for ${relativePath}: ${err.message}`);
123
+ callback(null, source);
124
+ }
125
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"]
8
+ }