create-avocado-site 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +65 -0
  3. package/assets/generated-images/gdrive_1ql_bdk2KM_27u01rkMd8KoiCRJ_R7UOy.webp +0 -0
  4. package/assets/generated-images/gen_1774991902955_0p1i2o.webp +0 -0
  5. package/assets/generated-images/gen_1776458907692_zlrbkg.webp +0 -0
  6. package/assets/generated-images/gen_1788955681679_454dc316.webp +0 -0
  7. package/assets/generated-images/var_1773434968055_8g99nk.webp +0 -0
  8. package/assets/generated-images/var_1773435042084_wvet78.webp +0 -0
  9. package/assets/generated-images/var_1773435061562_6hf8s6.webp +0 -0
  10. package/assets/generated-images/var_1773435080764_6ocahu.webp +0 -0
  11. package/assets/generated-images/var_1773435101045_1y544z.webp +0 -0
  12. package/assets/generated-images/var_1773872370443_poycxy.webp +0 -0
  13. package/assets/generated-images/var_1776858715876_iuu72w.webp +0 -0
  14. package/assets/logos/avocado-hub.svg +7 -0
  15. package/dist/cms-config.d.ts +34 -0
  16. package/dist/cms-config.js +103 -0
  17. package/dist/generator.d.ts +22 -0
  18. package/dist/generator.js +92 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +109 -0
  21. package/dist/instructions.d.ts +19 -0
  22. package/dist/instructions.js +82 -0
  23. package/dist/next-version.d.ts +15 -0
  24. package/dist/next-version.js +43 -0
  25. package/dist/ports.d.ts +11 -0
  26. package/dist/ports.js +61 -0
  27. package/dist/prompts.d.ts +13 -0
  28. package/dist/prompts.js +119 -0
  29. package/dist/templates/assets.d.ts +2 -0
  30. package/dist/templates/assets.js +41 -0
  31. package/dist/templates/common.d.ts +45 -0
  32. package/dist/templates/common.js +174 -0
  33. package/dist/templates/contentful.d.ts +2 -0
  34. package/dist/templates/contentful.js +379 -0
  35. package/dist/templates/demo-app.d.ts +3 -0
  36. package/dist/templates/demo-app.js +465 -0
  37. package/dist/templates/demo-content.d.ts +2 -0
  38. package/dist/templates/demo-content.js +7 -0
  39. package/dist/templates/sanity.d.ts +2 -0
  40. package/dist/templates/sanity.js +289 -0
  41. package/dist/templates/static.d.ts +2 -0
  42. package/dist/templates/static.js +81 -0
  43. package/dist/templates/strapi.d.ts +2 -0
  44. package/dist/templates/strapi.js +235 -0
  45. package/dist/types.d.ts +48 -0
  46. package/dist/types.js +1 -0
  47. package/dist/versions.d.ts +32 -0
  48. package/dist/versions.js +32 -0
  49. package/package.json +51 -0
package/dist/index.js ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import * as p from "@clack/prompts";
3
+ import { spawn } from "node:child_process";
4
+ import { mkdir, readdir } from "node:fs/promises";
5
+ import { existsSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import { runPrompts, demoConfig } from "./prompts.js";
8
+ import { collectFiles, collectDemoFiles, generateFiles } from "./generator.js";
9
+ import { printInstructions } from "./instructions.js";
10
+ async function main() {
11
+ const cwd = process.cwd();
12
+ /*
13
+ * `npm create avocado-site my-demo` — a directory argument means the demo,
14
+ * with no questions. Somebody who has typed a name has already answered the
15
+ * only one that mattered, and the fastest path to seeing the product should
16
+ * not be gated behind three selects.
17
+ */
18
+ const target = process.argv.slice(2).find((arg) => !arg.startsWith("-"));
19
+ const config = target ? await demoConfig(target) : await runPrompts(cwd);
20
+ if (!config)
21
+ return;
22
+ if (config.mode === "demo") {
23
+ await bootstrapDemo(cwd, target ?? config.siteId, config);
24
+ return;
25
+ }
26
+ await scaffoldInto(cwd, collectFiles(config), config);
27
+ }
28
+ /** The integrate path: write wiring into an app that already exists, then say what is left. */
29
+ async function scaffoldInto(dir, files, config) {
30
+ const fileList = files.map((f) => ` ${f.path}`).join("\n");
31
+ const confirm = await p.confirm({ message: `Generate ${files.length} files?\n${fileList}` });
32
+ if (p.isCancel(confirm) || !confirm) {
33
+ p.cancel("Cancelled.");
34
+ return;
35
+ }
36
+ const { written, skipped } = await generateFiles(dir, files);
37
+ if (skipped.length > 0) {
38
+ p.log.warn(`Skipped (already exist):\n${skipped.map((f) => ` ${f}`).join("\n")}`);
39
+ }
40
+ p.log.success(`Generated ${written.length} files`);
41
+ p.log.message(printInstructions(config));
42
+ p.outro("Done!");
43
+ }
44
+ /**
45
+ * The demo path: a directory, a project in it, and its dependencies installed.
46
+ *
47
+ * Installing is the part that makes this a bootstrap rather than a scaffold.
48
+ * Printing `npm install` as step three of five is how the previous version
49
+ * ended, and every printed step is a place for somebody evaluating the product
50
+ * to put it down.
51
+ */
52
+ async function bootstrapDemo(cwd, dirName, config) {
53
+ const dir = resolve(cwd, dirName);
54
+ if (existsSync(dir) && (await readdir(dir)).length > 0) {
55
+ p.log.error(`${dirName} already exists and is not empty.`);
56
+ p.outro("Pick a name that is not taken.");
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+ await mkdir(dir, { recursive: true });
61
+ const files = collectDemoFiles(config);
62
+ await generateFiles(dir, files);
63
+ p.log.success(`Created ${dirName} — ${files.length} files`);
64
+ const spinner = p.spinner();
65
+ spinner.start("Installing dependencies");
66
+ const installed = await run("npm", ["install", "--no-audit", "--no-fund"], dir);
67
+ if (installed) {
68
+ spinner.stop("Installed dependencies");
69
+ }
70
+ else {
71
+ /*
72
+ * Not fatal, and worth being explicit about rather than exiting. A failed
73
+ * install is usually a registry or proxy problem on the user's side, and
74
+ * the project on disk is complete and correct — `npm install` in it will
75
+ * work once that is fixed. Deleting it would throw away the good half.
76
+ */
77
+ spinner.stop("Could not install dependencies");
78
+ p.log.warn(`Run \`npm install\` in ${dirName} yourself — everything else is in place.`);
79
+ }
80
+ p.log.message([
81
+ ` cd ${dirName}`,
82
+ ` npm run dev`,
83
+ ``,
84
+ ` Opens the editor at`,
85
+ ``,
86
+ ` http://localhost:${config.editorPort}/?siteId=${config.siteId}&session=dev`,
87
+ ``,
88
+ ` with the site on http://localhost:${config.sitePort}. The ?siteId= is`,
89
+ ` load-bearing: the editor bundle is prebuilt and falls back to its own`,
90
+ ` default site when the URL does not name one. \`npm run dev\` opens this`,
91
+ ` URL for you.`,
92
+ ``,
93
+ ` No API key needed to look around; add ANTHROPIC_API_KEY to .env.local`,
94
+ ` when you want to edit by chat.`,
95
+ ].join("\n"));
96
+ p.outro("Done.");
97
+ }
98
+ /** Resolves false rather than throwing — the caller decides what a failure means. */
99
+ function run(command, args, cwd) {
100
+ return new Promise((resolve) => {
101
+ const child = spawn(command, args, { cwd, stdio: "ignore", shell: process.platform === "win32" });
102
+ child.on("error", () => resolve(false));
103
+ child.on("exit", (code) => resolve(code === 0));
104
+ });
105
+ }
106
+ main().catch((err) => {
107
+ console.error(err);
108
+ process.exit(1);
109
+ });
@@ -0,0 +1,19 @@
1
+ import type { ScaffoldConfig } from "./types.js";
2
+ /**
3
+ * What is left to do after the integrate path writes its files.
4
+ *
5
+ * Shorter than it was, because most of it was wrong. Step 1 used to print a
6
+ * `transpilePackages` array and six `images.remotePatterns` entries to copy by
7
+ * hand; `withAvocado` has supplied both — plus `serverExternalPackages`, the
8
+ * matching server externals, and `skipTrailingSlashRedirect` — for several
9
+ * releases. Hand-copying four settings that a wrapper sets correctly is four
10
+ * chances to get one wrong, and the failures are all quiet: an unlisted image
11
+ * host is a render error, a missing server external is a native module that
12
+ * will not load, a missing `skipTrailingSlashRedirect` is a 308 on a CORS
13
+ * preflight that the browser declines to follow.
14
+ *
15
+ * The old step 5 read "Start the orchestrator and editor (see
16
+ * docs/integration/README.md)". That is the entire product, delegated to a
17
+ * path that does not exist.
18
+ */
19
+ export declare function printInstructions(config: ScaffoldConfig): string;
@@ -0,0 +1,82 @@
1
+ import { CMS_CONFIGS } from "./cms-config.js";
2
+ import { AVOCADO } from "./versions.js";
3
+ /**
4
+ * What is left to do after the integrate path writes its files.
5
+ *
6
+ * Shorter than it was, because most of it was wrong. Step 1 used to print a
7
+ * `transpilePackages` array and six `images.remotePatterns` entries to copy by
8
+ * hand; `withAvocado` has supplied both — plus `serverExternalPackages`, the
9
+ * matching server externals, and `skipTrailingSlashRedirect` — for several
10
+ * releases. Hand-copying four settings that a wrapper sets correctly is four
11
+ * chances to get one wrong, and the failures are all quiet: an unlisted image
12
+ * host is a render error, a missing server external is a native module that
13
+ * will not load, a missing `skipTrailingSlashRedirect` is a 308 on a CORS
14
+ * preflight that the browser declines to follow.
15
+ *
16
+ * The old step 5 read "Start the orchestrator and editor (see
17
+ * docs/integration/README.md)". That is the entire product, delegated to a
18
+ * path that does not exist.
19
+ */
20
+ export function printInstructions(config) {
21
+ const c = CMS_CONFIGS[config.cms];
22
+ const imageLine = c.imageHostname
23
+ ? `\n images: {\n remotePatterns: [{ protocol: "https", hostname: "${c.imageHostname}" }],\n },`
24
+ : "";
25
+ const compiler = c.compilerConfig ? `\n${c.compilerConfig.trimEnd()}` : "";
26
+ return `
27
+ Next steps:
28
+
29
+ 1. Install:
30
+
31
+ npm install @avocadostudio-ai/site-sdk@^${AVOCADO} @avocadostudio-ai/blocks@^${AVOCADO} \\
32
+ @avocadostudio-ai/shared@^${AVOCADO} @avocadostudio-ai/preview-adapter@^${AVOCADO} \\
33
+ @avocadostudio-ai/orchestrator-core@^${AVOCADO}${c.npmDeps}
34
+
35
+ 2. next.config.ts — wrap the exported config:
36
+
37
+ import { withAvocado } from "@avocadostudio-ai/site-sdk/next-config"
38
+
39
+ export default withAvocado({${compiler}${imageLine}
40
+ })
41
+
42
+ Do not hand-write transpilePackages or serverExternalPackages. withAvocado
43
+ sets those, the server externals and skipTrailingSlashRedirect together, and
44
+ setting one of them alone fails quietly on the native dependencies.
45
+
46
+ 3. app/globals.css — add as the first line:
47
+
48
+ @import "@avocadostudio-ai/blocks/styles.css";
49
+
50
+ Without it every block renders correct, complete, unstyled HTML.
51
+
52
+ 4. Mount the orchestrator inside this app, at app/api/avocado/[[...path]]/route.ts:
53
+
54
+ import { createOrchestrator } from "@avocadostudio-ai/site-sdk/server"
55
+
56
+ export const runtime = "nodejs"
57
+ export const dynamic = "force-dynamic"
58
+
59
+ const handler = createOrchestrator({ siteId: "${config.siteId}" })
60
+ export const GET = handler
61
+ export const POST = handler
62
+ export const OPTIONS = handler
63
+
64
+ createOrchestrator returns one callable — assign it three times rather than
65
+ destructuring, which yields undefined with no type error.
66
+
67
+ 5. Copy .env.local.example to .env.local. Set DRAFT_MODE_SECRET to any long
68
+ random string: without it draft mode silently does not work, and the editor
69
+ renders the published page while ignoring every edit.
70
+
71
+ 6. Add .data/ to .gitignore — the orchestrator writes SQLite state there on the
72
+ first request.
73
+
74
+ 7. Run your dev server, then the editor beside it:
75
+
76
+ npx @avocadostudio-ai/cli@^${AVOCADO} start \\
77
+ --orchestrator http://localhost:3000/api/avocado \\
78
+ --preview http://localhost:3000
79
+
80
+ Full reference: https://docs.avocadostudio.dev/integration/nextjs-integration
81
+ `;
82
+ }
@@ -0,0 +1,15 @@
1
+ import { createRequire } from "node:module";
2
+ /**
3
+ * Best-effort detection of the Next.js major version in the target project.
4
+ *
5
+ * Next.js 16 renamed the `middleware` file convention to `proxy`, so the
6
+ * scaffolder needs to know which one to write. Returns `undefined` when the
7
+ * version can't be determined — callers should fall back to the older
8
+ * convention, which still works (deprecated) on 16.
9
+ *
10
+ * `makeRequire` is a seam: module resolution walks up from `cwd` to the
11
+ * filesystem root, so under a test runner (or an `npx` cache) it can find a
12
+ * *different* project's Next and answer confidently with the wrong number.
13
+ * Tests pass a resolver that fails, to exercise the manifest path.
14
+ */
15
+ export declare function detectNextMajor(cwd: string, makeRequire?: typeof createRequire): number | undefined;
@@ -0,0 +1,43 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { join } from "node:path";
4
+ /**
5
+ * Best-effort detection of the Next.js major version in the target project.
6
+ *
7
+ * Next.js 16 renamed the `middleware` file convention to `proxy`, so the
8
+ * scaffolder needs to know which one to write. Returns `undefined` when the
9
+ * version can't be determined — callers should fall back to the older
10
+ * convention, which still works (deprecated) on 16.
11
+ *
12
+ * `makeRequire` is a seam: module resolution walks up from `cwd` to the
13
+ * filesystem root, so under a test runner (or an `npx` cache) it can find a
14
+ * *different* project's Next and answer confidently with the wrong number.
15
+ * Tests pass a resolver that fails, to exercise the manifest path.
16
+ */
17
+ export function detectNextMajor(cwd, makeRequire = createRequire) {
18
+ // Prefer the installed copy — that's the version the app actually runs.
19
+ try {
20
+ const require = makeRequire(join(cwd, "__scaffold__.js"));
21
+ const pkg = require("next/package.json");
22
+ const major = parseMajor(pkg.version);
23
+ if (major)
24
+ return major;
25
+ }
26
+ catch {
27
+ // next not installed yet — fall through to the manifest range
28
+ }
29
+ try {
30
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
31
+ return parseMajor(pkg.dependencies?.next ?? pkg.devDependencies?.next);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ /** Pull the major out of a version or a range (`^16.3.1`, `>=15.0.0`, `15.5.23`, `16`). */
38
+ function parseMajor(version) {
39
+ if (!version)
40
+ return undefined;
41
+ const match = /^(\d+)/.exec(version.replace(/^[^\d]*/, ""));
42
+ return match ? Number(match[1]) : undefined;
43
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * First free port at or above `start`.
3
+ *
4
+ * A demo that fails on "port 3000 is already in use" fails at the only moment
5
+ * the user has no context for fixing it, and on a developer's machine 3000 is
6
+ * occupied more often than not. Next would pick another port by itself, but
7
+ * the editor is told where the orchestrator lives at startup and the
8
+ * orchestrator is inside the site — so the port has to be decided once, here,
9
+ * and both halves told the same answer.
10
+ */
11
+ export declare function findFreePort(start: number, attempts?: number): Promise<number>;
package/dist/ports.js ADDED
@@ -0,0 +1,61 @@
1
+ import { createConnection, createServer } from "node:net";
2
+ /**
3
+ * First free port at or above `start`.
4
+ *
5
+ * A demo that fails on "port 3000 is already in use" fails at the only moment
6
+ * the user has no context for fixing it, and on a developer's machine 3000 is
7
+ * occupied more often than not. Next would pick another port by itself, but
8
+ * the editor is told where the orchestrator lives at startup and the
9
+ * orchestrator is inside the site — so the port has to be decided once, here,
10
+ * and both halves told the same answer.
11
+ */
12
+ export async function findFreePort(start, attempts = 20) {
13
+ for (let port = start; port < start + attempts; port++) {
14
+ if (await isFree(port))
15
+ return port;
16
+ }
17
+ return start;
18
+ }
19
+ /**
20
+ * Free means both "nothing answers" and "we can bind".
21
+ *
22
+ * Neither test is sufficient alone, and each one alone shipped a bug:
23
+ *
24
+ * - Binding `127.0.0.1` reported 3000 free while `next dev` was listening on
25
+ * `*:3000`, so the scaffold wrote 3000 into five files and the demo died on
26
+ * EADDRINUSE.
27
+ * - Binding the wildcard reported 4100 free while the editor was listening on
28
+ * `[::1]:4100`. Node sets SO_REUSEADDR, so a wildcard bind succeeds beside a
29
+ * loopback-specific one — and the result is two servers answering
30
+ * `localhost:4100`, chosen by whichever family the client resolves.
31
+ *
32
+ * So: connect to both loopback addresses, and if nothing accepts, confirm we
33
+ * can actually take the port.
34
+ */
35
+ async function isFree(port) {
36
+ const answered = await Promise.all([accepts(port, "127.0.0.1"), accepts(port, "::1")]);
37
+ if (answered.some(Boolean))
38
+ return false;
39
+ return bindable(port);
40
+ }
41
+ /** Whether something accepts a connection on this address. */
42
+ function accepts(port, host) {
43
+ return new Promise((resolve) => {
44
+ const socket = createConnection({ port, host });
45
+ const done = (answer) => {
46
+ socket.destroy();
47
+ resolve(answer);
48
+ };
49
+ socket.setTimeout(250, () => done(false));
50
+ socket.once("connect", () => done(true));
51
+ socket.once("error", () => done(false));
52
+ });
53
+ }
54
+ function bindable(port) {
55
+ return new Promise((resolve) => {
56
+ const server = createServer();
57
+ server.once("error", () => resolve(false));
58
+ server.once("listening", () => server.close(() => resolve(true)));
59
+ server.listen(port);
60
+ });
61
+ }
@@ -0,0 +1,13 @@
1
+ import type { ScaffoldConfig } from "./types.js";
2
+ /** A directory name turned into something the orchestrator will accept as a site id. */
3
+ export declare function toSiteId(name: string): string;
4
+ /** Everything a demo scaffold needs, with no questions asked. */
5
+ export declare function demoConfig(dirName: string): Promise<ScaffoldConfig>;
6
+ /**
7
+ * The mode question, asked first.
8
+ *
9
+ * It used to be "Which CMS?", which is a reasonable question to ask somebody
10
+ * mid-integration and the wrong first thing to ask somebody who has not yet
11
+ * seen what the product does.
12
+ */
13
+ export declare function runPrompts(cwd: string): Promise<ScaffoldConfig | null>;
@@ -0,0 +1,119 @@
1
+ import * as p from "@clack/prompts";
2
+ import { randomBytes } from "node:crypto";
3
+ import { basename } from "node:path";
4
+ import { detectNextMajor } from "./next-version.js";
5
+ import { findFreePort } from "./ports.js";
6
+ /** A directory name turned into something the orchestrator will accept as a site id. */
7
+ export function toSiteId(name) {
8
+ const cleaned = name.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
9
+ return cleaned.length > 0 ? cleaned : "avocado-demo";
10
+ }
11
+ /** Everything a demo scaffold needs, with no questions asked. */
12
+ export async function demoConfig(dirName) {
13
+ const sitePort = await findFreePort(3000);
14
+ const editorPort = await findFreePort(4100);
15
+ return {
16
+ mode: "demo",
17
+ cms: "none",
18
+ siteId: toSiteId(dirName),
19
+ blocksMode: "default",
20
+ sitePort,
21
+ editorPort,
22
+ draftSecret: randomBytes(24).toString("hex"),
23
+ };
24
+ }
25
+ /**
26
+ * The mode question, asked first.
27
+ *
28
+ * It used to be "Which CMS?", which is a reasonable question to ask somebody
29
+ * mid-integration and the wrong first thing to ask somebody who has not yet
30
+ * seen what the product does.
31
+ */
32
+ export async function runPrompts(cwd) {
33
+ p.intro("Avocado Studio");
34
+ const mode = await p.select({
35
+ message: "What would you like to do?",
36
+ options: [
37
+ {
38
+ value: "demo",
39
+ label: "See the demo site",
40
+ hint: "creates a new project — nine pages, editable by chat",
41
+ },
42
+ {
43
+ value: "integrate",
44
+ label: "Wire Avocado into this project",
45
+ hint: "an existing Next.js app, in this directory",
46
+ },
47
+ ],
48
+ });
49
+ if (p.isCancel(mode)) {
50
+ p.cancel("Cancelled.");
51
+ return null;
52
+ }
53
+ if (mode === "demo") {
54
+ const dir = await p.text({
55
+ message: "Directory name",
56
+ placeholder: "avocado-demo",
57
+ defaultValue: "avocado-demo",
58
+ validate: (v) => (v.includes("/") || v.includes("\\") ? "A single directory name, not a path" : undefined),
59
+ });
60
+ if (p.isCancel(dir)) {
61
+ p.cancel("Cancelled.");
62
+ return null;
63
+ }
64
+ return demoConfig(dir);
65
+ }
66
+ return integratePrompts(cwd);
67
+ }
68
+ async function integratePrompts(cwd) {
69
+ const cms = await p.select({
70
+ message: "Which CMS?",
71
+ options: [
72
+ { value: "sanity", label: "Sanity" },
73
+ { value: "contentful", label: "Contentful" },
74
+ { value: "strapi", label: "Strapi" },
75
+ { value: "none", label: "None (static JSON)" },
76
+ ],
77
+ });
78
+ if (p.isCancel(cms)) {
79
+ p.cancel("Cancelled.");
80
+ return null;
81
+ }
82
+ const siteId = await p.text({
83
+ message: "Site ID (used for orchestrator sessions)",
84
+ placeholder: basename(cwd),
85
+ defaultValue: basename(cwd),
86
+ validate: (v) => /^[a-zA-Z0-9_-]+$/.test(v) ? undefined : "Only letters, numbers, hyphens, and underscores",
87
+ });
88
+ if (p.isCancel(siteId)) {
89
+ p.cancel("Cancelled.");
90
+ return null;
91
+ }
92
+ const blocksMode = await p.select({
93
+ message: "Block types?",
94
+ options: [
95
+ { value: "default", label: "Default blocks", hint: "Hero, CTA, FeatureGrid, etc." },
96
+ { value: "custom", label: "Custom blocks", hint: "Generates stub manifest for you to fill in" },
97
+ ],
98
+ });
99
+ if (p.isCancel(blocksMode)) {
100
+ p.cancel("Cancelled.");
101
+ return null;
102
+ }
103
+ return {
104
+ mode: "integrate",
105
+ cms: cms,
106
+ siteId: siteId,
107
+ blocksMode: blocksMode,
108
+ nextMajor: detectNextMajor(cwd),
109
+ // The host app owns its own ports; these are only the defaults the emitted
110
+ // `.env.local.example` and the printed instructions assume.
111
+ sitePort: 3000,
112
+ editorPort: 4100,
113
+ // Generated here for the same reason the demo path generates one: the
114
+ // example env used to ship `DRAFT_MODE_SECRET=dev-secret` next to a step
115
+ // telling the reader to replace it with a long random string, and a value
116
+ // that already looks filled in is a step nobody performs.
117
+ draftSecret: randomBytes(24).toString("hex"),
118
+ };
119
+ }
@@ -0,0 +1,2 @@
1
+ import type { GeneratedFile } from "../types.js";
2
+ export declare function demoAssets(): GeneratedFile[];
@@ -0,0 +1,41 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { join, posix, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /*
5
+ * The files the demo content points at.
6
+ *
7
+ * Twelve of them: a logo, used in the header of every page, and eleven images
8
+ * across five of the nine pages. `apps/site` keeps them in its own `public/`,
9
+ * so nothing in the monorepo ever noticed they were not part of the content —
10
+ * and a scaffolded project has no `public/` at all, so every page rendered a
11
+ * broken logo and five of them rendered broken images. A demo that ships
12
+ * broken images is the placeholder problem again, wearing better copy.
13
+ *
14
+ * `scripts/sync-demo-seed.mjs` fills this directory from the same source as the
15
+ * content itself and fails if an asset the content names is missing, so the two
16
+ * cannot come apart.
17
+ *
18
+ * Resolved relative to the package root, which is two levels up from both
19
+ * `src/templates` and `dist/templates` — the same answer whether this runs from
20
+ * source under tsx or from the published build.
21
+ */
22
+ const ASSETS = fileURLToPath(new URL("../../assets", import.meta.url));
23
+ export function demoAssets() {
24
+ return walk(ASSETS).map((absolute) => ({
25
+ // Published under `public/`, because that is where Next serves the root-
26
+ // relative paths the content uses (`/logos/avocado-hub.svg`).
27
+ path: posix.join("public", absolute.slice(ASSETS.length + 1).split(sep).join("/")),
28
+ source: absolute,
29
+ }));
30
+ }
31
+ function walk(dir) {
32
+ const out = [];
33
+ for (const entry of readdirSync(dir)) {
34
+ const full = join(dir, entry);
35
+ if (statSync(full).isDirectory())
36
+ out.push(...walk(full));
37
+ else
38
+ out.push(full);
39
+ }
40
+ return out.sort();
41
+ }
@@ -0,0 +1,45 @@
1
+ import type { ScaffoldConfig } from "../types.js";
2
+ export declare function editorApiRoute(config: ScaffoldConfig): string;
3
+ export declare function revalidateRoute(config: ScaffoldConfig): string | null;
4
+ export declare function manifestFile(config: ScaffoldConfig): string;
5
+ /**
6
+ * Which of `createSitePage`'s three modes a scaffolded route file uses.
7
+ *
8
+ * `auto` is one route that decides published-vs-editor per request, and needs
9
+ * neither a middleware nor a proxy nor a second route file. That costs static
10
+ * rendering — reading `searchParams` opts every visitor into a dynamic render —
11
+ * which is the right trade for a demo and the wrong one for a production site.
12
+ * `static` + `preview` is the pair a real site wants, and is what the
13
+ * integrate path still scaffolds.
14
+ */
15
+ export type PageMode = "auto" | "static" | "preview";
16
+ export declare function pageFile(config: ScaffoldConfig, mode?: PageMode): string;
17
+ /**
18
+ * `createSitePage` calls `notFound()` for an unknown slug, which renders this
19
+ * file with a real 404 status. Without it Next falls back to its own bare
20
+ * default page — correct status, but none of the site's chrome.
21
+ */
22
+ export declare function notFoundFile(): string;
23
+ /** Next.js 15 and earlier: the rewrite lives in `middleware.ts`. */
24
+ export declare function middlewareFile(): string;
25
+ /**
26
+ * Next.js 16+: `middleware` was renamed to the `proxy` convention, and `config`
27
+ * must be a static object literal — Next 16 reads it by static analysis and
28
+ * rejects a factory return value or an imported constant.
29
+ */
30
+ export declare function proxyFile(): string;
31
+ /**
32
+ * The integrate path's `.env.local.example`.
33
+ *
34
+ * It used to contradict the instructions printed beside it. Step 4 mounts the
35
+ * orchestrator inside the app at `/api/avocado` — there is no separate service
36
+ * — while this file set `ORCHESTRATOR_URL=http://localhost:4200`, pointing at
37
+ * the standalone server the integrator was never told to run. The env var
38
+ * wins, so every draft read went to a port with nothing on it and the editor
39
+ * showed published content with the edits silently dropped.
40
+ *
41
+ * `DRAFT_MODE_SECRET` was likewise pre-filled with `dev-secret` next to a step
42
+ * saying to set it to a long random string; it is generated now, as on the
43
+ * demo path.
44
+ */
45
+ export declare function envExample(config: ScaffoldConfig): string;