minutework 0.1.32 → 0.1.34
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/assets/claude-local/skills/README.md +2 -0
- package/assets/claude-local/skills/app-pack-authoring/SKILL.md +24 -1
- package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +19 -3
- package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +9 -6
- package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +5 -4
- package/assets/templates/next-tenant-app/README.md +37 -135
- package/assets/templates/next-tenant-app/package.json +1 -0
- package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
- package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
- package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
- package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
- package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
- package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
- package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
- package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
- package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
- package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
- package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
- package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
- package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
- package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
- package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
- package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
- package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
- package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
- package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
- package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
- package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
- package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
- package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
- package/assets/templates/next-tenant-app/template.json +3 -3
- package/assets/templates/next-tenant-app/template.schema.json +1 -0
- package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
- package/package.json +2 -2
- package/vendor/workspace-mcp/types.d.ts +4 -0
- package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
- package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
- package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
- package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
- package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
- package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
- package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
- package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
- package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
- package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
- package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
- package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
- package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
- package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
- package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
- package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
- package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
- package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
- package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
- package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
- package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
- package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
- package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
- package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
- package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
- package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
- package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
- package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
- package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
- package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
- package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
- package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
- package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
- package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
- package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
- package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
- package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
BlogEntrySummary,
|
|
9
|
-
DocsEntrySummary,
|
|
10
|
-
MarketingPageKey,
|
|
11
|
-
PublicContentAdapter,
|
|
12
|
-
PublicContentKind,
|
|
13
|
-
} from "@/lib/content/contracts";
|
|
14
|
-
import { publicSiteFixtureSnapshot } from "@/lib/content/__fixtures__/public-site-snapshot";
|
|
15
|
-
import {
|
|
16
|
-
MinuteWorkPublicSiteContentAdapter,
|
|
17
|
-
PUBLIC_CONTENT_REVALIDATE_SECONDS,
|
|
18
|
-
fetchPublicSiteSnapshotEnvelope,
|
|
19
|
-
} from "@/lib/content/adapter.server";
|
|
20
|
-
|
|
21
|
-
const originalEnv = { ...process.env };
|
|
22
|
-
|
|
23
|
-
function jsonResponse(body: unknown, status = 200) {
|
|
24
|
-
return new Response(JSON.stringify(body), {
|
|
25
|
-
status,
|
|
26
|
-
headers: {
|
|
27
|
-
"content-type": "application/json",
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function normalizeSlugParts(slugParts: readonly string[]) {
|
|
33
|
-
return slugParts.map((part) => part.trim().toLowerCase()).filter(Boolean).join("/");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function getFixtureMarketingPage(pageKey: MarketingPageKey) {
|
|
37
|
-
return (
|
|
38
|
-
publicSiteFixtureSnapshot.marketingPages.find((page) => page.pageKey === pageKey) ??
|
|
39
|
-
null
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getFixtureEntry(kind: PublicContentKind, slugParts: readonly string[]) {
|
|
44
|
-
return (
|
|
45
|
-
publicSiteFixtureSnapshot[kind].find(
|
|
46
|
-
(entry) => normalizeSlugParts(entry.slug) === normalizeSlugParts(slugParts),
|
|
47
|
-
) ?? null
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function toDocsEntrySummary(
|
|
52
|
-
entry: typeof publicSiteFixtureSnapshot.docs[number],
|
|
53
|
-
): DocsEntrySummary {
|
|
54
|
-
return {
|
|
55
|
-
description: entry.description,
|
|
56
|
-
eyebrow: entry.eyebrow,
|
|
57
|
-
kind: entry.kind,
|
|
58
|
-
publishedAt: entry.publishedAt,
|
|
59
|
-
readingTime: entry.readingTime,
|
|
60
|
-
slug: entry.slug,
|
|
61
|
-
title: entry.title,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function toBlogEntrySummary(
|
|
66
|
-
entry: typeof publicSiteFixtureSnapshot.blog[number],
|
|
67
|
-
): BlogEntrySummary {
|
|
68
|
-
return {
|
|
69
|
-
description: entry.description,
|
|
70
|
-
eyebrow: entry.eyebrow,
|
|
71
|
-
kind: entry.kind,
|
|
72
|
-
publishedAt: entry.publishedAt,
|
|
73
|
-
readingTime: entry.readingTime,
|
|
74
|
-
slug: entry.slug,
|
|
75
|
-
title: entry.title,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function getFixtureEntrySummaries(kind: "docs"): DocsEntrySummary[];
|
|
80
|
-
function getFixtureEntrySummaries(kind: "blog"): BlogEntrySummary[];
|
|
81
|
-
function getFixtureEntrySummaries(kind: PublicContentKind) {
|
|
82
|
-
if (kind === "docs") {
|
|
83
|
-
return publicSiteFixtureSnapshot.docs.map(toDocsEntrySummary);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return publicSiteFixtureSnapshot.blog.map(toBlogEntrySummary);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function createCustomAdapter(
|
|
90
|
-
overrides: Partial<PublicContentAdapter> = {},
|
|
91
|
-
): PublicContentAdapter {
|
|
92
|
-
return {
|
|
93
|
-
async getSiteConfig() {
|
|
94
|
-
return publicSiteFixtureSnapshot.site;
|
|
95
|
-
},
|
|
96
|
-
async getMarketingPage(pageKey) {
|
|
97
|
-
return getFixtureMarketingPage(pageKey);
|
|
98
|
-
},
|
|
99
|
-
async listEntries(kind) {
|
|
100
|
-
return kind === "docs"
|
|
101
|
-
? getFixtureEntrySummaries("docs")
|
|
102
|
-
: getFixtureEntrySummaries("blog");
|
|
103
|
-
},
|
|
104
|
-
async getEntry(kind, slugParts) {
|
|
105
|
-
return getFixtureEntry(kind, slugParts);
|
|
106
|
-
},
|
|
107
|
-
...overrides,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function createEnvelope(
|
|
112
|
-
overrides: Partial<Awaited<ReturnType<typeof fetchPublicSiteSnapshotEnvelope>>> = {},
|
|
113
|
-
) {
|
|
114
|
-
return {
|
|
115
|
-
environment: "preview" as const,
|
|
116
|
-
source_boundary: "runtime_preview" as const,
|
|
117
|
-
snapshot: publicSiteFixtureSnapshot,
|
|
118
|
-
...overrides,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function restoreProcessEnv() {
|
|
123
|
-
for (const key of Object.keys(process.env)) {
|
|
124
|
-
if (!(key in originalEnv)) {
|
|
125
|
-
delete process.env[key];
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
Object.assign(process.env, originalEnv);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function applyServerEnv(overrides: Record<string, string | undefined>) {
|
|
133
|
-
restoreProcessEnv();
|
|
134
|
-
process.env.MW_PLATFORM_BASE_URL = "http://127.0.0.1:8000";
|
|
135
|
-
process.env.MW_PUBLIC_CONTENT_SOURCE = "minutework_cms";
|
|
136
|
-
process.env.MW_CONTENT_API_TOKEN = "test-content-token";
|
|
137
|
-
process.env.MW_PUBLIC_BASE_URL = "http://127.0.0.1:3000";
|
|
138
|
-
process.env.MW_PUBLIC_SITE_PROPERTY_KEY = "main-site";
|
|
139
|
-
process.env.MW_PUBLIC_SITE_ENV = "preview";
|
|
140
|
-
process.env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE = "false";
|
|
141
|
-
|
|
142
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
143
|
-
if (value === undefined) {
|
|
144
|
-
delete process.env[key];
|
|
145
|
-
} else {
|
|
146
|
-
process.env[key] = value;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
describe("public content adapter", () => {
|
|
152
|
-
afterEach(() => {
|
|
153
|
-
restoreProcessEnv();
|
|
154
|
-
vi.unstubAllGlobals();
|
|
155
|
-
vi.resetModules();
|
|
156
|
-
vi.doUnmock("@/lib/content/custom-adapter");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("loads a public-site snapshot envelope through a cacheable bearer-authenticated fetch", async () => {
|
|
160
|
-
const fetchMock = vi.fn().mockResolvedValue(jsonResponse(createEnvelope()));
|
|
161
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
162
|
-
|
|
163
|
-
const envelope = await fetchPublicSiteSnapshotEnvelope({
|
|
164
|
-
contentApiToken: "content-token",
|
|
165
|
-
environment: "preview",
|
|
166
|
-
platformBaseUrl: "https://platform.example.com",
|
|
167
|
-
propertyKey: "main-site",
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
expect(envelope.snapshot.site.siteName).toBe("MinuteWork Combined Starter");
|
|
171
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
172
|
-
"https://platform.example.com/api/v1/developer/public-site/snapshots/main-site/?environment=preview",
|
|
173
|
-
expect.objectContaining({
|
|
174
|
-
headers: {
|
|
175
|
-
accept: "application/json",
|
|
176
|
-
authorization: "Bearer content-token",
|
|
177
|
-
},
|
|
178
|
-
next: {
|
|
179
|
-
revalidate: PUBLIC_CONTENT_REVALIDATE_SECONDS,
|
|
180
|
-
tags: [
|
|
181
|
-
"public-site-content",
|
|
182
|
-
"public-site-content:main-site",
|
|
183
|
-
"public-site-content:preview",
|
|
184
|
-
],
|
|
185
|
-
},
|
|
186
|
-
}),
|
|
187
|
-
);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("rejects invalid environment and source-boundary combinations", async () => {
|
|
191
|
-
const fetchMock = vi.fn().mockResolvedValue(
|
|
192
|
-
jsonResponse(
|
|
193
|
-
createEnvelope({
|
|
194
|
-
environment: "live",
|
|
195
|
-
source_boundary: "runtime_preview",
|
|
196
|
-
}),
|
|
197
|
-
),
|
|
198
|
-
);
|
|
199
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
200
|
-
|
|
201
|
-
await expect(
|
|
202
|
-
fetchPublicSiteSnapshotEnvelope({
|
|
203
|
-
contentApiToken: "content-token",
|
|
204
|
-
environment: "live",
|
|
205
|
-
platformBaseUrl: "https://platform.example.com",
|
|
206
|
-
propertyKey: "main-site",
|
|
207
|
-
}),
|
|
208
|
-
).rejects.toThrow("published_live");
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("accepts runtime_live for live runtime-served publications", async () => {
|
|
212
|
-
const fetchMock = vi.fn().mockResolvedValue(
|
|
213
|
-
jsonResponse(
|
|
214
|
-
createEnvelope({
|
|
215
|
-
environment: "live",
|
|
216
|
-
source_boundary: "runtime_live",
|
|
217
|
-
}),
|
|
218
|
-
),
|
|
219
|
-
);
|
|
220
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
221
|
-
|
|
222
|
-
await expect(
|
|
223
|
-
fetchPublicSiteSnapshotEnvelope({
|
|
224
|
-
contentApiToken: "content-token",
|
|
225
|
-
environment: "live",
|
|
226
|
-
platformBaseUrl: "https://platform.example.com",
|
|
227
|
-
propertyKey: "main-site",
|
|
228
|
-
}),
|
|
229
|
-
).resolves.toMatchObject({
|
|
230
|
-
environment: "live",
|
|
231
|
-
source_boundary: "runtime_live",
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("maps MinuteWork public-site snapshots through the adapter interface", async () => {
|
|
236
|
-
const adapter = new MinuteWorkPublicSiteContentAdapter(async () =>
|
|
237
|
-
createEnvelope({
|
|
238
|
-
environment: "live",
|
|
239
|
-
source_boundary: "published_live",
|
|
240
|
-
}),
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
await expect(adapter.getMarketingPage("home")).resolves.toEqual(
|
|
244
|
-
publicSiteFixtureSnapshot.marketingPages[0],
|
|
245
|
-
);
|
|
246
|
-
await expect(adapter.listEntries("blog")).resolves.toEqual(
|
|
247
|
-
publicSiteFixtureSnapshot.blog,
|
|
248
|
-
);
|
|
249
|
-
await expect(
|
|
250
|
-
adapter.getEntry("blog", ["public-site-api-default"]),
|
|
251
|
-
).resolves.toEqual(publicSiteFixtureSnapshot.blog[0]);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("allows empty marketing-page snapshots from the default gateway adapter", async () => {
|
|
255
|
-
const adapter = new MinuteWorkPublicSiteContentAdapter(async () =>
|
|
256
|
-
createEnvelope({
|
|
257
|
-
snapshot: {
|
|
258
|
-
...publicSiteFixtureSnapshot,
|
|
259
|
-
marketingPages: [],
|
|
260
|
-
},
|
|
261
|
-
}),
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
await expect(adapter.getMarketingPage("pricing")).resolves.toBeNull();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("loads the default MinuteWork public-site path when no custom adapter is provided", async () => {
|
|
268
|
-
const fetchMock = vi.fn().mockResolvedValue(jsonResponse(createEnvelope()));
|
|
269
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
270
|
-
applyServerEnv({});
|
|
271
|
-
vi.doMock("@/lib/content/custom-adapter", () => ({
|
|
272
|
-
resolveCustomPublicContentAdapter: () => null,
|
|
273
|
-
}));
|
|
274
|
-
|
|
275
|
-
const adapterModule = await import("./adapter.server");
|
|
276
|
-
|
|
277
|
-
await expect(adapterModule.getSiteConfig()).resolves.toEqual(
|
|
278
|
-
publicSiteFixtureSnapshot.site,
|
|
279
|
-
);
|
|
280
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("accepts valid custom adapter output through the shared contract wrapper", async () => {
|
|
284
|
-
applyServerEnv({
|
|
285
|
-
MW_PUBLIC_CONTENT_SOURCE: "custom",
|
|
286
|
-
MW_CONTENT_API_TOKEN: undefined,
|
|
287
|
-
MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
|
|
288
|
-
});
|
|
289
|
-
vi.doMock("@/lib/content/custom-adapter", () => ({
|
|
290
|
-
resolveCustomPublicContentAdapter: () => createCustomAdapter(),
|
|
291
|
-
}));
|
|
292
|
-
|
|
293
|
-
const adapterModule = await import("./adapter.server");
|
|
294
|
-
|
|
295
|
-
await expect(adapterModule.getMarketingPage("pricing")).resolves.toEqual(
|
|
296
|
-
getFixtureMarketingPage("pricing"),
|
|
297
|
-
);
|
|
298
|
-
await expect(adapterModule.listEntries("blog")).resolves.toEqual(
|
|
299
|
-
getFixtureEntrySummaries("blog"),
|
|
300
|
-
);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it("requires an explicit adapter when MW_PUBLIC_CONTENT_SOURCE=custom", async () => {
|
|
304
|
-
applyServerEnv({
|
|
305
|
-
MW_PUBLIC_CONTENT_SOURCE: "custom",
|
|
306
|
-
MW_CONTENT_API_TOKEN: undefined,
|
|
307
|
-
MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
|
|
308
|
-
});
|
|
309
|
-
vi.doMock("@/lib/content/custom-adapter", () => ({
|
|
310
|
-
resolveCustomPublicContentAdapter: () => null,
|
|
311
|
-
}));
|
|
312
|
-
|
|
313
|
-
const adapterModule = await import("./adapter.server");
|
|
314
|
-
|
|
315
|
-
expect(() => adapterModule.getPublicContentAdapter()).toThrow(
|
|
316
|
-
"MW_PUBLIC_CONTENT_SOURCE=custom requires resolveCustomPublicContentAdapter()",
|
|
317
|
-
);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("loads valid static JSON public content", async () => {
|
|
321
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "mw-public-content-"));
|
|
322
|
-
const contentPath = join(tempRoot, "public-site.json");
|
|
323
|
-
|
|
324
|
-
try {
|
|
325
|
-
await writeFile(
|
|
326
|
-
contentPath,
|
|
327
|
-
JSON.stringify(publicSiteFixtureSnapshot),
|
|
328
|
-
"utf8",
|
|
329
|
-
);
|
|
330
|
-
applyServerEnv({
|
|
331
|
-
MW_PUBLIC_CONTENT_SOURCE: "static_json",
|
|
332
|
-
MW_CONTENT_API_TOKEN: undefined,
|
|
333
|
-
MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
|
|
334
|
-
MW_STATIC_PUBLIC_CONTENT_PATH: contentPath,
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
const adapterModule = await import("./adapter.server");
|
|
338
|
-
|
|
339
|
-
await expect(adapterModule.getMarketingPage("home")).resolves.toEqual(
|
|
340
|
-
getFixtureMarketingPage("home"),
|
|
341
|
-
);
|
|
342
|
-
} finally {
|
|
343
|
-
await rm(tempRoot, { force: true, recursive: true });
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it("rejects invalid static JSON public content", async () => {
|
|
348
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "mw-public-content-"));
|
|
349
|
-
const contentPath = join(tempRoot, "public-site.json");
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
await writeFile(contentPath, JSON.stringify({ site: {} }), "utf8");
|
|
353
|
-
applyServerEnv({
|
|
354
|
-
MW_PUBLIC_CONTENT_SOURCE: "static_json",
|
|
355
|
-
MW_CONTENT_API_TOKEN: undefined,
|
|
356
|
-
MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
|
|
357
|
-
MW_STATIC_PUBLIC_CONTENT_PATH: contentPath,
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
const adapterModule = await import("./adapter.server");
|
|
361
|
-
|
|
362
|
-
await expect(adapterModule.getSiteConfig()).rejects.toThrow(
|
|
363
|
-
"Static public content is invalid",
|
|
364
|
-
);
|
|
365
|
-
} finally {
|
|
366
|
-
await rm(tempRoot, { force: true, recursive: true });
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it("keeps public content disabled for private-only mode", async () => {
|
|
371
|
-
applyServerEnv({
|
|
372
|
-
MW_PUBLIC_CONTENT_SOURCE: "none",
|
|
373
|
-
MW_CONTENT_API_TOKEN: undefined,
|
|
374
|
-
MW_PUBLIC_BASE_URL: undefined,
|
|
375
|
-
MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
const adapterModule = await import("./adapter.server");
|
|
379
|
-
|
|
380
|
-
await expect(adapterModule.getSiteConfig()).rejects.toThrow(
|
|
381
|
-
"Public content is disabled",
|
|
382
|
-
);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it("rejects a custom adapter when a marketing page violates the canonical route contract", async () => {
|
|
386
|
-
applyServerEnv({
|
|
387
|
-
MW_PUBLIC_CONTENT_SOURCE: "custom",
|
|
388
|
-
MW_CONTENT_API_TOKEN: undefined,
|
|
389
|
-
MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
|
|
390
|
-
});
|
|
391
|
-
vi.doMock("@/lib/content/custom-adapter", () => ({
|
|
392
|
-
resolveCustomPublicContentAdapter: () =>
|
|
393
|
-
createCustomAdapter({
|
|
394
|
-
async getMarketingPage(pageKey) {
|
|
395
|
-
const page = getFixtureMarketingPage(pageKey);
|
|
396
|
-
|
|
397
|
-
if (!page || pageKey !== "pricing") {
|
|
398
|
-
return page;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
...page,
|
|
403
|
-
path: "/plans",
|
|
404
|
-
};
|
|
405
|
-
},
|
|
406
|
-
}),
|
|
407
|
-
}));
|
|
408
|
-
|
|
409
|
-
const adapterModule = await import("./adapter.server");
|
|
410
|
-
|
|
411
|
-
await expect(adapterModule.getMarketingPage("pricing")).rejects.toThrow(
|
|
412
|
-
'"pricing" marketing page must use the "/pricing" route.',
|
|
413
|
-
);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it("rejects a custom adapter when blog summaries use multi-segment slugs", async () => {
|
|
417
|
-
applyServerEnv({
|
|
418
|
-
MW_PUBLIC_CONTENT_SOURCE: "custom",
|
|
419
|
-
MW_CONTENT_API_TOKEN: undefined,
|
|
420
|
-
MW_PUBLIC_SITE_PROPERTY_KEY: undefined,
|
|
421
|
-
});
|
|
422
|
-
vi.doMock("@/lib/content/custom-adapter", () => ({
|
|
423
|
-
resolveCustomPublicContentAdapter: () =>
|
|
424
|
-
createCustomAdapter({
|
|
425
|
-
async listEntries(kind) {
|
|
426
|
-
if (kind !== "blog") {
|
|
427
|
-
return getFixtureEntrySummaries("docs");
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return publicSiteFixtureSnapshot.blog.map((entry) => ({
|
|
431
|
-
...toBlogEntrySummary(entry),
|
|
432
|
-
slug: ["release", "v1"],
|
|
433
|
-
})) as unknown as Awaited<ReturnType<PublicContentAdapter["listEntries"]>>;
|
|
434
|
-
},
|
|
435
|
-
}),
|
|
436
|
-
}));
|
|
437
|
-
|
|
438
|
-
const adapterModule = await import("./adapter.server");
|
|
439
|
-
|
|
440
|
-
await expect(adapterModule.listEntries("blog")).rejects.toThrow(
|
|
441
|
-
"Array must contain at most 1 element(s)",
|
|
442
|
-
);
|
|
443
|
-
});
|
|
444
|
-
});
|