riverbankcms 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acceptance/matrix.mjs +62 -6
- package/dist/cli/index.mjs +11820 -2431
- package/dist/cli/init-docs/content/agents-section.md +9 -2
- package/dist/cli/init-docs/content/cli-reference.md +69 -5
- package/dist/cli/init-docs/content/content-management.md +12 -3
- package/dist/cli/init-docs/content/getting-started.md +8 -10
- package/dist/cli/init-docs/content/workflow-add-block.md +8 -1
- package/dist/cli/init-docs/content/workflow-block-extensions.md +25 -9
- package/dist/cli/init-docs/content/workflow-custom-block.md +35 -8
- package/dist/cli/init-docs/content/workflow-preview-mode.md +30 -6
- package/dist/cli/init-docs/content/workflow-remote-setup.md +66 -10
- package/dist/{eventUpsertInputSchema-BUeXa6jk.mjs → eventUpsertInputSchema-CzMX5BZI.mjs} +8 -7
- package/dist/load-config-226EuL6w.mjs +5 -0
- package/dist/{load-config-CJUwrGHA.mjs → load-config-Boa0tkmk.mjs} +1 -1
- package/dist/plan-2c4zVtlW.mjs +2213 -0
- package/dist/scaffold/index.mjs +5 -0
- package/dist/sdk-event-upsert-BCZUdIrL.mjs +887 -0
- package/dist/{sdk-peer-D9FHJ54i.mjs → sdk-peer-DiZYsNXj.mjs} +5 -2
- package/dist/{sdk-event-upsert-CTqsrhFT.mjs → src-XEJYAKbQ.mjs} +138 -925
- package/dist/{style-lftMrun_.mjs → style-3BjmymXd.mjs} +25860 -25137
- package/dist/testing.mjs +4 -3
- package/package.json +6 -1
- package/dist/load-config-CcQyVy3r.mjs +0 -5
|
@@ -0,0 +1,2213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { K as parseSiteId, bt as ok, yt as err } from "./src-XEJYAKbQ.mjs";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
//#region src/scaffold/package-contract.ts
|
|
8
|
+
const LOCAL_PACKAGE_VENDOR_DIR = ".riverbank/local-packages";
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function parseStableVersion(value) {
|
|
13
|
+
if (typeof value !== "string") return undefined;
|
|
14
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(value);
|
|
15
|
+
if (!match) return undefined;
|
|
16
|
+
return {
|
|
17
|
+
major: Number(match[1]),
|
|
18
|
+
minor: Number(match[2]),
|
|
19
|
+
patch: Number(match[3])
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Pure package-contract derivation from a parsed manifest. */
|
|
23
|
+
function deriveGeneratedPackageRanges(manifest) {
|
|
24
|
+
if (!isRecord(manifest)) return {
|
|
25
|
+
ok: false,
|
|
26
|
+
error: { tag: "invalid_manifest_shape" }
|
|
27
|
+
};
|
|
28
|
+
const version = parseStableVersion(manifest.version);
|
|
29
|
+
if (!version) return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: {
|
|
32
|
+
tag: "invalid_cli_version",
|
|
33
|
+
value: manifest.version
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const peerDependencies = manifest.peerDependencies;
|
|
37
|
+
const sdkRange = isRecord(peerDependencies) ? peerDependencies["@riverbankcms/sdk"] : undefined;
|
|
38
|
+
if (typeof sdkRange !== "string" || sdkRange.trim().length === 0) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: {
|
|
42
|
+
tag: "invalid_cli_peer_range",
|
|
43
|
+
value: sdkRange
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
value: {
|
|
50
|
+
sdk: sdkRange,
|
|
51
|
+
cli: `^${version.major}.${version.minor}.0`
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Find the nearest package root from a module or executable path. Walking
|
|
57
|
+
* upward works for both the source tree and a packed npm install; it does not
|
|
58
|
+
* rely on the repository's `scripts/` layout.
|
|
59
|
+
*/
|
|
60
|
+
function findPackageManifestPath(startPath = fileURLToPath(import.meta.url), fileExists = existsSync) {
|
|
61
|
+
let directory = dirname(startPath);
|
|
62
|
+
while (true) {
|
|
63
|
+
const candidate = join(directory, "package.json");
|
|
64
|
+
if (fileExists(candidate)) return candidate;
|
|
65
|
+
const parent = dirname(directory);
|
|
66
|
+
if (parent === directory || directory === parse(directory).root) return undefined;
|
|
67
|
+
directory = parent;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Effectful package-boundary load; all range derivation remains pure above. */
|
|
71
|
+
function loadGeneratedPackageRanges(manifestPath = findPackageManifestPath()) {
|
|
72
|
+
if (!manifestPath) throw new Error("Cannot locate the riverbankcms package manifest.");
|
|
73
|
+
const parsed = deriveGeneratedPackageRanges(JSON.parse(readFileSync(manifestPath, "utf8")));
|
|
74
|
+
if (!parsed.ok) throw new Error(`Invalid riverbankcms package contract (${parsed.error.tag}).`);
|
|
75
|
+
return parsed.value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/scaffold/local-manifest.ts
|
|
80
|
+
const DEFAULT_CONTENT_TYPES = {
|
|
81
|
+
withBlog: false,
|
|
82
|
+
withEvents: false,
|
|
83
|
+
withTestimonials: false,
|
|
84
|
+
withPortfolio: false
|
|
85
|
+
};
|
|
86
|
+
function parseProjectPath(value) {
|
|
87
|
+
if (typeof value !== "string" || value.length === 0 || value !== value.trim()) return undefined;
|
|
88
|
+
if (!isAbsolute(value) || value.includes("\0")) return undefined;
|
|
89
|
+
const normalized = resolve(value);
|
|
90
|
+
return normalized === parse(normalized).root ? undefined : normalized;
|
|
91
|
+
}
|
|
92
|
+
function parseFeatures(input) {
|
|
93
|
+
const preview = input.preview;
|
|
94
|
+
const portal = input.portal;
|
|
95
|
+
if (preview !== "enabled" && preview !== "disabled") return undefined;
|
|
96
|
+
if (portal !== "enabled" && portal !== "disabled") return undefined;
|
|
97
|
+
const contentTypesValue = input.contentTypes;
|
|
98
|
+
if (contentTypesValue === undefined) {
|
|
99
|
+
return {
|
|
100
|
+
preview,
|
|
101
|
+
portal,
|
|
102
|
+
contentTypes: DEFAULT_CONTENT_TYPES
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (typeof contentTypesValue !== "object" || contentTypesValue === null || Array.isArray(contentTypesValue)) return undefined;
|
|
106
|
+
const value = contentTypesValue;
|
|
107
|
+
const contentTypes = {
|
|
108
|
+
withBlog: value.withBlog === true,
|
|
109
|
+
withEvents: value.withEvents === true,
|
|
110
|
+
withTestimonials: value.withTestimonials === true,
|
|
111
|
+
withPortfolio: value.withPortfolio === true
|
|
112
|
+
};
|
|
113
|
+
const known = Object.keys(value);
|
|
114
|
+
if (known.some((key) => !Object.keys(DEFAULT_CONTENT_TYPES).includes(key) || typeof value[key] !== "boolean")) return undefined;
|
|
115
|
+
return {
|
|
116
|
+
preview,
|
|
117
|
+
portal,
|
|
118
|
+
contentTypes
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Derive only local target state. This boundary can run before a remote site
|
|
123
|
+
* is created because it does not depend on site identity or dashboard data.
|
|
124
|
+
*/
|
|
125
|
+
function deriveScaffoldLocalManifest(input) {
|
|
126
|
+
const projectPath = parseProjectPath(input.projectPath);
|
|
127
|
+
if (!projectPath) return err({
|
|
128
|
+
tag: "invalid_project_path",
|
|
129
|
+
value: input.projectPath
|
|
130
|
+
});
|
|
131
|
+
const features = parseFeatures(input.features);
|
|
132
|
+
if (!features) return err({
|
|
133
|
+
tag: "invalid_features",
|
|
134
|
+
field: "features"
|
|
135
|
+
});
|
|
136
|
+
return ok(Object.freeze({
|
|
137
|
+
projectPath,
|
|
138
|
+
features: Object.freeze({
|
|
139
|
+
...features,
|
|
140
|
+
contentTypes: Object.freeze({ ...features.contentTypes })
|
|
141
|
+
})
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/scaffold/writer.ts
|
|
147
|
+
function errorMessage(error) {
|
|
148
|
+
return error instanceof Error ? error.message : String(error);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The single post-link completion authority used by fresh and resumed
|
|
152
|
+
* scaffolds. Keeping it here prevents init from growing a second docs/build
|
|
153
|
+
* implementation with subtly different ordering or error mapping.
|
|
154
|
+
*/
|
|
155
|
+
function createScaffoldCompletion(projectPath, process) {
|
|
156
|
+
return async () => {
|
|
157
|
+
try {
|
|
158
|
+
await process.initializeDocs(projectPath);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
error: {
|
|
163
|
+
tag: "docs_initialization_failed",
|
|
164
|
+
message: errorMessage(error)
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
await process.verifyBuild(projectPath);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
error: {
|
|
174
|
+
tag: "build_verification_failed",
|
|
175
|
+
message: errorMessage(error)
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return { ok: true };
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Read every target before the first write/process effect. A missing project
|
|
184
|
+
* root is valid when missing or empty; create-next-app is therefore the only
|
|
185
|
+
* process effect allowed to populate a clean target before canonical writes.
|
|
186
|
+
*/
|
|
187
|
+
function preflightScaffoldPlan(plan, filesystem) {
|
|
188
|
+
return preflightScaffoldTargets(plan, filesystem);
|
|
189
|
+
}
|
|
190
|
+
/** Preflight the local project target before any site-creation/process effect. */
|
|
191
|
+
function preflightScaffoldTargets(manifest, filesystem) {
|
|
192
|
+
let rootState;
|
|
193
|
+
try {
|
|
194
|
+
rootState = filesystem.inspect(manifest.projectPath);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return {
|
|
197
|
+
tag: "path_inspection_failed",
|
|
198
|
+
path: manifest.projectPath,
|
|
199
|
+
message: errorMessage(error)
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (rootState !== "missing" && rootState !== "directory") {
|
|
203
|
+
return {
|
|
204
|
+
tag: "project_target_occupied",
|
|
205
|
+
path: manifest.projectPath,
|
|
206
|
+
state: rootState
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (rootState === "directory") {
|
|
210
|
+
let entries;
|
|
211
|
+
try {
|
|
212
|
+
entries = filesystem.listDirectory(manifest.projectPath);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return {
|
|
215
|
+
tag: "path_inspection_failed",
|
|
216
|
+
path: manifest.projectPath,
|
|
217
|
+
message: errorMessage(error)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (entries.length > 0) return {
|
|
221
|
+
tag: "project_target_occupied",
|
|
222
|
+
path: manifest.projectPath,
|
|
223
|
+
state: "directory"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
/** Write only a preflighted immutable manifest. */
|
|
229
|
+
function writePreflightedScaffoldFiles(plan, filesystem) {
|
|
230
|
+
try {
|
|
231
|
+
if (filesystem.inspect(plan.projectPath) === "missing") {
|
|
232
|
+
try {
|
|
233
|
+
filesystem.mkdir(plan.projectPath);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
error: {
|
|
238
|
+
tag: "directory_create_failed",
|
|
239
|
+
path: plan.projectPath,
|
|
240
|
+
message: errorMessage(error)
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (const file of plan.files) {
|
|
246
|
+
const target = join(plan.projectPath, file.path);
|
|
247
|
+
try {
|
|
248
|
+
filesystem.mkdir(dirname(target));
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
error: {
|
|
253
|
+
tag: "directory_create_failed",
|
|
254
|
+
path: dirname(target),
|
|
255
|
+
message: errorMessage(error)
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
filesystem.writeFile(target, file.content, Boolean(file.executable));
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
error: {
|
|
265
|
+
tag: "file_write_failed",
|
|
266
|
+
path: target,
|
|
267
|
+
message: errorMessage(error)
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { ok: true };
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
error: {
|
|
277
|
+
tag: "path_inspection_failed",
|
|
278
|
+
path: plan.projectPath,
|
|
279
|
+
message: errorMessage(error)
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** Validate all targets, then write the immutable manifest. */
|
|
285
|
+
function writeScaffoldFiles(plan, filesystem) {
|
|
286
|
+
const preflightError = preflightScaffoldPlan(plan, filesystem);
|
|
287
|
+
if (preflightError) return {
|
|
288
|
+
ok: false,
|
|
289
|
+
error: preflightError
|
|
290
|
+
};
|
|
291
|
+
return writePreflightedScaffoldFiles(plan, filesystem);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Prepare a two-stage effect shell so `init` can insert credential link
|
|
295
|
+
* between materialization and docs/build without duplicating the shell.
|
|
296
|
+
*/
|
|
297
|
+
function prepareScaffoldExecution(plan, dependencies, packageSourceOverride) {
|
|
298
|
+
const preflightError = preflightScaffoldPlan(plan, dependencies.filesystem);
|
|
299
|
+
if (preflightError) return {
|
|
300
|
+
ok: false,
|
|
301
|
+
error: preflightError
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
value: { materialize: () => {
|
|
306
|
+
try {
|
|
307
|
+
dependencies.process.createNextApp(plan.projectPath);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
error: {
|
|
312
|
+
tag: "create_next_app_failed",
|
|
313
|
+
message: errorMessage(error)
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const written = writePreflightedScaffoldFiles(plan, dependencies.filesystem);
|
|
318
|
+
if (!written.ok) return written;
|
|
319
|
+
try {
|
|
320
|
+
dependencies.process.cleanupGeneratedDefaults(plan.projectPath);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
error: {
|
|
325
|
+
tag: "generated_defaults_cleanup_failed",
|
|
326
|
+
message: errorMessage(error)
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
dependencies.process.configureProject(plan.projectPath, plan);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
error: {
|
|
336
|
+
tag: "file_write_failed",
|
|
337
|
+
path: join(plan.projectPath, "package.json"),
|
|
338
|
+
message: errorMessage(error)
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
dependencies.process.installPackages(plan.projectPath, packageSourceOverride ?? plan.packageSource, plan.packageSource.ranges);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return {
|
|
346
|
+
ok: false,
|
|
347
|
+
error: {
|
|
348
|
+
tag: "package_install_failed",
|
|
349
|
+
message: errorMessage(error)
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
ok: true,
|
|
355
|
+
value: { completeAfterLink: createScaffoldCompletion(plan.projectPath, dependencies.process) }
|
|
356
|
+
};
|
|
357
|
+
} }
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/scaffold/templates/agents-md.ts
|
|
363
|
+
function agentsMdTemplate() {
|
|
364
|
+
return {
|
|
365
|
+
path: "AGENTS.md",
|
|
366
|
+
content: `# Code Guidelines
|
|
367
|
+
|
|
368
|
+
Guidelines for AI agents and developers working on this codebase.
|
|
369
|
+
|
|
370
|
+
## Principles
|
|
371
|
+
|
|
372
|
+
- **DRY**: Don't repeat yourself. Extract shared logic into helpers.
|
|
373
|
+
- **Modularity**: Small, focused functions that do one thing well.
|
|
374
|
+
- **Functional style**: Prefer pure functions, avoid mutation.
|
|
375
|
+
- **Composition over inheritance**: Build complex behavior from simple pieces.
|
|
376
|
+
- **Simplicity**: The simplest solution that works is often the best.
|
|
377
|
+
|
|
378
|
+
## SDK Development
|
|
379
|
+
|
|
380
|
+
This site uses \`@riverbankcms/sdk\` from Riverbank Digital. **We control the SDK repository.**
|
|
381
|
+
|
|
382
|
+
### When you encounter SDK limitations:
|
|
383
|
+
|
|
384
|
+
1. **Don't work around SDK shortcomings** in this codebase
|
|
385
|
+
2. Document the limitation and desired behavior
|
|
386
|
+
3. Create a plan/issue for SDK improvements
|
|
387
|
+
4. Implement the fix in the SDK, then use it here
|
|
388
|
+
|
|
389
|
+
This approach keeps the site code clean and improves the SDK for all users.
|
|
390
|
+
|
|
391
|
+
## File Organization
|
|
392
|
+
|
|
393
|
+
\`\`\`
|
|
394
|
+
src/
|
|
395
|
+
app/ # Next.js App Router pages
|
|
396
|
+
[[...slug]]/ # CMS-managed pages (catch-all route)
|
|
397
|
+
lib/ # Shared utilities and SDK client
|
|
398
|
+
components/ # React components (if needed)
|
|
399
|
+
__tests__/ # Test files
|
|
400
|
+
\`\`\`
|
|
401
|
+
|
|
402
|
+
## Naming Conventions
|
|
403
|
+
|
|
404
|
+
| Type | Convention | Example |
|
|
405
|
+
|------|------------|---------|
|
|
406
|
+
| Files | kebab-case | \`riverbank-client.ts\` |
|
|
407
|
+
| Functions | camelCase | \`getRiverbankClient\` |
|
|
408
|
+
| Types/Interfaces | PascalCase | \`PageProps\` |
|
|
409
|
+
| Constants | SCREAMING_SNAKE_CASE | \`PREVIEW_MODE\` |
|
|
410
|
+
| Components | PascalCase | \`CustomHero.tsx\` |
|
|
411
|
+
|
|
412
|
+
## Code Quality
|
|
413
|
+
|
|
414
|
+
### Before committing:
|
|
415
|
+
|
|
416
|
+
\`\`\`bash
|
|
417
|
+
pnpm verify # Full check: lint + typecheck + tests
|
|
418
|
+
\`\`\`
|
|
419
|
+
|
|
420
|
+
### Guidelines:
|
|
421
|
+
|
|
422
|
+
- Keep functions small and focused
|
|
423
|
+
- Prefer explicit over implicit
|
|
424
|
+
- Add types - avoid \`any\`
|
|
425
|
+
- Handle errors explicitly
|
|
426
|
+
- Write tests for business logic
|
|
427
|
+
`
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/scaffold/templates/block-extensions.ts
|
|
433
|
+
function blockExtensionsTemplate() {
|
|
434
|
+
return {
|
|
435
|
+
path: "src/config/block-extensions.ts",
|
|
436
|
+
content: `import type { BlockFieldExtensionsMap, BlockFieldOptionsMap } from '@riverbankcms/sdk/config';
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Block Field Extensions
|
|
440
|
+
*
|
|
441
|
+
* Add custom fields to built-in SDK blocks.
|
|
442
|
+
* These fields appear in the CMS editor alongside the block's default fields.
|
|
443
|
+
*
|
|
444
|
+
* Example:
|
|
445
|
+
* \`\`\`typescript
|
|
446
|
+
* 'block.hero': {
|
|
447
|
+
* fields: [
|
|
448
|
+
* {
|
|
449
|
+
* id: 'layout',
|
|
450
|
+
* type: 'select',
|
|
451
|
+
* label: 'Layout',
|
|
452
|
+
* required: false,
|
|
453
|
+
* multiple: false,
|
|
454
|
+
* options: [
|
|
455
|
+
* { value: 'default', label: 'Default' },
|
|
456
|
+
* { value: 'centered', label: 'Centered' },
|
|
457
|
+
* ],
|
|
458
|
+
* defaultValue: 'default',
|
|
459
|
+
* },
|
|
460
|
+
* ],
|
|
461
|
+
* }
|
|
462
|
+
* \`\`\`
|
|
463
|
+
*/
|
|
464
|
+
export const blockFieldExtensions: BlockFieldExtensionsMap = {
|
|
465
|
+
// Add your block field extensions here
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Block Field Options
|
|
470
|
+
*
|
|
471
|
+
* Customize the options available for existing fields on built-in blocks.
|
|
472
|
+
*
|
|
473
|
+
* Example:
|
|
474
|
+
* \`\`\`typescript
|
|
475
|
+
* 'block.embed': {
|
|
476
|
+
* layout: {
|
|
477
|
+
* options: [
|
|
478
|
+
* { value: 'grid', label: 'Grid Layout' },
|
|
479
|
+
* { value: 'list', label: 'List Layout' },
|
|
480
|
+
* ],
|
|
481
|
+
* },
|
|
482
|
+
* }
|
|
483
|
+
* \`\`\`
|
|
484
|
+
*/
|
|
485
|
+
export const blockFieldOptions: BlockFieldOptionsMap = {
|
|
486
|
+
// Add your block field option overrides here
|
|
487
|
+
};
|
|
488
|
+
`
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region src/scaffold/templates/block-overrides.ts
|
|
494
|
+
function blockOverridesTemplate() {
|
|
495
|
+
return {
|
|
496
|
+
path: "src/lib/block-overrides.tsx",
|
|
497
|
+
content: `/**
|
|
498
|
+
* Host block override registrations
|
|
499
|
+
*
|
|
500
|
+
* Fully headless sites own rendering for every block kind they use. The empty
|
|
501
|
+
* registry is intentional while the starter homepage has no blocks. Add a
|
|
502
|
+
* registration before adding a block to content; an unregistered kind raises
|
|
503
|
+
* MissingBlockOverrideError rather than using a built-in renderer.
|
|
504
|
+
*
|
|
505
|
+
* Example:
|
|
506
|
+
* \`\`\`typescript
|
|
507
|
+
* import {
|
|
508
|
+
* defineBlockOverrideRegistrations,
|
|
509
|
+
* defineLazyBlockOverride,
|
|
510
|
+
* type BlockOverrideProps,
|
|
511
|
+
* type BlockOverrideRegistrations,
|
|
512
|
+
* type HeroContent,
|
|
513
|
+
* } from '@riverbankcms/sdk/rendering';
|
|
514
|
+
* import { CustomHero } from '@/components/CustomHero';
|
|
515
|
+
*
|
|
516
|
+
* const TypedHero = (props: BlockOverrideProps<HeroContent>) => <CustomHero {...props} />;
|
|
517
|
+
*
|
|
518
|
+
* export const blockOverrides = defineBlockOverrideRegistrations({
|
|
519
|
+
* // Override the built-in hero block
|
|
520
|
+
* hero: TypedHero,
|
|
521
|
+
*
|
|
522
|
+
* // Lazy-load client-heavy or rarely used custom blocks
|
|
523
|
+
* 'custom.quiz': defineLazyBlockOverride(async () =>
|
|
524
|
+
* import('@/components/blocks/QuizBlock').then((module) => module.QuizBlock),
|
|
525
|
+
* ),
|
|
526
|
+
* }) satisfies BlockOverrideRegistrations;
|
|
527
|
+
* \`\`\`
|
|
528
|
+
*/
|
|
529
|
+
|
|
530
|
+
import { defineBlockOverrideRegistrations } from '@riverbankcms/sdk/rendering';
|
|
531
|
+
import type { BlockOverrideRegistrations } from '@riverbankcms/sdk/rendering';
|
|
532
|
+
|
|
533
|
+
export const blockOverrides = defineBlockOverrideRegistrations({}) satisfies
|
|
534
|
+
BlockOverrideRegistrations;
|
|
535
|
+
`
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/scaffold/templates/catch-all-page.ts
|
|
541
|
+
function catchAllPageTemplate(config) {
|
|
542
|
+
const previewImports = config.withPreviewMode ? `import { createPreviewModeBeforeRender } from '@riverbankcms/sdk/preview-next/before-render'\nimport { SdkPreviewModeClient } from '@/components/preview/SdkPreviewModeClient'\n` : "";
|
|
543
|
+
const previewBeforeRender = config.withPreviewMode ? `,
|
|
544
|
+
beforeRender: createPreviewModeBeforeRender({
|
|
545
|
+
getClient: getRiverbankClient,
|
|
546
|
+
siteId: config.siteId,
|
|
547
|
+
siteSlug: ${JSON.stringify(config.siteSlug)},
|
|
548
|
+
PreviewModeClient: SdkPreviewModeClient,
|
|
549
|
+
})` : "";
|
|
550
|
+
return {
|
|
551
|
+
path: "src/app/[[...slug]]/page.tsx",
|
|
552
|
+
content: `import {
|
|
553
|
+
createCatchAllPage,
|
|
554
|
+
createGenerateStaticParams,
|
|
555
|
+
} from '@riverbankcms/sdk/next';
|
|
556
|
+
import { getRiverbankClient } from '@/lib/riverbank-client';
|
|
557
|
+
import { SITE_RENDERING } from '@/lib/rendering';
|
|
558
|
+
import config from '../../../riverbank.config';
|
|
559
|
+
import { blockOverrides } from '@/lib/block-overrides';
|
|
560
|
+
import { dataLoaderOverrides } from '@/lib/data-loader-overrides.server';
|
|
561
|
+
${previewImports}
|
|
562
|
+
|
|
563
|
+
// Next.js route config values must remain statically analyzable literals.
|
|
564
|
+
export const revalidate = 300;
|
|
565
|
+
export const dynamicParams = true;
|
|
566
|
+
// Route discovery falls back to the homepage when the CMS is unavailable so
|
|
567
|
+
// the scaffold verification build does not require a live API. Add
|
|
568
|
+
// failureMode: 'throw' when production CI should reject discovery failures.
|
|
569
|
+
export const generateStaticParams = createGenerateStaticParams({
|
|
570
|
+
getClient: getRiverbankClient,
|
|
571
|
+
siteId: config.siteId,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Create catch-all page using SDK helper
|
|
576
|
+
*
|
|
577
|
+
* The createCatchAllPage helper handles:
|
|
578
|
+
* - Loading page data from CMS
|
|
579
|
+
* - Rendering blocks with the Page component
|
|
580
|
+
* - Generating metadata for SEO
|
|
581
|
+
* - 404 handling for non-existent pages
|
|
582
|
+
*
|
|
583
|
+
* Add an override for every block kind used by the site's content. The empty
|
|
584
|
+
* starter registry is intentional; an unregistered kind raises the SDK's
|
|
585
|
+
* MissingBlockOverrideError instead of falling back to built-in rendering.
|
|
586
|
+
*/
|
|
587
|
+
const { Page, generateMetadata } = createCatchAllPage({
|
|
588
|
+
getClient: getRiverbankClient,
|
|
589
|
+
config,
|
|
590
|
+
rendering: SITE_RENDERING,
|
|
591
|
+
searchParamsMode: 'ignore',
|
|
592
|
+
blockOverrides${previewBeforeRender},
|
|
593
|
+
dataLoaderOverrides,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
export default Page;
|
|
597
|
+
export { generateMetadata };
|
|
598
|
+
`
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/scaffold/templates/cms-push-staging.ts
|
|
604
|
+
function cmsPushStagingTemplate() {
|
|
605
|
+
return {
|
|
606
|
+
path: "scripts/cms-push-staging.sh",
|
|
607
|
+
executable: true,
|
|
608
|
+
content: `#!/usr/bin/env bash
|
|
609
|
+
set -euo pipefail
|
|
610
|
+
|
|
611
|
+
PROJECT_ROOT="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
|
|
612
|
+
ENV_FILE="\${PROJECT_ROOT}/.env.staging"
|
|
613
|
+
|
|
614
|
+
APPLY=0
|
|
615
|
+
if [[ "\${1:-}" == "--apply" ]]; then
|
|
616
|
+
APPLY=1
|
|
617
|
+
shift
|
|
618
|
+
fi
|
|
619
|
+
|
|
620
|
+
if [[ ! -f "\${ENV_FILE}" ]]; then
|
|
621
|
+
echo "Missing \${ENV_FILE}"
|
|
622
|
+
echo ""
|
|
623
|
+
echo "Create it by copying the template:"
|
|
624
|
+
echo " cp .env.staging.example .env.staging"
|
|
625
|
+
echo ""
|
|
626
|
+
echo "Then fill in:"
|
|
627
|
+
echo " RIVERBANK_STAGING_SITE_ID=..."
|
|
628
|
+
echo " RIVERBANK_STAGING_DASHBOARD_URL=https://..."
|
|
629
|
+
echo " RIVERBANK_STAGING_MGMT_API_KEY=bld_mgmt_sk_..."
|
|
630
|
+
exit 1
|
|
631
|
+
fi
|
|
632
|
+
|
|
633
|
+
cd "\${PROJECT_ROOT}"
|
|
634
|
+
|
|
635
|
+
set -a
|
|
636
|
+
# shellcheck disable=SC1090
|
|
637
|
+
source "\${ENV_FILE}"
|
|
638
|
+
set +a
|
|
639
|
+
|
|
640
|
+
: "\${RIVERBANK_STAGING_SITE_ID:?Missing RIVERBANK_STAGING_SITE_ID in .env.staging}"
|
|
641
|
+
: "\${RIVERBANK_STAGING_DASHBOARD_URL:?Missing RIVERBANK_STAGING_DASHBOARD_URL in .env.staging}"
|
|
642
|
+
: "\${RIVERBANK_STAGING_MGMT_API_KEY:?Missing RIVERBANK_STAGING_MGMT_API_KEY in .env.staging}"
|
|
643
|
+
|
|
644
|
+
dashboard_url="\${RIVERBANK_STAGING_DASHBOARD_URL}"
|
|
645
|
+
if [[ "\${dashboard_url}" != http://* && "\${dashboard_url}" != https://* ]]; then
|
|
646
|
+
dashboard_url="https://\${dashboard_url}"
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
dashboard_host="$(DASHBOARD_URL="\${dashboard_url}" node -e "console.log(new URL(process.env.DASHBOARD_URL).host)")"
|
|
650
|
+
|
|
651
|
+
if [[ "\${dashboard_host}" == "www.riverbankcms.com" || "\${dashboard_host}" == "riverbankcms.com" ]]; then
|
|
652
|
+
echo "Refusing to run staging push against production dashboard host: \${dashboard_host}"
|
|
653
|
+
exit 1
|
|
654
|
+
fi
|
|
655
|
+
|
|
656
|
+
export RIVERBANK_REMOTE_SITE_ID="\${RIVERBANK_STAGING_SITE_ID}"
|
|
657
|
+
export RIVERBANK_REMOTE_DASHBOARD_URL="\${dashboard_url}"
|
|
658
|
+
export RIVERBANK_REMOTE_MGMT_API_KEY="\${RIVERBANK_STAGING_MGMT_API_KEY}"
|
|
659
|
+
|
|
660
|
+
cmd=(pnpm exec riverbankcms push --env=remote)
|
|
661
|
+
if [[ "\${APPLY}" == "1" ]]; then
|
|
662
|
+
cmd+=(--yes)
|
|
663
|
+
else
|
|
664
|
+
cmd+=(--dry-run)
|
|
665
|
+
fi
|
|
666
|
+
cmd+=("$@")
|
|
667
|
+
|
|
668
|
+
echo "Target: \${RIVERBANK_REMOTE_DASHBOARD_URL} (site: \${RIVERBANK_REMOTE_SITE_ID})"
|
|
669
|
+
echo "Running: \${cmd[*]}"
|
|
670
|
+
"\${cmd[@]}"
|
|
671
|
+
`
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
//#endregion
|
|
676
|
+
//#region src/scaffold/templates/content-types.ts
|
|
677
|
+
const BLOG_CONTENT_TYPE = ` // Blog posts
|
|
678
|
+
{
|
|
679
|
+
key: 'post',
|
|
680
|
+
name: 'Blog Post',
|
|
681
|
+
description: 'Blog articles and news',
|
|
682
|
+
hasPages: true,
|
|
683
|
+
routePattern: '/blog/{slug}',
|
|
684
|
+
fields: [
|
|
685
|
+
{ id: 'excerpt', type: 'text', label: 'Excerpt', required: false, multiline: true, description: 'Short summary for listings' },
|
|
686
|
+
{ id: 'image', type: 'media', label: 'Featured Image', required: false, mediaKinds: ['image'] },
|
|
687
|
+
{ id: 'content', type: 'richText', label: 'Content', required: true },
|
|
688
|
+
{ id: 'author', type: 'text', label: 'Author', required: false, multiline: false },
|
|
689
|
+
{ id: 'publishedAt', type: 'datetime', label: 'Publish Date', required: false },
|
|
690
|
+
],
|
|
691
|
+
}`;
|
|
692
|
+
const TESTIMONIALS_CONTENT_TYPE = ` // Testimonials
|
|
693
|
+
{
|
|
694
|
+
key: 'testimonial',
|
|
695
|
+
name: 'Testimonial',
|
|
696
|
+
description: 'Customer testimonials and reviews',
|
|
697
|
+
hasPages: false,
|
|
698
|
+
fields: [
|
|
699
|
+
{ id: 'name', type: 'text', label: 'Name', required: true, multiline: false },
|
|
700
|
+
{ id: 'jobTitle', type: 'text', label: 'Job Title', required: false, multiline: false },
|
|
701
|
+
{ id: 'company', type: 'text', label: 'Company', required: false, multiline: false },
|
|
702
|
+
{ id: 'body', type: 'richText', label: 'Quote', required: true },
|
|
703
|
+
],
|
|
704
|
+
titleField: 'name',
|
|
705
|
+
}`;
|
|
706
|
+
const EVENTS_CONTENT_TYPE = ` // Events
|
|
707
|
+
{
|
|
708
|
+
key: 'event',
|
|
709
|
+
name: 'Event',
|
|
710
|
+
description: 'Events and workshops',
|
|
711
|
+
hasPages: true,
|
|
712
|
+
routePattern: '/events/{slug}',
|
|
713
|
+
fields: [
|
|
714
|
+
{ id: 'excerpt', type: 'text', label: 'Summary', required: false, multiline: true },
|
|
715
|
+
{ id: 'startsAt', type: 'datetime', label: 'Starts At', required: false },
|
|
716
|
+
{ id: 'endsAt', type: 'datetime', label: 'Ends At', required: false },
|
|
717
|
+
{ id: 'location', type: 'text', label: 'Location', required: false, multiline: false },
|
|
718
|
+
{ id: 'content', type: 'richText', label: 'Details', required: true },
|
|
719
|
+
],
|
|
720
|
+
}`;
|
|
721
|
+
const PORTFOLIO_CONTENT_TYPE = ` // Portfolio items
|
|
722
|
+
{
|
|
723
|
+
key: 'portfolio-item',
|
|
724
|
+
name: 'Portfolio Item',
|
|
725
|
+
description: 'Work portfolio entries with case study pages',
|
|
726
|
+
hasPages: true,
|
|
727
|
+
routePattern: '/work/{slug}',
|
|
728
|
+
fields: [
|
|
729
|
+
{ id: 'short-description', type: 'text', label: 'Short Description', required: false, multiline: true, description: 'Brief summary shown in carousel/list views' },
|
|
730
|
+
{ id: 'full-description', type: 'richText', label: 'Full Description', required: false, description: 'Detailed description for the case study page' },
|
|
731
|
+
{ id: 'image', type: 'media', label: 'Featured Image', required: false, mediaKinds: ['image'], description: 'Screenshot or hero image' },
|
|
732
|
+
{ id: 'content', type: 'richText', label: 'Case Study Content', required: false, description: 'Full case study content' },
|
|
733
|
+
{ id: 'link', type: 'link', label: 'External Link', required: false, description: 'Link to the live project' },
|
|
734
|
+
],
|
|
735
|
+
}`;
|
|
736
|
+
function contentTypesTemplate(options = {}) {
|
|
737
|
+
const contentTypes = [];
|
|
738
|
+
if (options.withBlog) {
|
|
739
|
+
contentTypes.push(BLOG_CONTENT_TYPE);
|
|
740
|
+
}
|
|
741
|
+
if (options.withTestimonials) {
|
|
742
|
+
contentTypes.push(TESTIMONIALS_CONTENT_TYPE);
|
|
743
|
+
}
|
|
744
|
+
if (options.withEvents) {
|
|
745
|
+
contentTypes.push(EVENTS_CONTENT_TYPE);
|
|
746
|
+
}
|
|
747
|
+
if (options.withPortfolio) {
|
|
748
|
+
contentTypes.push(PORTFOLIO_CONTENT_TYPE);
|
|
749
|
+
}
|
|
750
|
+
const contentTypesArray = contentTypes.length > 0 ? `[\n${contentTypes.join(",\n")},\n]` : `[
|
|
751
|
+
// Add your content types here
|
|
752
|
+
]`;
|
|
753
|
+
return {
|
|
754
|
+
path: "src/config/content-types.ts",
|
|
755
|
+
content: `import type { ContentTypeConfig } from '@riverbankcms/sdk/config';
|
|
756
|
+
import { getContentEntryPrefixesFromContentTypes } from '@riverbankcms/sdk/routing';
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Content Type Definitions
|
|
760
|
+
*
|
|
761
|
+
* Defines the schema for each content type (testimonials, services, blog posts, etc.)
|
|
762
|
+
* These are synced to the CMS dashboard via \`pnpm exec riverbankcms push-config\`.
|
|
763
|
+
*
|
|
764
|
+
* Example:
|
|
765
|
+
* \`\`\`typescript
|
|
766
|
+
* {
|
|
767
|
+
* key: 'testimonial',
|
|
768
|
+
* name: 'Testimonial',
|
|
769
|
+
* description: 'Customer testimonials and reviews',
|
|
770
|
+
* hasPages: false,
|
|
771
|
+
* fields: [
|
|
772
|
+
* { id: 'name', type: 'text', label: 'Name', required: true, multiline: false },
|
|
773
|
+
* { id: 'body', type: 'richText', label: 'Quote', required: true },
|
|
774
|
+
* ],
|
|
775
|
+
* titleField: 'name',
|
|
776
|
+
* }
|
|
777
|
+
* \`\`\`
|
|
778
|
+
*/
|
|
779
|
+
export const contentTypes: ContentTypeConfig[] = ${contentTypesArray};
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Route prefixes for content types with pages.
|
|
783
|
+
* Used by the catch-all route to determine whether a path is a content entry.
|
|
784
|
+
*
|
|
785
|
+
* Derived from routePattern values (e.g., '/blog/{slug}' → 'blog').
|
|
786
|
+
*/
|
|
787
|
+
export const CONTENT_ENTRY_PREFIXES = getContentEntryPrefixesFromContentTypes(contentTypes);
|
|
788
|
+
`
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region src/scaffold/templates/custom-blocks.ts
|
|
794
|
+
function customBlocksTemplate() {
|
|
795
|
+
return {
|
|
796
|
+
path: "src/config/custom-blocks.ts",
|
|
797
|
+
content: `import type { SdkCustomBlock } from '@riverbankcms/sdk/config';
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Custom Block Definitions
|
|
801
|
+
*
|
|
802
|
+
* Site-specific blocks that extend the SDK's built-in block library.
|
|
803
|
+
* Each block needs a corresponding React component in src/components
|
|
804
|
+
* and an entry in blockOverrides.
|
|
805
|
+
*
|
|
806
|
+
* Example:
|
|
807
|
+
* \`\`\`typescript
|
|
808
|
+
* {
|
|
809
|
+
* id: 'custom.hero-banner',
|
|
810
|
+
* title: 'Hero Banner',
|
|
811
|
+
* description: 'Custom hero with animated background',
|
|
812
|
+
* category: 'hero',
|
|
813
|
+
* icon: 'Sparkles',
|
|
814
|
+
* tags: ['hero', 'banner', 'animated'],
|
|
815
|
+
* fields: [
|
|
816
|
+
* { id: 'title', type: 'text', label: 'Title', required: true, multiline: false },
|
|
817
|
+
* { id: 'subtitle', type: 'text', label: 'Subtitle', required: false, multiline: true },
|
|
818
|
+
* ],
|
|
819
|
+
* }
|
|
820
|
+
* \`\`\`
|
|
821
|
+
*/
|
|
822
|
+
export const customBlocks: SdkCustomBlock[] = [
|
|
823
|
+
// Add your custom blocks here
|
|
824
|
+
];
|
|
825
|
+
`
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region src/scaffold/templates/data-loader-overrides.ts
|
|
831
|
+
function dataLoaderOverridesTemplate() {
|
|
832
|
+
return {
|
|
833
|
+
path: "src/lib/data-loader-overrides.server.ts",
|
|
834
|
+
content: `import 'server-only';
|
|
835
|
+
|
|
836
|
+
import type { DataLoaderOverrides } from '@riverbankcms/sdk/next';
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Host data loaders run only while rendering Server Components. Keep database
|
|
840
|
+
* clients, private API credentials, and loader functions in this server module.
|
|
841
|
+
*/
|
|
842
|
+
export const dataLoaderOverrides = {
|
|
843
|
+
// If a loader reads searchParams, set searchParamsMode: 'dynamic' in the catch-all page.
|
|
844
|
+
// 'custom.product-grid': {
|
|
845
|
+
// products: async ({ content, searchParams, previewStage, renderTarget }) => {
|
|
846
|
+
// const category =
|
|
847
|
+
// typeof content.category === 'string' ? content.category : undefined;
|
|
848
|
+
// const response = await fetch(
|
|
849
|
+
// \`https://host-api.example/products?category=\${encodeURIComponent(category ?? '')}\`,
|
|
850
|
+
// {
|
|
851
|
+
// headers: {
|
|
852
|
+
// // Use credentials only for the server request. Never return them.
|
|
853
|
+
// Authorization: \`Bearer \${process.env.HOST_API_TOKEN ?? ''}\`,
|
|
854
|
+
// },
|
|
855
|
+
// },
|
|
856
|
+
// );
|
|
857
|
+
// const payload: unknown = await response.json();
|
|
858
|
+
// return {
|
|
859
|
+
// products: parseProductPayload(payload),
|
|
860
|
+
// query: searchParams.query ?? null,
|
|
861
|
+
// previewStage,
|
|
862
|
+
// owner: describeRenderTarget(renderTarget),
|
|
863
|
+
// };
|
|
864
|
+
// },
|
|
865
|
+
// },
|
|
866
|
+
} satisfies DataLoaderOverrides;
|
|
867
|
+
|
|
868
|
+
// Import or implement an application parser before enabling the example.
|
|
869
|
+
// import type { DataLoaderRenderTarget } from '@riverbankcms/sdk/next';
|
|
870
|
+
// declare function parseProductPayload(value: unknown): readonly Product[];
|
|
871
|
+
//
|
|
872
|
+
// function describeRenderTarget(target: DataLoaderRenderTarget): string {
|
|
873
|
+
// switch (target.kind) {
|
|
874
|
+
// case 'page':
|
|
875
|
+
// return target.pageId;
|
|
876
|
+
// case 'entry-template':
|
|
877
|
+
// return target.entryId;
|
|
878
|
+
// case 'page-surface-template':
|
|
879
|
+
// return target.pageSurfaceId;
|
|
880
|
+
// default: {
|
|
881
|
+
// const exhaustive: never = target;
|
|
882
|
+
// return exhaustive;
|
|
883
|
+
// }
|
|
884
|
+
// }
|
|
885
|
+
// }
|
|
886
|
+
`
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/scaffold/templates/env-config.ts
|
|
892
|
+
function envConfigTemplate(config) {
|
|
893
|
+
const previewConfig = config.withPreviewMode ? ` /** Whether preview mode is enabled */
|
|
894
|
+
previewMode: boolean;
|
|
895
|
+
/** Preview API key for draft content */
|
|
896
|
+
previewApiKey: string | undefined;
|
|
897
|
+
` : "";
|
|
898
|
+
const previewReturn = config.withPreviewMode ? ` previewMode: process.env.RIVERBANK_PREVIEW_MODE === 'true',
|
|
899
|
+
previewApiKey: process.env[\`\${prefix}PREVIEW_API_KEY\`],
|
|
900
|
+
` : "";
|
|
901
|
+
return {
|
|
902
|
+
path: "src/lib/env.ts",
|
|
903
|
+
content: `/**
|
|
904
|
+
* Multi-environment CMS configuration helper.
|
|
905
|
+
*
|
|
906
|
+
* Reads CMS config based on RIVERBANK_ENV (local | remote).
|
|
907
|
+
* All environment-specific variables use the pattern: RIVERBANK_{ENV}_{KEY}
|
|
908
|
+
*/
|
|
909
|
+
|
|
910
|
+
type CmsEnv = 'local' | 'remote';
|
|
911
|
+
|
|
912
|
+
function getCmsEnv(): CmsEnv {
|
|
913
|
+
// Respect RIVERBANK_ENV if explicitly set (allows build with local CMS)
|
|
914
|
+
const envOverride = process.env.RIVERBANK_ENV;
|
|
915
|
+
if (envOverride === 'local' || envOverride === 'remote') {
|
|
916
|
+
return envOverride;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// In production runtime (not build), default to remote
|
|
920
|
+
// Deployed sites should use remote CMS unless explicitly overridden
|
|
921
|
+
if (process.env.NODE_ENV === 'production') {
|
|
922
|
+
return 'remote';
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// In development, default to 'local'
|
|
926
|
+
return 'local';
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export interface CmsConfig {
|
|
930
|
+
/** Current environment (local or remote) */
|
|
931
|
+
env: CmsEnv;
|
|
932
|
+
${previewConfig}
|
|
933
|
+
/** Site UUID for this environment */
|
|
934
|
+
siteId: string | undefined;
|
|
935
|
+
/** Dashboard URL used for CMS API requests */
|
|
936
|
+
dashboardUrl: string | undefined;
|
|
937
|
+
/** Live API key for published content */
|
|
938
|
+
apiKey: string | undefined;
|
|
939
|
+
/** Management API key for SDK write operations */
|
|
940
|
+
mgmtApiKey: string | undefined;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Get CMS configuration for the current environment.
|
|
945
|
+
*
|
|
946
|
+
* Reads from environment variables with pattern: RIVERBANK_{ENV}_{KEY}
|
|
947
|
+
* where ENV is determined by RIVERBANK_ENV (defaults to 'local').
|
|
948
|
+
*
|
|
949
|
+
* @example
|
|
950
|
+
* \`\`\`typescript
|
|
951
|
+
* const { siteId, dashboardUrl, apiKey } = getCmsConfig();
|
|
952
|
+
* \`\`\`
|
|
953
|
+
*/
|
|
954
|
+
export function getCmsConfig(): CmsConfig {
|
|
955
|
+
const env = getCmsEnv();
|
|
956
|
+
const prefix = \`RIVERBANK_\${env.toUpperCase()}_\`;
|
|
957
|
+
|
|
958
|
+
return {
|
|
959
|
+
env,
|
|
960
|
+
${previewReturn}
|
|
961
|
+
siteId: process.env[\`\${prefix}SITE_ID\`],
|
|
962
|
+
dashboardUrl: process.env[\`\${prefix}DASHBOARD_URL\`],
|
|
963
|
+
apiKey: process.env[\`\${prefix}API_KEY\`],
|
|
964
|
+
mgmtApiKey: process.env[\`\${prefix}MGMT_API_KEY\`],
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Get the current CMS environment name.
|
|
970
|
+
*/
|
|
971
|
+
export function getCurrentEnv(): CmsEnv {
|
|
972
|
+
return getCmsEnv();
|
|
973
|
+
}
|
|
974
|
+
`
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/scaffold/templates/env-example.ts
|
|
980
|
+
function envExampleTemplate(config) {
|
|
981
|
+
const previewBlock = config.withPreviewMode ? `# Preview mode: set to "true" to fetch draft content
|
|
982
|
+
RIVERBANK_PREVIEW_MODE=false
|
|
983
|
+
` : "";
|
|
984
|
+
const previewKeys = config.withPreviewMode ? `RIVERBANK_LOCAL_PREVIEW_API_KEY=""
|
|
985
|
+
` : "";
|
|
986
|
+
const remotePreviewKey = config.withPreviewMode ? `RIVERBANK_REMOTE_PREVIEW_API_KEY=""
|
|
987
|
+
` : "";
|
|
988
|
+
return {
|
|
989
|
+
path: ".env.example",
|
|
990
|
+
content: `# Riverbank CMS Configuration
|
|
991
|
+
# Copy this to .env.local and fill in your values
|
|
992
|
+
# Switch environments by changing RIVERBANK_ENV
|
|
993
|
+
|
|
994
|
+
# Environment selector: local | remote
|
|
995
|
+
RIVERBANK_ENV=local
|
|
996
|
+
|
|
997
|
+
${previewBlock}
|
|
998
|
+
|
|
999
|
+
# ============ LOCAL CMS ============
|
|
1000
|
+
# Your local development CMS instance
|
|
1001
|
+
RIVERBANK_LOCAL_SITE_ID=""
|
|
1002
|
+
RIVERBANK_LOCAL_DASHBOARD_URL=""
|
|
1003
|
+
RIVERBANK_LOCAL_API_KEY=""
|
|
1004
|
+
${previewKeys}RIVERBANK_LOCAL_MGMT_API_KEY=""
|
|
1005
|
+
|
|
1006
|
+
# ============ REMOTE CMS ============
|
|
1007
|
+
# Your production/staging CMS instance
|
|
1008
|
+
RIVERBANK_REMOTE_SITE_ID=""
|
|
1009
|
+
RIVERBANK_REMOTE_DASHBOARD_URL=""
|
|
1010
|
+
RIVERBANK_REMOTE_API_KEY=""
|
|
1011
|
+
${remotePreviewKey}RIVERBANK_REMOTE_MGMT_API_KEY=""
|
|
1012
|
+
|
|
1013
|
+
# ============ SHARED ============
|
|
1014
|
+
# Set after deployment with \`riverbankcms webhooks setup\`; init never registers a webhook.
|
|
1015
|
+
RIVERBANK_WEBHOOK_SECRET=""
|
|
1016
|
+
|
|
1017
|
+
`
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/scaffold/templates/env-staging-example.ts
|
|
1023
|
+
function envStagingExampleTemplate() {
|
|
1024
|
+
return {
|
|
1025
|
+
path: ".env.staging.example",
|
|
1026
|
+
content: `RIVERBANK_STAGING_SITE_ID=
|
|
1027
|
+
RIVERBANK_STAGING_DASHBOARD_URL=
|
|
1028
|
+
RIVERBANK_STAGING_MGMT_API_KEY=
|
|
1029
|
+
`
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/scaffold/templates/error.ts
|
|
1035
|
+
function errorTemplate() {
|
|
1036
|
+
return {
|
|
1037
|
+
path: "src/app/error.tsx",
|
|
1038
|
+
content: `'use client';
|
|
1039
|
+
|
|
1040
|
+
import { useEffect } from 'react';
|
|
1041
|
+
import Link from 'next/link';
|
|
1042
|
+
|
|
1043
|
+
export default function Error({
|
|
1044
|
+
error,
|
|
1045
|
+
reset,
|
|
1046
|
+
}: {
|
|
1047
|
+
error: Error & { digest?: string };
|
|
1048
|
+
reset: () => void;
|
|
1049
|
+
}) {
|
|
1050
|
+
useEffect(() => {
|
|
1051
|
+
// Log the error to an error reporting service
|
|
1052
|
+
console.error(error);
|
|
1053
|
+
}, [error]);
|
|
1054
|
+
|
|
1055
|
+
return (
|
|
1056
|
+
<div className="min-h-[60vh] flex items-center justify-center px-4">
|
|
1057
|
+
<div className="text-center max-w-md">
|
|
1058
|
+
{/* Error icon */}
|
|
1059
|
+
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-[var(--color-accent,#f59e0b)]/20 flex items-center justify-center">
|
|
1060
|
+
<svg
|
|
1061
|
+
className="w-10 h-10 text-[var(--color-accent,#f59e0b)]"
|
|
1062
|
+
fill="none"
|
|
1063
|
+
viewBox="0 0 24 24"
|
|
1064
|
+
stroke="currentColor"
|
|
1065
|
+
strokeWidth={1.5}
|
|
1066
|
+
>
|
|
1067
|
+
<path
|
|
1068
|
+
strokeLinecap="round"
|
|
1069
|
+
strokeLinejoin="round"
|
|
1070
|
+
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
|
|
1071
|
+
/>
|
|
1072
|
+
</svg>
|
|
1073
|
+
</div>
|
|
1074
|
+
|
|
1075
|
+
{/* Message */}
|
|
1076
|
+
<h1 className="text-2xl sm:text-3xl text-[var(--color-text,#1f2937)] font-bold">
|
|
1077
|
+
Something went wrong
|
|
1078
|
+
</h1>
|
|
1079
|
+
<p className="mt-4 text-lg text-[var(--color-text,#1f2937)]/70">
|
|
1080
|
+
We encountered an unexpected error. Please try again, or contact us if
|
|
1081
|
+
the problem persists.
|
|
1082
|
+
</p>
|
|
1083
|
+
|
|
1084
|
+
{/* Action buttons */}
|
|
1085
|
+
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
|
1086
|
+
<button
|
|
1087
|
+
onClick={() => reset()}
|
|
1088
|
+
className="px-8 py-3 bg-[var(--color-primary,#6d28d9)] text-white rounded-full transition-colors hover:opacity-90 font-medium"
|
|
1089
|
+
>
|
|
1090
|
+
Try Again
|
|
1091
|
+
</button>
|
|
1092
|
+
<Link
|
|
1093
|
+
href="/"
|
|
1094
|
+
className="px-8 py-3 border border-[var(--color-primary,#6d28d9)] text-[var(--color-primary,#6d28d9)] rounded-full transition-colors hover:bg-[var(--color-primary,#6d28d9)]/10 font-medium"
|
|
1095
|
+
>
|
|
1096
|
+
Back to Home
|
|
1097
|
+
</Link>
|
|
1098
|
+
</div>
|
|
1099
|
+
</div>
|
|
1100
|
+
</div>
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
`
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
//#endregion
|
|
1108
|
+
//#region src/scaffold/templates/example-test.ts
|
|
1109
|
+
function exampleTestTemplate() {
|
|
1110
|
+
return {
|
|
1111
|
+
path: "src/__tests__/example.test.mjs",
|
|
1112
|
+
content: `import assert from 'node:assert/strict';
|
|
1113
|
+
import { describe, it } from 'node:test';
|
|
1114
|
+
|
|
1115
|
+
describe('Example test suite', () => {
|
|
1116
|
+
it('should pass a basic test', () => {
|
|
1117
|
+
assert.equal(1 + 1, 2);
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
it('should handle string operations', () => {
|
|
1121
|
+
const greeting = 'Hello, World!';
|
|
1122
|
+
assert.ok(greeting.includes('Hello'));
|
|
1123
|
+
assert.ok(greeting.length > 0);
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
`
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
//#endregion
|
|
1131
|
+
//#region src/scaffold/templates/gitignore.ts
|
|
1132
|
+
const LOCAL_MEDIA_CACHE_GITIGNORE_SECTION = `# Riverbank local media cache and migration working files.
|
|
1133
|
+
# See .riverbank/docs/content-management.md for when to intentionally track fixtures.
|
|
1134
|
+
content/media/*
|
|
1135
|
+
!content/media/fixtures/
|
|
1136
|
+
!content/media/fixtures/**
|
|
1137
|
+
`;
|
|
1138
|
+
function gitignoreTemplate() {
|
|
1139
|
+
return {
|
|
1140
|
+
path: ".gitignore",
|
|
1141
|
+
content: `# Dependencies
|
|
1142
|
+
node_modules/
|
|
1143
|
+
.pnp
|
|
1144
|
+
.pnp.*
|
|
1145
|
+
|
|
1146
|
+
# Next.js
|
|
1147
|
+
.next/
|
|
1148
|
+
out/
|
|
1149
|
+
|
|
1150
|
+
# Production
|
|
1151
|
+
build/
|
|
1152
|
+
dist/
|
|
1153
|
+
|
|
1154
|
+
# Test output
|
|
1155
|
+
coverage/
|
|
1156
|
+
|
|
1157
|
+
# Environment files
|
|
1158
|
+
.env*
|
|
1159
|
+
!.env.example
|
|
1160
|
+
!.env.staging.example
|
|
1161
|
+
|
|
1162
|
+
${LOCAL_MEDIA_CACHE_GITIGNORE_SECTION}
|
|
1163
|
+
|
|
1164
|
+
# Deployment
|
|
1165
|
+
.vercel/
|
|
1166
|
+
|
|
1167
|
+
# TypeScript
|
|
1168
|
+
*.tsbuildinfo
|
|
1169
|
+
|
|
1170
|
+
# Logs
|
|
1171
|
+
*.log
|
|
1172
|
+
npm-debug.log*
|
|
1173
|
+
yarn-debug.log*
|
|
1174
|
+
yarn-error.log*
|
|
1175
|
+
pnpm-debug.log*
|
|
1176
|
+
|
|
1177
|
+
# OS/editor files
|
|
1178
|
+
.DS_Store
|
|
1179
|
+
*.swp
|
|
1180
|
+
`
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
//#endregion
|
|
1185
|
+
//#region src/scaffold/templates/globals-css.ts
|
|
1186
|
+
function globalsCssTemplate(config) {
|
|
1187
|
+
const previewBlock = config.withPreviewMode ? `/* Preview editor chrome (Tailwind-managed).
|
|
1188
|
+
*
|
|
1189
|
+
* The preview chrome uses Tailwind utilities, but importing Tailwind from the SDK can
|
|
1190
|
+
* collide with site styling (preflight, layer order, @theme directives).
|
|
1191
|
+
*
|
|
1192
|
+
* Recommended: keep Tailwind owned by the site, and add Riverbank package @source globs
|
|
1193
|
+
* alongside your own Tailwind entrypoint.
|
|
1194
|
+
*
|
|
1195
|
+
* If your site does NOT use Tailwind, use the legacy single import instead:
|
|
1196
|
+
* @import "@riverbankcms/sdk/preview-next/styles.css";
|
|
1197
|
+
*/
|
|
1198
|
+
@import "tailwindcss";
|
|
1199
|
+
@import "@riverbankcms/sdk/preview-next/tailwind-sources.css";
|
|
1200
|
+
@import "@riverbankcms/sdk/preview-next/chrome.css";
|
|
1201
|
+
|
|
1202
|
+
` : "";
|
|
1203
|
+
return {
|
|
1204
|
+
path: "src/app/globals.css",
|
|
1205
|
+
content: `${previewBlock}/* Add host-owned styles below. */
|
|
1206
|
+
`
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
//#endregion
|
|
1211
|
+
//#region src/scaffold/templates/layout.ts
|
|
1212
|
+
function layoutTemplate(config) {
|
|
1213
|
+
const escapedSiteName = JSON.stringify(config.siteName);
|
|
1214
|
+
const chromeRoot = config.withPreviewMode ? `\n <div id="builder-editor-chrome-root" className="builder-editor-chrome builder-dashboard-chrome-theme" />` : "";
|
|
1215
|
+
return {
|
|
1216
|
+
path: "src/app/layout.tsx",
|
|
1217
|
+
content: `import type { Metadata } from 'next';
|
|
1218
|
+
import './globals.css';
|
|
1219
|
+
|
|
1220
|
+
export const metadata: Metadata = {
|
|
1221
|
+
title: ${escapedSiteName},
|
|
1222
|
+
description: 'Built with Riverbank CMS',
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
export default function RootLayout({
|
|
1226
|
+
children,
|
|
1227
|
+
}: {
|
|
1228
|
+
children: React.ReactNode;
|
|
1229
|
+
}) {
|
|
1230
|
+
return (
|
|
1231
|
+
<html lang="en">
|
|
1232
|
+
<head />
|
|
1233
|
+
<body>
|
|
1234
|
+
${chromeRoot}
|
|
1235
|
+
{children}
|
|
1236
|
+
</body>
|
|
1237
|
+
</html>
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
`
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
//#endregion
|
|
1245
|
+
//#region src/scaffold/templates/next-config.ts
|
|
1246
|
+
function nextConfigTemplate() {
|
|
1247
|
+
return {
|
|
1248
|
+
path: "next.config.ts",
|
|
1249
|
+
content: `import type { NextConfig } from "next";
|
|
1250
|
+
|
|
1251
|
+
const nextConfig: NextConfig = {
|
|
1252
|
+
// Transpile workspace packages (needed for linked packages / local development).
|
|
1253
|
+
transpilePackages: ${JSON.stringify(["@riverbankcms/sdk"])},
|
|
1254
|
+
|
|
1255
|
+
// Allow imports from outside the project directory
|
|
1256
|
+
experimental: {
|
|
1257
|
+
externalDir: true,
|
|
1258
|
+
},
|
|
1259
|
+
|
|
1260
|
+
// Add remote origins required by your deployment. The SDK preserves storage
|
|
1261
|
+
// metadata in content responses, so this scaffold does not assume a provider.
|
|
1262
|
+
images: {
|
|
1263
|
+
remotePatterns: [],
|
|
1264
|
+
dangerouslyAllowSVG: true,
|
|
1265
|
+
contentDispositionType: 'inline',
|
|
1266
|
+
},
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
export default nextConfig;
|
|
1270
|
+
`
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
//#endregion
|
|
1275
|
+
//#region src/scaffold/templates/not-found.ts
|
|
1276
|
+
function notFoundTemplate() {
|
|
1277
|
+
return {
|
|
1278
|
+
path: "src/app/not-found.tsx",
|
|
1279
|
+
content: `import Link from 'next/link';
|
|
1280
|
+
|
|
1281
|
+
export default function NotFound() {
|
|
1282
|
+
return (
|
|
1283
|
+
<div className="min-h-[60vh] flex items-center justify-center px-4">
|
|
1284
|
+
<div className="text-center max-w-md">
|
|
1285
|
+
{/* Large 404 with brand color */}
|
|
1286
|
+
<h1 className="text-[120px] sm:text-[160px] leading-none text-[var(--color-primary,#6d28d9)]/20 font-bold">
|
|
1287
|
+
404
|
|
1288
|
+
</h1>
|
|
1289
|
+
|
|
1290
|
+
{/* Message */}
|
|
1291
|
+
<h2 className="text-2xl sm:text-3xl text-[var(--color-text,#1f2937)] -mt-4 font-bold">
|
|
1292
|
+
Page not found
|
|
1293
|
+
</h2>
|
|
1294
|
+
<p className="mt-4 text-lg text-[var(--color-text,#1f2937)]/70">
|
|
1295
|
+
Sorry, we couldn't find the page you're looking for. It may have been
|
|
1296
|
+
moved or no longer exists.
|
|
1297
|
+
</p>
|
|
1298
|
+
|
|
1299
|
+
{/* Back to home button */}
|
|
1300
|
+
<Link
|
|
1301
|
+
href="/"
|
|
1302
|
+
className="inline-block mt-8 px-8 py-3 bg-[var(--color-primary,#6d28d9)] text-white rounded-full transition-colors hover:opacity-90 font-medium"
|
|
1303
|
+
>
|
|
1304
|
+
Back to Home
|
|
1305
|
+
</Link>
|
|
1306
|
+
</div>
|
|
1307
|
+
</div>
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
`
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
//#endregion
|
|
1315
|
+
//#region src/scaffold/templates/portal-callback-route.ts
|
|
1316
|
+
function portalCallbackRouteTemplate() {
|
|
1317
|
+
return {
|
|
1318
|
+
path: "src/app/api/portal/callback/route.ts",
|
|
1319
|
+
content: `/**
|
|
1320
|
+
* Portal callback route.
|
|
1321
|
+
*
|
|
1322
|
+
* Exchanges a short-lived portal code for a session token, stores the token in
|
|
1323
|
+
* an httpOnly cookie, and redirects back to the requested portal path.
|
|
1324
|
+
*/
|
|
1325
|
+
|
|
1326
|
+
import { handlePortalCallback } from '@riverbankcms/sdk/portal';
|
|
1327
|
+
|
|
1328
|
+
export const GET = handlePortalCallback();
|
|
1329
|
+
`
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
//#endregion
|
|
1334
|
+
//#region src/scaffold/templates/portal-logout-route.ts
|
|
1335
|
+
function portalLogoutRouteTemplate() {
|
|
1336
|
+
return {
|
|
1337
|
+
path: "src/app/api/portal/logout/route.ts",
|
|
1338
|
+
content: `/**
|
|
1339
|
+
* Portal logout route.
|
|
1340
|
+
*
|
|
1341
|
+
* Invalidates the portal session upstream when possible and always clears the
|
|
1342
|
+
* local portal cookie before redirecting.
|
|
1343
|
+
*/
|
|
1344
|
+
|
|
1345
|
+
import { handlePortalLogout } from '@riverbankcms/sdk/portal';
|
|
1346
|
+
|
|
1347
|
+
export const POST = handlePortalLogout();
|
|
1348
|
+
`
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
//#endregion
|
|
1353
|
+
//#region src/scaffold/templates/preview-keep-alive-route.ts
|
|
1354
|
+
function previewKeepAliveRouteTemplate() {
|
|
1355
|
+
return {
|
|
1356
|
+
path: "src/app/api/preview/keep-alive/route.ts",
|
|
1357
|
+
content: `import type { NextRequest } from 'next/server'
|
|
1358
|
+
|
|
1359
|
+
import { createPreviewKeepAliveRouteHandler } from '@riverbankcms/sdk/preview-next/keep-alive'
|
|
1360
|
+
import { getCmsConfig } from '@/lib/env'
|
|
1361
|
+
|
|
1362
|
+
const handler = createPreviewKeepAliveRouteHandler({
|
|
1363
|
+
dashboardUrl: getCmsConfig().dashboardUrl,
|
|
1364
|
+
})
|
|
1365
|
+
|
|
1366
|
+
export function GET(request: NextRequest) {
|
|
1367
|
+
return handler(request)
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
export function POST(request: NextRequest) {
|
|
1371
|
+
return handler(request)
|
|
1372
|
+
}
|
|
1373
|
+
`
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
//#endregion
|
|
1378
|
+
//#region src/scaffold/templates/preview-mode-client.ts
|
|
1379
|
+
function previewModeClientTemplate() {
|
|
1380
|
+
return {
|
|
1381
|
+
path: "src/components/preview/SdkPreviewModeClient.tsx",
|
|
1382
|
+
content: `'use client'
|
|
1383
|
+
|
|
1384
|
+
import { PreviewQueryProvider, SdkPreviewMode, type SdkPreviewModePropsWithoutBlockOverrides } from '@riverbankcms/sdk/preview-next/client'
|
|
1385
|
+
|
|
1386
|
+
import { blockOverrides } from '@/lib/block-overrides'
|
|
1387
|
+
import { SITE_RENDERING } from '@/lib/rendering'
|
|
1388
|
+
|
|
1389
|
+
export function SdkPreviewModeClient(props: SdkPreviewModePropsWithoutBlockOverrides) {
|
|
1390
|
+
const { themeRuntime: _themeRuntime, ...previewProps } = props
|
|
1391
|
+
|
|
1392
|
+
return (
|
|
1393
|
+
<PreviewQueryProvider>
|
|
1394
|
+
<SdkPreviewMode
|
|
1395
|
+
{...previewProps}
|
|
1396
|
+
rendering={SITE_RENDERING}
|
|
1397
|
+
blockOverrides={blockOverrides}
|
|
1398
|
+
/>
|
|
1399
|
+
</PreviewQueryProvider>
|
|
1400
|
+
)
|
|
1401
|
+
}
|
|
1402
|
+
`
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
//#endregion
|
|
1407
|
+
//#region src/scaffold/templates/proxy.ts
|
|
1408
|
+
function proxyTemplate(config) {
|
|
1409
|
+
const withPreviewMode = Boolean(config?.withPreviewMode);
|
|
1410
|
+
const previewImport = withPreviewMode ? `import { handlePreviewTokens } from '@riverbankcms/sdk/preview-next/middleware';
|
|
1411
|
+
import { handleRouteOutcomeRedirect } from '@riverbankcms/sdk/next/middleware';
|
|
1412
|
+
|
|
1413
|
+
import { getCmsConfig } from './lib/env';
|
|
1414
|
+
import { getRiverbankClient, getSiteId } from './lib/riverbank-client';
|
|
1415
|
+
` : `import { handleRouteOutcomeRedirect } from '@riverbankcms/sdk/next/middleware';
|
|
1416
|
+
|
|
1417
|
+
import { getRiverbankClient, getSiteId } from './lib/riverbank-client';
|
|
1418
|
+
`;
|
|
1419
|
+
const previewBlock = withPreviewMode ? `
|
|
1420
|
+
if (process.env.RIVERBANK_PREVIEW_MODE === 'true') {
|
|
1421
|
+
const { dashboardUrl } = getCmsConfig();
|
|
1422
|
+
const response = await handlePreviewTokens(request, {
|
|
1423
|
+
dashboardUrl: dashboardUrl ?? null,
|
|
1424
|
+
onExchangeFailure: (failure) => {
|
|
1425
|
+
console.warn('[riverbank] preview token exchange failed', failure);
|
|
1426
|
+
},
|
|
1427
|
+
});
|
|
1428
|
+
if (response) return response;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
` : "\n";
|
|
1432
|
+
return {
|
|
1433
|
+
path: "src/proxy.ts",
|
|
1434
|
+
content: `import { NextResponse } from 'next/server';
|
|
1435
|
+
import type { NextRequest } from 'next/server';
|
|
1436
|
+
${previewImport}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Proxy to handle system paths that shouldn't go through the CMS.
|
|
1440
|
+
*
|
|
1441
|
+
* Chrome DevTools and other tools request paths like:
|
|
1442
|
+
* - /.well-known/appspecific/com.chrome.devtools.json
|
|
1443
|
+
* - /apple-touch-icon-*.png
|
|
1444
|
+
*
|
|
1445
|
+
* Without this, these requests hit the catch-all route and cause
|
|
1446
|
+
* noisy error logs when the CMS can't find them.
|
|
1447
|
+
*/
|
|
1448
|
+
export async function proxy(request: NextRequest) {
|
|
1449
|
+
const { pathname } = request.nextUrl;
|
|
1450
|
+
|
|
1451
|
+
${previewBlock} // System paths that shouldn't go through the CMS
|
|
1452
|
+
const systemPaths = [
|
|
1453
|
+
/^\\/\\.well-known\\//,
|
|
1454
|
+
/^\\/apple-touch-icon/,
|
|
1455
|
+
/^\\/browserconfig\\.xml$/,
|
|
1456
|
+
];
|
|
1457
|
+
|
|
1458
|
+
if (systemPaths.some(pattern => pattern.test(pathname))) {
|
|
1459
|
+
return new Response(null, { status: 404 });
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const redirectResponse = await handleRouteOutcomeRedirect(request, {
|
|
1463
|
+
client: getRiverbankClient(),
|
|
1464
|
+
siteId: getSiteId(),
|
|
1465
|
+
onFailure: (error, context) => {
|
|
1466
|
+
console.warn('[riverbank] route outcome redirect check failed', {
|
|
1467
|
+
...context,
|
|
1468
|
+
error,
|
|
1469
|
+
});
|
|
1470
|
+
},
|
|
1471
|
+
});
|
|
1472
|
+
if (redirectResponse) return redirectResponse;
|
|
1473
|
+
|
|
1474
|
+
return NextResponse.next();
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
export const config = {
|
|
1478
|
+
matcher: [
|
|
1479
|
+
// Only run on paths that might be system paths
|
|
1480
|
+
// Exclude _next, api, and common static files
|
|
1481
|
+
'/((?!_next/static|_next/image|api|favicon.ico|robots.txt|sitemap.xml).*)',
|
|
1482
|
+
],
|
|
1483
|
+
};
|
|
1484
|
+
`
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
//#endregion
|
|
1489
|
+
//#region src/scaffold/templates/readme.ts
|
|
1490
|
+
function gettingStartedSection(config) {
|
|
1491
|
+
const { cli: cliRange } = config.packageRanges;
|
|
1492
|
+
const previewEnvironmentRows = config.withPreviewMode ? `| \`RIVERBANK_PREVIEW_MODE\` | Set to \`true\` to fetch draft content |
|
|
1493
|
+
| \`RIVERBANK_LOCAL_PREVIEW_API_KEY\` | Local preview key |
|
|
1494
|
+
` : "";
|
|
1495
|
+
const previewSection = config.withPreviewMode ? `## Preview Mode
|
|
1496
|
+
|
|
1497
|
+
To preview unpublished content:
|
|
1498
|
+
|
|
1499
|
+
1. Set \`RIVERBANK_PREVIEW_MODE=true\` in \`.env.local\`
|
|
1500
|
+
2. Ensure \`riverbank.config.ts\` has \`dashboard.previewEditingEnabled: true\` and push config so dashboard preview links include Riverbank preview sessions
|
|
1501
|
+
3. Restart the dev server (\`pnpm dev\`)
|
|
1502
|
+
|
|
1503
|
+
Preview mode:
|
|
1504
|
+
- Fetches draft content instead of published
|
|
1505
|
+
- Disables ISR caching for real-time updates
|
|
1506
|
+
- Uses the preview API key for authentication
|
|
1507
|
+
- Uses \`PreviewQueryProvider\` (React Query) for preview shell state
|
|
1508
|
+
|
|
1509
|
+
` : "";
|
|
1510
|
+
return `# ${config.siteName}
|
|
1511
|
+
|
|
1512
|
+
A Next.js site powered by Riverbank CMS SDK with host-owned rendering.
|
|
1513
|
+
|
|
1514
|
+
## Getting Started
|
|
1515
|
+
|
|
1516
|
+
\`\`\`bash
|
|
1517
|
+
# Install dependencies. The SDK and CLI are separate packages.
|
|
1518
|
+
pnpm install
|
|
1519
|
+
|
|
1520
|
+
# Start development server
|
|
1521
|
+
pnpm dev
|
|
1522
|
+
\`\`\`
|
|
1523
|
+
|
|
1524
|
+
Open [http://localhost:${config.devPort}](http://localhost:${config.devPort}) to view your site.
|
|
1525
|
+
|
|
1526
|
+
## Content Workflow
|
|
1527
|
+
|
|
1528
|
+
\`riverbank.config.ts\` is for schema and configuration: content types, custom blocks, block field options, and preview settings.
|
|
1529
|
+
|
|
1530
|
+
Document content lives in \`content/**\`:
|
|
1531
|
+
|
|
1532
|
+
\`\`\`
|
|
1533
|
+
content/
|
|
1534
|
+
pages/home.json # CMS pages
|
|
1535
|
+
entries/<type>/<entry>.json # Content entries
|
|
1536
|
+
navigation.json # Navigation menus
|
|
1537
|
+
settings.json # Site settings such as homepage/SEO defaults
|
|
1538
|
+
media/ # Local media cache / migration working files
|
|
1539
|
+
\`\`\`
|
|
1540
|
+
|
|
1541
|
+
\`content/media/*\` is gitignored by default because it can hold large downloaded
|
|
1542
|
+
or migration-working files. Content JSON should reference media by identifier.
|
|
1543
|
+
If you intentionally want to version a few small fixtures, put them under
|
|
1544
|
+
\`content/media/fixtures/\`.
|
|
1545
|
+
|
|
1546
|
+
Use this flow after editing schema or starter content:
|
|
1547
|
+
|
|
1548
|
+
\`\`\`bash
|
|
1549
|
+
pnpm exec riverbankcms push-config --env=local
|
|
1550
|
+
pnpm exec riverbankcms push --env=local --with-config
|
|
1551
|
+
\`\`\`
|
|
1552
|
+
|
|
1553
|
+
\`--with-config\` syncs schema/config alongside the file-based document push. It does not push document content from \`riverbank.config.ts\`.
|
|
1554
|
+
|
|
1555
|
+
The generated project declares both packages explicitly: \`@riverbankcms/sdk\`
|
|
1556
|
+
provides the runtime and \`riverbankcms\` provides the command-line tools.
|
|
1557
|
+
The CLI is released independently; the generated \`${cliRange}\` range is derived
|
|
1558
|
+
from the published package contract. Run commands through \`pnpm exec riverbankcms\`
|
|
1559
|
+
to use the project-local binary.
|
|
1560
|
+
|
|
1561
|
+
## Environment Variables
|
|
1562
|
+
|
|
1563
|
+
The \`.env.local\` file contains your CMS configuration:
|
|
1564
|
+
|
|
1565
|
+
| Variable | Description |
|
|
1566
|
+
|----------|-------------|
|
|
1567
|
+
| \`RIVERBANK_ENV\` | Environment selector (\`local\` or \`remote\`) |
|
|
1568
|
+
${previewEnvironmentRows}
|
|
1569
|
+
| \`RIVERBANK_LOCAL_SITE_ID\` | Site ID for local dashboard |
|
|
1570
|
+
| \`RIVERBANK_LOCAL_DASHBOARD_URL\` | Local dashboard URL |
|
|
1571
|
+
| \`RIVERBANK_LOCAL_API_KEY\` | Local published-content key |
|
|
1572
|
+
| \`RIVERBANK_LOCAL_MGMT_API_KEY\` | Local management key (for push) |
|
|
1573
|
+
| \`RIVERBANK_WEBHOOK_SECRET\` | Signing secret, added by post-deployment webhook setup |
|
|
1574
|
+
|
|
1575
|
+
${previewSection}
|
|
1576
|
+
`;
|
|
1577
|
+
}
|
|
1578
|
+
function renderingSection() {
|
|
1579
|
+
return `## Fully Headless Rendering
|
|
1580
|
+
|
|
1581
|
+
Generated sites use override-only rendering with no CMS theme or SDK runtime styling. The canonical choice lives in \`src/lib/rendering.ts\`; keep using \`SITE_RENDERING\` in any additional CMS route or preview entry point.
|
|
1582
|
+
|
|
1583
|
+
The starter homepage and override registry are intentionally empty. Add a typed registration in \`src/lib/block-overrides.tsx\` for every block kind that content uses. The SDK raises \`MissingBlockOverrideError\` for a missing registration instead of silently falling back to a built-in renderer.
|
|
1584
|
+
|
|
1585
|
+
Built-in semantic kinds are namespaced as \`block.*\` (for example \`block.hero\`, \`block.body-text\`, \`block.media-text\`, \`block.image-gallery\`, \`block.cta-full\`, and \`block.embed\`). The override registry accepts their short names too, such as \`hero\` or \`body-text\`, alongside your \`custom.*\` kinds.
|
|
1586
|
+
|
|
1587
|
+
Stateful kinds such as forms, appointment booking, event registration, and commerce need host-owned UI without reimplementing Riverbank-owned behavior. Where a public headless controller exists, compose it behind the server-safe override; the SDK currently exposes booking and event-registration controllers under \`@riverbankcms/sdk/bookings\`. Controller coverage varies by block, so check the public SDK surface rather than assuming every stateful kind is ready for a custom UI.
|
|
1588
|
+
|
|
1589
|
+
`;
|
|
1590
|
+
}
|
|
1591
|
+
function customizationSection() {
|
|
1592
|
+
return `## Customization
|
|
1593
|
+
|
|
1594
|
+
### Block Overrides
|
|
1595
|
+
|
|
1596
|
+
Register host-owned block rendering. Keep small server-safe overrides as direct components, and use \`defineLazyBlockOverride()\` for client-heavy or rarely used custom blocks so the catch-all route does not eagerly import every block implementation:
|
|
1597
|
+
|
|
1598
|
+
\`\`\`tsx
|
|
1599
|
+
// In src/lib/block-overrides.tsx
|
|
1600
|
+
import {
|
|
1601
|
+
defineBlockOverrideRegistrations,
|
|
1602
|
+
defineLazyBlockOverride,
|
|
1603
|
+
type BlockOverrideRegistrations,
|
|
1604
|
+
} from '@riverbankcms/sdk/rendering';
|
|
1605
|
+
import { CustomHero } from '@/components/CustomHero';
|
|
1606
|
+
|
|
1607
|
+
export const blockOverrides = defineBlockOverrideRegistrations({
|
|
1608
|
+
hero: CustomHero,
|
|
1609
|
+
'custom.quiz': defineLazyBlockOverride(async () =>
|
|
1610
|
+
import('@/components/blocks/QuizBlock').then((module) => module.QuizBlock),
|
|
1611
|
+
),
|
|
1612
|
+
}) satisfies BlockOverrideRegistrations;
|
|
1613
|
+
\`\`\`
|
|
1614
|
+
|
|
1615
|
+
Keep the \`import()\` inside the lazy factory. Importing a heavy client block at the top of the file and then wrapping it is still eager from the bundler's point of view.
|
|
1616
|
+
|
|
1617
|
+
### Server-only Block Data
|
|
1618
|
+
|
|
1619
|
+
Use \`src/lib/data-loader-overrides.server.ts\` when a custom block needs data from your own database or API. The generated catch-all already passes this registry to \`createCatchAllPage()\`, and the registry imports Next.js's \`server-only\` marker so accidental Client Component imports fail the build.
|
|
1620
|
+
|
|
1621
|
+
Loaders receive the resolved block content, URL search parameters, preview stage, and page/template identity. Return JSON-safe values only: strings, finite numbers, booleans, null, arrays, and plain objects. Functions, class instances, dates, symbols, \`undefined\`, and cyclic values are rejected at runtime while the page continues rendering.
|
|
1622
|
+
|
|
1623
|
+
\`generateMetadata()\` never runs these host loaders, including when you provide \`customMetadata\`. Keep secrets and database clients inside the marked registry.
|
|
1624
|
+
|
|
1625
|
+
### Public Performance Checklist
|
|
1626
|
+
|
|
1627
|
+
- Avoid broad client imports such as \`@riverbankcms/sdk/rendering/client\` or \`@riverbankcms/blocks/client\` in route-level client code; prefer server-safe SDK imports or narrow block subpaths.
|
|
1628
|
+
- Do not mark decorative images as \`priority\` in \`next/image\`. Use CSS backgrounds or plain decorative images with empty alt text for non-content flourishes so Lighthouse does not treat them as the LCP candidate.
|
|
1629
|
+
- After adding a client-heavy block, run \`pnpm build\` and inspect the production bundle. The SDK keeps built-in islands lazy, but your site code can still accidentally import custom widgets eagerly.
|
|
1630
|
+
|
|
1631
|
+
### Custom Pages
|
|
1632
|
+
|
|
1633
|
+
Add pages outside the CMS by creating files in \`src/app/\`:
|
|
1634
|
+
|
|
1635
|
+
\`\`\`
|
|
1636
|
+
src/app/
|
|
1637
|
+
[[...slug]]/page.tsx # CMS-managed pages (catch-all)
|
|
1638
|
+
custom/page.tsx # Custom page at /custom
|
|
1639
|
+
\`\`\`
|
|
1640
|
+
|
|
1641
|
+
## Scripts
|
|
1642
|
+
|
|
1643
|
+
| Script | Description |
|
|
1644
|
+
|--------|-------------|
|
|
1645
|
+
| \`pnpm dev\` | Start development server |
|
|
1646
|
+
| \`pnpm build\` | Build for production |
|
|
1647
|
+
| \`pnpm start\` | Start production server |
|
|
1648
|
+
| \`pnpm dev:local\` | Run against local CMS config |
|
|
1649
|
+
| \`pnpm dev:remote\` | Run against remote CMS config |
|
|
1650
|
+
| \`pnpm verify\` | Run lint, typecheck, and tests |
|
|
1651
|
+
| \`pnpm test\` | Run tests |
|
|
1652
|
+
| \`pnpm typecheck\` | Run TypeScript type checking |
|
|
1653
|
+
| \`pnpm cms:push:staging\` | Dry-run push to staging (uses \`.env.staging\`) |
|
|
1654
|
+
| \`pnpm cms:push:staging:apply\` | Apply push to staging |
|
|
1655
|
+
|
|
1656
|
+
## Staging Push
|
|
1657
|
+
|
|
1658
|
+
1. Copy \`.env.staging.example\` to \`.env.staging\`
|
|
1659
|
+
2. Fill in staging values
|
|
1660
|
+
3. Run \`pnpm cms:push:staging\` first
|
|
1661
|
+
4. Apply with \`pnpm cms:push:staging:apply\` once output looks correct
|
|
1662
|
+
|
|
1663
|
+
`;
|
|
1664
|
+
}
|
|
1665
|
+
function deploymentSection(config) {
|
|
1666
|
+
return `## Cache revalidation
|
|
1667
|
+
|
|
1668
|
+
The scaffold includes \`/api/revalidate\` but does not create a webhook or secret.
|
|
1669
|
+
After deploying to a public HTTPS origin, run:
|
|
1670
|
+
|
|
1671
|
+
\`\`\`bash
|
|
1672
|
+
riverbankcms webhooks setup --env=remote --url=https://your-site.com/api/revalidate
|
|
1673
|
+
\`\`\`
|
|
1674
|
+
|
|
1675
|
+
Until that explicit setup, the generated pages use the normal ISR interval.
|
|
1676
|
+
|
|
1677
|
+
## Deployment
|
|
1678
|
+
|
|
1679
|
+
This is a standard Next.js application. Deploy to any platform that supports Next.js:
|
|
1680
|
+
|
|
1681
|
+
- [Vercel](https://vercel.com)
|
|
1682
|
+
- [Netlify](https://netlify.com)
|
|
1683
|
+
- [AWS Amplify](https://aws.amazon.com/amplify/)
|
|
1684
|
+
|
|
1685
|
+
Remember to set your environment variables in the deployment platform.
|
|
1686
|
+
|
|
1687
|
+
## Learn More
|
|
1688
|
+
|
|
1689
|
+
- [Riverbank SDK Documentation](${config.dashboardUrl}/docs)
|
|
1690
|
+
- [Next.js Documentation](https://nextjs.org/docs)
|
|
1691
|
+
`;
|
|
1692
|
+
}
|
|
1693
|
+
function readmeTemplate(config) {
|
|
1694
|
+
return {
|
|
1695
|
+
path: "README.md",
|
|
1696
|
+
content: [
|
|
1697
|
+
gettingStartedSection(config),
|
|
1698
|
+
renderingSection(),
|
|
1699
|
+
customizationSection(),
|
|
1700
|
+
deploymentSection(config)
|
|
1701
|
+
].join("\n")
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
//#endregion
|
|
1706
|
+
//#region src/scaffold/templates/revalidate-route.ts
|
|
1707
|
+
function revalidateRouteTemplate() {
|
|
1708
|
+
return {
|
|
1709
|
+
path: "src/app/api/revalidate/route.ts",
|
|
1710
|
+
content: `/**
|
|
1711
|
+
* Webhook endpoint for CMS cache revalidation
|
|
1712
|
+
*
|
|
1713
|
+
* This route receives webhook notifications from Riverbank CMS
|
|
1714
|
+
* and invalidates the appropriate Next.js cache entries.
|
|
1715
|
+
*
|
|
1716
|
+
* After deploying, register this endpoint explicitly with:
|
|
1717
|
+
* riverbankcms webhooks setup --env=remote --url=https://your-site.com/api/revalidate
|
|
1718
|
+
* Init does not register a webhook or create a signing secret.
|
|
1719
|
+
*/
|
|
1720
|
+
|
|
1721
|
+
import { createRevalidateHandler } from '@riverbankcms/sdk/next/revalidate';
|
|
1722
|
+
import { getRiverbankClient } from '@/lib/riverbank-client';
|
|
1723
|
+
|
|
1724
|
+
/**
|
|
1725
|
+
* POST handler for webhook requests
|
|
1726
|
+
*
|
|
1727
|
+
* The handler:
|
|
1728
|
+
* 1. Verifies the webhook signature using HMAC-SHA256
|
|
1729
|
+
* 2. Calls revalidateTag() for each cache tag in the payload
|
|
1730
|
+
* 3. Calls revalidatePath() if a path is included
|
|
1731
|
+
* 4. Best-effort clears this process's SDK client response cache
|
|
1732
|
+
* 5. Returns a JSON response with revalidation results
|
|
1733
|
+
*/
|
|
1734
|
+
export const POST = createRevalidateHandler({
|
|
1735
|
+
getClient: getRiverbankClient,
|
|
1736
|
+
// Optional: Add logging or analytics
|
|
1737
|
+
onRevalidate: (payload, items) => {
|
|
1738
|
+
console.log(\`[Revalidate] \${payload.event}: \${items.join(', ')}\`);
|
|
1739
|
+
},
|
|
1740
|
+
});
|
|
1741
|
+
`
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
//#endregion
|
|
1746
|
+
//#region src/scaffold/templates/riverbank-client.ts
|
|
1747
|
+
function riverbankClientTemplate(config) {
|
|
1748
|
+
const previewConfig = config.withPreviewMode ? ` const { dashboardUrl, apiKey, previewApiKey, env, previewMode } = getCmsConfig();
|
|
1749
|
+
|
|
1750
|
+
// Use preview API key when in preview mode, otherwise use live key
|
|
1751
|
+
const key = previewMode ? previewApiKey : apiKey;
|
|
1752
|
+
|
|
1753
|
+
if (!key) {
|
|
1754
|
+
const keyName = previewMode ? 'PREVIEW_API_KEY' : 'API_KEY';
|
|
1755
|
+
throw new Error(
|
|
1756
|
+
\`RIVERBANK_\${env.toUpperCase()}_\${keyName} is not set. Check your .env.local file.\`,
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
` : ` const { dashboardUrl, apiKey, env } = getCmsConfig();
|
|
1760
|
+
const key = apiKey;
|
|
1761
|
+
|
|
1762
|
+
if (!key) {
|
|
1763
|
+
throw new Error(
|
|
1764
|
+
\`RIVERBANK_\${env.toUpperCase()}_API_KEY is not set. Check your .env.local file.\`,
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
`;
|
|
1768
|
+
const cacheConfig = config.withPreviewMode ? ` enabled: !previewMode, // Disable caching in preview mode
|
|
1769
|
+
` : ` enabled: true,
|
|
1770
|
+
`;
|
|
1771
|
+
const previewExport = config.withPreviewMode ? `
|
|
1772
|
+
/** Check if preview mode is enabled. */
|
|
1773
|
+
export function isPreviewMode(): boolean {
|
|
1774
|
+
return getCmsConfig().previewMode;
|
|
1775
|
+
}
|
|
1776
|
+
` : "";
|
|
1777
|
+
return {
|
|
1778
|
+
path: "src/lib/riverbank-client.ts",
|
|
1779
|
+
content: `import { createRiverbankClient, type RiverbankClient } from '@riverbankcms/sdk/server';
|
|
1780
|
+
import config from '../../riverbank.config';
|
|
1781
|
+
import { getCmsConfig } from './env';
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Singleton client instance.
|
|
1785
|
+
*
|
|
1786
|
+
* The client is created once per process and reused for all subsequent calls.
|
|
1787
|
+
* This provides shared caching across all SDK operations, improving performance
|
|
1788
|
+
* by avoiding redundant API calls within the cache TTL window.
|
|
1789
|
+
*
|
|
1790
|
+
* Note: In Next.js server components, each request typically runs in a fresh
|
|
1791
|
+
* context, so cache is effectively per-request unless using persistent processes.
|
|
1792
|
+
* The singleton pattern primarily benefits scenarios with multiple SDK calls
|
|
1793
|
+
* within a single request or when running in long-lived processes.
|
|
1794
|
+
*/
|
|
1795
|
+
let clientInstance: RiverbankClient | null = null;
|
|
1796
|
+
|
|
1797
|
+
/**
|
|
1798
|
+
* Get the configured Riverbank SDK client (singleton).
|
|
1799
|
+
*
|
|
1800
|
+
* Returns a cached client instance, creating one if it doesn't exist.
|
|
1801
|
+
* The client handles:
|
|
1802
|
+
* - API authentication via environment-specific API key
|
|
1803
|
+
* - Request caching (300s TTL, 100 entries max, shared across all calls)
|
|
1804
|
+
* - Type-safe API responses
|
|
1805
|
+
*
|
|
1806
|
+
* Configuration is read based on RIVERBANK_ENV (local | remote).
|
|
1807
|
+
*
|
|
1808
|
+
* @returns The singleton RiverbankClient instance
|
|
1809
|
+
* @throws Error if required environment variables are not set
|
|
1810
|
+
*/
|
|
1811
|
+
export function getRiverbankClient(): RiverbankClient {
|
|
1812
|
+
if (clientInstance) return clientInstance;
|
|
1813
|
+
|
|
1814
|
+
${previewConfig}
|
|
1815
|
+
|
|
1816
|
+
if (!dashboardUrl) {
|
|
1817
|
+
throw new Error(
|
|
1818
|
+
\`RIVERBANK_\${env.toUpperCase()}_DASHBOARD_URL is not set. Check your .env.local file.\`
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
clientInstance = createRiverbankClient({
|
|
1823
|
+
apiKey: key,
|
|
1824
|
+
baseUrl: \`\${dashboardUrl}/api\`,
|
|
1825
|
+
cache: {
|
|
1826
|
+
${cacheConfig}
|
|
1827
|
+
ttl: 300,
|
|
1828
|
+
maxSize: 100,
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
return clientInstance;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Get the site ID from riverbank.config.ts
|
|
1837
|
+
*/
|
|
1838
|
+
export function getSiteId(): string {
|
|
1839
|
+
return config.siteId;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Check if preview mode is enabled
|
|
1844
|
+
*/
|
|
1845
|
+
${previewExport}
|
|
1846
|
+
`
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
//#endregion
|
|
1851
|
+
//#region src/scaffold/templates/riverbank-config.ts
|
|
1852
|
+
function riverbankConfigTemplate(config) {
|
|
1853
|
+
const previewDashboardConfig = config.withPreviewMode ? `
|
|
1854
|
+
// Dashboard preview links should pass short-lived auth tokens for editable preview mode.
|
|
1855
|
+
dashboard: {
|
|
1856
|
+
previewEditingEnabled: true,
|
|
1857
|
+
},
|
|
1858
|
+
` : "";
|
|
1859
|
+
const portalConfig = config.withPortalAuth ? `
|
|
1860
|
+
// Enable portal session token plumbing for customer portal blocks.
|
|
1861
|
+
enablePortal: true,
|
|
1862
|
+
` : "";
|
|
1863
|
+
return {
|
|
1864
|
+
path: "riverbank.config.ts",
|
|
1865
|
+
content: `import { defineConfig } from '@riverbankcms/sdk/config';
|
|
1866
|
+
import { getCmsConfig } from './src/lib/env';
|
|
1867
|
+
import { contentTypes } from './src/config/content-types';
|
|
1868
|
+
import { customBlocks } from './src/config/custom-blocks';
|
|
1869
|
+
import { blockFieldExtensions, blockFieldOptions } from './src/config/block-extensions';
|
|
1870
|
+
|
|
1871
|
+
const { siteId, env } = getCmsConfig();
|
|
1872
|
+
|
|
1873
|
+
if (!siteId) {
|
|
1874
|
+
throw new Error(
|
|
1875
|
+
\`RIVERBANK_\${env.toUpperCase()}_SITE_ID is not set. Check your .env.local file.\`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
export default defineConfig({
|
|
1880
|
+
siteId,
|
|
1881
|
+
${previewDashboardConfig}
|
|
1882
|
+
${portalConfig}
|
|
1883
|
+
|
|
1884
|
+
// Schema/config surfaces sync with \`riverbankcms push-config\`.
|
|
1885
|
+
// Document content lives in content/** and syncs with \`riverbankcms push\`.
|
|
1886
|
+
content: {
|
|
1887
|
+
contentTypes,
|
|
1888
|
+
},
|
|
1889
|
+
|
|
1890
|
+
// Block field options (customize existing field options)
|
|
1891
|
+
blockFieldOptions,
|
|
1892
|
+
|
|
1893
|
+
// Block field extensions (add custom fields to built-in blocks)
|
|
1894
|
+
blockFieldExtensions,
|
|
1895
|
+
|
|
1896
|
+
// Custom blocks
|
|
1897
|
+
customBlocks,
|
|
1898
|
+
|
|
1899
|
+
// Sync configuration - allow updating existing entries
|
|
1900
|
+
sync: {
|
|
1901
|
+
existingEntries: 'update',
|
|
1902
|
+
},
|
|
1903
|
+
|
|
1904
|
+
});
|
|
1905
|
+
`
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
//#endregion
|
|
1910
|
+
//#region src/scaffold/templates/rendering.ts
|
|
1911
|
+
function renderingTemplate() {
|
|
1912
|
+
return {
|
|
1913
|
+
path: "src/lib/rendering.ts",
|
|
1914
|
+
content: `import type { SdkOverrideOnlyRendering } from '@riverbankcms/sdk/rendering';
|
|
1915
|
+
|
|
1916
|
+
/** The host owns all block rendering and the runtime styling wrapper. */
|
|
1917
|
+
export const SITE_RENDERING = {
|
|
1918
|
+
kind: 'overrides-only',
|
|
1919
|
+
runtime: { kind: 'none' },
|
|
1920
|
+
} satisfies SdkOverrideOnlyRendering;
|
|
1921
|
+
`
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
//#endregion
|
|
1926
|
+
//#region src/scaffold/templates/starter-content.ts
|
|
1927
|
+
function jsonTemplate(path$1, value) {
|
|
1928
|
+
return {
|
|
1929
|
+
path: path$1,
|
|
1930
|
+
content: `${JSON.stringify(value, null, 2)}\n`
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
function richTextDoc(text) {
|
|
1934
|
+
return {
|
|
1935
|
+
type: "doc",
|
|
1936
|
+
content: [{
|
|
1937
|
+
type: "paragraph",
|
|
1938
|
+
content: [{
|
|
1939
|
+
type: "text",
|
|
1940
|
+
text
|
|
1941
|
+
}]
|
|
1942
|
+
}]
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
function slugifySiteName(siteName) {
|
|
1946
|
+
const slug = siteName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1947
|
+
return slug || "starter-site";
|
|
1948
|
+
}
|
|
1949
|
+
function starterEntryTemplates(config) {
|
|
1950
|
+
const templates = [];
|
|
1951
|
+
const siteSlug = slugifySiteName(config.siteName);
|
|
1952
|
+
const contentTypes = config.contentTypes ?? {};
|
|
1953
|
+
if (contentTypes.withBlog) {
|
|
1954
|
+
templates.push(jsonTemplate("content/entries/post/welcome.json", {
|
|
1955
|
+
identifier: "welcome",
|
|
1956
|
+
slug: "welcome",
|
|
1957
|
+
status: "draft",
|
|
1958
|
+
data: {
|
|
1959
|
+
title: `Welcome to ${config.siteName}`,
|
|
1960
|
+
excerpt: "A short starter post you can edit or replace.",
|
|
1961
|
+
content: richTextDoc(`Use this first post to introduce ${config.siteName}.`),
|
|
1962
|
+
author: "Team"
|
|
1963
|
+
}
|
|
1964
|
+
}));
|
|
1965
|
+
}
|
|
1966
|
+
if (contentTypes.withTestimonials) {
|
|
1967
|
+
templates.push(jsonTemplate("content/entries/testimonial/example-testimonial.json", {
|
|
1968
|
+
identifier: "example-testimonial",
|
|
1969
|
+
status: "draft",
|
|
1970
|
+
data: {
|
|
1971
|
+
name: "Example Customer",
|
|
1972
|
+
jobTitle: "Founder",
|
|
1973
|
+
company: config.siteName,
|
|
1974
|
+
body: richTextDoc("Add a real testimonial here when you are ready.")
|
|
1975
|
+
}
|
|
1976
|
+
}));
|
|
1977
|
+
}
|
|
1978
|
+
if (contentTypes.withEvents) {
|
|
1979
|
+
templates.push(jsonTemplate("content/entries/event/starter-event.json", {
|
|
1980
|
+
identifier: "starter-event",
|
|
1981
|
+
slug: "starter-event",
|
|
1982
|
+
status: "draft",
|
|
1983
|
+
data: {
|
|
1984
|
+
title: "Starter event",
|
|
1985
|
+
excerpt: "A starter event entry you can edit or replace.",
|
|
1986
|
+
startsAt: "2026-01-01T10:00:00.000Z",
|
|
1987
|
+
endsAt: "2026-01-01T11:00:00.000Z",
|
|
1988
|
+
location: "Online",
|
|
1989
|
+
content: richTextDoc("Add the event details here.")
|
|
1990
|
+
}
|
|
1991
|
+
}));
|
|
1992
|
+
}
|
|
1993
|
+
if (contentTypes.withPortfolio) {
|
|
1994
|
+
templates.push(jsonTemplate("content/entries/portfolio-item/starter-project.json", {
|
|
1995
|
+
identifier: "starter-project",
|
|
1996
|
+
slug: "starter-project",
|
|
1997
|
+
status: "draft",
|
|
1998
|
+
data: {
|
|
1999
|
+
title: `${config.siteName} project`,
|
|
2000
|
+
"short-description": "A starter portfolio item you can edit or replace.",
|
|
2001
|
+
"full-description": richTextDoc(`Describe a ${siteSlug} project here.`),
|
|
2002
|
+
content: richTextDoc("Add the project story here.")
|
|
2003
|
+
}
|
|
2004
|
+
}));
|
|
2005
|
+
}
|
|
2006
|
+
return templates;
|
|
2007
|
+
}
|
|
2008
|
+
function starterContentTemplates(config) {
|
|
2009
|
+
return [
|
|
2010
|
+
jsonTemplate("content/pages/home.json", {
|
|
2011
|
+
identifier: "home",
|
|
2012
|
+
title: config.siteName,
|
|
2013
|
+
path: "/",
|
|
2014
|
+
status: "published",
|
|
2015
|
+
blocks: []
|
|
2016
|
+
}),
|
|
2017
|
+
jsonTemplate("content/navigation.json", { menus: [{
|
|
2018
|
+
identifier: "main",
|
|
2019
|
+
name: "Main",
|
|
2020
|
+
isPrimary: true,
|
|
2021
|
+
items: [{
|
|
2022
|
+
label: "Home",
|
|
2023
|
+
link: {
|
|
2024
|
+
kind: "page",
|
|
2025
|
+
identifier: "home"
|
|
2026
|
+
}
|
|
2027
|
+
}]
|
|
2028
|
+
}] }),
|
|
2029
|
+
jsonTemplate("content/settings.json", {
|
|
2030
|
+
homepageId: "home",
|
|
2031
|
+
seoDefaults: {
|
|
2032
|
+
title: config.siteName,
|
|
2033
|
+
description: `${config.siteName} website`,
|
|
2034
|
+
allowIndexingInProduction: true
|
|
2035
|
+
}
|
|
2036
|
+
}),
|
|
2037
|
+
...starterEntryTemplates(config)
|
|
2038
|
+
];
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
//#endregion
|
|
2042
|
+
//#region src/scaffold/templates/index.ts
|
|
2043
|
+
function generateAllTemplates(config) {
|
|
2044
|
+
const preview = config.withPreviewMode ? [previewKeepAliveRouteTemplate(), previewModeClientTemplate()] : [];
|
|
2045
|
+
const portal = config.withPortalAuth ? [portalCallbackRouteTemplate(), portalLogoutRouteTemplate()] : [];
|
|
2046
|
+
return [
|
|
2047
|
+
gitignoreTemplate(),
|
|
2048
|
+
envConfigTemplate(config),
|
|
2049
|
+
envExampleTemplate(config),
|
|
2050
|
+
envStagingExampleTemplate(),
|
|
2051
|
+
riverbankConfigTemplate(config),
|
|
2052
|
+
riverbankClientTemplate(config),
|
|
2053
|
+
renderingTemplate(),
|
|
2054
|
+
nextConfigTemplate(),
|
|
2055
|
+
globalsCssTemplate(config),
|
|
2056
|
+
layoutTemplate(config),
|
|
2057
|
+
catchAllPageTemplate(config),
|
|
2058
|
+
errorTemplate(),
|
|
2059
|
+
notFoundTemplate(),
|
|
2060
|
+
proxyTemplate({ withPreviewMode: config.withPreviewMode }),
|
|
2061
|
+
...preview,
|
|
2062
|
+
revalidateRouteTemplate(),
|
|
2063
|
+
...portal,
|
|
2064
|
+
cmsPushStagingTemplate(),
|
|
2065
|
+
contentTypesTemplate(config.contentTypes ?? {}),
|
|
2066
|
+
customBlocksTemplate(),
|
|
2067
|
+
blockExtensionsTemplate(),
|
|
2068
|
+
blockOverridesTemplate(),
|
|
2069
|
+
dataLoaderOverridesTemplate(),
|
|
2070
|
+
...starterContentTemplates(config),
|
|
2071
|
+
exampleTestTemplate(),
|
|
2072
|
+
readmeTemplate(config),
|
|
2073
|
+
agentsMdTemplate()
|
|
2074
|
+
];
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
//#endregion
|
|
2078
|
+
//#region src/scaffold/plan.ts
|
|
2079
|
+
const REVALIDATE_ROUTE = "src/app/api/revalidate/route.ts";
|
|
2080
|
+
const PREVIEW_PATHS = new Set(["src/app/api/preview/keep-alive/route.ts", "src/components/preview/SdkPreviewModeClient.tsx"]);
|
|
2081
|
+
function safeScaffoldTargetPath(path$1) {
|
|
2082
|
+
if (!path$1 || path$1.includes("\\") || path$1.includes("\0") || path$1.startsWith("/")) return false;
|
|
2083
|
+
const normalized = path$1.split("/").join(sep);
|
|
2084
|
+
const fromRoot = relative("", normalized);
|
|
2085
|
+
return fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !normalized.split(sep).includes("");
|
|
2086
|
+
}
|
|
2087
|
+
function parseText(value, maxLength) {
|
|
2088
|
+
if (typeof value !== "string") return undefined;
|
|
2089
|
+
const normalized = value.trim();
|
|
2090
|
+
return normalized.length > 0 && normalized.length <= maxLength ? normalized : undefined;
|
|
2091
|
+
}
|
|
2092
|
+
function parseSlug(value) {
|
|
2093
|
+
const slug = parseText(value, 120);
|
|
2094
|
+
return slug && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug) ? slug : undefined;
|
|
2095
|
+
}
|
|
2096
|
+
function parseDashboardUrl(value) {
|
|
2097
|
+
const text = parseText(value, 2048);
|
|
2098
|
+
if (!text) return undefined;
|
|
2099
|
+
try {
|
|
2100
|
+
const url = new URL(text);
|
|
2101
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return undefined;
|
|
2102
|
+
if (url.username || url.password || url.search || url.hash) return undefined;
|
|
2103
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
2104
|
+
} catch {
|
|
2105
|
+
return undefined;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
function validPackageSource(input) {
|
|
2109
|
+
return input.tag === "npm" && input.ranges.sdk.trim().length > 0 && input.ranges.cli.trim().length > 0;
|
|
2110
|
+
}
|
|
2111
|
+
function validateManifest(files, features) {
|
|
2112
|
+
const seen = new Set();
|
|
2113
|
+
for (const file of files) {
|
|
2114
|
+
if (!safeScaffoldTargetPath(file.path)) return {
|
|
2115
|
+
tag: "unsafe_template_path",
|
|
2116
|
+
path: file.path
|
|
2117
|
+
};
|
|
2118
|
+
if (seen.has(file.path)) return {
|
|
2119
|
+
tag: "duplicate_template_path",
|
|
2120
|
+
path: file.path
|
|
2121
|
+
};
|
|
2122
|
+
seen.add(file.path);
|
|
2123
|
+
}
|
|
2124
|
+
if (!seen.has(REVALIDATE_ROUTE)) return { tag: "missing_revalidate_route" };
|
|
2125
|
+
const hasPreviewPaths = [...seen].some((path$1) => PREVIEW_PATHS.has(path$1));
|
|
2126
|
+
const hasPreviewImports = files.filter((file) => PREVIEW_PATHS.has(file.path)).some((file) => file.content.includes("@riverbankcms/sdk/preview-next"));
|
|
2127
|
+
if (features.preview === "enabled" && (!hasPreviewPaths || !hasPreviewImports)) return { tag: "preview_manifest_mismatch" };
|
|
2128
|
+
if (features.preview === "disabled" && (hasPreviewPaths || hasPreviewImports)) return { tag: "preview_manifest_mismatch" };
|
|
2129
|
+
return undefined;
|
|
2130
|
+
}
|
|
2131
|
+
function deriveScaffoldPlan(input) {
|
|
2132
|
+
const localManifest = deriveScaffoldLocalManifest({
|
|
2133
|
+
projectPath: input.projectPath,
|
|
2134
|
+
features: input.features
|
|
2135
|
+
});
|
|
2136
|
+
if (!localManifest.ok) return err(localManifest.error);
|
|
2137
|
+
const { projectPath, features } = localManifest.value;
|
|
2138
|
+
const parsedSiteId = parseSiteId(input.siteId);
|
|
2139
|
+
if (!parsedSiteId.ok) return err({
|
|
2140
|
+
tag: "invalid_site_id",
|
|
2141
|
+
value: input.siteId
|
|
2142
|
+
});
|
|
2143
|
+
const siteSlug = parseSlug(input.siteSlug);
|
|
2144
|
+
if (!siteSlug) return err({
|
|
2145
|
+
tag: "invalid_site_slug",
|
|
2146
|
+
value: input.siteSlug
|
|
2147
|
+
});
|
|
2148
|
+
const siteName = parseText(input.siteName, 160);
|
|
2149
|
+
if (!siteName) return err({
|
|
2150
|
+
tag: "invalid_site_name",
|
|
2151
|
+
value: input.siteName
|
|
2152
|
+
});
|
|
2153
|
+
const dashboardUrl = parseDashboardUrl(input.dashboardUrl);
|
|
2154
|
+
if (!dashboardUrl) return err({
|
|
2155
|
+
tag: "invalid_dashboard_url",
|
|
2156
|
+
value: input.dashboardUrl
|
|
2157
|
+
});
|
|
2158
|
+
if (typeof input.devPort !== "number" || !Number.isInteger(input.devPort) || input.devPort < 1 || input.devPort > 65535) {
|
|
2159
|
+
return err({
|
|
2160
|
+
tag: "invalid_dev_port",
|
|
2161
|
+
value: input.devPort
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
if (!validPackageSource(input.packageSource)) {
|
|
2165
|
+
return err({
|
|
2166
|
+
tag: "invalid_package_source",
|
|
2167
|
+
field: input.packageSource.ranges.sdk.trim().length > 0 ? "cli" : "sdk"
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
const templateConfig = {
|
|
2171
|
+
siteId: parsedSiteId.value,
|
|
2172
|
+
siteSlug,
|
|
2173
|
+
dashboardUrl,
|
|
2174
|
+
siteName,
|
|
2175
|
+
devPort: input.devPort,
|
|
2176
|
+
packageRanges: input.packageSource.ranges,
|
|
2177
|
+
withPreviewMode: features.preview === "enabled",
|
|
2178
|
+
withPortalAuth: features.portal === "enabled",
|
|
2179
|
+
contentTypes: features.contentTypes
|
|
2180
|
+
};
|
|
2181
|
+
const files = generateAllTemplates(templateConfig).map((file) => Object.freeze({ ...file }));
|
|
2182
|
+
const manifestError = validateManifest(files, features);
|
|
2183
|
+
if (manifestError) return err(manifestError);
|
|
2184
|
+
const credentialRequirements = features.preview === "enabled" ? {
|
|
2185
|
+
tag: "with_preview",
|
|
2186
|
+
live: true,
|
|
2187
|
+
management: true,
|
|
2188
|
+
preview: true
|
|
2189
|
+
} : {
|
|
2190
|
+
tag: "standard",
|
|
2191
|
+
live: true,
|
|
2192
|
+
management: true,
|
|
2193
|
+
preview: false
|
|
2194
|
+
};
|
|
2195
|
+
return ok(Object.freeze({
|
|
2196
|
+
projectPath,
|
|
2197
|
+
siteId: parsedSiteId.value,
|
|
2198
|
+
siteSlug,
|
|
2199
|
+
siteName,
|
|
2200
|
+
dashboardUrl,
|
|
2201
|
+
devPort: input.devPort,
|
|
2202
|
+
features,
|
|
2203
|
+
packageSource: Object.freeze({
|
|
2204
|
+
tag: "npm",
|
|
2205
|
+
ranges: Object.freeze({ ...input.packageSource.ranges })
|
|
2206
|
+
}),
|
|
2207
|
+
credentialRequirements,
|
|
2208
|
+
files: Object.freeze(files)
|
|
2209
|
+
}));
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
//#endregion
|
|
2213
|
+
export { prepareScaffoldExecution as a, LOCAL_PACKAGE_VENDOR_DIR as c, loadGeneratedPackageRanges as d, preflightScaffoldTargets as i, deriveGeneratedPackageRanges as l, createScaffoldCompletion as n, writeScaffoldFiles as o, preflightScaffoldPlan as r, deriveScaffoldLocalManifest as s, deriveScaffoldPlan as t, findPackageManifestPath as u };
|