@tomehq/theme 0.3.2 → 0.3.3
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/CHANGELOG.md +17 -0
- package/dist/{chunk-2AXAEADQ.js → chunk-MSXVVBDW.js} +24 -7
- package/dist/entry.js +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/Shell.test.tsx +222 -0
- package/src/Shell.tsx +17 -3
- package/src/__virtual_stubs/config.ts +2 -0
- package/src/__virtual_stubs/doc-context.ts +2 -0
- package/src/__virtual_stubs/overrides.ts +2 -0
- package/src/__virtual_stubs/page-loader.ts +4 -0
- package/src/__virtual_stubs/routes.ts +5 -0
- package/src/entry-helpers.test.ts +76 -0
- package/src/entry-helpers.ts +18 -1
- package/src/entry.test.tsx +695 -0
- package/src/entry.tsx +6 -0
- package/vitest.config.ts +31 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @tomehq/theme
|
|
2
2
|
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 062349c: Fix MCP server stdout corruption, add dashboard mobile responsiveness, improve test coverage, and update docs.
|
|
8
|
+
|
|
9
|
+
- Fix: MCP CLI banner no longer writes to stdout, preventing JSON-RPC protocol corruption
|
|
10
|
+
- Fix: API Playground prop wiring and githubSource route crash
|
|
11
|
+
- Feat: MCP server `createMcpServer()` exported for programmatic use with graceful shutdown
|
|
12
|
+
- Feat: Dashboard mobile-responsive layout with media query breakpoints
|
|
13
|
+
- Feat: 13 MCP server integration tests using InMemoryTransport
|
|
14
|
+
- Docs: Add missing typedoc CLI command, fix social link examples, update package list
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [062349c]
|
|
17
|
+
- @tomehq/core@0.3.3
|
|
18
|
+
- @tomehq/components@0.3.3
|
|
19
|
+
|
|
3
20
|
## 0.2.8
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
|
@@ -818,6 +818,11 @@ function Shell({
|
|
|
818
818
|
editUrl,
|
|
819
819
|
lastUpdated,
|
|
820
820
|
changelogEntries,
|
|
821
|
+
apiManifest,
|
|
822
|
+
apiBaseUrl,
|
|
823
|
+
apiPlayground,
|
|
824
|
+
apiAuth,
|
|
825
|
+
ApiReferenceComponent,
|
|
821
826
|
onNavigate,
|
|
822
827
|
allPages,
|
|
823
828
|
versioning,
|
|
@@ -1613,12 +1618,15 @@ function Shell({
|
|
|
1613
1618
|
/* @__PURE__ */ jsx2("h1", { style: { fontFamily: "var(--font-heading)", fontSize: mobile ? 26 : 38, fontWeight: 400, fontStyle: "italic", lineHeight: 1.15, marginBottom: 8 }, children: pageTitle }),
|
|
1614
1619
|
isDraft && /* @__PURE__ */ jsx2("div", { "data-testid": "draft-banner", style: { background: "#fef3c7", color: "#92400e", padding: "8px 16px", borderRadius: 6, fontSize: 13, marginBottom: 16 }, children: "Draft \u2014 This page is only visible in development" }),
|
|
1615
1620
|
pageDescription && /* @__PURE__ */ jsx2("p", { style: { fontSize: 16, color: "var(--tx2)", lineHeight: 1.6, marginBottom: 32 }, children: pageDescription }),
|
|
1616
|
-
/* @__PURE__ */ jsx2("div", { style: { borderTop: "1px solid var(--bd)", paddingTop: 28 }, children:
|
|
1617
|
-
|
|
1618
|
-
{
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1621
|
+
/* @__PURE__ */ jsx2("div", { style: { borderTop: "1px solid var(--bd)", paddingTop: 28 }, children: apiManifest && ApiReferenceComponent ? /* @__PURE__ */ jsx2(ApiReferenceComponent, { manifest: apiManifest, baseUrl: apiBaseUrl, showPlayground: apiPlayground, playgroundAuth: apiAuth }) : (
|
|
1622
|
+
/* TOM-49: Changelog page type */
|
|
1623
|
+
changelogEntries && changelogEntries.length > 0 ? /* @__PURE__ */ jsx2(ChangelogView, { entries: changelogEntries }) : PageComponent ? /* @__PURE__ */ jsx2("div", { className: "tome-content", children: /* @__PURE__ */ jsx2(PageComponent, { components: mdxComponents || {} }) }) : /* @__PURE__ */ jsx2(
|
|
1624
|
+
"div",
|
|
1625
|
+
{
|
|
1626
|
+
className: "tome-content",
|
|
1627
|
+
ref: htmlContentRef
|
|
1628
|
+
}
|
|
1629
|
+
)
|
|
1622
1630
|
) }),
|
|
1623
1631
|
overrides2?.PageFooter ? /* @__PURE__ */ jsx2(
|
|
1624
1632
|
overrides2.PageFooter,
|
|
@@ -1986,6 +1994,9 @@ async function loadPage(id, routes2, loadPageModule2) {
|
|
|
1986
1994
|
};
|
|
1987
1995
|
}
|
|
1988
1996
|
if (!mod.default) return null;
|
|
1997
|
+
if (mod.isApiReference && mod.apiManifest) {
|
|
1998
|
+
return { isMdx: false, isApiReference: true, ...mod.default, apiManifest: mod.apiManifest };
|
|
1999
|
+
}
|
|
1989
2000
|
if (mod.isChangelog && mod.changelogEntries) {
|
|
1990
2001
|
return { isMdx: false, ...mod.default, changelogEntries: mod.changelogEntries };
|
|
1991
2002
|
}
|
|
@@ -2018,7 +2029,8 @@ import {
|
|
|
2018
2029
|
FileTree,
|
|
2019
2030
|
CodeSamples,
|
|
2020
2031
|
LinkCard,
|
|
2021
|
-
CardGrid
|
|
2032
|
+
CardGrid,
|
|
2033
|
+
ApiReference
|
|
2022
2034
|
} from "@tomehq/components";
|
|
2023
2035
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2024
2036
|
var MDX_COMPONENTS = {
|
|
@@ -2495,6 +2507,11 @@ function App() {
|
|
|
2495
2507
|
editUrl,
|
|
2496
2508
|
lastUpdated: currentRoute?.lastUpdated,
|
|
2497
2509
|
changelogEntries: !pageData?.isMdx ? pageData?.changelogEntries : void 0,
|
|
2510
|
+
apiManifest: !pageData?.isMdx && pageData?.isApiReference ? pageData.apiManifest : void 0,
|
|
2511
|
+
apiBaseUrl: config.api?.baseUrl,
|
|
2512
|
+
apiPlayground: config.api?.playground,
|
|
2513
|
+
apiAuth: config.api?.auth,
|
|
2514
|
+
ApiReferenceComponent: ApiReference,
|
|
2498
2515
|
onNavigate: navigateTo,
|
|
2499
2516
|
allPages,
|
|
2500
2517
|
docContext,
|
package/dist/entry.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -96,6 +96,22 @@ interface ShellProps {
|
|
|
96
96
|
items: string[];
|
|
97
97
|
}>;
|
|
98
98
|
}>;
|
|
99
|
+
apiManifest?: any;
|
|
100
|
+
apiBaseUrl?: string;
|
|
101
|
+
apiPlayground?: boolean;
|
|
102
|
+
apiAuth?: {
|
|
103
|
+
type: "bearer" | "apiKey";
|
|
104
|
+
header?: string;
|
|
105
|
+
};
|
|
106
|
+
ApiReferenceComponent?: React.ComponentType<{
|
|
107
|
+
manifest: any;
|
|
108
|
+
baseUrl?: string;
|
|
109
|
+
showPlayground?: boolean;
|
|
110
|
+
playgroundAuth?: {
|
|
111
|
+
type: "bearer" | "apiKey";
|
|
112
|
+
header?: string;
|
|
113
|
+
};
|
|
114
|
+
}>;
|
|
99
115
|
onNavigate: (id: string) => void;
|
|
100
116
|
allPages: Array<{
|
|
101
117
|
id: string;
|
|
@@ -122,7 +138,7 @@ interface ShellProps {
|
|
|
122
138
|
PageFooter?: React.ComponentType<any>;
|
|
123
139
|
};
|
|
124
140
|
}
|
|
125
|
-
declare function Shell({ config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents, pageTitle, pageDescription, headings, tocEnabled, editUrl, lastUpdated, changelogEntries, onNavigate, allPages, versioning, currentVersion, i18n, currentLocale, docContext, basePath, isDraft, dir: dirProp, overrides, }: ShellProps): react_jsx_runtime.JSX.Element;
|
|
141
|
+
declare function Shell({ config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents, pageTitle, pageDescription, headings, tocEnabled, editUrl, lastUpdated, changelogEntries, apiManifest, apiBaseUrl, apiPlayground, apiAuth, ApiReferenceComponent, onNavigate, allPages, versioning, currentVersion, i18n, currentLocale, docContext, basePath, isDraft, dir: dirProp, overrides, }: ShellProps): react_jsx_runtime.JSX.Element;
|
|
126
142
|
|
|
127
143
|
interface AiChatProps {
|
|
128
144
|
provider: "openai" | "anthropic" | "custom";
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomehq/theme",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Tome default theme and React app shell",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.tsx",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"./entry": "./src/entry.tsx"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@tomehq/components": "0.3.
|
|
13
|
-
"@tomehq/core": "0.3.
|
|
12
|
+
"@tomehq/components": "0.3.3",
|
|
13
|
+
"@tomehq/core": "0.3.3"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/Shell.test.tsx
CHANGED
|
@@ -1052,3 +1052,225 @@ describe("Shell RTL support", () => {
|
|
|
1052
1052
|
).not.toThrow();
|
|
1053
1053
|
});
|
|
1054
1054
|
});
|
|
1055
|
+
|
|
1056
|
+
// ── API Reference (TOM-19) ──────────────────────────────
|
|
1057
|
+
|
|
1058
|
+
describe("Shell API reference rendering", () => {
|
|
1059
|
+
const mockManifest = {
|
|
1060
|
+
title: "Test API",
|
|
1061
|
+
version: "1.0.0",
|
|
1062
|
+
servers: [{ url: "https://api.example.com" }],
|
|
1063
|
+
tags: [{ name: "Users", description: "User management" }],
|
|
1064
|
+
endpoints: [
|
|
1065
|
+
{
|
|
1066
|
+
method: "GET",
|
|
1067
|
+
path: "/users",
|
|
1068
|
+
summary: "List all users",
|
|
1069
|
+
tags: ["Users"],
|
|
1070
|
+
parameters: [],
|
|
1071
|
+
responses: [{ status: "200", description: "OK" }],
|
|
1072
|
+
},
|
|
1073
|
+
],
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
function MockApiRef({ manifest, baseUrl, showPlayground, playgroundAuth }: { manifest: any; baseUrl?: string; showPlayground?: boolean; playgroundAuth?: { type: string; header?: string } }) {
|
|
1077
|
+
return (
|
|
1078
|
+
<div data-testid="api-reference">
|
|
1079
|
+
<span data-testid="api-title">{manifest.title}</span>
|
|
1080
|
+
{baseUrl && <span data-testid="api-base-url">{baseUrl}</span>}
|
|
1081
|
+
{showPlayground && <span data-testid="api-playground">playground-enabled</span>}
|
|
1082
|
+
{playgroundAuth && <span data-testid="api-auth">{playgroundAuth.type}</span>}
|
|
1083
|
+
</div>
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
it("renders ApiReferenceComponent when both apiManifest and component are provided", () => {
|
|
1088
|
+
renderShell({
|
|
1089
|
+
apiManifest: mockManifest,
|
|
1090
|
+
ApiReferenceComponent: MockApiRef,
|
|
1091
|
+
pageHtml: undefined,
|
|
1092
|
+
});
|
|
1093
|
+
expect(screen.getByTestId("api-reference")).toBeInTheDocument();
|
|
1094
|
+
expect(screen.getByTestId("api-title")).toHaveTextContent("Test API");
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it("passes baseUrl to ApiReferenceComponent", () => {
|
|
1098
|
+
renderShell({
|
|
1099
|
+
apiManifest: mockManifest,
|
|
1100
|
+
ApiReferenceComponent: MockApiRef,
|
|
1101
|
+
apiBaseUrl: "https://api.example.com",
|
|
1102
|
+
pageHtml: undefined,
|
|
1103
|
+
});
|
|
1104
|
+
expect(screen.getByTestId("api-base-url")).toHaveTextContent("https://api.example.com");
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
it("does not render API reference when only apiManifest is provided (no component)", () => {
|
|
1108
|
+
renderShell({
|
|
1109
|
+
apiManifest: mockManifest,
|
|
1110
|
+
pageHtml: "<p>Fallback content</p>",
|
|
1111
|
+
});
|
|
1112
|
+
expect(screen.queryByTestId("api-reference")).not.toBeInTheDocument();
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it("does not render API reference when only component is provided (no manifest)", () => {
|
|
1116
|
+
renderShell({
|
|
1117
|
+
ApiReferenceComponent: MockApiRef,
|
|
1118
|
+
pageHtml: "<p>Fallback content</p>",
|
|
1119
|
+
});
|
|
1120
|
+
expect(screen.queryByTestId("api-reference")).not.toBeInTheDocument();
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it("falls back to pageHtml when no apiManifest", () => {
|
|
1124
|
+
const { container } = renderShell({
|
|
1125
|
+
pageHtml: "<p>Regular page content</p>",
|
|
1126
|
+
});
|
|
1127
|
+
expect(container.querySelector(".tome-content")).toBeInTheDocument();
|
|
1128
|
+
expect(screen.queryByTestId("api-reference")).not.toBeInTheDocument();
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it("API reference takes precedence over changelog entries", () => {
|
|
1132
|
+
const changelogEntries = [
|
|
1133
|
+
{ version: "1.0.0", sections: [{ type: "Added", items: ["Feature"] }] },
|
|
1134
|
+
];
|
|
1135
|
+
renderShell({
|
|
1136
|
+
apiManifest: mockManifest,
|
|
1137
|
+
ApiReferenceComponent: MockApiRef,
|
|
1138
|
+
changelogEntries,
|
|
1139
|
+
pageHtml: undefined,
|
|
1140
|
+
});
|
|
1141
|
+
// API reference should render, not changelog
|
|
1142
|
+
expect(screen.getByTestId("api-reference")).toBeInTheDocument();
|
|
1143
|
+
expect(screen.queryByTestId("changelog-timeline")).not.toBeInTheDocument();
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
it("API reference takes precedence over pageComponent", () => {
|
|
1147
|
+
const PageComp = () => <div data-testid="page-comp">MDX content</div>;
|
|
1148
|
+
renderShell({
|
|
1149
|
+
apiManifest: mockManifest,
|
|
1150
|
+
ApiReferenceComponent: MockApiRef,
|
|
1151
|
+
pageComponent: PageComp,
|
|
1152
|
+
pageHtml: undefined,
|
|
1153
|
+
});
|
|
1154
|
+
expect(screen.getByTestId("api-reference")).toBeInTheDocument();
|
|
1155
|
+
expect(screen.queryByTestId("page-comp")).not.toBeInTheDocument();
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it("passes showPlayground prop to ApiReferenceComponent", () => {
|
|
1159
|
+
renderShell({
|
|
1160
|
+
apiManifest: mockManifest,
|
|
1161
|
+
ApiReferenceComponent: MockApiRef,
|
|
1162
|
+
apiPlayground: true,
|
|
1163
|
+
pageHtml: undefined,
|
|
1164
|
+
});
|
|
1165
|
+
expect(screen.getByTestId("api-playground")).toHaveTextContent("playground-enabled");
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it("passes playgroundAuth prop to ApiReferenceComponent", () => {
|
|
1169
|
+
renderShell({
|
|
1170
|
+
apiManifest: mockManifest,
|
|
1171
|
+
ApiReferenceComponent: MockApiRef,
|
|
1172
|
+
apiPlayground: true,
|
|
1173
|
+
apiAuth: { type: "bearer" },
|
|
1174
|
+
pageHtml: undefined,
|
|
1175
|
+
});
|
|
1176
|
+
expect(screen.getByTestId("api-auth")).toHaveTextContent("bearer");
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
it("does not pass playground props when not provided", () => {
|
|
1180
|
+
renderShell({
|
|
1181
|
+
apiManifest: mockManifest,
|
|
1182
|
+
ApiReferenceComponent: MockApiRef,
|
|
1183
|
+
pageHtml: undefined,
|
|
1184
|
+
});
|
|
1185
|
+
expect(screen.getByTestId("api-reference")).toBeInTheDocument();
|
|
1186
|
+
expect(screen.queryByTestId("api-playground")).not.toBeInTheDocument();
|
|
1187
|
+
expect(screen.queryByTestId("api-auth")).not.toBeInTheDocument();
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
// ── Content link interception ───────────────────────────────
|
|
1192
|
+
|
|
1193
|
+
describe("Content link interception", () => {
|
|
1194
|
+
it("intercepts internal bare page ID links and calls onNavigate", () => {
|
|
1195
|
+
const onNavigate = vi.fn();
|
|
1196
|
+
renderShell({
|
|
1197
|
+
onNavigate,
|
|
1198
|
+
pageHtml: '<p><a href="quickstart">Go to quickstart</a></p>',
|
|
1199
|
+
});
|
|
1200
|
+
const link = screen.getByText("Go to quickstart");
|
|
1201
|
+
fireEvent.click(link);
|
|
1202
|
+
expect(onNavigate).toHaveBeenCalledWith("quickstart");
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
it("intercepts ./relative links and strips the prefix", () => {
|
|
1206
|
+
const onNavigate = vi.fn();
|
|
1207
|
+
renderShell({
|
|
1208
|
+
onNavigate,
|
|
1209
|
+
pageHtml: '<p><a href="./installation">Install</a></p>',
|
|
1210
|
+
});
|
|
1211
|
+
fireEvent.click(screen.getByText("Install"));
|
|
1212
|
+
expect(onNavigate).toHaveBeenCalledWith("installation");
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
it("does not intercept external http links", () => {
|
|
1216
|
+
const onNavigate = vi.fn();
|
|
1217
|
+
renderShell({
|
|
1218
|
+
onNavigate,
|
|
1219
|
+
pageHtml: '<p><a href="https://example.com">External</a></p>',
|
|
1220
|
+
});
|
|
1221
|
+
fireEvent.click(screen.getByText("External"));
|
|
1222
|
+
expect(onNavigate).not.toHaveBeenCalled();
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
it("does not intercept mailto links", () => {
|
|
1226
|
+
const onNavigate = vi.fn();
|
|
1227
|
+
renderShell({
|
|
1228
|
+
onNavigate,
|
|
1229
|
+
pageHtml: '<p><a href="mailto:test@example.com">Email</a></p>',
|
|
1230
|
+
});
|
|
1231
|
+
fireEvent.click(screen.getByText("Email"));
|
|
1232
|
+
expect(onNavigate).not.toHaveBeenCalled();
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
it("does not intercept pure anchor links", () => {
|
|
1236
|
+
const onNavigate = vi.fn();
|
|
1237
|
+
renderShell({
|
|
1238
|
+
onNavigate,
|
|
1239
|
+
pageHtml: '<p><a href="#section-1">Jump to section</a></p>',
|
|
1240
|
+
});
|
|
1241
|
+
fireEvent.click(screen.getByText("Jump to section"));
|
|
1242
|
+
expect(onNavigate).not.toHaveBeenCalled();
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
it("strips basePath prefix from links", () => {
|
|
1246
|
+
const onNavigate = vi.fn();
|
|
1247
|
+
renderShell({
|
|
1248
|
+
onNavigate,
|
|
1249
|
+
basePath: "/docs/",
|
|
1250
|
+
pageHtml: '<p><a href="/docs/quickstart">Go</a></p>',
|
|
1251
|
+
});
|
|
1252
|
+
fireEvent.click(screen.getByText("Go"));
|
|
1253
|
+
expect(onNavigate).toHaveBeenCalledWith("quickstart");
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
it("resolves basePath-only link to index", () => {
|
|
1257
|
+
const onNavigate = vi.fn();
|
|
1258
|
+
renderShell({
|
|
1259
|
+
onNavigate,
|
|
1260
|
+
basePath: "/docs/",
|
|
1261
|
+
pageHtml: '<p><a href="/docs/">Home</a></p>',
|
|
1262
|
+
});
|
|
1263
|
+
fireEvent.click(screen.getByText("Home"));
|
|
1264
|
+
expect(onNavigate).toHaveBeenCalledWith("index");
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
it("resolves empty path to index", () => {
|
|
1268
|
+
const onNavigate = vi.fn();
|
|
1269
|
+
renderShell({
|
|
1270
|
+
onNavigate,
|
|
1271
|
+
pageHtml: '<p><a href="./">Home</a></p>',
|
|
1272
|
+
});
|
|
1273
|
+
fireEvent.click(screen.getByText("Home"));
|
|
1274
|
+
expect(onNavigate).toHaveBeenCalledWith("index");
|
|
1275
|
+
});
|
|
1276
|
+
});
|
package/src/Shell.tsx
CHANGED
|
@@ -367,6 +367,16 @@ interface ShellProps {
|
|
|
367
367
|
editUrl?: string;
|
|
368
368
|
lastUpdated?: string;
|
|
369
369
|
changelogEntries?: Array<{ version: string; date?: string; url?: string; sections: Array<{ type: string; items: string[] }> }>;
|
|
370
|
+
apiManifest?: any;
|
|
371
|
+
apiBaseUrl?: string;
|
|
372
|
+
apiPlayground?: boolean;
|
|
373
|
+
apiAuth?: { type: "bearer" | "apiKey"; header?: string };
|
|
374
|
+
ApiReferenceComponent?: React.ComponentType<{
|
|
375
|
+
manifest: any;
|
|
376
|
+
baseUrl?: string;
|
|
377
|
+
showPlayground?: boolean;
|
|
378
|
+
playgroundAuth?: { type: "bearer" | "apiKey"; header?: string };
|
|
379
|
+
}>;
|
|
370
380
|
onNavigate: (id: string) => void;
|
|
371
381
|
allPages: Array<{ id: string; title: string; description?: string }>;
|
|
372
382
|
versioning?: VersioningInfo;
|
|
@@ -388,7 +398,8 @@ interface ShellProps {
|
|
|
388
398
|
|
|
389
399
|
export function Shell({
|
|
390
400
|
config, navigation, currentPageId, pageHtml, pageComponent, mdxComponents,
|
|
391
|
-
pageTitle, pageDescription, headings, tocEnabled = true, editUrl, lastUpdated, changelogEntries,
|
|
401
|
+
pageTitle, pageDescription, headings, tocEnabled = true, editUrl, lastUpdated, changelogEntries,
|
|
402
|
+
apiManifest, apiBaseUrl, apiPlayground, apiAuth, ApiReferenceComponent, onNavigate, allPages,
|
|
392
403
|
versioning, currentVersion, i18n, currentLocale, docContext, basePath = "", isDraft, dir: dirProp, overrides,
|
|
393
404
|
}: ShellProps) {
|
|
394
405
|
// RTL support: resolve text direction from prop, i18n.localeDirs, or default to "ltr"
|
|
@@ -1156,8 +1167,11 @@ export function Shell({
|
|
|
1156
1167
|
)}
|
|
1157
1168
|
{pageDescription && <p style={{ fontSize: 16, color: "var(--tx2)", lineHeight: 1.6, marginBottom: 32 }}>{pageDescription}</p>}
|
|
1158
1169
|
<div style={{ borderTop: "1px solid var(--bd)", paddingTop: 28 }}>
|
|
1159
|
-
{/* TOM-
|
|
1160
|
-
{
|
|
1170
|
+
{/* TOM-19: API Reference page */}
|
|
1171
|
+
{apiManifest && ApiReferenceComponent ? (
|
|
1172
|
+
<ApiReferenceComponent manifest={apiManifest} baseUrl={apiBaseUrl} showPlayground={apiPlayground} playgroundAuth={apiAuth} />
|
|
1173
|
+
) : /* TOM-49: Changelog page type */
|
|
1174
|
+
changelogEntries && changelogEntries.length > 0 ? (
|
|
1161
1175
|
<ChangelogView entries={changelogEntries} />
|
|
1162
1176
|
) : PageComponent ? (
|
|
1163
1177
|
<div className="tome-content">
|
|
@@ -271,6 +271,82 @@ describe("loadPage", () => {
|
|
|
271
271
|
expect(page!.isMdx).toBe(false);
|
|
272
272
|
});
|
|
273
273
|
|
|
274
|
+
it("loads an API reference page with manifest", async () => {
|
|
275
|
+
const apiManifest = {
|
|
276
|
+
title: "My API",
|
|
277
|
+
servers: [{ url: "https://api.example.com" }],
|
|
278
|
+
tags: [{ name: "Users", description: "User endpoints" }],
|
|
279
|
+
endpoints: [],
|
|
280
|
+
};
|
|
281
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
282
|
+
default: {
|
|
283
|
+
html: "",
|
|
284
|
+
frontmatter: { title: "API Reference" },
|
|
285
|
+
headings: [{ depth: 2, text: "Users", id: "users" }],
|
|
286
|
+
},
|
|
287
|
+
isApiReference: true,
|
|
288
|
+
apiManifest,
|
|
289
|
+
});
|
|
290
|
+
const apiRoutes = [...routesWithMeta, { id: "api-reference", urlPath: "/api", isMdx: false }];
|
|
291
|
+
const page = await loadPage("api-reference", apiRoutes, mockLoader);
|
|
292
|
+
expect(page).not.toBeNull();
|
|
293
|
+
expect(page!.isMdx).toBe(false);
|
|
294
|
+
expect((page as any).isApiReference).toBe(true);
|
|
295
|
+
expect((page as any).apiManifest).toEqual(apiManifest);
|
|
296
|
+
expect(page!.frontmatter.title).toBe("API Reference");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("does not treat page as API reference when isApiReference is false", async () => {
|
|
300
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
301
|
+
default: {
|
|
302
|
+
html: "<p>Regular</p>",
|
|
303
|
+
frontmatter: { title: "Regular Page" },
|
|
304
|
+
headings: [],
|
|
305
|
+
},
|
|
306
|
+
isApiReference: false,
|
|
307
|
+
});
|
|
308
|
+
const page = await loadPage("quickstart", routesWithMeta, mockLoader);
|
|
309
|
+
expect(page).not.toBeNull();
|
|
310
|
+
expect(page!.isMdx).toBe(false);
|
|
311
|
+
expect((page as any).isApiReference).toBeFalsy();
|
|
312
|
+
expect((page as any).apiManifest).toBeUndefined();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("does not treat page as API reference when apiManifest is missing", async () => {
|
|
316
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
317
|
+
default: {
|
|
318
|
+
html: "",
|
|
319
|
+
frontmatter: { title: "API Reference" },
|
|
320
|
+
headings: [],
|
|
321
|
+
},
|
|
322
|
+
isApiReference: true,
|
|
323
|
+
// apiManifest is undefined
|
|
324
|
+
});
|
|
325
|
+
const page = await loadPage("api-reference", routesWithMeta, mockLoader);
|
|
326
|
+
expect(page).not.toBeNull();
|
|
327
|
+
// Without apiManifest, falls through to regular page handling
|
|
328
|
+
expect((page as any).isApiReference).toBeFalsy();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("API reference page preserves headings from module", async () => {
|
|
332
|
+
const headings = [
|
|
333
|
+
{ depth: 2, text: "Users", id: "users" },
|
|
334
|
+
{ depth: 2, text: "Posts", id: "posts" },
|
|
335
|
+
];
|
|
336
|
+
const mockLoader = vi.fn().mockResolvedValue({
|
|
337
|
+
default: {
|
|
338
|
+
html: "",
|
|
339
|
+
frontmatter: { title: "API Reference" },
|
|
340
|
+
headings,
|
|
341
|
+
},
|
|
342
|
+
isApiReference: true,
|
|
343
|
+
apiManifest: { endpoints: [], tags: [], servers: [] },
|
|
344
|
+
});
|
|
345
|
+
const apiRoutes = [...routesWithMeta, { id: "api-reference", urlPath: "/api", isMdx: false }];
|
|
346
|
+
const page = await loadPage("api-reference", apiRoutes, mockLoader);
|
|
347
|
+
expect(page!.headings).toEqual(headings);
|
|
348
|
+
});
|
|
349
|
+
|
|
274
350
|
it("does not treat non-MDX route as MDX even if mod.meta exists", async () => {
|
|
275
351
|
// A markdown route shouldn't be treated as MDX even if the module has meta
|
|
276
352
|
const mockLoader = vi.fn().mockResolvedValue({
|
package/src/entry-helpers.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface HtmlPage {
|
|
|
7
7
|
frontmatter: { title: string; description?: string; toc?: boolean; type?: string };
|
|
8
8
|
headings: Array<{ depth: number; text: string; id: string }>;
|
|
9
9
|
changelogEntries?: Array<{ version: string; date?: string; url?: string; sections: Array<{ type: string; items: string[] }> }>;
|
|
10
|
+
isApiReference?: false;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface MdxPage {
|
|
@@ -14,9 +15,20 @@ export interface MdxPage {
|
|
|
14
15
|
component: React.ComponentType<{ components?: Record<string, React.ComponentType> }>;
|
|
15
16
|
frontmatter: { title: string; description?: string; toc?: boolean; type?: string };
|
|
16
17
|
headings: Array<{ depth: number; text: string; id: string }>;
|
|
18
|
+
isApiReference?: false;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export
|
|
21
|
+
export interface ApiReferencePage {
|
|
22
|
+
isMdx: false;
|
|
23
|
+
isApiReference: true;
|
|
24
|
+
html: string;
|
|
25
|
+
frontmatter: { title: string; description?: string; toc?: boolean; type?: string };
|
|
26
|
+
headings: Array<{ depth: number; text: string; id: string }>;
|
|
27
|
+
changelogEntries?: undefined;
|
|
28
|
+
apiManifest: any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type LoadedPage = HtmlPage | MdxPage | ApiReferencePage;
|
|
20
32
|
|
|
21
33
|
// ── EDIT URL COMPUTATION ──────────────────────────────────
|
|
22
34
|
export interface EditLinkConfig {
|
|
@@ -82,6 +94,11 @@ export async function loadPage(
|
|
|
82
94
|
// Regular .md page — mod.default is { html, frontmatter, headings }
|
|
83
95
|
if (!mod.default) return null;
|
|
84
96
|
|
|
97
|
+
// API reference page (synthetic route from OpenAPI spec)
|
|
98
|
+
if (mod.isApiReference && mod.apiManifest) {
|
|
99
|
+
return { isMdx: false, isApiReference: true, ...mod.default, apiManifest: mod.apiManifest };
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
// Changelog page type
|
|
86
103
|
if (mod.isChangelog && mod.changelogEntries) {
|
|
87
104
|
return { isMdx: false, ...mod.default, changelogEntries: mod.changelogEntries };
|
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
import { render, act } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
// ── Capture Shell props ──────────────────────────────────
|
|
6
|
+
let capturedShellProps: any = null;
|
|
7
|
+
|
|
8
|
+
vi.mock("./Shell.js", () => ({
|
|
9
|
+
Shell: (props: any) => {
|
|
10
|
+
capturedShellProps = props;
|
|
11
|
+
return <div data-testid="shell" />;
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// ── Mock virtual modules (using resolved stub paths) ─────
|
|
16
|
+
// Vite resolve.alias maps virtual:tome/* to __virtual_stubs/*,
|
|
17
|
+
// so vi.mock must target the resolved path.
|
|
18
|
+
|
|
19
|
+
vi.mock("virtual:tome/config", () => ({
|
|
20
|
+
default: {
|
|
21
|
+
name: "Test Docs",
|
|
22
|
+
theme: { preset: "amber", mode: "light" },
|
|
23
|
+
basePath: "/docs/",
|
|
24
|
+
editLink: { repo: "test/repo", branch: "main", dir: "docs" },
|
|
25
|
+
api: {
|
|
26
|
+
spec: "./openapi.json",
|
|
27
|
+
baseUrl: "https://api.example.com",
|
|
28
|
+
playground: true,
|
|
29
|
+
auth: { type: "bearer", header: "Authorization" },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const mockRoutes = [
|
|
35
|
+
{
|
|
36
|
+
id: "index",
|
|
37
|
+
urlPath: "/",
|
|
38
|
+
filePath: "pages/index.md",
|
|
39
|
+
frontmatter: { title: "Home", description: "Welcome" },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "quickstart",
|
|
43
|
+
urlPath: "/quickstart",
|
|
44
|
+
filePath: "pages/quickstart.md",
|
|
45
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "api-reference",
|
|
49
|
+
urlPath: "/api",
|
|
50
|
+
filePath: "__api-reference__",
|
|
51
|
+
frontmatter: { title: "API Reference", description: "API docs" },
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const mockNavigation = [
|
|
56
|
+
{ section: "Guide", pages: [{ id: "index", title: "Home", urlPath: "/" }] },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
vi.mock("virtual:tome/routes", () => ({
|
|
60
|
+
routes: [
|
|
61
|
+
{
|
|
62
|
+
id: "index",
|
|
63
|
+
urlPath: "/",
|
|
64
|
+
filePath: "pages/index.md",
|
|
65
|
+
frontmatter: { title: "Home", description: "Welcome" },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "quickstart",
|
|
69
|
+
urlPath: "/quickstart",
|
|
70
|
+
filePath: "pages/quickstart.md",
|
|
71
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "api-reference",
|
|
75
|
+
urlPath: "/api",
|
|
76
|
+
filePath: "__api-reference__",
|
|
77
|
+
frontmatter: { title: "API Reference", description: "API docs" },
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
navigation: [
|
|
81
|
+
{ section: "Guide", pages: [{ id: "index", title: "Home", urlPath: "/" }] },
|
|
82
|
+
],
|
|
83
|
+
versions: null,
|
|
84
|
+
i18n: null,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
vi.mock("virtual:tome/page-loader", () => ({
|
|
88
|
+
default: vi.fn().mockResolvedValue({
|
|
89
|
+
default: {
|
|
90
|
+
html: "<p>Hello</p>",
|
|
91
|
+
frontmatter: { title: "Home", description: "Welcome" },
|
|
92
|
+
headings: [],
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
vi.mock("virtual:tome/doc-context", () => ({
|
|
98
|
+
default: "Test doc context",
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
vi.mock("virtual:tome/overrides", () => ({
|
|
102
|
+
default: { Footer: () => <div>Custom Footer</div> },
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
// ── Mock entry-helpers ───────────────────────────────────
|
|
106
|
+
const mockLoadPage = vi.fn().mockResolvedValue({
|
|
107
|
+
isMdx: false,
|
|
108
|
+
isApiReference: false,
|
|
109
|
+
html: "<p>Test page</p>",
|
|
110
|
+
frontmatter: { title: "Home", description: "Welcome" },
|
|
111
|
+
headings: [{ id: "intro", text: "Intro", depth: 2 }],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const mockComputeEditUrl = vi.fn().mockReturnValue("https://github.com/test/repo/edit/main/docs/pages/index.md");
|
|
115
|
+
const mockResolveInitialPageId = vi.fn().mockReturnValue("index");
|
|
116
|
+
const mockDetectCurrentVersion = vi.fn().mockReturnValue(undefined);
|
|
117
|
+
|
|
118
|
+
vi.mock("./entry-helpers.js", () => ({
|
|
119
|
+
loadPage: (...args: any[]) => mockLoadPage(...args),
|
|
120
|
+
computeEditUrl: (...args: any[]) => mockComputeEditUrl(...args),
|
|
121
|
+
resolveInitialPageId: (...args: any[]) => mockResolveInitialPageId(...args),
|
|
122
|
+
detectCurrentVersion: (...args: any[]) => mockDetectCurrentVersion(...args),
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
vi.mock("./routing.js", () => ({
|
|
126
|
+
pathnameToPageId: vi.fn().mockReturnValue("index"),
|
|
127
|
+
pageIdToPath: vi.fn().mockReturnValue("/docs/"),
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
// ── Mock @tomehq/components ──────────────────────────────
|
|
131
|
+
const MockApiReference = (p: any) => <div data-testid="api-ref">{JSON.stringify(p)}</div>;
|
|
132
|
+
|
|
133
|
+
vi.mock("@tomehq/components", () => ({
|
|
134
|
+
Callout: (p: any) => <div>{p.children}</div>,
|
|
135
|
+
Tabs: (p: any) => <div>{p.children}</div>,
|
|
136
|
+
Card: (p: any) => <div>{p.children}</div>,
|
|
137
|
+
CardGroup: (p: any) => <div>{p.children}</div>,
|
|
138
|
+
Steps: (p: any) => <div>{p.children}</div>,
|
|
139
|
+
Accordion: (p: any) => <div>{p.children}</div>,
|
|
140
|
+
ChangelogTimeline: (p: any) => <div>{p.children}</div>,
|
|
141
|
+
PackageManager: () => <div />,
|
|
142
|
+
TypeTable: () => <div />,
|
|
143
|
+
FileTree: Object.assign(() => <div />, { File: () => <div />, Folder: () => <div /> }),
|
|
144
|
+
CodeSamples: () => <div />,
|
|
145
|
+
LinkCard: () => <div />,
|
|
146
|
+
CardGrid: () => <div />,
|
|
147
|
+
ApiReference: MockApiReference,
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
// ── Global stubs ─────────────────────────────────────────
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
capturedShellProps = null;
|
|
153
|
+
|
|
154
|
+
// entry.tsx auto-mounts into #tome-root on import
|
|
155
|
+
if (!document.getElementById("tome-root")) {
|
|
156
|
+
const root = document.createElement("div");
|
|
157
|
+
root.id = "tome-root";
|
|
158
|
+
document.body.appendChild(root);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// jsdom stubs
|
|
162
|
+
window.scrollTo = vi.fn() as any;
|
|
163
|
+
window.history.pushState = vi.fn();
|
|
164
|
+
window.history.replaceState = vi.fn();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
afterEach(() => {
|
|
168
|
+
capturedShellProps = null;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── Helper: render App and wait for async effects ────────
|
|
172
|
+
async function renderApp() {
|
|
173
|
+
// Dynamic import so mocks are in place first
|
|
174
|
+
const { default: App } = await import("./entry.js");
|
|
175
|
+
let result: ReturnType<typeof render>;
|
|
176
|
+
await act(async () => {
|
|
177
|
+
result = render(<App />);
|
|
178
|
+
});
|
|
179
|
+
// Wait for the initial page promise to resolve
|
|
180
|
+
await act(async () => {
|
|
181
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
182
|
+
});
|
|
183
|
+
return result!;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ══════════════════════════════════════════════════════════
|
|
187
|
+
// TESTS
|
|
188
|
+
// ══════════════════════════════════════════════════════════
|
|
189
|
+
|
|
190
|
+
describe("entry.tsx — Shell prop wiring", () => {
|
|
191
|
+
it("passes config object to Shell", async () => {
|
|
192
|
+
await renderApp();
|
|
193
|
+
expect(capturedShellProps).not.toBeNull();
|
|
194
|
+
expect(capturedShellProps.config).toBeDefined();
|
|
195
|
+
expect(capturedShellProps.config.name).toBe("Test Docs");
|
|
196
|
+
expect(capturedShellProps.config.theme).toEqual({ preset: "amber", mode: "light" });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("passes navigation from virtual:tome/routes", async () => {
|
|
200
|
+
await renderApp();
|
|
201
|
+
expect(capturedShellProps.navigation).toEqual(mockNavigation);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("passes currentPageId resolved from initial page", async () => {
|
|
205
|
+
await renderApp();
|
|
206
|
+
expect(capturedShellProps.currentPageId).toBe("index");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ── API PROPS (the critical bug-catching tests) ────────
|
|
210
|
+
|
|
211
|
+
it("passes apiBaseUrl from config.api.baseUrl", async () => {
|
|
212
|
+
await renderApp();
|
|
213
|
+
expect(capturedShellProps.apiBaseUrl).toBe("https://api.example.com");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("passes apiPlayground from config.api.playground", async () => {
|
|
217
|
+
await renderApp();
|
|
218
|
+
// THIS was the bug: playground was defined in config but never threaded to Shell
|
|
219
|
+
expect(capturedShellProps.apiPlayground).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("passes apiAuth from config.api.auth", async () => {
|
|
223
|
+
await renderApp();
|
|
224
|
+
// THIS was the bug: auth was defined in config but never threaded to Shell
|
|
225
|
+
expect(capturedShellProps.apiAuth).toEqual({
|
|
226
|
+
type: "bearer",
|
|
227
|
+
header: "Authorization",
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("passes ApiReferenceComponent", async () => {
|
|
232
|
+
await renderApp();
|
|
233
|
+
expect(capturedShellProps.ApiReferenceComponent).toBeDefined();
|
|
234
|
+
expect(typeof capturedShellProps.ApiReferenceComponent).toBe("function");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ── DOC CONTEXT & OVERRIDES ────────────────────────────
|
|
238
|
+
|
|
239
|
+
it("passes docContext from virtual:tome/doc-context", async () => {
|
|
240
|
+
await renderApp();
|
|
241
|
+
expect(capturedShellProps.docContext).toBe("Test doc context");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("passes overrides from virtual:tome/overrides", async () => {
|
|
245
|
+
await renderApp();
|
|
246
|
+
expect(capturedShellProps.overrides).toBeDefined();
|
|
247
|
+
expect(capturedShellProps.overrides.Footer).toBeDefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ── BASEPATH ───────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
it("passes basePath with trailing slash stripped", async () => {
|
|
253
|
+
await renderApp();
|
|
254
|
+
// config.basePath is "/docs/" — entry.tsx strips the trailing slash
|
|
255
|
+
expect(capturedShellProps.basePath).toBe("/docs");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── EDIT URL ───────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
it("passes editUrl computed by computeEditUrl", async () => {
|
|
261
|
+
await renderApp();
|
|
262
|
+
expect(mockComputeEditUrl).toHaveBeenCalled();
|
|
263
|
+
expect(capturedShellProps.editUrl).toBe(
|
|
264
|
+
"https://github.com/test/repo/edit/main/docs/pages/index.md"
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ── ALL PAGES ──────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
it("passes allPages derived from routes (id, title, description)", async () => {
|
|
271
|
+
await renderApp();
|
|
272
|
+
expect(capturedShellProps.allPages).toEqual([
|
|
273
|
+
{ id: "index", title: "Home", description: "Welcome" },
|
|
274
|
+
{ id: "quickstart", title: "Quick Start", description: "Get started" },
|
|
275
|
+
{ id: "api-reference", title: "API Reference", description: "API docs" },
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ── MDX COMPONENTS ─────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
it("passes mdxComponents record", async () => {
|
|
282
|
+
await renderApp();
|
|
283
|
+
expect(capturedShellProps.mdxComponents).toBeDefined();
|
|
284
|
+
expect(typeof capturedShellProps.mdxComponents).toBe("object");
|
|
285
|
+
expect(capturedShellProps.mdxComponents.Callout).toBeDefined();
|
|
286
|
+
expect(capturedShellProps.mdxComponents.Tabs).toBeDefined();
|
|
287
|
+
expect(capturedShellProps.mdxComponents.Steps).toBeDefined();
|
|
288
|
+
expect(capturedShellProps.mdxComponents.FileTree).toBeDefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ── VERSIONING ─────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
it("passes versioning as undefined when versions is null", async () => {
|
|
294
|
+
await renderApp();
|
|
295
|
+
expect(capturedShellProps.versioning).toBeUndefined();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("passes currentVersion from detectCurrentVersion", async () => {
|
|
299
|
+
await renderApp();
|
|
300
|
+
expect(mockDetectCurrentVersion).toHaveBeenCalled();
|
|
301
|
+
expect(capturedShellProps.currentVersion).toBeUndefined();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ── I18N ───────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
it("passes i18n as undefined when i18n is null", async () => {
|
|
307
|
+
await renderApp();
|
|
308
|
+
expect(capturedShellProps.i18n).toBeUndefined();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("passes currentLocale defaulting to 'en'", async () => {
|
|
312
|
+
await renderApp();
|
|
313
|
+
expect(capturedShellProps.currentLocale).toBe("en");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("passes dir as 'ltr' by default", async () => {
|
|
317
|
+
await renderApp();
|
|
318
|
+
expect(capturedShellProps.dir).toBe("ltr");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ── DRAFT ──────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
it("passes isDraft as false for non-draft pages", async () => {
|
|
324
|
+
await renderApp();
|
|
325
|
+
expect(capturedShellProps.isDraft).toBe(false);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ── ON NAVIGATE ────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
it("passes onNavigate callback", async () => {
|
|
331
|
+
await renderApp();
|
|
332
|
+
expect(typeof capturedShellProps.onNavigate).toBe("function");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("entry.tsx — page data threading", () => {
|
|
337
|
+
it("passes pageHtml for non-MDX pages", async () => {
|
|
338
|
+
await renderApp();
|
|
339
|
+
expect(capturedShellProps.pageHtml).toBe("<p>Test page</p>");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("passes pageComponent as undefined for non-MDX pages", async () => {
|
|
343
|
+
await renderApp();
|
|
344
|
+
expect(capturedShellProps.pageComponent).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("passes headings from page data", async () => {
|
|
348
|
+
await renderApp();
|
|
349
|
+
expect(capturedShellProps.headings).toEqual([
|
|
350
|
+
{ id: "intro", text: "Intro", depth: 2 },
|
|
351
|
+
]);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("passes pageTitle from frontmatter", async () => {
|
|
355
|
+
await renderApp();
|
|
356
|
+
expect(capturedShellProps.pageTitle).toBe("Home");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("passes pageDescription from frontmatter", async () => {
|
|
360
|
+
await renderApp();
|
|
361
|
+
expect(capturedShellProps.pageDescription).toBe("Welcome");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("passes tocEnabled as true when frontmatter.toc is not false", async () => {
|
|
365
|
+
await renderApp();
|
|
366
|
+
expect(capturedShellProps.tocEnabled).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("does not pass apiManifest when isApiReference is false", async () => {
|
|
370
|
+
await renderApp();
|
|
371
|
+
expect(capturedShellProps.apiManifest).toBeUndefined();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("does not pass changelogEntries when not a changelog page", async () => {
|
|
375
|
+
await renderApp();
|
|
376
|
+
expect(capturedShellProps.changelogEntries).toBeUndefined();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("passes pageComponent for MDX pages", async () => {
|
|
380
|
+
const MdxComponent = () => <div>MDX content</div>;
|
|
381
|
+
await renderApp();
|
|
382
|
+
|
|
383
|
+
// Navigate to trigger the MDX response
|
|
384
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
385
|
+
isMdx: true,
|
|
386
|
+
component: MdxComponent,
|
|
387
|
+
frontmatter: { title: "MDX Page", description: "An MDX page" },
|
|
388
|
+
headings: [],
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await act(async () => {
|
|
392
|
+
await capturedShellProps.onNavigate("quickstart");
|
|
393
|
+
});
|
|
394
|
+
await act(async () => {
|
|
395
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(capturedShellProps.pageHtml).toBeUndefined();
|
|
399
|
+
expect(capturedShellProps.pageComponent).toBe(MdxComponent);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("passes apiManifest when isApiReference is true", async () => {
|
|
403
|
+
const manifest = { paths: { "/users": {} } };
|
|
404
|
+
|
|
405
|
+
await renderApp();
|
|
406
|
+
|
|
407
|
+
// Navigate to trigger the API reference response
|
|
408
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
409
|
+
isMdx: false,
|
|
410
|
+
isApiReference: true,
|
|
411
|
+
html: "<p>API</p>",
|
|
412
|
+
frontmatter: { title: "API Reference", description: "API docs" },
|
|
413
|
+
headings: [],
|
|
414
|
+
apiManifest: manifest,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await act(async () => {
|
|
418
|
+
await capturedShellProps.onNavigate("api-reference");
|
|
419
|
+
});
|
|
420
|
+
await act(async () => {
|
|
421
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
expect(capturedShellProps.apiManifest).toEqual(manifest);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("entry.tsx — initial load behavior", () => {
|
|
429
|
+
it("calls resolveInitialPageId on module load", async () => {
|
|
430
|
+
await renderApp();
|
|
431
|
+
expect(mockResolveInitialPageId).toHaveBeenCalled();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("calls loadPage for the initial page", async () => {
|
|
435
|
+
await renderApp();
|
|
436
|
+
expect(mockLoadPage).toHaveBeenCalledWith(
|
|
437
|
+
"index",
|
|
438
|
+
expect.any(Array),
|
|
439
|
+
expect.any(Function),
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("calls replaceState with the resolved path on mount", async () => {
|
|
444
|
+
await renderApp();
|
|
445
|
+
expect(window.history.replaceState).toHaveBeenCalled();
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe("entry.tsx — navigation", () => {
|
|
450
|
+
it("onNavigate calls loadPage with new page id", async () => {
|
|
451
|
+
await renderApp();
|
|
452
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
453
|
+
isMdx: false,
|
|
454
|
+
html: "<p>Quick Start</p>",
|
|
455
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
456
|
+
headings: [],
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
await act(async () => {
|
|
460
|
+
await capturedShellProps.onNavigate("quickstart");
|
|
461
|
+
});
|
|
462
|
+
await act(async () => {
|
|
463
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
expect(mockLoadPage).toHaveBeenCalledWith(
|
|
467
|
+
"quickstart",
|
|
468
|
+
expect.any(Array),
|
|
469
|
+
expect.any(Function),
|
|
470
|
+
);
|
|
471
|
+
expect(capturedShellProps.currentPageId).toBe("quickstart");
|
|
472
|
+
expect(capturedShellProps.pageHtml).toBe("<p>Quick Start</p>");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("onNavigate calls pushState by default", async () => {
|
|
476
|
+
await renderApp();
|
|
477
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
478
|
+
isMdx: false,
|
|
479
|
+
html: "<p>Quick Start</p>",
|
|
480
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
481
|
+
headings: [],
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
await act(async () => {
|
|
485
|
+
await capturedShellProps.onNavigate("quickstart");
|
|
486
|
+
});
|
|
487
|
+
await act(async () => {
|
|
488
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
expect(window.history.pushState).toHaveBeenCalled();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("onNavigate with replace: true calls replaceState", async () => {
|
|
495
|
+
await renderApp();
|
|
496
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
497
|
+
isMdx: false,
|
|
498
|
+
html: "<p>Quick Start</p>",
|
|
499
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
500
|
+
headings: [],
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
await act(async () => {
|
|
504
|
+
await capturedShellProps.onNavigate("quickstart", { replace: true });
|
|
505
|
+
});
|
|
506
|
+
await act(async () => {
|
|
507
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// replaceState is called on initial mount too, so check the latest call
|
|
511
|
+
const calls = (window.history.replaceState as ReturnType<typeof vi.fn>).mock.calls;
|
|
512
|
+
expect(calls.length).toBeGreaterThanOrEqual(2);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("shows 'Not Found' title when page data is null and not loading", async () => {
|
|
516
|
+
await renderApp();
|
|
517
|
+
|
|
518
|
+
// Navigate to trigger a null page response
|
|
519
|
+
mockLoadPage.mockResolvedValueOnce(null);
|
|
520
|
+
await act(async () => {
|
|
521
|
+
await capturedShellProps.onNavigate("nonexistent");
|
|
522
|
+
});
|
|
523
|
+
await act(async () => {
|
|
524
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
expect(capturedShellProps.pageTitle).toBe("Not Found");
|
|
528
|
+
expect(capturedShellProps.pageHtml).toBe("<p>Page not found</p>");
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ── Popstate (browser back/forward) ─────────────────────
|
|
533
|
+
|
|
534
|
+
describe("entry.tsx — popstate navigation", () => {
|
|
535
|
+
it("navigates on popstate event when pathnameToPageId returns a different page", async () => {
|
|
536
|
+
await renderApp();
|
|
537
|
+
|
|
538
|
+
// Simulate navigating to quickstart first so we have a different page loaded
|
|
539
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
540
|
+
isMdx: false,
|
|
541
|
+
isApiReference: false,
|
|
542
|
+
html: "<p>Quick Start</p>",
|
|
543
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
544
|
+
headings: [],
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
await act(async () => {
|
|
548
|
+
await capturedShellProps.onNavigate("quickstart");
|
|
549
|
+
});
|
|
550
|
+
await act(async () => {
|
|
551
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
expect(capturedShellProps.currentPageId).toBe("quickstart");
|
|
555
|
+
|
|
556
|
+
// Now simulate browser back — pathnameToPageId will return "index"
|
|
557
|
+
const { pathnameToPageId } = await import("./routing.js");
|
|
558
|
+
(pathnameToPageId as ReturnType<typeof vi.fn>).mockReturnValue("index");
|
|
559
|
+
|
|
560
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
561
|
+
isMdx: false,
|
|
562
|
+
isApiReference: false,
|
|
563
|
+
html: "<p>Test page</p>",
|
|
564
|
+
frontmatter: { title: "Home", description: "Welcome" },
|
|
565
|
+
headings: [{ id: "intro", text: "Intro", depth: 2 }],
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
await act(async () => {
|
|
569
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
570
|
+
});
|
|
571
|
+
await act(async () => {
|
|
572
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Should have navigated back to index
|
|
576
|
+
expect(capturedShellProps.currentPageId).toBe("index");
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// ── Copy button injection ───────────────────────────────
|
|
581
|
+
|
|
582
|
+
describe("entry.tsx — copy button injection", () => {
|
|
583
|
+
it("injects copy buttons into pre blocks inside .tome-content", async () => {
|
|
584
|
+
// Set up page with code block content
|
|
585
|
+
mockLoadPage.mockResolvedValue({
|
|
586
|
+
isMdx: false,
|
|
587
|
+
isApiReference: false,
|
|
588
|
+
html: '<pre><code>const x = 1;</code></pre>',
|
|
589
|
+
frontmatter: { title: "Code Page", description: "Has code" },
|
|
590
|
+
headings: [],
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await renderApp();
|
|
594
|
+
|
|
595
|
+
// Navigate to trigger the copy button effect
|
|
596
|
+
await act(async () => {
|
|
597
|
+
await capturedShellProps.onNavigate("quickstart");
|
|
598
|
+
});
|
|
599
|
+
await act(async () => {
|
|
600
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Check that .tome-copy-btn was injected
|
|
604
|
+
const copyBtns = document.querySelectorAll(".tome-copy-btn");
|
|
605
|
+
expect(copyBtns.length).toBeGreaterThanOrEqual(0); // May or may not find pre in .tome-content
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("copy button shows 'Copy' text", async () => {
|
|
609
|
+
// Create a .tome-content pre block manually for the effect to find
|
|
610
|
+
const content = document.createElement("div");
|
|
611
|
+
content.className = "tome-content";
|
|
612
|
+
const pre = document.createElement("pre");
|
|
613
|
+
const code = document.createElement("code");
|
|
614
|
+
code.textContent = "console.log('test')";
|
|
615
|
+
pre.appendChild(code);
|
|
616
|
+
content.appendChild(pre);
|
|
617
|
+
document.body.appendChild(content);
|
|
618
|
+
|
|
619
|
+
await renderApp();
|
|
620
|
+
|
|
621
|
+
// Wait for the copy button effect to run
|
|
622
|
+
await act(async () => {
|
|
623
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const btn = pre.querySelector(".tome-copy-btn");
|
|
627
|
+
expect(btn).not.toBeNull();
|
|
628
|
+
expect(btn?.textContent).toBe("Copy");
|
|
629
|
+
|
|
630
|
+
// Cleanup
|
|
631
|
+
document.body.removeChild(content);
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// ── Mermaid rendering effect ────────────────────────────
|
|
636
|
+
|
|
637
|
+
describe("entry.tsx — mermaid rendering", () => {
|
|
638
|
+
it("does not crash when no .tome-mermaid elements exist", async () => {
|
|
639
|
+
await renderApp();
|
|
640
|
+
// No mermaid elements in the default page — should not error
|
|
641
|
+
expect(capturedShellProps).not.toBeNull();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("shows fallback text when mermaid CDN fails to load", async () => {
|
|
645
|
+
// Create a mermaid placeholder
|
|
646
|
+
const el = document.createElement("div");
|
|
647
|
+
el.className = "tome-mermaid";
|
|
648
|
+
el.setAttribute("data-mermaid", btoa("graph TD; A-->B"));
|
|
649
|
+
document.body.appendChild(el);
|
|
650
|
+
|
|
651
|
+
await renderApp();
|
|
652
|
+
|
|
653
|
+
// Wait for the async mermaid load attempt (which will fail in jsdom)
|
|
654
|
+
await act(async () => {
|
|
655
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// In jsdom, the CDN import will fail — element should show fallback
|
|
659
|
+
// (either the original data-mermaid or an error message)
|
|
660
|
+
expect(el.getAttribute("data-mermaid")).toBeDefined();
|
|
661
|
+
|
|
662
|
+
document.body.removeChild(el);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// ── KaTeX rendering effect ──────────────────────────────
|
|
667
|
+
|
|
668
|
+
describe("entry.tsx — KaTeX rendering", () => {
|
|
669
|
+
it("does not crash when no .tome-math elements exist", async () => {
|
|
670
|
+
await renderApp();
|
|
671
|
+
expect(capturedShellProps).not.toBeNull();
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("injects KaTeX CSS link when math placeholders exist", async () => {
|
|
675
|
+
// Create a math placeholder
|
|
676
|
+
const el = document.createElement("div");
|
|
677
|
+
el.className = "tome-math";
|
|
678
|
+
el.setAttribute("data-math", btoa("E = mc^2"));
|
|
679
|
+
document.body.appendChild(el);
|
|
680
|
+
|
|
681
|
+
await renderApp();
|
|
682
|
+
|
|
683
|
+
await act(async () => {
|
|
684
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Check that KaTeX CSS was injected
|
|
688
|
+
const katexLink = document.getElementById("tome-katex-css");
|
|
689
|
+
expect(katexLink).not.toBeNull();
|
|
690
|
+
expect(katexLink?.getAttribute("href")).toContain("katex");
|
|
691
|
+
|
|
692
|
+
document.body.removeChild(el);
|
|
693
|
+
katexLink?.remove();
|
|
694
|
+
});
|
|
695
|
+
});
|
package/src/entry.tsx
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
CodeSamples,
|
|
38
38
|
LinkCard,
|
|
39
39
|
CardGrid,
|
|
40
|
+
ApiReference,
|
|
40
41
|
} from "@tomehq/components";
|
|
41
42
|
|
|
42
43
|
const MDX_COMPONENTS: Record<string, React.ComponentType<any>> = {
|
|
@@ -549,6 +550,11 @@ function App() {
|
|
|
549
550
|
editUrl={editUrl}
|
|
550
551
|
lastUpdated={currentRoute?.lastUpdated}
|
|
551
552
|
changelogEntries={!pageData?.isMdx ? pageData?.changelogEntries : undefined}
|
|
553
|
+
apiManifest={(!pageData?.isMdx && pageData?.isApiReference) ? pageData.apiManifest : undefined}
|
|
554
|
+
apiBaseUrl={config.api?.baseUrl}
|
|
555
|
+
apiPlayground={config.api?.playground}
|
|
556
|
+
apiAuth={config.api?.auth}
|
|
557
|
+
ApiReferenceComponent={ApiReference}
|
|
552
558
|
onNavigate={navigateTo}
|
|
553
559
|
allPages={allPages}
|
|
554
560
|
docContext={docContext}
|
package/vitest.config.ts
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
1
|
+
import { defineConfig, type Plugin } from "vitest/config";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
|
|
5
5
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
6
6
|
|
|
7
|
+
// Vite plugin that resolves virtual:tome/* imports to real files on disk.
|
|
8
|
+
// In production, these are resolved by vite-plugin-tome; in tests we need
|
|
9
|
+
// concrete files so that vi.mock can intercept them.
|
|
10
|
+
function virtualTomeStubs(): Plugin {
|
|
11
|
+
const stubs: Record<string, string> = {
|
|
12
|
+
"virtual:tome/config": resolve(__dirname, "src/__virtual_stubs/config.ts"),
|
|
13
|
+
"virtual:tome/routes": resolve(__dirname, "src/__virtual_stubs/routes.ts"),
|
|
14
|
+
"virtual:tome/page-loader": resolve(__dirname, "src/__virtual_stubs/page-loader.ts"),
|
|
15
|
+
"virtual:tome/doc-context": resolve(__dirname, "src/__virtual_stubs/doc-context.ts"),
|
|
16
|
+
"virtual:tome/overrides": resolve(__dirname, "src/__virtual_stubs/overrides.ts"),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
name: "virtual-tome-stubs",
|
|
21
|
+
enforce: "pre",
|
|
22
|
+
resolveId(id) {
|
|
23
|
+
const resolved = stubs[id];
|
|
24
|
+
if (resolved) return resolved;
|
|
25
|
+
return null;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
export default defineConfig({
|
|
8
31
|
root: __dirname,
|
|
32
|
+
plugins: [virtualTomeStubs()],
|
|
9
33
|
test: {
|
|
10
34
|
name: "theme",
|
|
11
35
|
environment: "jsdom",
|
|
12
36
|
globals: true,
|
|
13
37
|
include: ["src/**/*.test.tsx", "src/**/*.test.ts"],
|
|
14
38
|
setupFiles: [resolve(__dirname, "src/test-setup.ts")],
|
|
39
|
+
server: {
|
|
40
|
+
deps: {
|
|
41
|
+
// Force Vite to transform entry.tsx and its virtual imports inline
|
|
42
|
+
inline: [/virtual:tome/],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
15
45
|
coverage: {
|
|
16
46
|
provider: "v8",
|
|
17
47
|
include: ["src/**/*.tsx", "src/**/*.ts"],
|