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.
- package/LICENSE +201 -0
- package/README.md +65 -0
- package/assets/generated-images/gdrive_1ql_bdk2KM_27u01rkMd8KoiCRJ_R7UOy.webp +0 -0
- package/assets/generated-images/gen_1774991902955_0p1i2o.webp +0 -0
- package/assets/generated-images/gen_1776458907692_zlrbkg.webp +0 -0
- package/assets/generated-images/gen_1788955681679_454dc316.webp +0 -0
- package/assets/generated-images/var_1773434968055_8g99nk.webp +0 -0
- package/assets/generated-images/var_1773435042084_wvet78.webp +0 -0
- package/assets/generated-images/var_1773435061562_6hf8s6.webp +0 -0
- package/assets/generated-images/var_1773435080764_6ocahu.webp +0 -0
- package/assets/generated-images/var_1773435101045_1y544z.webp +0 -0
- package/assets/generated-images/var_1773872370443_poycxy.webp +0 -0
- package/assets/generated-images/var_1776858715876_iuu72w.webp +0 -0
- package/assets/logos/avocado-hub.svg +7 -0
- package/dist/cms-config.d.ts +34 -0
- package/dist/cms-config.js +103 -0
- package/dist/generator.d.ts +22 -0
- package/dist/generator.js +92 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +109 -0
- package/dist/instructions.d.ts +19 -0
- package/dist/instructions.js +82 -0
- package/dist/next-version.d.ts +15 -0
- package/dist/next-version.js +43 -0
- package/dist/ports.d.ts +11 -0
- package/dist/ports.js +61 -0
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.js +119 -0
- package/dist/templates/assets.d.ts +2 -0
- package/dist/templates/assets.js +41 -0
- package/dist/templates/common.d.ts +45 -0
- package/dist/templates/common.js +174 -0
- package/dist/templates/contentful.d.ts +2 -0
- package/dist/templates/contentful.js +379 -0
- package/dist/templates/demo-app.d.ts +3 -0
- package/dist/templates/demo-app.js +465 -0
- package/dist/templates/demo-content.d.ts +2 -0
- package/dist/templates/demo-content.js +7 -0
- package/dist/templates/sanity.d.ts +2 -0
- package/dist/templates/sanity.js +289 -0
- package/dist/templates/static.d.ts +2 -0
- package/dist/templates/static.js +81 -0
- package/dist/templates/strapi.d.ts +2 -0
- package/dist/templates/strapi.js +235 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.js +1 -0
- package/dist/versions.d.ts +32 -0
- package/dist/versions.js +32 -0
- 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
|
+
}
|
package/dist/ports.d.ts
ADDED
|
@@ -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>;
|
package/dist/prompts.js
ADDED
|
@@ -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,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;
|