@designtools/next-plugin 0.1.9 → 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/README.md +18 -4
- package/dist/codesurface-mount-loader.js +2 -2
- package/dist/codesurface-mount-loader.mjs +2 -2
- package/dist/codesurface.js +28 -16
- package/dist/codesurface.mjs +28 -16
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -3
- package/dist/index.mjs +9 -3
- package/dist/surface-mount-loader.d.mts +15 -0
- package/dist/surface-mount-loader.d.ts +15 -0
- package/dist/surface-mount-loader.js +53 -0
- package/dist/surface-mount-loader.mjs +34 -0
- package/dist/surface.d.mts +5 -0
- package/dist/surface.d.ts +5 -0
- package/dist/surface.js +1050 -0
- package/dist/surface.mjs +1027 -0
- package/package.json +9 -5
- package/src/index.ts +4 -4
- package/src/preview-route.ts +8 -2
- package/src/{codesurface-mount-loader.ts → surface-mount-loader.ts} +10 -10
- package/src/{codesurface.tsx → surface.tsx} +102 -34
package/package.json
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@designtools/next-plugin",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
"./
|
|
11
|
-
"require": "./dist/
|
|
12
|
-
"import": "./dist/
|
|
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/
|
|
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 <
|
|
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 <
|
|
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, "
|
|
51
|
+
loader: path.resolve(__dirname, "surface-mount-loader.js"),
|
|
52
52
|
},
|
|
53
53
|
],
|
|
54
54
|
});
|
|
@@ -79,7 +79,7 @@ export function withDesigntools<T extends Record<string, any>>(nextConfig: T = {
|
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
const mountLoader = {
|
|
82
|
-
loader: path.resolve(__dirname, "
|
|
82
|
+
loader: path.resolve(__dirname, "surface-mount-loader.js"),
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
// Source annotation loader for all .tsx/.jsx files (excluding node_modules)
|
package/src/preview-route.ts
CHANGED
|
@@ -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 <
|
|
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
|
|
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 <
|
|
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 <
|
|
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
|
|
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
|
|
26
|
-
if (source.includes("
|
|
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
|
|
32
|
+
// The registry is rendered as a component to prevent tree-shaking.
|
|
33
33
|
const importStatements = [
|
|
34
|
-
`import {
|
|
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 <
|
|
48
|
+
// Add <Surface /> and <DesigntoolsRegistry /> just before {children}
|
|
49
49
|
modified = modified.replace(
|
|
50
50
|
/(\{children\})/,
|
|
51
|
-
`<
|
|
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
|
-
* <
|
|
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
|
|
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", "
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
|
422
|
-
if (name === "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
|
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 (
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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 = "
|
|
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;
|