minutework 0.1.31 → 0.1.33

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.
Files changed (168) hide show
  1. package/EXTERNAL_ALPHA.md +33 -33
  2. package/README.md +34 -34
  3. package/assets/claude-local/CLAUDE.md.template +12 -12
  4. package/assets/claude-local/skills/README.md +3 -1
  5. package/assets/claude-local/skills/app-pack-authoring/SKILL.md +17 -4
  6. package/assets/claude-local/skills/capability-gap-reporting/SKILL.md +3 -3
  7. package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
  8. package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +12 -5
  9. package/assets/claude-local/skills/layering-and-import-modes/SKILL.md +2 -2
  10. package/assets/claude-local/skills/openclaw-skill-importer/SKILL.md +2 -2
  11. package/assets/claude-local/skills/project-overview-and-strategy/SKILL.md +8 -8
  12. package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +11 -8
  13. package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +6 -5
  14. package/assets/claude-local/skills/vuilder-discovery-output-contract/SKILL.md +6 -6
  15. package/assets/claude-local/skills/workspace-guidance-refresh/SKILL.md +4 -4
  16. package/assets/templates/fastapi-sidecar/pyproject.toml +1 -1
  17. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/main.py +1 -1
  18. package/assets/templates/mobile-app/.env.example +4 -4
  19. package/assets/templates/mobile-app/AGENTS.md +3 -3
  20. package/assets/templates/mobile-app/README.md +10 -10
  21. package/assets/templates/mobile-app/app/(app)/_layout.tsx +2 -2
  22. package/assets/templates/mobile-app/app/(app)/index.tsx +2 -2
  23. package/assets/templates/mobile-app/app/(auth)/login.tsx +3 -3
  24. package/assets/templates/mobile-app/app/_layout.tsx +1 -1
  25. package/assets/templates/mobile-app/babel.config.js +1 -1
  26. package/assets/templates/mobile-app/eas.json +1 -1
  27. package/assets/templates/mobile-app/expo-env.d.ts +1 -1
  28. package/assets/templates/mobile-app/metro.config.js +2 -2
  29. package/assets/templates/mobile-app/package.json +1 -1
  30. package/assets/templates/mobile-app/src/mw/client.ts +3 -3
  31. package/assets/templates/mobile-app/src/mw/contracts.ts +2 -2
  32. package/assets/templates/mobile-app/src/mw/endpoints.ts +2 -2
  33. package/assets/templates/mobile-app/src/mw/env.ts +4 -4
  34. package/assets/templates/mobile-app/src/mw/session.ts +1 -1
  35. package/assets/templates/mobile-app/template.json +1 -1
  36. package/assets/templates/mobile-app/tools/template/validate-template.mjs +2 -2
  37. package/assets/templates/mobile-app/tsconfig.json +1 -1
  38. package/assets/templates/next-tenant-app/.env.example +1 -1
  39. package/assets/templates/next-tenant-app/README.md +26 -138
  40. package/assets/templates/next-tenant-app/package.json +1 -0
  41. package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
  42. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
  43. package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
  44. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
  45. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
  46. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
  47. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
  48. package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
  49. package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
  50. package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
  51. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
  52. package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
  53. package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
  54. package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
  55. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
  56. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
  57. package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
  58. package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
  59. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
  60. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
  61. package/assets/templates/next-tenant-app/src/lib/public-site.test.ts +1 -1
  62. package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
  63. package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
  64. package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
  65. package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
  66. package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
  67. package/assets/templates/next-tenant-app/template.json +3 -3
  68. package/assets/templates/next-tenant-app/template.schema.json +1 -0
  69. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
  70. package/assets/templates/next-tenant-app/tools/template/with-public-site-fixture.mjs +2 -2
  71. package/bin/minutework.js +1 -1
  72. package/dist/agent.js +7 -7
  73. package/dist/agent.js.map +1 -1
  74. package/dist/auth.js +7 -7
  75. package/dist/auth.js.map +1 -1
  76. package/dist/compile.js +5 -5
  77. package/dist/config.js +6 -6
  78. package/dist/config.js.map +1 -1
  79. package/dist/deploy.js +7 -7
  80. package/dist/deploy.js.map +1 -1
  81. package/dist/developer-client.js +2 -2
  82. package/dist/developer-client.js.map +1 -1
  83. package/dist/index.js +30 -30
  84. package/dist/index.js.map +1 -1
  85. package/dist/init.js +10 -10
  86. package/dist/init.js.map +1 -1
  87. package/dist/launcher.js +1 -1
  88. package/dist/launcher.js.map +1 -1
  89. package/dist/managed-engine.js +6 -6
  90. package/dist/managed-engine.js.map +1 -1
  91. package/dist/orchestrator-context.js +1 -1
  92. package/dist/orchestrator-context.js.map +1 -1
  93. package/dist/orchestrator.js +15 -15
  94. package/dist/orchestrator.js.map +1 -1
  95. package/dist/paths.js +1 -1
  96. package/dist/paths.js.map +1 -1
  97. package/dist/publish.js +3 -3
  98. package/dist/publish.js.map +1 -1
  99. package/dist/reporting.js +8 -8
  100. package/dist/reporting.js.map +1 -1
  101. package/dist/sandbox.js +5 -5
  102. package/dist/sandbox.js.map +1 -1
  103. package/dist/state.js +1 -1
  104. package/dist/state.js.map +1 -1
  105. package/dist/tokens.js +9 -9
  106. package/dist/tokens.js.map +1 -1
  107. package/dist/workspace-assets.js +6 -6
  108. package/dist/workspace-assets.js.map +1 -1
  109. package/dist/workspace.js +3 -3
  110. package/dist/workspace.js.map +1 -1
  111. package/package.json +3 -3
  112. package/vendor/workspace-mcp/context.d.ts +6 -6
  113. package/vendor/workspace-mcp/context.js +56 -56
  114. package/vendor/workspace-mcp/context.js.map +1 -1
  115. package/vendor/workspace-mcp/types.d.ts +4 -0
  116. package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
  117. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
  118. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
  119. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
  120. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
  121. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
  122. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
  123. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
  124. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
  125. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
  126. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
  127. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
  128. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
  129. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
  130. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
  131. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
  132. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
  133. package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
  134. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
  135. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
  136. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
  137. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
  138. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
  139. package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
  140. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
  141. package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
  142. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
  143. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
  144. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
  145. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
  146. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
  147. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
  148. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
  149. package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
  150. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
  151. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
  152. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
  153. package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
  154. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
  155. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
  156. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
  157. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
  158. package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
  159. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
  160. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
  161. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
  162. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
  163. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
  164. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
  165. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
  166. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
  167. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
  168. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
@@ -1,383 +0,0 @@
1
- import "server-only";
2
-
3
- import { cache } from "react";
4
- import { z, type ZodType } from "zod";
5
-
6
- import type {
7
- ContentStructurePage,
8
- MarketingPage,
9
- MarketingPageKey,
10
- PublicContentAdapter,
11
- PublicContentKind,
12
- PublicContentSnapshot,
13
- PublicEntry,
14
- PublicEntrySummary,
15
- PublicSiteSnapshotEnvelope,
16
- SiteConfig,
17
- } from "@/lib/content/contracts";
18
- import {
19
- contentStructurePageSchema,
20
- marketingPageResultSchema,
21
- publicEntryResultSchema,
22
- publicEntrySummaryListSchema,
23
- publicContentSnapshotSchema,
24
- publicSiteSnapshotEnvelopeSchema,
25
- siteConfigSchema,
26
- } from "@/lib/content/contracts";
27
- import { resolveCustomPublicContentAdapter } from "@/lib/content/custom-adapter";
28
- import { env } from "@/lib/platform/env.server";
29
-
30
- export const PUBLIC_CONTENT_REVALIDATE_SECONDS = 300;
31
-
32
- function normalizeSlugParts(slugParts: readonly string[]) {
33
- return slugParts.map((part) => part.trim().toLowerCase()).filter(Boolean).join("/");
34
- }
35
-
36
- function compareSlugParts(left: readonly string[], right: readonly string[]) {
37
- return normalizeSlugParts(left) === normalizeSlugParts(right);
38
- }
39
-
40
- function sortEntriesByDate(entries: readonly PublicEntrySummary[]) {
41
- return [...entries].sort((left, right) => {
42
- const leftTime = left.publishedAt ? Date.parse(left.publishedAt) : 0;
43
- const rightTime = right.publishedAt ? Date.parse(right.publishedAt) : 0;
44
- return rightTime - leftTime;
45
- });
46
- }
47
-
48
- function formatZodIssues(
49
- issues: readonly { message: string; path: readonly (string | number)[] }[],
50
- ) {
51
- return issues
52
- .map((issue) => {
53
- const path = issue.path.length > 0 ? issue.path.join(".") : "root";
54
- return `${path}: ${issue.message}`;
55
- })
56
- .join("; ");
57
- }
58
-
59
- function validateAdapterResult<T>(
60
- adapterLabel: string,
61
- methodLabel: string,
62
- schema: ZodType<T>,
63
- value: unknown,
64
- ) {
65
- const parsedValue = schema.safeParse(value);
66
-
67
- if (!parsedValue.success) {
68
- throw new Error(
69
- `${adapterLabel} returned invalid ${methodLabel}: ${formatZodIssues(parsedValue.error.issues)}`,
70
- );
71
- }
72
-
73
- return parsedValue.data;
74
- }
75
-
76
- function requirePublicContentEnv(key: string, value: string | undefined) {
77
- if (!value) {
78
- throw new Error(`${key} is required for MW_PUBLIC_CONTENT_SOURCE=minutework_cms.`);
79
- }
80
-
81
- return value;
82
- }
83
-
84
- export class ValidatedPublicContentAdapter implements PublicContentAdapter {
85
- constructor(
86
- private readonly adapter: PublicContentAdapter,
87
- private readonly adapterLabel: string,
88
- ) {}
89
-
90
- async getSiteConfig(): Promise<SiteConfig> {
91
- return validateAdapterResult(
92
- this.adapterLabel,
93
- "site config",
94
- siteConfigSchema,
95
- await this.adapter.getSiteConfig(),
96
- );
97
- }
98
-
99
- async getMarketingPage(pageKey: MarketingPageKey): Promise<MarketingPage | null> {
100
- return validateAdapterResult(
101
- this.adapterLabel,
102
- `marketing page for "${pageKey}"`,
103
- marketingPageResultSchema(pageKey),
104
- await this.adapter.getMarketingPage(pageKey),
105
- );
106
- }
107
-
108
- async listEntries(kind: PublicContentKind): Promise<PublicEntrySummary[]> {
109
- return validateAdapterResult(
110
- this.adapterLabel,
111
- `${kind} entry summaries`,
112
- publicEntrySummaryListSchema(kind),
113
- await this.adapter.listEntries(kind),
114
- );
115
- }
116
-
117
- async getEntry(kind: PublicContentKind, slugParts: string[]): Promise<PublicEntry | null> {
118
- return validateAdapterResult(
119
- this.adapterLabel,
120
- `${kind} entry for "${normalizeSlugParts(slugParts)}"`,
121
- publicEntryResultSchema(kind, slugParts),
122
- await this.adapter.getEntry(kind, slugParts),
123
- );
124
- }
125
-
126
- async getPageByPath(path: string): Promise<ContentStructurePage | null> {
127
- if (!this.adapter.getPageByPath) return null;
128
- return validateAdapterResult(
129
- this.adapterLabel,
130
- `CMS page for path "${path}"`,
131
- contentStructurePageSchema.nullable(),
132
- await this.adapter.getPageByPath(path),
133
- );
134
- }
135
-
136
- async listPages(status?: string): Promise<ContentStructurePage[]> {
137
- if (!this.adapter.listPages) return [];
138
- return validateAdapterResult(
139
- this.adapterLabel,
140
- "CMS page list",
141
- z.array(contentStructurePageSchema),
142
- await this.adapter.listPages(status),
143
- );
144
- }
145
- }
146
-
147
- export class PublicSiteSnapshotContentAdapter implements PublicContentAdapter {
148
- constructor(private readonly loadSnapshot: () => Promise<PublicContentSnapshot>) {}
149
-
150
- async getSiteConfig(): Promise<SiteConfig> {
151
- return (await this.loadSnapshot()).site;
152
- }
153
-
154
- async getMarketingPage(pageKey: MarketingPageKey): Promise<MarketingPage | null> {
155
- return (
156
- (await this.loadSnapshot()).marketingPages.find((page) => page.pageKey === pageKey) ??
157
- null
158
- );
159
- }
160
-
161
- async listEntries(kind: PublicContentKind): Promise<PublicEntrySummary[]> {
162
- return sortEntriesByDate((await this.loadSnapshot())[kind]);
163
- }
164
-
165
- async getEntry(kind: PublicContentKind, slugParts: string[]): Promise<PublicEntry | null> {
166
- return (
167
- (await this.loadSnapshot())[kind].find((entry) =>
168
- compareSlugParts(entry.slug, slugParts),
169
- ) ?? null
170
- );
171
- }
172
-
173
- async getPageByPath(path: string): Promise<ContentStructurePage | null> {
174
- const snapshot = await this.loadSnapshot();
175
- const pages = snapshot.pages ?? [];
176
- const normalizedPath = path.replace(/\/+$/, "") || "/";
177
- return pages.find((p) => p.path === normalizedPath) ?? null;
178
- }
179
-
180
- async listPages(status?: string): Promise<ContentStructurePage[]> {
181
- const snapshot = await this.loadSnapshot();
182
- const pages = snapshot.pages ?? [];
183
- if (status) {
184
- return pages.filter((p) => p.status === status);
185
- }
186
- return pages;
187
- }
188
- }
189
-
190
- export async function fetchPublicSiteSnapshotEnvelope(input: {
191
- contentApiToken: string;
192
- environment: "preview" | "live";
193
- platformBaseUrl: string;
194
- propertyKey: string;
195
- }): Promise<PublicSiteSnapshotEnvelope> {
196
- const requestUrl = new URL(
197
- `/api/v1/developer/public-site/snapshots/${encodeURIComponent(input.propertyKey)}/`,
198
- `${input.platformBaseUrl.replace(/\/$/, "")}/`,
199
- );
200
- requestUrl.searchParams.set("environment", input.environment);
201
-
202
- const response = await fetch(requestUrl.toString(), {
203
- headers: {
204
- accept: "application/json",
205
- authorization: `Bearer ${input.contentApiToken}`,
206
- },
207
- next: {
208
- revalidate: PUBLIC_CONTENT_REVALIDATE_SECONDS,
209
- tags: [
210
- "public-site-content",
211
- `public-site-content:${input.propertyKey}`,
212
- `public-site-content:${input.environment}`,
213
- ],
214
- },
215
- });
216
-
217
- if (!response.ok) {
218
- throw new Error(
219
- `Unable to load public-site ${input.environment} snapshot for "${input.propertyKey}" (${response.status}).`,
220
- );
221
- }
222
-
223
- const payload = await response.json();
224
- const parsedEnvelope = publicSiteSnapshotEnvelopeSchema.safeParse(payload);
225
-
226
- if (!parsedEnvelope.success) {
227
- throw new Error(
228
- `Public-site snapshot envelope is invalid: ${formatZodIssues(parsedEnvelope.error.issues)}`,
229
- );
230
- }
231
-
232
- return parsedEnvelope.data;
233
- }
234
-
235
- const loadPublicSiteSnapshotEnvelope = cache(async () =>
236
- fetchPublicSiteSnapshotEnvelope({
237
- contentApiToken: requirePublicContentEnv(
238
- "MW_CONTENT_API_TOKEN",
239
- env.MW_CONTENT_API_TOKEN,
240
- ),
241
- environment: env.MW_PUBLIC_SITE_ENV,
242
- platformBaseUrl: env.MW_PLATFORM_BASE_URL,
243
- propertyKey: requirePublicContentEnv(
244
- "MW_PUBLIC_SITE_PROPERTY_KEY",
245
- env.MW_PUBLIC_SITE_PROPERTY_KEY,
246
- ),
247
- }),
248
- );
249
-
250
- export class MinuteWorkPublicSiteContentAdapter extends PublicSiteSnapshotContentAdapter {
251
- constructor(
252
- loadEnvelope: () => Promise<PublicSiteSnapshotEnvelope> = loadPublicSiteSnapshotEnvelope,
253
- ) {
254
- super(async () => (await loadEnvelope()).snapshot);
255
- }
256
- }
257
-
258
- const loadStaticPublicContentSnapshot = cache(async () => {
259
- const [{ readFile }, { resolve }] = await Promise.all([
260
- import("node:fs/promises"),
261
- import("node:path"),
262
- ]);
263
- const contentPath = resolve(
264
- /* turbopackIgnore: true */ process.cwd(),
265
- env.MW_STATIC_PUBLIC_CONTENT_PATH,
266
- );
267
- let payload: unknown;
268
-
269
- try {
270
- payload = JSON.parse(await readFile(contentPath, "utf8"));
271
- } catch (error) {
272
- throw new Error(
273
- `Unable to load static public content from "${env.MW_STATIC_PUBLIC_CONTENT_PATH}": ${error instanceof Error ? error.message : String(error)}`,
274
- );
275
- }
276
-
277
- const parsedSnapshot = publicContentSnapshotSchema.safeParse(payload);
278
-
279
- if (!parsedSnapshot.success) {
280
- throw new Error(
281
- `Static public content is invalid: ${formatZodIssues(parsedSnapshot.error.issues)}`,
282
- );
283
- }
284
-
285
- return parsedSnapshot.data;
286
- });
287
-
288
- export class StaticJsonPublicContentAdapter extends PublicSiteSnapshotContentAdapter {
289
- constructor(
290
- loadSnapshot: () => Promise<PublicContentSnapshot> = loadStaticPublicContentSnapshot,
291
- ) {
292
- super(loadSnapshot);
293
- }
294
- }
295
-
296
- class DisabledPublicContentAdapter implements PublicContentAdapter {
297
- private throwDisabled(): never {
298
- throw new Error("Public content is disabled by MW_PUBLIC_CONTENT_SOURCE=none.");
299
- }
300
-
301
- async getSiteConfig(): Promise<SiteConfig> {
302
- this.throwDisabled();
303
- }
304
-
305
- async getMarketingPage(): Promise<MarketingPage | null> {
306
- this.throwDisabled();
307
- }
308
-
309
- async listEntries(): Promise<PublicEntrySummary[]> {
310
- this.throwDisabled();
311
- }
312
-
313
- async getEntry(): Promise<PublicEntry | null> {
314
- this.throwDisabled();
315
- }
316
-
317
- async getPageByPath(): Promise<ContentStructurePage | null> {
318
- this.throwDisabled();
319
- }
320
-
321
- async listPages(): Promise<ContentStructurePage[]> {
322
- this.throwDisabled();
323
- }
324
- }
325
-
326
- const defaultPublicContentAdapter = new ValidatedPublicContentAdapter(
327
- new MinuteWorkPublicSiteContentAdapter(),
328
- "PandaWork public-site content adapter",
329
- );
330
- const staticJsonPublicContentAdapter = new ValidatedPublicContentAdapter(
331
- new StaticJsonPublicContentAdapter(),
332
- "Static JSON public content adapter",
333
- );
334
- const disabledPublicContentAdapter = new DisabledPublicContentAdapter();
335
-
336
- export const getPublicContentAdapter = cache(() => {
337
- if (env.MW_PUBLIC_CONTENT_SOURCE === "custom") {
338
- const customAdapter = resolveCustomPublicContentAdapter();
339
-
340
- if (!customAdapter) {
341
- throw new Error(
342
- "MW_PUBLIC_CONTENT_SOURCE=custom requires resolveCustomPublicContentAdapter() to return a PublicContentAdapter.",
343
- );
344
- }
345
-
346
- return new ValidatedPublicContentAdapter(
347
- customAdapter,
348
- "Custom public content adapter",
349
- );
350
- }
351
-
352
- if (env.MW_PUBLIC_CONTENT_SOURCE === "static_json") {
353
- return staticJsonPublicContentAdapter;
354
- }
355
-
356
- if (env.MW_PUBLIC_CONTENT_SOURCE === "none") {
357
- return disabledPublicContentAdapter;
358
- }
359
-
360
- return defaultPublicContentAdapter;
361
- });
362
-
363
- export const getSiteConfig = cache(async () => getPublicContentAdapter().getSiteConfig());
364
-
365
- export const getMarketingPage = cache(async (pageKey: MarketingPageKey) =>
366
- getPublicContentAdapter().getMarketingPage(pageKey),
367
- );
368
-
369
- export const listEntries = cache(async (kind: PublicContentKind) =>
370
- getPublicContentAdapter().listEntries(kind),
371
- );
372
-
373
- export const getEntry = cache(async (kind: PublicContentKind, slugParts: string[]) =>
374
- getPublicContentAdapter().getEntry(kind, slugParts),
375
- );
376
-
377
- export const getPageByPath = cache(async (path: string) =>
378
- getPublicContentAdapter().getPageByPath(path),
379
- );
380
-
381
- export const listPages = cache(async (status?: string) =>
382
- getPublicContentAdapter().listPages(status),
383
- );
@@ -1,138 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import {
4
- contentSectionSchema,
5
- contentStructurePageSchema,
6
- publicContentSnapshotSchema,
7
- } from "./contracts";
8
-
9
- describe("content_structure contracts", () => {
10
- it("validates a page with arbitrary section types", () => {
11
- const page = {
12
- page_key: "landing",
13
- path: "/landing",
14
- title: "Landing Page",
15
- status: "published",
16
- sort_order: 0,
17
- content_structure: {
18
- sections: [
19
- {
20
- section_key: "hero",
21
- section_type: "hero_banner",
22
- sort_order: 0,
23
- data: { heading: "Welcome", subheading: "To our site" },
24
- },
25
- {
26
- section_key: "features",
27
- section_type: "custom_feature_grid",
28
- sort_order: 1,
29
- data: {
30
- columns: 3,
31
- items: [
32
- { title: "Fast", icon: "bolt" },
33
- { title: "Secure", icon: "lock" },
34
- ],
35
- },
36
- },
37
- {
38
- section_key: "tenant_custom",
39
- section_type: "my_custom_widget",
40
- sort_order: 2,
41
- data: { arbitrary: "tenant-defined", nested: { deep: true } },
42
- },
43
- ],
44
- },
45
- seo_metadata: {
46
- title: "Landing Page",
47
- description: "A landing page with arbitrary sections",
48
- },
49
- };
50
-
51
- const result = contentStructurePageSchema.safeParse(page);
52
- expect(result.success).toBe(true);
53
- if (result.success) {
54
- expect(result.data.content_structure.sections).toHaveLength(3);
55
- expect(result.data.content_structure.sections[2]!.section_type).toBe(
56
- "my_custom_widget",
57
- );
58
- }
59
- });
60
-
61
- it("rejects a section without section_key", () => {
62
- const section = {
63
- section_type: "text",
64
- data: { heading: "Heading" },
65
- };
66
- const result = contentSectionSchema.safeParse(section);
67
- expect(result.success).toBe(false);
68
- });
69
-
70
- it("allows empty sections array", () => {
71
- const page = {
72
- page_key: "empty",
73
- path: "/empty",
74
- title: "Empty Page",
75
- status: "draft",
76
- content_structure: { sections: [] },
77
- };
78
- const result = contentStructurePageSchema.safeParse(page);
79
- expect(result.success).toBe(true);
80
- });
81
-
82
- it("snapshot schema accepts optional pages array", () => {
83
- const snapshot = {
84
- site: {
85
- siteName: "Test",
86
- siteDescription: "Test site",
87
- organizationName: "Org",
88
- footerBlurb: "Footer",
89
- primaryCta: { label: "CTA", href: "/" },
90
- secondaryCta: { label: "CTA2", href: "/docs" },
91
- primaryNavigation: [{ label: "Home", href: "/" }],
92
- collections: {
93
- docs: { eyebrow: "Docs", title: "Docs", description: "Documentation" },
94
- blog: { eyebrow: "Blog", title: "Blog", description: "Updates" },
95
- },
96
- },
97
- marketingPages: [],
98
- docs: [],
99
- blog: [],
100
- pages: [
101
- {
102
- page_key: "about",
103
- path: "/about",
104
- title: "About",
105
- status: "published",
106
- content_structure: { sections: [] },
107
- },
108
- ],
109
- };
110
-
111
- const result = publicContentSnapshotSchema.safeParse(snapshot);
112
- expect(result.success).toBe(true);
113
- });
114
-
115
- it("snapshot schema works without pages field (backward compat)", () => {
116
- const snapshot = {
117
- site: {
118
- siteName: "Test",
119
- siteDescription: "Test site",
120
- organizationName: "Org",
121
- footerBlurb: "Footer",
122
- primaryCta: { label: "CTA", href: "/" },
123
- secondaryCta: { label: "CTA2", href: "/docs" },
124
- primaryNavigation: [{ label: "Home", href: "/" }],
125
- collections: {
126
- docs: { eyebrow: "Docs", title: "Docs", description: "Documentation" },
127
- blog: { eyebrow: "Blog", title: "Blog", description: "Updates" },
128
- },
129
- },
130
- marketingPages: [],
131
- docs: [],
132
- blog: [],
133
- };
134
-
135
- const result = publicContentSnapshotSchema.safeParse(snapshot);
136
- expect(result.success).toBe(true);
137
- });
138
- });