create-avocado-site 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +65 -0
  3. package/assets/generated-images/gdrive_1ql_bdk2KM_27u01rkMd8KoiCRJ_R7UOy.webp +0 -0
  4. package/assets/generated-images/gen_1774991902955_0p1i2o.webp +0 -0
  5. package/assets/generated-images/gen_1776458907692_zlrbkg.webp +0 -0
  6. package/assets/generated-images/gen_1788955681679_454dc316.webp +0 -0
  7. package/assets/generated-images/var_1773434968055_8g99nk.webp +0 -0
  8. package/assets/generated-images/var_1773435042084_wvet78.webp +0 -0
  9. package/assets/generated-images/var_1773435061562_6hf8s6.webp +0 -0
  10. package/assets/generated-images/var_1773435080764_6ocahu.webp +0 -0
  11. package/assets/generated-images/var_1773435101045_1y544z.webp +0 -0
  12. package/assets/generated-images/var_1773872370443_poycxy.webp +0 -0
  13. package/assets/generated-images/var_1776858715876_iuu72w.webp +0 -0
  14. package/assets/logos/avocado-hub.svg +7 -0
  15. package/dist/cms-config.d.ts +34 -0
  16. package/dist/cms-config.js +103 -0
  17. package/dist/generator.d.ts +22 -0
  18. package/dist/generator.js +92 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +109 -0
  21. package/dist/instructions.d.ts +19 -0
  22. package/dist/instructions.js +82 -0
  23. package/dist/next-version.d.ts +15 -0
  24. package/dist/next-version.js +43 -0
  25. package/dist/ports.d.ts +11 -0
  26. package/dist/ports.js +61 -0
  27. package/dist/prompts.d.ts +13 -0
  28. package/dist/prompts.js +119 -0
  29. package/dist/templates/assets.d.ts +2 -0
  30. package/dist/templates/assets.js +41 -0
  31. package/dist/templates/common.d.ts +45 -0
  32. package/dist/templates/common.js +174 -0
  33. package/dist/templates/contentful.d.ts +2 -0
  34. package/dist/templates/contentful.js +379 -0
  35. package/dist/templates/demo-app.d.ts +3 -0
  36. package/dist/templates/demo-app.js +465 -0
  37. package/dist/templates/demo-content.d.ts +2 -0
  38. package/dist/templates/demo-content.js +7 -0
  39. package/dist/templates/sanity.d.ts +2 -0
  40. package/dist/templates/sanity.js +289 -0
  41. package/dist/templates/static.d.ts +2 -0
  42. package/dist/templates/static.js +81 -0
  43. package/dist/templates/strapi.d.ts +2 -0
  44. package/dist/templates/strapi.js +235 -0
  45. package/dist/types.d.ts +48 -0
  46. package/dist/types.js +1 -0
  47. package/dist/versions.d.ts +32 -0
  48. package/dist/versions.js +32 -0
  49. package/package.json +51 -0
@@ -0,0 +1,174 @@
1
+ import { CMS_CONFIGS } from "../cms-config.js";
2
+ export function editorApiRoute(config) {
3
+ const c = CMS_CONFIGS[config.cms];
4
+ return `import { createEditorApiHandler } from "@avocadostudio-ai/site-sdk/routes"
5
+ ${c.publishImport}
6
+ ${c.getPagesImport ? c.getPagesImport + "\n" : ""}
7
+ export const { GET, POST, OPTIONS } = createEditorApiHandler({
8
+ getPages: ${c.getPages},
9
+ publishSecret: process.env.PUBLISH_TOKEN?.trim() || undefined,
10
+ onPublish: ${c.onPublish},
11
+ })
12
+ `;
13
+ }
14
+ export function revalidateRoute(config) {
15
+ const c = CMS_CONFIGS[config.cms];
16
+ if (!c.revalidate)
17
+ return null;
18
+ const r = c.revalidate;
19
+ const headerLine = r.secretHeader
20
+ ? ` secretHeader: ${r.secretHeader},\n`
21
+ : "";
22
+ return `import { createRevalidateHandler } from "@avocadostudio-ai/site-sdk/routes"
23
+ ${r.getPagesImport}
24
+
25
+ export const POST = createRevalidateHandler({
26
+ secretEnvVar: "${r.secretEnvVar}",
27
+ ${headerLine} extractSlug: ${r.extractSlug},
28
+ getPages: ${r.getPagesFn},
29
+ siteId: "${config.siteId}",
30
+ })
31
+ `;
32
+ }
33
+ export function manifestFile(config) {
34
+ if (config.blocksMode === "custom") {
35
+ return `import type { BlockManifest } from "@avocadostudio-ai/shared"
36
+ import { getManifestImageFields } from "@avocadostudio-ai/site-sdk/routes"
37
+
38
+ const manifest: BlockManifest = {
39
+ version: 1,
40
+ blocks: [
41
+ // {
42
+ // type: "Hero",
43
+ // displayName: "Hero Section",
44
+ // propsSchema: {
45
+ // type: "object",
46
+ // properties: {
47
+ // heading: { type: "string" },
48
+ // imageUrl: { type: "string" },
49
+ // },
50
+ // required: ["heading"],
51
+ // },
52
+ // defaultProps: { heading: "Welcome" },
53
+ // },
54
+ ],
55
+ }
56
+
57
+ export function getManifest() { return manifest }
58
+ export const { imageFields, listImageFields, listFieldNames } = getManifestImageFields(manifest)
59
+ `;
60
+ }
61
+ return `import { buildBlockManifest } from "@avocadostudio-ai/site-sdk/editor-manifest"
62
+ import { getManifestImageFields } from "@avocadostudio-ai/site-sdk/routes"
63
+
64
+ export const { imageFields, listImageFields, listFieldNames } = getManifestImageFields(buildBlockManifest())
65
+ `;
66
+ }
67
+ function adjustImportDepth(pageImport, mode) {
68
+ // CMS config encodes import paths for the single-route layout
69
+ // (`app/[[...slug]]/page.tsx`). The preview page lives one directory deeper
70
+ // (`app/preview-draft/[[...slug]]/page.tsx`), so prefix one extra `../` on
71
+ // every relative import. "auto" and "static" share the shallower depth.
72
+ if (mode !== "preview")
73
+ return pageImport;
74
+ return pageImport.replace(/from "\.\.\//g, `from "../../`);
75
+ }
76
+ export function pageFile(config, mode = "static") {
77
+ const c = CMS_CONFIGS[config.cms];
78
+ const pageImport = adjustImportDepth(c.pageImport, mode);
79
+ // `generateStaticParams` is returned in every mode but is meaningless in the
80
+ // two dynamic ones, and exporting it from a `force-dynamic` route is a
81
+ // contradiction Next does not report.
82
+ const staticParams = mode === "static";
83
+ const factoryReturn = staticParams ? "{ Page, generateStaticParams, generateMetadata }" : "{ Page, generateMetadata }";
84
+ const dynamicLine = mode === "preview" ? `\nexport const dynamic = "force-dynamic"\n` : "";
85
+ const exports = staticParams
86
+ ? "\nexport default Page\nexport { generateStaticParams, generateMetadata }\n"
87
+ : "\nexport default Page\nexport { generateMetadata }\n";
88
+ return `import { createSitePage } from "@avocadostudio-ai/site-sdk/page"
89
+ ${pageImport}
90
+ ${dynamicLine}
91
+ const ${factoryReturn} = createSitePage({
92
+ mode: "${mode}",
93
+ siteId: "${config.siteId}",
94
+ getPage: ${c.getPage},
95
+ getSlugs: ${c.getSlugs},${c.getSiteConfig ? `\n getSiteConfig: ${c.getSiteConfig},` : ""}
96
+ })${exports}`;
97
+ }
98
+ /**
99
+ * `createSitePage` calls `notFound()` for an unknown slug, which renders this
100
+ * file with a real 404 status. Without it Next falls back to its own bare
101
+ * default page — correct status, but none of the site's chrome.
102
+ */
103
+ export function notFoundFile() {
104
+ return `export default function NotFound() {
105
+ return (
106
+ <main style={{ padding: "4rem", textAlign: "center" }}>
107
+ <h1>404</h1>
108
+ <p>Page not found.</p>
109
+ </main>
110
+ )
111
+ }
112
+ `;
113
+ }
114
+ /** Next.js 15 and earlier: the rewrite lives in `middleware.ts`. */
115
+ export function middlewareFile() {
116
+ return `import { createEditorMiddleware } from "@avocadostudio-ai/site-sdk/middleware"
117
+
118
+ export const { middleware, config } = createEditorMiddleware()
119
+ `;
120
+ }
121
+ /**
122
+ * Next.js 16+: `middleware` was renamed to the `proxy` convention, and `config`
123
+ * must be a static object literal — Next 16 reads it by static analysis and
124
+ * rejects a factory return value or an imported constant.
125
+ */
126
+ export function proxyFile() {
127
+ return `import { createEditorProxy } from "@avocadostudio-ai/site-sdk/proxy"
128
+
129
+ export const proxy = createEditorProxy().proxy
130
+
131
+ // Must stay a literal — Next.js 16 cannot read \`config\` from a factory result.
132
+ export const config = {
133
+ matcher: [
134
+ "/((?!_next|preview-draft|api|favicon\\\\.ico|icon\\\\.svg|logos/|generated-images/|.*\\\\.).*)",
135
+ ],
136
+ }
137
+ `;
138
+ }
139
+ /**
140
+ * The integrate path's `.env.local.example`.
141
+ *
142
+ * It used to contradict the instructions printed beside it. Step 4 mounts the
143
+ * orchestrator inside the app at `/api/avocado` — there is no separate service
144
+ * — while this file set `ORCHESTRATOR_URL=http://localhost:4200`, pointing at
145
+ * the standalone server the integrator was never told to run. The env var
146
+ * wins, so every draft read went to a port with nothing on it and the editor
147
+ * showed published content with the edits silently dropped.
148
+ *
149
+ * `DRAFT_MODE_SECRET` was likewise pre-filled with `dev-secret` next to a step
150
+ * saying to set it to a long random string; it is generated now, as on the
151
+ * demo path.
152
+ */
153
+ export function envExample(config) {
154
+ const c = CMS_CONFIGS[config.cms];
155
+ const common = `# The orchestrator runs inside this app, at /api/avocado. There is no separate
156
+ # service to start — this must name this app's own origin, not port 4200.
157
+ ORCHESTRATOR_URL=http://localhost:${config.sitePort}/api/avocado
158
+
159
+ # Where the editor is served from. Named twice on every request — CORS on the
160
+ # editor API and \`frame-ancestors\` on the preview — and \`withAvocado\` derives
161
+ # both from this one variable. Get it wrong and the site reads as offline while
162
+ # the preview stays a blank rectangle, neither of which mentions a port.
163
+ EDITOR_CORS_ORIGINS=http://localhost:${config.editorPort}
164
+
165
+ # Any long random string; this one was generated for you. Without it draft mode
166
+ # does not work, and the editor quietly renders the published page instead of
167
+ # your edits.
168
+ DRAFT_MODE_SECRET=${config.draftSecret}
169
+
170
+ # Optional: protects the publish endpoint.
171
+ # PUBLISH_TOKEN=
172
+ `;
173
+ return c.envVars ? `${c.envVars}\n\n${common}` : common;
174
+ }
@@ -0,0 +1,2 @@
1
+ import type { GeneratedFile, ScaffoldConfig } from "../types.js";
2
+ export declare function contentfulTemplates(_config: ScaffoldConfig): GeneratedFile[];
@@ -0,0 +1,379 @@
1
+ export function contentfulTemplates(_config) {
2
+ return [
3
+ { path: "lib/contentful.ts", content: CONTENTFUL_ADAPTER },
4
+ { path: "lib/publish.ts", content: CONTENTFUL_PUBLISH },
5
+ ];
6
+ }
7
+ const CONTENTFUL_ADAPTER = `import { createClient, type Entry, type Asset } from "contentful"
8
+ import { imageFields, listImageFields } from "./manifest"
9
+ import type { PageDoc, SiteConfig, BlockInstance } from "@avocadostudio-ai/shared"
10
+
11
+ let cachedClient: ReturnType<typeof createClient> | null = null
12
+
13
+ function getClient() {
14
+ if (cachedClient) return cachedClient
15
+ const spaceId = process.env.CONTENTFUL_SPACE_ID
16
+ const accessToken = process.env.CONTENTFUL_DELIVERY_TOKEN
17
+ if (!spaceId || !accessToken) throw new Error("CONTENTFUL_SPACE_ID and CONTENTFUL_DELIVERY_TOKEN required")
18
+ cachedClient = createClient({
19
+ space: spaceId, accessToken,
20
+ environment: process.env.CONTENTFUL_ENVIRONMENT ?? "master",
21
+ })
22
+ return cachedClient
23
+ }
24
+
25
+ function assetToUrl(asset: Asset | undefined): string {
26
+ if (!asset?.fields?.file) return ""
27
+ const file = asset.fields.file as { url?: string }
28
+ if (!file.url) return ""
29
+ return file.url.startsWith("//") ? \`https:\${file.url}\` : file.url
30
+ }
31
+
32
+ function entryToBlockInstance(entry: Entry, includes?: { Asset?: Asset[] }): BlockInstance | null {
33
+ const contentTypeId = entry.sys.contentType?.sys?.id
34
+ if (!contentTypeId?.startsWith("block")) return null
35
+ const blockType = contentTypeId.replace(/^block/, "")
36
+ const imgFields = imageFields.get(blockType) ?? new Set<string>()
37
+ const listImgFieldsForType = listImageFields.get(blockType)
38
+ const props: Record<string, unknown> = {}
39
+ const fields = entry.fields as Record<string, unknown>
40
+
41
+ for (const [key, value] of Object.entries(fields)) {
42
+ if (imgFields.has(key)) {
43
+ const linked = value as { sys?: { id?: string } } | undefined
44
+ if (linked?.sys?.id && includes?.Asset) {
45
+ const asset = includes.Asset.find((a) => a.sys.id === linked.sys.id)
46
+ props[key] = assetToUrl(asset as Asset)
47
+ } else if (linked && typeof linked === "object" && "fields" in linked) {
48
+ props[key] = assetToUrl(linked as Asset)
49
+ } else { props[key] = "" }
50
+ } else if (listImgFieldsForType?.has(key) && blockType === "CardGrid" && key === "cards") {
51
+ const refs = value as Array<Entry | { sys: { id: string } }>
52
+ if (Array.isArray(refs)) {
53
+ props[key] = refs.map((ref) => {
54
+ if ("fields" in ref) {
55
+ const cardInstance = entryToBlockInstance(ref as Entry, includes)
56
+ return cardInstance?.props
57
+ }
58
+ return null
59
+ }).filter(Boolean)
60
+ } else { props[key] = [] }
61
+ } else { props[key] = value }
62
+ }
63
+ return { id: entry.sys.id.replace(/^block_/, ""), type: blockType, props }
64
+ }
65
+
66
+ function entryToPageDoc(page: Entry, includes?: { Asset?: Asset[] }): PageDoc | null {
67
+ const fields = page.fields as Record<string, unknown>
68
+ const slug = fields.slug as string | undefined
69
+ if (!slug) return null
70
+ const blockRefs = fields.blocks as Array<Entry | { sys: { id: string } }> | undefined
71
+ const blocks: BlockInstance[] = []
72
+ if (Array.isArray(blockRefs)) {
73
+ for (const ref of blockRefs) {
74
+ if ("fields" in ref) {
75
+ const block = entryToBlockInstance(ref as Entry, includes)
76
+ if (block) blocks.push(block)
77
+ }
78
+ }
79
+ }
80
+ return {
81
+ id: (fields.pageId as string) ?? page.sys.id,
82
+ slug, title: fields.title as string, blocks,
83
+ meta: fields.meta as PageDoc["meta"],
84
+ updatedAt: (fields.updatedAt as string) ?? page.sys.updatedAt,
85
+ }
86
+ }
87
+
88
+ export async function getContentfulPage(slug: string): Promise<PageDoc | null> {
89
+ const entries = await getClient().getEntries({ content_type: "page", "fields.slug": slug, include: 2, limit: 1 })
90
+ if (entries.items.length === 0) return null
91
+ return entryToPageDoc(entries.items[0], entries.includes as { Asset?: Asset[] })
92
+ }
93
+
94
+ export async function getContentfulSlugs(): Promise<string[]> {
95
+ const entries = await getClient().getEntries({ content_type: "page", select: ["fields.slug"], limit: 100 })
96
+ return entries.items
97
+ .map((item) => (item.fields as Record<string, unknown>).slug)
98
+ .filter((slug): slug is string => typeof slug === "string" && slug.length > 0)
99
+ }
100
+
101
+ export async function getContentfulPages(): Promise<PageDoc[]> {
102
+ const entries = await getClient().getEntries({ content_type: "page", include: 2, limit: 100 })
103
+ return entries.items
104
+ .map((page) => entryToPageDoc(page, entries.includes as { Asset?: Asset[] }))
105
+ .filter((p): p is PageDoc => p !== null)
106
+ }
107
+
108
+ export async function getContentfulSiteConfig(): Promise<SiteConfig> {
109
+ try {
110
+ const entries = await getClient().getEntries({ content_type: "siteConfig", "fields.configKey": "default", limit: 1 })
111
+ if (entries.items.length === 0) return {}
112
+ const fields = entries.items[0].fields as Record<string, unknown>
113
+ return {
114
+ name: (fields.name as string) || undefined,
115
+ logo: (fields.logo as string) || undefined,
116
+ navLabels: (fields.navLabels as Record<string, string>) || undefined,
117
+ }
118
+ } catch { return {} }
119
+ }
120
+ `;
121
+ const CONTENTFUL_PUBLISH = `import type { OnPublishFn, InlineAsset } from "@avocadostudio-ai/site-sdk/routes"
122
+ import { isSafeImageUrl } from "@avocadostudio-ai/site-sdk/routes"
123
+ import { imageFields, listFieldNames } from "./manifest"
124
+
125
+ export interface ContentfulPublishOptions {
126
+ spaceId: string
127
+ environmentId?: string
128
+ managementToken: string
129
+ locale?: string
130
+ }
131
+
132
+ export function createContentfulPublishHandler(opts: ContentfulPublishOptions): OnPublishFn {
133
+ const locale = opts.locale ?? "en-US"
134
+
135
+ let cachedEnvPromise: ReturnType<typeof loadEnvironment> | null = null
136
+
137
+ async function loadEnvironment() {
138
+ const mod = await import("contentful-management")
139
+ const factory = ("default" in mod && typeof (mod.default as Record<string, unknown>)?.createClient === "function")
140
+ ? (mod.default as { createClient: typeof mod.createClient }).createClient
141
+ : mod.createClient
142
+ const client = factory({ accessToken: opts.managementToken })
143
+ const space = await client.getSpace(opts.spaceId)
144
+ return space.getEnvironment(opts.environmentId ?? "master")
145
+ }
146
+
147
+ function getEnvironment() {
148
+ if (!cachedEnvPromise) {
149
+ cachedEnvPromise = loadEnvironment().catch((err) => { cachedEnvPromise = null; throw err })
150
+ }
151
+ return cachedEnvPromise
152
+ }
153
+
154
+ function isReferenceList(blockType: string, listKey: string): string | null {
155
+ if (blockType === "CardGrid" && listKey === "cards") return "blockCard"
156
+ return null
157
+ }
158
+
159
+ type CfEnv = Awaited<ReturnType<typeof loadEnvironment>>
160
+ type CfEntry = Awaited<ReturnType<CfEnv["getEntries"]>>["items"][0]
161
+
162
+ async function ensureAsset(
163
+ env: CfEnv, imageUrl: string, alt: string, assets?: Record<string, InlineAsset>
164
+ ): Promise<string | null> {
165
+ const inlineAsset = assets?.[imageUrl]
166
+ if (inlineAsset) {
167
+ try {
168
+ const bytes = Buffer.from(inlineAsset.data, "base64")
169
+ const upload = await env.createUpload({ file: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer })
170
+ const asset = await env.createAsset({
171
+ fields: {
172
+ title: { [locale]: alt || inlineAsset.fileName },
173
+ description: { [locale]: imageUrl },
174
+ file: { [locale]: { contentType: inlineAsset.mimeType, fileName: inlineAsset.fileName, uploadFrom: { sys: { type: "Link", linkType: "Upload", id: upload.sys.id } } } },
175
+ },
176
+ })
177
+ const processed = await asset.processForAllLocales()
178
+ const published = await processed.publish()
179
+ return published.sys.id
180
+ } catch (err) {
181
+ console.warn(\`[contentful-publish] inline asset upload failed for \${imageUrl}: \${err instanceof Error ? err.message : err}\`)
182
+ return null
183
+ }
184
+ }
185
+
186
+ if (!imageUrl.startsWith("http") || !isSafeImageUrl(imageUrl)) {
187
+ try {
188
+ const asset = await env.createAsset({
189
+ fields: {
190
+ title: { [locale]: alt || imageUrl },
191
+ description: { [locale]: imageUrl },
192
+ file: { [locale]: { contentType: "image/png", fileName: imageUrl.split("/").pop() || "image.png", upload: \`https://placehold.co/800x600/e4e4e7/52525b.png?text=\${encodeURIComponent(alt || "Image")}\` } },
193
+ },
194
+ })
195
+ const processed = await asset.processForAllLocales()
196
+ const published = await processed.publish()
197
+ return published.sys.id
198
+ } catch { return null }
199
+ }
200
+
201
+ try {
202
+ const existing = await env.getAssets({ "fields.description": imageUrl, limit: 1 })
203
+ if (existing.items.length > 0) return existing.items[0].sys.id
204
+ } catch { /* continue */ }
205
+
206
+ try {
207
+ const ext = imageUrl.toLowerCase().split(".").pop()?.split("?")[0] ?? ""
208
+ const ct = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : ext === "gif" ? "image/gif" : ext === "svg" ? "image/svg+xml" : "image/jpeg"
209
+ const asset = await env.createAsset({
210
+ fields: {
211
+ title: { [locale]: alt || imageUrl },
212
+ description: { [locale]: imageUrl },
213
+ file: { [locale]: { contentType: ct, fileName: imageUrl.split("/").pop()?.split("?")[0] || "image.jpg", upload: imageUrl } },
214
+ },
215
+ })
216
+ const processed = await asset.processForAllLocales()
217
+ const published = await processed.publish()
218
+ return published.sys.id
219
+ } catch (err) {
220
+ console.warn(\`[contentful-publish] asset upload failed for \${imageUrl}: \${err instanceof Error ? err.message : err}\`)
221
+ return null
222
+ }
223
+ }
224
+
225
+ function assetLink(assetId: string) {
226
+ return { [locale]: { sys: { type: "Link", linkType: "Asset", id: assetId } } }
227
+ }
228
+
229
+ function entryLink(entryId: string) {
230
+ return { sys: { type: "Link" as const, linkType: "Entry" as const, id: entryId } }
231
+ }
232
+
233
+ function existingAssetId(fieldValue: unknown): string | null {
234
+ if (!fieldValue || typeof fieldValue !== "object") return null
235
+ const localeVal = (fieldValue as Record<string, unknown>)[locale]
236
+ if (!localeVal || typeof localeVal !== "object") return null
237
+ const sys = (localeVal as Record<string, unknown>).sys as { id?: string; linkType?: string } | undefined
238
+ return sys?.linkType === "Asset" && sys.id ? sys.id : null
239
+ }
240
+
241
+ async function buildBlockFields(
242
+ env: CfEnv, blockType: string, props: Record<string, unknown>,
243
+ resolveAsset: (url: string, alt: string) => Promise<string | null>,
244
+ existingFields?: Record<string, unknown>,
245
+ ): Promise<Record<string, unknown>> {
246
+ const imgFields = imageFields.get(blockType) ?? new Set<string>()
247
+ const listFieldsForType = listFieldNames.get(blockType) ?? new Set<string>()
248
+ const fields: Record<string, unknown> = {}
249
+
250
+ for (const [key, value] of Object.entries(props)) {
251
+ if (key === "headingLevel") continue
252
+ if (imgFields.has(key) && typeof value === "string" && value) {
253
+ const altKey = key.replace(/Url$/, "Alt").replace(/^image$/, "imageAlt")
254
+ const alt = typeof props[altKey] === "string" ? (props[altKey] as string) : ""
255
+ const assetId = await resolveAsset(value, alt)
256
+ if (assetId) { fields[key] = assetLink(assetId) }
257
+ else {
258
+ const prevAssetId = existingAssetId(existingFields?.[key])
259
+ if (prevAssetId) fields[key] = assetLink(prevAssetId)
260
+ }
261
+ } else if (listFieldsForType.has(key)) {
262
+ if (!isReferenceList(blockType, key)) { fields[key] = { [locale]: value } }
263
+ } else { fields[key] = { [locale]: value } }
264
+ }
265
+ return fields
266
+ }
267
+
268
+ return async (pages, config, context) => {
269
+ const env = await getEnvironment()
270
+ const assetCache = new Map<string, Promise<string | null>>()
271
+ const cachedEnsureAsset = (imageUrl: string, alt: string) => {
272
+ const cached = assetCache.get(imageUrl)
273
+ if (cached) return cached
274
+ const promise = ensureAsset(env, imageUrl, alt, context?.assets)
275
+ assetCache.set(imageUrl, promise)
276
+ return promise
277
+ }
278
+
279
+ const upsertResults = await Promise.allSettled(pages.map(async (page) => {
280
+ const blockEntries = await Promise.all(page.blocks.map(async (block) => {
281
+ const contentTypeId = \`block\${block.type}\`
282
+ const rawId = block.id.startsWith("block_") ? block.id : \`block_\${block.id}\`
283
+ const entryId = rawId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64)
284
+
285
+ let existingEntry: CfEntry | null = null
286
+ try { existingEntry = await env.getEntry(entryId) } catch { /* first publish */ }
287
+ const existingFields = existingEntry?.fields as Record<string, unknown> | undefined
288
+
289
+ const blockFields = await buildBlockFields(env, block.type, block.props, cachedEnsureAsset, existingFields)
290
+
291
+ const blockListFields = listFieldNames.get(block.type) ?? new Set<string>()
292
+ for (const [key, value] of Object.entries(block.props)) {
293
+ const refTarget = isReferenceList(block.type, key)
294
+ if (refTarget && Array.isArray(value) && blockListFields.has(key)) {
295
+ const items = value as Record<string, unknown>[]
296
+ const cardRefs = await Promise.all(
297
+ items.map(async (item, i) => {
298
+ const cardEntryId = \`card_\${entryId}_\${i}\`.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64)
299
+ let existingCard: CfEntry | null = null
300
+ try { existingCard = await env.getEntry(cardEntryId) } catch { /* first publish */ }
301
+ const childFields = await buildBlockFields(env, "Card", item, cachedEnsureAsset,
302
+ existingCard?.fields as Record<string, unknown> | undefined)
303
+ if (existingCard) {
304
+ existingCard.fields = childFields
305
+ const updated = await existingCard.update()
306
+ await updated.publish()
307
+ } else {
308
+ const created = await env.createEntryWithId(refTarget, cardEntryId, { fields: childFields })
309
+ await created.publish()
310
+ }
311
+ return entryLink(cardEntryId)
312
+ })
313
+ )
314
+ blockFields[key] = { [locale]: cardRefs }
315
+ for (let i = items.length; i < items.length + 20; i++) {
316
+ const staleCardId = \`card_\${entryId}_\${i}\`.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64)
317
+ try {
318
+ const staleCard = await env.getEntry(staleCardId)
319
+ try { await staleCard.unpublish() } catch {}
320
+ await staleCard.delete()
321
+ } catch { break }
322
+ }
323
+ }
324
+ }
325
+
326
+ if (existingEntry) {
327
+ existingEntry.fields = blockFields
328
+ const updated = await existingEntry.update()
329
+ await updated.publish()
330
+ return entryLink(updated.sys.id)
331
+ } else {
332
+ const entry = await env.createEntryWithId(contentTypeId, entryId, { fields: blockFields })
333
+ await entry.publish()
334
+ return entryLink(entry.sys.id)
335
+ }
336
+ }))
337
+
338
+ const pageFields = {
339
+ slug: { [locale]: page.slug }, title: { [locale]: page.title },
340
+ pageId: { [locale]: page.id }, blocks: { [locale]: blockEntries },
341
+ meta: { [locale]: page.meta ?? null }, updatedAt: { [locale]: page.updatedAt },
342
+ }
343
+ const existingPages = await env.getEntries({ content_type: "page", "fields.slug": page.slug, limit: 1 })
344
+ if (existingPages.items.length > 0) {
345
+ const existing = existingPages.items[0]
346
+ existing.fields = pageFields
347
+ const updated = await existing.update()
348
+ await updated.publish()
349
+ } else {
350
+ const created = await env.createEntry("page", { fields: pageFields })
351
+ await created.publish()
352
+ }
353
+ return page.slug
354
+ }))
355
+
356
+ const failed = upsertResults
357
+ .filter((r): r is PromiseRejectedResult => r.status === "rejected")
358
+ .map((r, i) => \`\${pages[i]?.slug ?? "?"}: \${r.reason instanceof Error ? r.reason.message : String(r.reason)}\`)
359
+
360
+ if (config.name || config.logo || config.navLabels) {
361
+ const configFields = {
362
+ configKey: { [locale]: "default" }, name: { [locale]: config.name ?? "" },
363
+ logo: { [locale]: config.logo ?? "" }, navLabels: { [locale]: config.navLabels ?? {} },
364
+ }
365
+ const existingConfig = await env.getEntries({ content_type: "siteConfig", "fields.configKey": "default", limit: 1 })
366
+ let configEntry
367
+ if (existingConfig.items.length > 0) {
368
+ configEntry = existingConfig.items[0]
369
+ configEntry.fields = configFields
370
+ configEntry = await configEntry.update()
371
+ } else { configEntry = await env.createEntry("siteConfig", { fields: configFields }) }
372
+ await configEntry.publish()
373
+ }
374
+
375
+ if (failed.length > 0) return { ok: false, error: \`Failed to publish: \${failed.join("; ")}\` }
376
+ return { ok: true }
377
+ }
378
+ }
379
+ `;
@@ -0,0 +1,3 @@
1
+ import type { GeneratedFile, ScaffoldConfig } from "../types.js";
2
+ /** The files that exist only in a bootstrapped demo, on top of the shared wiring. */
3
+ export declare function demoAppTemplates(config: ScaffoldConfig): GeneratedFile[];