adaptar-vite-plugin 1.0.7 → 1.0.8

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/dist/html-tags.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ERROR_BRIDGE_SCRIPT } from "./scripts/error-bridge.js";
2
+ import { NAVIGATION_BRIDGE_SCRIPT } from "./scripts/navigation-bridge.js";
2
3
  import { SELECTION_BRIDGE_SCRIPT } from "./scripts/selection-bridge.js";
3
4
  /**
4
5
  * The inline module that subscribes to the `adaptar:error` custom HMR event
@@ -72,6 +73,12 @@ export function buildHtmlTags() {
72
73
  attrs: { "data-adaptar": "error-bridge" },
73
74
  children: ERROR_BRIDGE_SCRIPT,
74
75
  },
76
+ {
77
+ tag: "script",
78
+ injectTo: "head-prepend",
79
+ attrs: { "data-adaptar": "navigation-bridge" },
80
+ children: NAVIGATION_BRIDGE_SCRIPT,
81
+ },
75
82
  {
76
83
  tag: "script",
77
84
  injectTo: "head-prepend",
package/dist/index.d.ts CHANGED
@@ -1,13 +1,11 @@
1
1
  /**
2
2
  * Adaptar Vite Plugin
3
3
  *
4
- * Provides three layers of error and selection bridging between the
5
- * sandboxed preview iframe and the Adaptar host editor:
4
+ * Provides four preview bridges between the sandboxed iframe and the host:
6
5
  *
7
- * 1. `error-bridge` catches runtime JS errors, resource failures,
8
- * unhandled rejections, and blank-screen scenarios.
9
- * 2. `selection-bridge` powers element inspect / edit mode in the preview.
10
- * 3. `hmr-interceptor` intercepts Vite's internal WebSocket to surface
11
- * build/HMR errors as structured `adaptar:error` events.
6
+ * 1. `error-bridge` - reports blocking runtime and compilation failures.
7
+ * 2. `navigation-bridge` - reports document and History API navigation.
8
+ * 3. `selection-bridge` - powers element inspection and edit mode.
9
+ * 4. `hmr-interceptor` - reports Vite build and HMR failures.
12
10
  */
13
11
  export declare function adaptar(): any;
package/dist/index.js CHANGED
@@ -3,14 +3,12 @@ import { buildHtmlTags } from "./html-tags.js";
3
3
  /**
4
4
  * Adaptar Vite Plugin
5
5
  *
6
- * Provides three layers of error and selection bridging between the
7
- * sandboxed preview iframe and the Adaptar host editor:
6
+ * Provides four preview bridges between the sandboxed iframe and the host:
8
7
  *
9
- * 1. `error-bridge` catches runtime JS errors, resource failures,
10
- * unhandled rejections, and blank-screen scenarios.
11
- * 2. `selection-bridge` powers element inspect / edit mode in the preview.
12
- * 3. `hmr-interceptor` intercepts Vite's internal WebSocket to surface
13
- * build/HMR errors as structured `adaptar:error` events.
8
+ * 1. `error-bridge` - reports blocking runtime and compilation failures.
9
+ * 2. `navigation-bridge` - reports document and History API navigation.
10
+ * 3. `selection-bridge` - powers element inspection and edit mode.
11
+ * 4. `hmr-interceptor` - reports Vite build and HMR failures.
14
12
  */
15
13
  export function adaptar() {
16
14
  return {
@@ -20,7 +18,7 @@ export function adaptar() {
20
18
  return {
21
19
  server: {
22
20
  hmr: {
23
- // Disable Vite's default error overlay — Adaptar renders its own.
21
+ // Adaptar renders its own error surface.
24
22
  overlay: false,
25
23
  },
26
24
  },
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Reports the preview's browser location to the Adaptar host.
3
+ *
4
+ * The preview runs on a different origin, so the host cannot read
5
+ * `iframe.contentWindow.location`. Intercepting the History API also covers
6
+ * client-side routers, whose navigation does not trigger an iframe load.
7
+ */
8
+ export declare const NAVIGATION_BRIDGE_SCRIPT = "(function() {\n if (window.__ADAPTAR_NAVIGATION_BRIDGE__) return;\n window.__ADAPTAR_NAVIGATION_BRIDGE__ = true;\n\n var INTERNAL_OPERATION_PARAM = '__adaptarOperation';\n var operationId = new URL(window.location.href).searchParams.get(INTERNAL_OPERATION_PARAM) || undefined;\n var lastLocation = '';\n\n function readLocation() {\n var url = new URL(window.location.href);\n url.searchParams.delete(INTERNAL_OPERATION_PARAM);\n\n return {\n pathname: url.pathname || '/',\n search: url.search,\n hash: url.hash\n };\n }\n\n function reportLocation(reason) {\n try {\n var location = readLocation();\n var signature = location.pathname + location.search + location.hash;\n if (signature === lastLocation) return;\n lastLocation = signature;\n\n window.parent.postMessage({\n source: 'adaptar-preview',\n type: 'preview-navigation',\n operationId: operationId,\n reason: reason,\n location: location\n }, '*');\n } catch (_) {}\n }\n\n function scheduleReport(reason) {\n if (typeof queueMicrotask === 'function') {\n queueMicrotask(function() {\n reportLocation(reason);\n });\n return;\n }\n\n Promise.resolve().then(function() {\n reportLocation(reason);\n });\n }\n\n function instrumentHistory(method) {\n try {\n var original = window.history[method];\n if (typeof original !== 'function') return;\n\n window.history[method] = function() {\n var result = original.apply(this, arguments);\n scheduleReport(method);\n return result;\n };\n } catch (_) {}\n }\n\n instrumentHistory('pushState');\n instrumentHistory('replaceState');\n\n window.addEventListener('popstate', function() {\n scheduleReport('popstate');\n });\n window.addEventListener('hashchange', function() {\n scheduleReport('hashchange');\n });\n window.addEventListener('pageshow', function() {\n scheduleReport('pageshow');\n });\n\n reportLocation('initial');\n})();";
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Reports the preview's browser location to the Adaptar host.
3
+ *
4
+ * The preview runs on a different origin, so the host cannot read
5
+ * `iframe.contentWindow.location`. Intercepting the History API also covers
6
+ * client-side routers, whose navigation does not trigger an iframe load.
7
+ */
8
+ export const NAVIGATION_BRIDGE_SCRIPT = /* js */ `(function() {
9
+ if (window.__ADAPTAR_NAVIGATION_BRIDGE__) return;
10
+ window.__ADAPTAR_NAVIGATION_BRIDGE__ = true;
11
+
12
+ var INTERNAL_OPERATION_PARAM = '__adaptarOperation';
13
+ var operationId = new URL(window.location.href).searchParams.get(INTERNAL_OPERATION_PARAM) || undefined;
14
+ var lastLocation = '';
15
+
16
+ function readLocation() {
17
+ var url = new URL(window.location.href);
18
+ url.searchParams.delete(INTERNAL_OPERATION_PARAM);
19
+
20
+ return {
21
+ pathname: url.pathname || '/',
22
+ search: url.search,
23
+ hash: url.hash
24
+ };
25
+ }
26
+
27
+ function reportLocation(reason) {
28
+ try {
29
+ var location = readLocation();
30
+ var signature = location.pathname + location.search + location.hash;
31
+ if (signature === lastLocation) return;
32
+ lastLocation = signature;
33
+
34
+ window.parent.postMessage({
35
+ source: 'adaptar-preview',
36
+ type: 'preview-navigation',
37
+ operationId: operationId,
38
+ reason: reason,
39
+ location: location
40
+ }, '*');
41
+ } catch (_) {}
42
+ }
43
+
44
+ function scheduleReport(reason) {
45
+ if (typeof queueMicrotask === 'function') {
46
+ queueMicrotask(function() {
47
+ reportLocation(reason);
48
+ });
49
+ return;
50
+ }
51
+
52
+ Promise.resolve().then(function() {
53
+ reportLocation(reason);
54
+ });
55
+ }
56
+
57
+ function instrumentHistory(method) {
58
+ try {
59
+ var original = window.history[method];
60
+ if (typeof original !== 'function') return;
61
+
62
+ window.history[method] = function() {
63
+ var result = original.apply(this, arguments);
64
+ scheduleReport(method);
65
+ return result;
66
+ };
67
+ } catch (_) {}
68
+ }
69
+
70
+ instrumentHistory('pushState');
71
+ instrumentHistory('replaceState');
72
+
73
+ window.addEventListener('popstate', function() {
74
+ scheduleReport('popstate');
75
+ });
76
+ window.addEventListener('hashchange', function() {
77
+ scheduleReport('hashchange');
78
+ });
79
+ window.addEventListener('pageshow', function() {
80
+ scheduleReport('pageshow');
81
+ });
82
+
83
+ reportLocation('initial');
84
+ })();`;
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "adaptar-vite-plugin",
3
- "version": "1.0.7",
4
- "description": "Vite plugin for Adaptar preview error and selection bridging",
3
+ "version": "1.0.8",
4
+ "description": "Vite plugin for Adaptar preview error, navigation, and selection bridging",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "type": "module",
8
8
  "scripts": {
9
9
  "build": "tsc",
10
- "dev": "tsc -w"
10
+ "dev": "tsc -w",
11
+ "test": "npm run build && node --test test/*.test.mjs"
11
12
  },
12
13
  "devDependencies": {
13
14
  "@types/node": "^20.0.0",
package/src/html-tags.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { HtmlTagDescriptor } from "vite";
2
- import { ERROR_BRIDGE_SCRIPT } from "./scripts/error-bridge.js";
3
- import { SELECTION_BRIDGE_SCRIPT } from "./scripts/selection-bridge.js";
1
+ import type { HtmlTagDescriptor } from "vite";
2
+ import { ERROR_BRIDGE_SCRIPT } from "./scripts/error-bridge.js";
3
+ import { NAVIGATION_BRIDGE_SCRIPT } from "./scripts/navigation-bridge.js";
4
+ import { SELECTION_BRIDGE_SCRIPT } from "./scripts/selection-bridge.js";
4
5
 
5
6
  /**
6
7
  * The inline module that subscribes to the `adaptar:error` custom HMR event
@@ -75,11 +76,17 @@ export function buildHtmlTags(): HtmlTagDescriptor[] {
75
76
  attrs: { "data-adaptar": "error-bridge" },
76
77
  children: ERROR_BRIDGE_SCRIPT,
77
78
  },
78
- {
79
- tag: "script",
80
- injectTo: "head-prepend",
81
- attrs: { "data-adaptar": "selection-bridge" },
82
- children: SELECTION_BRIDGE_SCRIPT,
79
+ {
80
+ tag: "script",
81
+ injectTo: "head-prepend",
82
+ attrs: { "data-adaptar": "navigation-bridge" },
83
+ children: NAVIGATION_BRIDGE_SCRIPT,
84
+ },
85
+ {
86
+ tag: "script",
87
+ injectTo: "head-prepend",
88
+ attrs: { "data-adaptar": "selection-bridge" },
89
+ children: SELECTION_BRIDGE_SCRIPT,
83
90
  },
84
91
  {
85
92
  tag: "script",
package/src/index.ts CHANGED
@@ -4,14 +4,12 @@ import { buildHtmlTags } from "./html-tags.js";
4
4
  /**
5
5
  * Adaptar Vite Plugin
6
6
  *
7
- * Provides three layers of error and selection bridging between the
8
- * sandboxed preview iframe and the Adaptar host editor:
7
+ * Provides four preview bridges between the sandboxed iframe and the host:
9
8
  *
10
- * 1. `error-bridge` catches runtime JS errors, resource failures,
11
- * unhandled rejections, and blank-screen scenarios.
12
- * 2. `selection-bridge` powers element inspect / edit mode in the preview.
13
- * 3. `hmr-interceptor` intercepts Vite's internal WebSocket to surface
14
- * build/HMR errors as structured `adaptar:error` events.
9
+ * 1. `error-bridge` - reports blocking runtime and compilation failures.
10
+ * 2. `navigation-bridge` - reports document and History API navigation.
11
+ * 3. `selection-bridge` - powers element inspection and edit mode.
12
+ * 4. `hmr-interceptor` - reports Vite build and HMR failures.
15
13
  */
16
14
  export function adaptar(): any {
17
15
  return {
@@ -22,7 +20,7 @@ export function adaptar(): any {
22
20
  return {
23
21
  server: {
24
22
  hmr: {
25
- // Disable Vite's default error overlay — Adaptar renders its own.
23
+ // Adaptar renders its own error surface.
26
24
  overlay: false,
27
25
  },
28
26
  },
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Reports the preview's browser location to the Adaptar host.
3
+ *
4
+ * The preview runs on a different origin, so the host cannot read
5
+ * `iframe.contentWindow.location`. Intercepting the History API also covers
6
+ * client-side routers, whose navigation does not trigger an iframe load.
7
+ */
8
+ export const NAVIGATION_BRIDGE_SCRIPT = /* js */ `(function() {
9
+ if (window.__ADAPTAR_NAVIGATION_BRIDGE__) return;
10
+ window.__ADAPTAR_NAVIGATION_BRIDGE__ = true;
11
+
12
+ var INTERNAL_OPERATION_PARAM = '__adaptarOperation';
13
+ var operationId = new URL(window.location.href).searchParams.get(INTERNAL_OPERATION_PARAM) || undefined;
14
+ var lastLocation = '';
15
+
16
+ function readLocation() {
17
+ var url = new URL(window.location.href);
18
+ url.searchParams.delete(INTERNAL_OPERATION_PARAM);
19
+
20
+ return {
21
+ pathname: url.pathname || '/',
22
+ search: url.search,
23
+ hash: url.hash
24
+ };
25
+ }
26
+
27
+ function reportLocation(reason) {
28
+ try {
29
+ var location = readLocation();
30
+ var signature = location.pathname + location.search + location.hash;
31
+ if (signature === lastLocation) return;
32
+ lastLocation = signature;
33
+
34
+ window.parent.postMessage({
35
+ source: 'adaptar-preview',
36
+ type: 'preview-navigation',
37
+ operationId: operationId,
38
+ reason: reason,
39
+ location: location
40
+ }, '*');
41
+ } catch (_) {}
42
+ }
43
+
44
+ function scheduleReport(reason) {
45
+ if (typeof queueMicrotask === 'function') {
46
+ queueMicrotask(function() {
47
+ reportLocation(reason);
48
+ });
49
+ return;
50
+ }
51
+
52
+ Promise.resolve().then(function() {
53
+ reportLocation(reason);
54
+ });
55
+ }
56
+
57
+ function instrumentHistory(method) {
58
+ try {
59
+ var original = window.history[method];
60
+ if (typeof original !== 'function') return;
61
+
62
+ window.history[method] = function() {
63
+ var result = original.apply(this, arguments);
64
+ scheduleReport(method);
65
+ return result;
66
+ };
67
+ } catch (_) {}
68
+ }
69
+
70
+ instrumentHistory('pushState');
71
+ instrumentHistory('replaceState');
72
+
73
+ window.addEventListener('popstate', function() {
74
+ scheduleReport('popstate');
75
+ });
76
+ window.addEventListener('hashchange', function() {
77
+ scheduleReport('hashchange');
78
+ });
79
+ window.addEventListener('pageshow', function() {
80
+ scheduleReport('pageshow');
81
+ });
82
+
83
+ reportLocation('initial');
84
+ })();`;
@@ -0,0 +1,100 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import vm from "node:vm";
4
+ import { NAVIGATION_BRIDGE_SCRIPT } from "../dist/scripts/navigation-bridge.js";
5
+
6
+ function createPreviewRuntime(initialUrl) {
7
+ const messages = [];
8
+ const listeners = new Map();
9
+ const location = { href: initialUrl };
10
+ const parent = {
11
+ postMessage(message) {
12
+ messages.push(JSON.parse(JSON.stringify(message)));
13
+ },
14
+ };
15
+ const history = {
16
+ pushState(_state, _unused, nextUrl) {
17
+ location.href = new URL(String(nextUrl), location.href).toString();
18
+ },
19
+ replaceState(_state, _unused, nextUrl) {
20
+ location.href = new URL(String(nextUrl), location.href).toString();
21
+ },
22
+ };
23
+ const window = {
24
+ location,
25
+ parent,
26
+ history,
27
+ addEventListener(type, listener) {
28
+ const registered = listeners.get(type) || [];
29
+ registered.push(listener);
30
+ listeners.set(type, registered);
31
+ },
32
+ };
33
+ window.window = window;
34
+
35
+ vm.runInNewContext(NAVIGATION_BRIDGE_SCRIPT, {
36
+ Promise,
37
+ URL,
38
+ queueMicrotask(callback) {
39
+ callback();
40
+ },
41
+ window,
42
+ });
43
+
44
+ return {
45
+ history,
46
+ messages,
47
+ navigateByBrowser(nextUrl, eventType) {
48
+ location.href = new URL(nextUrl, location.href).toString();
49
+ for (const listener of listeners.get(eventType) || []) {
50
+ listener();
51
+ }
52
+ },
53
+ };
54
+ }
55
+
56
+ test("reports initial and History API navigation without internal query state", () => {
57
+ const runtime = createPreviewRuntime(
58
+ "https://workspace.preview.adaptar.dev/?__adaptarOperation=operation-1",
59
+ );
60
+
61
+ assert.deepEqual(runtime.messages[0], {
62
+ source: "adaptar-preview",
63
+ type: "preview-navigation",
64
+ operationId: "operation-1",
65
+ reason: "initial",
66
+ location: {
67
+ pathname: "/",
68
+ search: "",
69
+ hash: "",
70
+ },
71
+ });
72
+
73
+ runtime.history.pushState({}, "", "/work?filter=featured#project");
74
+
75
+ assert.deepEqual(runtime.messages[1], {
76
+ source: "adaptar-preview",
77
+ type: "preview-navigation",
78
+ operationId: "operation-1",
79
+ reason: "pushState",
80
+ location: {
81
+ pathname: "/work",
82
+ search: "?filter=featured",
83
+ hash: "#project",
84
+ },
85
+ });
86
+ });
87
+
88
+ test("reports browser history and suppresses duplicate locations", () => {
89
+ const runtime = createPreviewRuntime(
90
+ "https://workspace.preview.adaptar.dev/studio",
91
+ );
92
+
93
+ runtime.history.replaceState({}, "", "/studio");
94
+ assert.equal(runtime.messages.length, 1);
95
+
96
+ runtime.navigateByBrowser("/contact", "popstate");
97
+ assert.equal(runtime.messages.length, 2);
98
+ assert.equal(runtime.messages[1].location.pathname, "/contact");
99
+ assert.equal(runtime.messages[1].reason, "popstate");
100
+ });