@backmatter/next 0.0.31 → 0.0.32

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/config.cjs CHANGED
@@ -73,6 +73,7 @@ __export(exports_config, {
73
73
  withBackmatter: () => withBackmatter
74
74
  });
75
75
  module.exports = __toCommonJS(exports_config);
76
+ var import_node_module = require("node:module");
76
77
  var import_node_fs4 = require("node:fs");
77
78
  var import_node_path4 = __toESM(require("node:path"));
78
79
 
@@ -741,6 +742,18 @@ function matchBrace(source, open) {
741
742
  }
742
743
 
743
744
  // src/config.ts
745
+ var require2 = import_node_module.createRequire("file:///home/runner/work/backmatter/backmatter/packages/next/src/config.ts");
746
+ function studioWidgetAlias() {
747
+ try {
748
+ const studioRoot = import_node_path4.default.dirname(require2.resolve("@backmatter/studio/package.json"));
749
+ const dist = import_node_path4.default.join(studioRoot, "dist/site-widget.js");
750
+ if (!import_node_fs4.existsSync(dist))
751
+ return {};
752
+ return { "@backmatter/studio/widget": dist };
753
+ } catch {
754
+ return {};
755
+ }
756
+ }
744
757
  var LOADER = "@backmatter/next/loader";
745
758
  var SDK = "@backmatter/next";
746
759
  function warnMissingInstrumentation(root) {
@@ -807,6 +820,7 @@ function withBackmatter(nextConfig = {}, options = {}) {
807
820
  ...existingTurbo,
808
821
  resolveAlias: {
809
822
  ...registryTurbopackAlias(loaderOptions.root, loaderOptions.outDir),
823
+ ...studioWidgetAlias(),
810
824
  ...existingAlias
811
825
  },
812
826
  rules: { ...existingRules, [glob]: turbopackRule }
@@ -820,6 +834,7 @@ function withBackmatter(nextConfig = {}, options = {}) {
820
834
  const prev = prevAlias && typeof prevAlias === "object" && !Array.isArray(prevAlias) ? prevAlias : {};
821
835
  config.resolve.alias = {
822
836
  ...registryWebpackAlias(loaderOptions.root, loaderOptions.outDir),
837
+ ...studioWidgetAlias(),
823
838
  ...prev
824
839
  };
825
840
  return cfg.webpack ? cfg.webpack(config, context) : config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backmatter/next",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
4
4
  "description": "Experimental Next.js adapter for Backmatter. APIs will break. Not ready for production.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -47,10 +47,10 @@
47
47
  "test": "bun test"
48
48
  },
49
49
  "dependencies": {
50
- "@backmatter/core": "^0.0.25",
50
+ "@backmatter/core": "^0.0.26",
51
51
  "@mdx-js/mdx": "^3.1.1",
52
52
  "remark-gfm": "^4.0.1",
53
- "@backmatter/studio": "^0.0.8"
53
+ "@backmatter/studio": "^0.0.9"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "next": ">=16.3.0",
package/src/config.ts CHANGED
@@ -7,8 +7,23 @@
7
7
  // (Next 16 default) and webpack (`next build --webpack`). The loader declares
8
8
  // `models/`, `blocks/`, `richtext/`, and `mdx/` as context dependencies, so the bundler's
9
9
  // own file watcher keeps the registry fresh — no polling, no second watcher process.
10
+ import { createRequire } from "node:module";
10
11
  import { existsSync } from "node:fs";
11
12
  import path from "node:path";
13
+
14
+ const require = createRequire(import.meta.url);
15
+
16
+ /** Prebuilt widget (StyleX CSS must match JS). Avoids Next transpiling `src/site-widget.tsx`. */
17
+ function studioWidgetAlias(): Record<string, string> {
18
+ try {
19
+ const studioRoot = path.dirname(require.resolve("@backmatter/studio/package.json"));
20
+ const dist = path.join(studioRoot, "dist/site-widget.js");
21
+ if (!existsSync(dist)) return {};
22
+ return { "@backmatter/studio/widget": dist };
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
12
27
  import { DEFAULT_DIRS } from "@backmatter/core/generate";
13
28
  import {
14
29
  DEFAULT_OUT_DIR,
@@ -159,6 +174,7 @@ export function withBackmatter<T extends object>(
159
174
  ...existingTurbo,
160
175
  resolveAlias: {
161
176
  ...registryTurbopackAlias(loaderOptions.root, loaderOptions.outDir),
177
+ ...studioWidgetAlias(),
162
178
  ...existingAlias,
163
179
  },
164
180
  rules: { ...existingRules, [glob]: turbopackRule },
@@ -175,6 +191,7 @@ export function withBackmatter<T extends object>(
175
191
  prevAlias && typeof prevAlias === "object" && !Array.isArray(prevAlias) ? prevAlias : {};
176
192
  config.resolve.alias = {
177
193
  ...registryWebpackAlias(loaderOptions.root, loaderOptions.outDir),
194
+ ...studioWidgetAlias(),
178
195
  ...prev,
179
196
  };
180
197
  return cfg.webpack ? cfg.webpack(config, context) : config;
@@ -1,7 +1,8 @@
1
1
  // packages/next/src/devbar-client.tsx
2
2
  "use client";
3
3
 
4
- import { Suspense, lazy, useEffect, useState, type CSSProperties, type ReactNode } from "react";
4
+ import { Suspense, lazy, startTransition, useCallback, useEffect, useLayoutEffect, useState, type CSSProperties, type Dispatch, type ReactNode, type SetStateAction } from "react";
5
+ import { useRouter } from "next/navigation";
5
6
  import {
6
7
  clearHostPreviewSession,
7
8
  fetchCmsSession,
@@ -60,6 +61,26 @@ export function sessionGateAction(input: {
60
61
  return "quiet";
61
62
  }
62
63
 
64
+ /** Mode switch keeps a ready bar mounted. A closed gate stays hidden. */
65
+ export function withHostMode(state: DevModeState, mode: "preview" | "live"): DevModeState {
66
+ if (state.status !== "ready") return state;
67
+ return { ...state, mode };
68
+ }
69
+
70
+ /**
71
+ * Last verified session in this document. A refresh can remount the client
72
+ * bar; restoring this before paint keeps it on screen. Server renders stay
73
+ * hidden so one request cannot leak into the next.
74
+ */
75
+ let shownSession: DevModeState = { status: "hidden" };
76
+ let shownRev = 0;
77
+
78
+ function rememberShown(next: DevModeState) {
79
+ if (typeof window === "undefined") return;
80
+ shownSession = next;
81
+ shownRev += 1;
82
+ }
83
+
63
84
  export function devModeState(
64
85
  user: CmsSessionUser | null,
65
86
  session: {
@@ -340,14 +361,23 @@ function useGate(): Gate {
340
361
  * Only an explicit enter with no session leaves the page, and it leaves
341
362
  * top-level. A stale marker stays on the public page.
342
363
  */
343
- function useSiteSession(gate: Gate, previewEntryPath: string): DevModeState {
364
+ function useSiteSession(
365
+ gate: Gate,
366
+ previewEntryPath: string,
367
+ ): [DevModeState, Dispatch<SetStateAction<DevModeState>>] {
344
368
  const [state, setState] = useState<DevModeState>({ status: "hidden" });
345
369
 
370
+ useLayoutEffect(() => {
371
+ if (shownSession.status !== "ready") return;
372
+ setState(shownSession);
373
+ }, []);
374
+
346
375
  useEffect(() => {
347
376
  if (gate === "closed") return;
377
+ const rev = shownRev;
348
378
  let cancelled = false;
349
379
  void loadDevModeState("").then((next) => {
350
- if (cancelled) return;
380
+ if (cancelled || rev !== shownRev) return;
351
381
  const showBar = next.status === "ready";
352
382
  const reason = next.status === "hidden" ? (next.reason ?? "anonymous") : "anonymous";
353
383
  const action = sessionGateAction({
@@ -358,6 +388,7 @@ function useSiteSession(gate: Gate, previewEntryPath: string): DevModeState {
358
388
  });
359
389
  if (action === "show") {
360
390
  sessionStorage.removeItem(ENTER_ATTEMPT_KEY);
391
+ rememberShown(next);
361
392
  setState(next);
362
393
  return;
363
394
  }
@@ -379,36 +410,57 @@ function useSiteSession(gate: Gate, previewEntryPath: string): DevModeState {
379
410
  };
380
411
  }, [gate, previewEntryPath]);
381
412
 
382
- return state;
413
+ return [state, setState];
383
414
  }
384
415
 
385
416
  export function DevBarClient({
386
417
  studioPath,
387
418
  previewEntryPath = DEFAULT_PREVIEW_ENTRY_PATH,
388
419
  previewFrame,
420
+ previewFrameRadius,
421
+ dashboardUrl,
389
422
  }: {
390
423
  /** Unused since the bar stopped calling the CMS origin; kept for host layouts that still pass it. */
391
424
  apiUrl?: string;
392
425
  studioPath?: string;
393
426
  previewEntryPath?: string;
394
427
  previewFrame?: boolean;
428
+ previewFrameRadius?: number;
429
+ dashboardUrl?: string;
395
430
  }) {
396
431
  const gate = useGate();
397
- const state = useSiteSession(gate, previewEntryPath);
432
+ const [state, setState] = useSiteSession(gate, previewEntryPath);
433
+ const router = useRouter();
434
+ const refreshHost = useCallback(() => {
435
+ startTransition(() => {
436
+ router.refresh();
437
+ });
438
+ }, [router]);
398
439
  const [exiting, setExiting] = useState(false);
399
440
  const [entering, setEntering] = useState(false);
441
+
400
442
  const goLive = () => {
401
- if (exiting) return;
443
+ if (exiting || entering) return;
402
444
  setExiting(true);
403
- void goHostSiteLive().then((ok) => {
404
- if (!ok) setExiting(false);
445
+ void goHostSiteLive(setHostSiteLive, () => {
446
+ const next = withHostMode(state, "live");
447
+ rememberShown(next);
448
+ setState(next);
449
+ refreshHost();
450
+ }).then(() => {
451
+ setExiting(false);
405
452
  });
406
453
  };
407
454
  const goPreview = () => {
408
- if (entering) return;
455
+ if (exiting || entering) return;
409
456
  setEntering(true);
410
- void enterHostPreview().then((ok) => {
411
- if (!ok) setEntering(false);
457
+ void enterHostPreview(setHostSitePreview, () => {
458
+ const next = withHostMode(state, "preview");
459
+ rememberShown(next);
460
+ setState(next);
461
+ refreshHost();
462
+ }).then(() => {
463
+ setEntering(false);
412
464
  });
413
465
  };
414
466
  if (state.status !== "ready") return null;
@@ -424,6 +476,8 @@ export function DevBarClient({
424
476
  studioPath={studioPath}
425
477
  previewEntryPath={previewEntryPath}
426
478
  previewFrame={state.mode === "preview" ? previewFrame : false}
479
+ previewFrameRadius={previewFrameRadius}
480
+ dashboardUrl={dashboardUrl}
427
481
  />
428
482
  </Suspense>
429
483
  );
package/src/devbar.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  // packages/next/src/devbar.tsx
2
- import { resolveDevBarApiUrl } from "@backmatter/core";
2
+ import { resolveDashboardUrl, resolveDevBarApiUrl } from "@backmatter/core";
3
3
  import { DevBarClient } from "./devbar-client";
4
4
 
5
5
  export {
@@ -10,6 +10,7 @@ export {
10
10
  loadDevModeState,
11
11
  sessionGateAction,
12
12
  useDevMode,
13
+ withHostMode,
13
14
  type CmsSessionUser,
14
15
  type DevModeState,
15
16
  type SessionGateAction,
@@ -31,11 +32,13 @@ export function DevBar({
31
32
  studioPath,
32
33
  previewEntryPath,
33
34
  previewFrame,
35
+ previewFrameRadius,
34
36
  }: {
35
37
  apiUrl?: string;
36
38
  studioPath?: string;
37
39
  previewEntryPath?: string;
38
40
  previewFrame?: boolean;
41
+ previewFrameRadius?: number;
39
42
  } = {}) {
40
43
  return (
41
44
  <DevBarClient
@@ -43,6 +46,8 @@ export function DevBar({
43
46
  studioPath={studioPath}
44
47
  previewEntryPath={previewEntryPath}
45
48
  previewFrame={previewFrame}
49
+ previewFrameRadius={previewFrameRadius}
50
+ dashboardUrl={resolveDashboardUrl()}
46
51
  />
47
52
  );
48
53
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { usePathname, useRouter } from "next/navigation";
7
7
  import { startTransition, useCallback, useEffect, useRef, useState } from "react";
8
- import "@backmatter/studio/studio.css";
8
+ import "@backmatter/studio/site-widget.css";
9
9
  import studioPkg from "@backmatter/studio/package.json";
10
10
  import { SiteWidget } from "@backmatter/studio/widget";
11
11
  import { markerCookie } from "./gate";
@@ -68,6 +68,8 @@ export function SiteChrome({
68
68
  onEnterPreview,
69
69
  previewEntryPath = DEFAULT_PREVIEW_ENTRY_PATH,
70
70
  previewFrame,
71
+ previewFrameRadius,
72
+ dashboardUrl,
71
73
  }: {
72
74
  branch?: string;
73
75
  tree?: string;
@@ -80,6 +82,8 @@ export function SiteChrome({
80
82
  /** Ticket route. `POST ?renew=1` slides the site session. A 401 drops the bar. */
81
83
  previewEntryPath?: string;
82
84
  previewFrame?: boolean;
85
+ previewFrameRadius?: number;
86
+ dashboardUrl?: string;
83
87
  }) {
84
88
  const router = useRouter();
85
89
  const doc = useSiteDoc();
@@ -139,7 +143,10 @@ export function SiteChrome({
139
143
  onSessionExpired={leave}
140
144
  previewEntryPath={previewEntryPath}
141
145
  previewFrame={previewFrame}
146
+ previewFrameRadius={previewFrameRadius}
142
147
  version={studioPkg.version}
148
+ dashboardHref={dashboardUrl}
149
+ onSignOut={leave}
143
150
  />
144
151
  );
145
152
  }