@designtools/next-plugin 0.1.8 → 0.1.10

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/package.json CHANGED
@@ -1,19 +1,23 @@
1
1
  {
2
2
  "name": "@designtools/next-plugin",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "main": "dist/index.js",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/andflett/designtools"
8
+ },
5
9
  "exports": {
6
10
  ".": {
7
11
  "require": "./dist/index.js",
8
12
  "import": "./dist/index.mjs"
9
13
  },
10
- "./codesurface": {
11
- "require": "./dist/codesurface.js",
12
- "import": "./dist/codesurface.mjs"
14
+ "./surface": {
15
+ "require": "./dist/surface.js",
16
+ "import": "./dist/surface.mjs"
13
17
  }
14
18
  },
15
19
  "scripts": {
16
- "build": "tsup src/index.ts src/loader.ts src/codesurface.tsx src/codesurface-mount-loader.ts --format cjs,esm --dts --external @babel/core --external react --external react/jsx-runtime --external react-dom"
20
+ "build": "tsup src/index.ts src/loader.ts src/surface.tsx src/surface-mount-loader.ts --format cjs,esm --dts --external @babel/core --external react --external react/jsx-runtime --external react-dom"
17
21
  },
18
22
  "peerDependencies": {
19
23
  "next": ">=14.0.0",
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Next.js config wrapper that adds the designtools source annotation loader
3
- * and auto-mounts the <CodeSurface /> selection component in development.
3
+ * and auto-mounts the <Surface /> selection component in development.
4
4
  *
5
5
  * Usage:
6
6
  * import { withDesigntools } from "@designtools/next-plugin";
@@ -39,7 +39,7 @@ export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {
39
39
  ],
40
40
  });
41
41
 
42
- // Add a loader for root layout files that auto-mounts <CodeSurface />
42
+ // Add a loader for root layout files that auto-mounts <Surface />
43
43
  config.module.rules.push({
44
44
  test: /layout\.(tsx|jsx)$/,
45
45
  include: [
@@ -48,7 +48,7 @@ export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {
48
48
  ],
49
49
  use: [
50
50
  {
51
- loader: path.resolve(__dirname, "codesurface-mount-loader.js"),
51
+ loader: path.resolve(__dirname, "surface-mount-loader.js"),
52
52
  },
53
53
  ],
54
54
  });
@@ -63,8 +63,11 @@ export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {
63
63
  },
64
64
  };
65
65
 
66
- // Turbopack support — register the same loaders via turbopack.rules
67
- // following the exact pattern used by @next/mdx.
66
+ // Turbopack support — register the same loaders via turbopack.rules.
67
+ // Unlike @next/mdx (which converts .mdx .tsx and needs `as: '*.tsx'`),
68
+ // our loaders transform .tsx → .tsx (same extension), so we must NOT use `as`
69
+ // — that would cause Turbopack to re-append the extension (.tsx.tsx).
70
+ // Instead, use glob patterns that match the file extensions directly.
68
71
  if (process.env.TURBOPACK) {
69
72
  const sourceLoader = {
70
73
  loader: path.resolve(__dirname, "loader.js"),
@@ -76,47 +79,49 @@ export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {
76
79
  };
77
80
 
78
81
  const mountLoader = {
79
- loader: path.resolve(__dirname, "codesurface-mount-loader.js"),
82
+ loader: path.resolve(__dirname, "surface-mount-loader.js"),
80
83
  };
81
84
 
82
- // Source annotation rule uses a unique glob like @next/mdx does
85
+ // Source annotation loader for all .tsx/.jsx files (excluding node_modules)
83
86
  const sourceRule = {
84
87
  loaders: [sourceLoader],
85
- as: "*.tsx",
86
88
  condition: {
87
- path: /\.(tsx|jsx)$/,
89
+ not: "foreign",
88
90
  },
89
91
  };
90
92
 
91
- const wildcardGlob = "{*,designtools-source}";
92
- let wildcardRule = (nextConfig as any).turbopack?.rules?.[wildcardGlob] ?? [];
93
- wildcardRule = [
94
- ...(Array.isArray(wildcardRule) ? wildcardRule : [wildcardRule]),
95
- sourceRule,
96
- ];
97
-
98
- // Mount loader rule for root layout files
93
+ // Mount loader for layout files (the loader itself checks for <html to skip nested layouts)
99
94
  const mountRule = {
100
95
  loaders: [mountLoader],
101
- as: "*.tsx",
102
96
  condition: {
103
- path: /layout\.(tsx|jsx)$/,
97
+ all: [
98
+ { not: "foreign" },
99
+ { path: /layout\.(tsx|jsx)$/ },
100
+ ],
104
101
  },
105
102
  };
106
103
 
107
- const mountGlob = "{*,designtools-mount}";
108
- let mountGlobRule = (nextConfig as any).turbopack?.rules?.[mountGlob] ?? [];
109
- mountGlobRule = [
110
- ...(Array.isArray(mountGlobRule) ? mountGlobRule : [mountGlobRule]),
111
- mountRule,
112
- ];
104
+ // Use glob patterns that match tsx/jsx files.
105
+ // The source loader uses separate globs for .tsx and .jsx.
106
+ const existingRules = (nextConfig as any).turbopack?.rules ?? {};
107
+
108
+ const tsxRule = existingRules["*.tsx"] ?? [];
109
+ const jsxRule = existingRules["*.jsx"] ?? [];
113
110
 
114
111
  result.turbopack = {
115
112
  ...(nextConfig as any).turbopack,
116
113
  rules: {
117
- ...(nextConfig as any).turbopack?.rules,
118
- [wildcardGlob]: wildcardRule,
119
- [mountGlob]: mountGlobRule,
114
+ ...existingRules,
115
+ "*.tsx": [
116
+ ...(Array.isArray(tsxRule) ? tsxRule : [tsxRule]),
117
+ sourceRule,
118
+ mountRule,
119
+ ],
120
+ "*.jsx": [
121
+ ...(Array.isArray(jsxRule) ? jsxRule : [jsxRule]),
122
+ sourceRule,
123
+ mountRule,
124
+ ],
120
125
  },
121
126
  };
122
127
  }
@@ -2,7 +2,7 @@
2
2
  * Component registry generator for component isolation.
3
3
  *
4
4
  * Writes a static import map file into the target app's app directory.
5
- * The <CodeSurface /> component uses this registry to dynamically load
5
+ * The <Surface /> component uses this registry to dynamically load
6
6
  * components for the isolation overlay — no route change needed.
7
7
  */
8
8
 
@@ -29,11 +29,17 @@ const COMPONENT_REGISTRY: Record<string, () => Promise<any>> = {
29
29
  ${registryEntries}
30
30
  };
31
31
 
32
- // Self-register on window so CodeSurface can access it without
32
+ // Self-register on window so Surface can access it without
33
33
  // passing functions through the RSC serialization boundary.
34
34
  if (typeof window !== "undefined") {
35
35
  (window as any).__DESIGNTOOLS_REGISTRY__ = COMPONENT_REGISTRY;
36
36
  }
37
+
38
+ // Exported as a rendered component to prevent tree-shaking of the
39
+ // side-effect registration above. The mount loader renders this in layout.
40
+ export function DesigntoolsRegistry() {
41
+ return null;
42
+ }
37
43
  `;
38
44
 
39
45
  fs.writeFileSync(path.join(appDir, REGISTRY_FILE), content, "utf-8");
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Webpack loader that auto-mounts <CodeSurface /> in the root layout.
2
+ * Webpack loader that auto-mounts <Surface /> in the root layout.
3
3
  * Only runs in development. Injects the import and component into the JSX.
4
4
  *
5
5
  * Strategy: Simple string injection — find the {children} pattern in the layout
6
- * and add <CodeSurface /> alongside it.
6
+ * and add <Surface /> alongside it.
7
7
  */
8
8
 
9
9
  interface LoaderContext {
@@ -12,7 +12,7 @@ interface LoaderContext {
12
12
  async(): (err: Error | null, content?: string) => void;
13
13
  }
14
14
 
15
- export default function codesurfaceMountLoader(this: LoaderContext, source: string): void {
15
+ export default function surfaceMountLoader(this: LoaderContext, source: string): void {
16
16
  const callback = this.async();
17
17
 
18
18
  // Only inject into root layout (not nested layouts)
@@ -22,17 +22,17 @@ export default function codesurfaceMountLoader(this: LoaderContext, source: stri
22
22
  return;
23
23
  }
24
24
 
25
- // Skip if already has CodeSurface import
26
- if (source.includes("CodeSurface")) {
25
+ // Skip if already has Surface import
26
+ if (source.includes("Surface")) {
27
27
  callback(null, source);
28
28
  return;
29
29
  }
30
30
 
31
31
  // Add imports at the top (after "use client" or first import)
32
- // The registry import is a side-effect it self-registers on window.
32
+ // The registry is rendered as a component to prevent tree-shaking.
33
33
  const importStatements = [
34
- `import { CodeSurface } from "@designtools/next-plugin/codesurface";`,
35
- `import "./designtools-registry";`,
34
+ `import { Surface } from "@designtools/next-plugin/surface";`,
35
+ `import { DesigntoolsRegistry } from "./designtools-registry";`,
36
36
  ].join("\n") + "\n";
37
37
 
38
38
  let modified = source;
@@ -45,10 +45,10 @@ export default function codesurfaceMountLoader(this: LoaderContext, source: stri
45
45
  modified = importStatements + source;
46
46
  }
47
47
 
48
- // Add <CodeSurface /> just before {children}
48
+ // Add <Surface /> and <DesigntoolsRegistry /> just before {children}
49
49
  modified = modified.replace(
50
50
  /(\{children\})/,
51
- `<CodeSurface />\n $1`
51
+ `<Surface /><DesigntoolsRegistry />\n $1`
52
52
  );
53
53
 
54
54
  callback(null, modified);
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  /**
4
- * <CodeSurface /> — Selection overlay component for the target app.
4
+ * <Surface /> — Selection overlay component for the target app.
5
5
  * Mounted automatically by withDesigntools() in development.
6
6
  * Communicates with the editor UI via postMessage.
7
7
  *
@@ -21,7 +21,7 @@ interface PreviewCombination {
21
21
  props: Record<string, string>;
22
22
  }
23
23
 
24
- export function CodeSurface() {
24
+ export function Surface() {
25
25
  const stateRef = useRef({
26
26
  selectionMode: false,
27
27
  hoveredElement: null as Element | null,
@@ -157,7 +157,7 @@ export function CodeSurface() {
157
157
  // --- Component tree extraction (React Fiber) ---
158
158
 
159
159
  // IDs of overlay elements to skip during tree building
160
- const overlayIds = new Set(["tool-highlight", "tool-tooltip", "tool-selected", "codesurface-token-preview"]);
160
+ const overlayIds = new Set(["tool-highlight", "tool-tooltip", "tool-selected", "surface-token-preview"]);
161
161
 
162
162
  // Semantic HTML elements shown as structural landmarks
163
163
  const semanticTags = new Set(["header", "main", "nav", "section", "article", "footer", "aside"]);
@@ -364,29 +364,30 @@ export function CodeSurface() {
364
364
  };
365
365
  }
366
366
 
367
- // Show semantic HTML landmarks
367
+ // Show semantic HTML landmarks — always show these as structural markers
368
368
  if (semanticTags.has(tag)) {
369
369
  const children: TreeNode[] = [];
370
370
  if (fiber.child) walkFiber(fiber.child, children, scope);
371
371
  const text = el ? getDirectText(el) : "";
372
- if (children.length > 0 || text) {
373
- return {
374
- id: el ? getDomPath(el) : "",
375
- name: `<${tag}>`,
376
- type: "element",
377
- dataSlot: null,
378
- source: el?.getAttribute("data-source") || null,
379
- scope,
380
- textContent: text,
381
- children,
382
- };
383
- }
372
+ return {
373
+ id: el ? getDomPath(el) : "",
374
+ name: `<${tag}>`,
375
+ type: "element",
376
+ dataSlot: null,
377
+ source: el?.getAttribute("data-source") || null,
378
+ scope,
379
+ textContent: text,
380
+ children,
381
+ };
384
382
  }
385
383
 
386
384
  // Show authored elements — data-source is added by our Babel transform
387
385
  // to every JSX element, so its presence proves this was deliberately
388
386
  // written in user code. Skip document/void tags that aren't meaningful.
389
- if (el?.hasAttribute("data-source") && !skipTags.has(tag)) {
387
+ // In "components" mode, skip generic elements to reduce noise (only
388
+ // data-slot components and semantic landmarks show). In "dom" mode,
389
+ // show all authored elements.
390
+ if (currentTreeMode === "dom" && el?.hasAttribute("data-source") && !skipTags.has(tag)) {
390
391
  const children: TreeNode[] = [];
391
392
  if (fiber.child) walkFiber(fiber.child, children, scope);
392
393
  const text = el ? getDirectText(el) : "";
@@ -418,8 +419,8 @@ export function CodeSurface() {
418
419
  // Skip framework internals
419
420
  if (isFrameworkComponent(name)) return null;
420
421
 
421
- // Skip the CodeSurface component itself
422
- if (name === "CodeSurface") return null;
422
+ // Skip the Surface component itself
423
+ if (name === "Surface") return null;
423
424
 
424
425
  // Find this component's own root host element — only walk down through
425
426
  // non-host fibers (other components, fragments, etc.) to find the first
@@ -580,7 +581,11 @@ export function CodeSurface() {
580
581
  }
581
582
  }
582
583
 
583
- function sendComponentTree() {
584
+ // Current tree mode — stored so MutationObserver re-sends use the same mode
585
+ let currentTreeMode: "components" | "dom" = "components";
586
+
587
+ function sendComponentTree(mode?: "components" | "dom") {
588
+ if (mode) currentTreeMode = mode;
584
589
  const tree = buildComponentTree(document.body);
585
590
  window.parent.postMessage({ type: "tool:componentTree", tree }, "*");
586
591
  }
@@ -589,7 +594,7 @@ export function CodeSurface() {
589
594
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
590
595
  function debouncedSendTree() {
591
596
  if (debounceTimer) clearTimeout(debounceTimer);
592
- debounceTimer = setTimeout(sendComponentTree, 300);
597
+ debounceTimer = setTimeout(() => sendComponentTree(), 300);
593
598
  }
594
599
 
595
600
  // MutationObserver to send updated tree on DOM changes (HMR, dynamic content)
@@ -711,10 +716,11 @@ export function CodeSurface() {
711
716
  let instanceSourceLine: number | null = null;
712
717
  let instanceSourceCol: number | null = null;
713
718
  let componentName: string | null = null;
719
+ let packageName: string | null = null;
714
720
 
715
721
  const dataSlot = el.getAttribute("data-slot");
716
722
  const instanceSource = el.getAttribute("data-instance-source");
717
- if (instanceSource && dataSlot) {
723
+ if (instanceSource) {
718
724
  const lc = instanceSource.lastIndexOf(":");
719
725
  const slc = instanceSource.lastIndexOf(":", lc - 1);
720
726
  if (slc > 0) {
@@ -723,19 +729,44 @@ export function CodeSurface() {
723
729
  instanceSourceCol = parseInt(instanceSource.slice(lc + 1), 10);
724
730
  }
725
731
 
726
- // Derive component name from data-slot (e.g. "card-title" -> "CardTitle")
727
- componentName = dataSlot
728
- .split("-")
729
- .map((s: string) => s.charAt(0).toUpperCase() + s.slice(1))
730
- .join("");
732
+ if (dataSlot) {
733
+ // Derive component name from data-slot (e.g. "card-title" -> "CardTitle")
734
+ componentName = dataSlot
735
+ .split("-")
736
+ .map((s: string) => s.charAt(0).toUpperCase() + s.slice(1))
737
+ .join("");
738
+ }
731
739
  }
732
740
 
733
- // Extract runtime props from React fiber for component instances
741
+ // Extract runtime props and derive componentName/packageName from React fiber
742
+ const compFiber = (dataSlot || instanceSource) ? findComponentFiberAbove(el) : null;
743
+
734
744
  let fiberProps: Record<string, string | number | boolean> | null = null;
735
- if (dataSlot) {
736
- const compFiber = findComponentFiberAbove(el);
737
- if (compFiber) {
738
- fiberProps = extractFiberProps(compFiber);
745
+ if (compFiber) {
746
+ fiberProps = extractFiberProps(compFiber);
747
+
748
+ // Derive componentName from fiber when data-slot is not present
749
+ if (!componentName && instanceSource) {
750
+ const name = compFiber.type?.displayName || compFiber.type?.name;
751
+ if (name) componentName = name;
752
+ }
753
+
754
+ // Extract packageName from fiber._debugSource for npm components
755
+ const debugFile = compFiber._debugSource?.fileName;
756
+ if (debugFile) {
757
+ packageName = extractPackageName(debugFile);
758
+ }
759
+ }
760
+
761
+ // Also check data-source for packageName if no fiber source
762
+ if (!packageName && !sourceFile) {
763
+ // No project source — try walking up fibers to find node_modules origin
764
+ const anyFiber = findComponentFiberAbove(el);
765
+ if (anyFiber) {
766
+ const debugFile = anyFiber._debugSource?.fileName;
767
+ if (debugFile) {
768
+ packageName = extractPackageName(debugFile);
769
+ }
739
770
  }
740
771
  }
741
772
 
@@ -755,10 +786,22 @@ export function CodeSurface() {
755
786
  instanceSourceLine,
756
787
  instanceSourceCol,
757
788
  componentName,
789
+ packageName,
758
790
  fiberProps,
759
791
  };
760
792
  }
761
793
 
794
+ function extractPackageName(filePath: string): string | null {
795
+ const nmIdx = filePath.lastIndexOf("node_modules/");
796
+ if (nmIdx === -1) return null;
797
+ const rest = filePath.slice(nmIdx + "node_modules/".length);
798
+ if (rest.startsWith("@")) {
799
+ const parts = rest.split("/");
800
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
801
+ }
802
+ return rest.split("/")[0] || null;
803
+ }
804
+
762
805
  function selectElement(el: Element) {
763
806
  s.selectedElement = el;
764
807
  s.selectedDomPath = getDomPath(el);
@@ -832,6 +875,16 @@ export function CodeSurface() {
832
875
  s.hoveredElement = null;
833
876
  }
834
877
 
878
+ function clearSelection() {
879
+ s.selectedElement = null;
880
+ s.selectedDomPath = null;
881
+ if (s.selectedOverlay) s.selectedOverlay.style.display = "none";
882
+ if (s.overlayRafId) {
883
+ cancelAnimationFrame(s.overlayRafId);
884
+ s.overlayRafId = null;
885
+ }
886
+ }
887
+
835
888
  // --- Event handlers ---
836
889
  function onMouseMove(e: MouseEvent) {
837
890
  if (!s.selectionMode || !s.highlightOverlay || !s.tooltip) return;
@@ -877,6 +930,9 @@ export function CodeSurface() {
877
930
  case "tool:exitSelectionMode":
878
931
  exitSelectionMode();
879
932
  break;
933
+ case "tool:clearSelection":
934
+ clearSelection();
935
+ break;
880
936
  case "tool:previewInlineStyle": {
881
937
  if (s.selectedElement && s.selectedElement instanceof HTMLElement) {
882
938
  const prop = msg.property as string;
@@ -907,7 +963,7 @@ export function CodeSurface() {
907
963
  s.tokenPreviewValues.set(prop, value);
908
964
  if (!s.tokenPreviewStyle) {
909
965
  s.tokenPreviewStyle = document.createElement("style");
910
- s.tokenPreviewStyle.id = "codesurface-token-preview";
966
+ s.tokenPreviewStyle.id = "surface-token-preview";
911
967
  document.head.appendChild(s.tokenPreviewStyle);
912
968
  }
913
969
  const cssRules: string[] = [];
@@ -942,7 +998,7 @@ export function CodeSurface() {
942
998
  }
943
999
  break;
944
1000
  case "tool:requestComponentTree":
945
- sendComponentTree();
1001
+ sendComponentTree((msg as any).mode || "components");
946
1002
  break;
947
1003
  case "tool:highlightByTreeId": {
948
1004
  const id = msg.id as string;
@@ -973,6 +1029,18 @@ export function CodeSurface() {
973
1029
  }
974
1030
  break;
975
1031
  }
1032
+ case "tool:selectParentInstance": {
1033
+ if (!s.selectedElement) break;
1034
+ let el: Element | null = s.selectedElement.parentElement;
1035
+ while (el && el !== document.body) {
1036
+ if (el.getAttribute("data-instance-source")) {
1037
+ selectElement(el);
1038
+ break;
1039
+ }
1040
+ el = el.parentElement;
1041
+ }
1042
+ break;
1043
+ }
976
1044
  case "tool:renderPreview": {
977
1045
  const { componentPath, exportName, combinations: combos, defaultChildren: children } = msg;
978
1046
  const currentRegistry = (window as any).__DESIGNTOOLS_REGISTRY__ as Record<string, () => Promise<any>> | undefined;