@carlonicora/nextjs-jsonapi 6.14.0 → 6.15.0
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/{BlockNoteEditor-QIVV2OB4.mjs → BlockNoteEditor-I3J6YYXK.mjs} +2 -2
- package/dist/{BlockNoteEditor-CUNH4LSQ.js → BlockNoteEditor-UIAX72XW.js} +9 -9
- package/dist/{BlockNoteEditor-CUNH4LSQ.js.map → BlockNoteEditor-UIAX72XW.js.map} +1 -1
- package/dist/billing/index.js +378 -378
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-GSRTVIIK.js → chunk-RT7XFGIN.js} +222 -218
- package/dist/chunk-RT7XFGIN.js.map +1 -0
- package/dist/{chunk-YKNULEHB.mjs → chunk-RYY7USSW.mjs} +9 -5
- package/dist/chunk-RYY7USSW.mjs.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/editors/server.d.mts +7 -1
- package/dist/components/editors/server.d.ts +7 -1
- package/dist/components/editors/server.js +17 -4
- package/dist/components/editors/server.js.map +1 -1
- package/dist/components/editors/server.mjs +17 -4
- package/dist/components/editors/server.mjs.map +1 -1
- package/dist/components/index.js +2 -2
- package/dist/components/index.mjs +1 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/email-automations/index.js +58 -58
- package/dist/email-automations/index.mjs +1 -1
- package/dist/features/analytics/index.js +96 -96
- package/dist/features/analytics/index.mjs +1 -1
- package/dist/features/blog/index.js +93 -93
- package/dist/features/blog/index.mjs +1 -1
- package/dist/features/help/index.js +31 -31
- package/dist/features/help/index.mjs +1 -1
- package/dist/features/tokenusage/index.js +102 -102
- package/dist/features/tokenusage/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/editors/server/__tests__/renderBlockNoteMarkdown.spec.ts +45 -0
- package/src/components/editors/server/renderBlockNoteHTML.ts +2 -18
- package/src/components/editors/server/renderBlockNoteMarkdown.ts +10 -0
- package/src/components/editors/server/serverEditor.ts +25 -0
- package/src/components/editors/server-entry.ts +1 -0
- package/src/hooks/__tests__/usePageTracker.spec.tsx +61 -0
- package/src/hooks/usePageTracker.ts +17 -7
- package/dist/chunk-GSRTVIIK.js.map +0 -1
- package/dist/chunk-YKNULEHB.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-QIVV2OB4.mjs.map → BlockNoteEditor-I3J6YYXK.mjs.map} +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
// The server render runs in Node, where there is no window or document of our own:
|
|
3
|
+
// BlockNote's ServerBlockNoteEditor brings its JSDOM.
|
|
4
|
+
import type { PartialBlock } from "@blocknote/core";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { renderBlockNoteHTML } from "../renderBlockNoteHTML";
|
|
7
|
+
import { renderBlockNoteMarkdown } from "../renderBlockNoteMarkdown";
|
|
8
|
+
|
|
9
|
+
const blocks: PartialBlock[] = [
|
|
10
|
+
{ type: "heading", props: { level: 2 }, content: "Think of the Campaign as the lid on the box" },
|
|
11
|
+
{ type: "paragraph", content: "One Campaign is one story arc." },
|
|
12
|
+
{ type: "bulletListItem", content: "Characters" },
|
|
13
|
+
{
|
|
14
|
+
type: "paragraph",
|
|
15
|
+
content: [{ type: "link", href: "https://wyrd.li/help", content: "help" }],
|
|
16
|
+
},
|
|
17
|
+
{ type: "image", props: { url: "https://wyrd.li/marketing/og.png", caption: "Wyrdli" } },
|
|
18
|
+
{ type: "image", props: { url: "https://wyrd.li/marketing/plain.png" } },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
describe("renderBlockNoteMarkdown", () => {
|
|
22
|
+
it("renders headings, paragraphs, bullet lists and links", async () => {
|
|
23
|
+
const markdown = await renderBlockNoteMarkdown(blocks);
|
|
24
|
+
|
|
25
|
+
expect(markdown).toContain("## Think of the Campaign as the lid on the box");
|
|
26
|
+
expect(markdown).toContain("One Campaign is one story arc.");
|
|
27
|
+
expect(markdown).toMatch(/^\s*[-*+] Characters\s*$/m);
|
|
28
|
+
expect(markdown).toContain("[help](https://wyrd.li/help)");
|
|
29
|
+
// blocksToMarkdownLossy keeps a captioned image as an HTML <figure> (valid
|
|
30
|
+
// CommonMark HTML block); an image with no caption becomes .
|
|
31
|
+
expect(markdown).toMatch(/<img[^>]*src="https:\/\/wyrd\.li\/marketing\/og\.png"/);
|
|
32
|
+
expect(markdown).toContain("<figcaption>Wyrdli</figcaption>");
|
|
33
|
+
expect(markdown).toMatch(/!\[[^\]]*\]\(https:\/\/wyrd\.li\/marketing\/plain\.png\)/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("two overlapping calls both resolve and leave no JSDOM window on globalThis", async () => {
|
|
37
|
+
const before = typeof (globalThis as { window?: unknown }).window;
|
|
38
|
+
|
|
39
|
+
const [markdown, html] = await Promise.all([renderBlockNoteMarkdown(blocks), renderBlockNoteHTML(blocks)]);
|
|
40
|
+
|
|
41
|
+
expect(markdown).toContain("One Campaign is one story arc.");
|
|
42
|
+
expect(html).toContain("One Campaign is one story arc.");
|
|
43
|
+
expect(typeof (globalThis as { window?: unknown }).window).toBe(before);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
import type { PartialBlock } from "@blocknote/core";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
// Default schema only, the same one BlockNoteViewer uses: the editor's mention
|
|
5
|
-
// and diff inline specs are React specs from "use client" modules (next/link,
|
|
6
|
-
// hover cards), so they cannot run here.
|
|
7
|
-
let editor: ServerBlockNoteEditor | undefined;
|
|
8
|
-
|
|
9
|
-
// ServerBlockNoteEditor swaps globalThis.window/document for its JSDOM around
|
|
10
|
-
// each call and restores them after an await. Two overlapping calls would
|
|
11
|
-
// restore each other's values and could leave the JSDOM window on the server's
|
|
12
|
-
// globals, so calls run one at a time.
|
|
13
|
-
let queue: Promise<unknown> = Promise.resolve();
|
|
2
|
+
import { runWithServerEditor } from "./serverEditor";
|
|
14
3
|
|
|
15
4
|
/**
|
|
16
5
|
* Renders a BlockNote blocks array to the HTML the editor itself would render,
|
|
17
6
|
* through BlockNote's own server editor (`blocksToFullHTML`). Server only.
|
|
18
7
|
*/
|
|
19
8
|
export function renderBlockNoteHTML(blocks: PartialBlock[]): Promise<string> {
|
|
20
|
-
|
|
21
|
-
editor ??= ServerBlockNoteEditor.create();
|
|
22
|
-
return editor.blocksToFullHTML(blocks);
|
|
23
|
-
});
|
|
24
|
-
queue = run.catch(() => undefined);
|
|
25
|
-
return run;
|
|
9
|
+
return runWithServerEditor((editor) => editor.blocksToFullHTML(blocks));
|
|
26
10
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PartialBlock } from "@blocknote/core";
|
|
2
|
+
import { runWithServerEditor } from "./serverEditor";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Renders a BlockNote blocks array to Markdown through BlockNote's own server
|
|
6
|
+
* editor (`blocksToMarkdownLossy`). Server only.
|
|
7
|
+
*/
|
|
8
|
+
export function renderBlockNoteMarkdown(blocks: PartialBlock[]): Promise<string> {
|
|
9
|
+
return runWithServerEditor((editor) => editor.blocksToMarkdownLossy(blocks));
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ServerBlockNoteEditor } from "@blocknote/server-util";
|
|
2
|
+
|
|
3
|
+
// Default schema only, the same one BlockNoteViewer uses: the editor's mention
|
|
4
|
+
// and diff inline specs are React specs from "use client" modules (next/link,
|
|
5
|
+
// hover cards), so they cannot run here.
|
|
6
|
+
let editor: ServerBlockNoteEditor | undefined;
|
|
7
|
+
|
|
8
|
+
// ServerBlockNoteEditor swaps globalThis.window/document for its JSDOM around
|
|
9
|
+
// each call and restores them after an await. Two overlapping calls would
|
|
10
|
+
// restore each other's values and could leave the JSDOM window on the server's
|
|
11
|
+
// globals, so calls run one at a time.
|
|
12
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs `fn` with the shared server editor, one call at a time. Server only.
|
|
16
|
+
* Internal to the library: not exported from the barrel.
|
|
17
|
+
*/
|
|
18
|
+
export function runWithServerEditor<T>(fn: (editor: ServerBlockNoteEditor) => T | Promise<T>): Promise<T> {
|
|
19
|
+
const run = queue.then(() => {
|
|
20
|
+
editor ??= ServerBlockNoteEditor.create();
|
|
21
|
+
return fn(editor);
|
|
22
|
+
});
|
|
23
|
+
queue = run.catch(() => undefined);
|
|
24
|
+
return run;
|
|
25
|
+
}
|
|
@@ -299,4 +299,65 @@ describe("usePageTracker", () => {
|
|
|
299
299
|
|
|
300
300
|
await waitFor(() => expect(pages(store)[0]?.title).toBe("Venezia Obscura"));
|
|
301
301
|
});
|
|
302
|
+
|
|
303
|
+
describe("most-specific-first titles (`Name · Type · Context | App`)", () => {
|
|
304
|
+
it("records the entity name on a hard load", async () => {
|
|
305
|
+
pathnameRef.current = "/npcs/npc-1";
|
|
306
|
+
document.title = "Charlie · NPC · Venezia Obscura | App";
|
|
307
|
+
const store = createStore();
|
|
308
|
+
store.set(recentPagesAtom, []);
|
|
309
|
+
|
|
310
|
+
mount(store);
|
|
311
|
+
|
|
312
|
+
await waitFor(() => expect(pages(store)).toHaveLength(1));
|
|
313
|
+
expect(pages(store)[0]).toMatchObject({ url: "/npcs/npc-1", title: "Charlie", moduleType: "npcs" });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("records a single-part title such as a campaign's", async () => {
|
|
317
|
+
pathnameRef.current = "/campaigns/camp-1";
|
|
318
|
+
document.title = "Venezia Obscura | App";
|
|
319
|
+
const store = createStore();
|
|
320
|
+
store.set(recentPagesAtom, []);
|
|
321
|
+
|
|
322
|
+
mount(store);
|
|
323
|
+
|
|
324
|
+
await waitFor(() => expect(pages(store)[0]?.title).toBe("Venezia Obscura"));
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("never stamps an entity with the previous page's name, then upgrades when the title lands", async () => {
|
|
328
|
+
pathnameRef.current = "/campaigns/camp-1";
|
|
329
|
+
document.title = "Venezia Obscura | App";
|
|
330
|
+
const store = createStore();
|
|
331
|
+
store.set(recentPagesAtom, []);
|
|
332
|
+
const view = mount(store);
|
|
333
|
+
await waitFor(() => expect(pages(store)[0]?.title).toBe("Venezia Obscura"));
|
|
334
|
+
|
|
335
|
+
pathnameRef.current = "/npcs/npc-1";
|
|
336
|
+
view.rerender(
|
|
337
|
+
<Provider store={store}>
|
|
338
|
+
<Harness />
|
|
339
|
+
</Provider>,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
await waitFor(() => expect(pages(store)[0]?.url).toBe("/npcs/npc-1"));
|
|
343
|
+
expect(pages(store)[0].title).toBe("npcs");
|
|
344
|
+
|
|
345
|
+
document.title = "Charlie · NPC · Venezia Obscura | App";
|
|
346
|
+
|
|
347
|
+
await waitFor(() => expect(pages(store)[0]?.title).toBe("Charlie"));
|
|
348
|
+
expect(pages(store).find((page) => page.url === "/campaigns/camp-1")?.title).toBe("Venezia Obscura");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("keeps the module name when the title is only the app name", async () => {
|
|
352
|
+
pathnameRef.current = "/npcs/npc-1";
|
|
353
|
+
document.title = "App";
|
|
354
|
+
const store = createStore();
|
|
355
|
+
store.set(recentPagesAtom, []);
|
|
356
|
+
|
|
357
|
+
mount(store);
|
|
358
|
+
|
|
359
|
+
await waitFor(() => expect(pages(store)).toHaveLength(1));
|
|
360
|
+
expect(pages(store)[0].title).toBe("npcs");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
302
363
|
});
|
|
@@ -10,18 +10,28 @@ import { getTrackablePages } from "../client/config";
|
|
|
10
10
|
const EXCLUDED_ROUTES = ["/", "/login", "/register", "/forgot-password", "/reset-password", "/activate"];
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Detail pages title themselves
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Detail pages title themselves in one of two shapes:
|
|
14
|
+
*
|
|
15
|
+
* `[Entity type] Entity name | App name` (bracketed)
|
|
16
|
+
* `Entity name · Entity type · Context | App name` (most-specific-first)
|
|
17
|
+
*
|
|
18
|
+
* Returns the entity name, or null when the document title carries no page
|
|
19
|
+
* name — just the bare app name. Whether the title belongs to THIS route (and
|
|
20
|
+
* not the page we left) is decided by the caller, not here.
|
|
17
21
|
*/
|
|
18
22
|
function readEntityTitle(): string | null {
|
|
19
23
|
if (typeof document === "undefined") return null;
|
|
20
24
|
|
|
21
|
-
const
|
|
22
|
-
if (!afterEntityType) return null;
|
|
25
|
+
const title = document.title;
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
if (title.includes("]")) {
|
|
28
|
+
const afterEntityType = title.split("]")[1];
|
|
29
|
+
return afterEntityType?.split("|")[0]?.trim() || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!title.includes("|")) return null;
|
|
33
|
+
|
|
34
|
+
return title.split("|")[0].split("·")[0]?.trim() || null;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
export function usePageTracker() {
|