@goodfellow-cms/next 0.5.0 → 0.7.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/README.md CHANGED
@@ -4,7 +4,7 @@ Goodfellow pages and the admin panel in a Next.js App Router site, exported as s
4
4
 
5
5
  Start from the Next.js starter: `npm create goodfellow@latest my-site -- --template next`.
6
6
 
7
- [Goodfellow](https://github.com/goodfellow-cms/goodfellow-cms) is a website builder for people who aren't developers, with no server or database: pages are files in your own GitHub or GitLab repository, edited in a visual editor built on [Puck](https://puckeditor.com). To start a new site, run `npm create goodfellow@latest my-site`. See the [documentation](https://goodfellow-cms.github.io/goodfellow-cms/) for everything else.
7
+ [Goodfellow](https://github.com/goodfellow-cms/goodfellow-cms) is a website builder for people who aren't developers, with no server or database: pages are files in your own GitHub, GitLab or Bitbucket repository, edited in a visual editor built on [Puck](https://puckeditor.com). To start a new site, run `npm create goodfellow@latest my-site`. See the [documentation](https://goodfellow-cms.github.io/goodfellow-cms/) for everything else.
8
8
 
9
9
  More in the docs: [Next.js](https://goodfellow-cms.github.io/goodfellow-cms/docs/nextjs) and [Full-stack Next.js sites](https://goodfellow-cms.github.io/goodfellow-cms/docs/full-stack-nextjs).
10
10
 
@@ -1,19 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  // After `next build`: `goodfellow-next finish out` fixes the export's prefetch files on Windows,
3
3
  // writes the calendar files of collections of events, collections' feeds, the pages at redirects' old addresses and Cloudflare Pages' _redirects, and the search index for sites with a search block,
4
- // copies the site's Google Fonts into the export, starts every page with the admin address's guard for a site with one, and reports what automated checks find on the exported pages. `index` only writes the index.
4
+ // copies the site's Google Fonts into the export, builds plugins' admin frames and runs their build steps, starts every page with the admin address's guard for a site with one, and reports what automated checks find on the exported pages. `index` only writes the index.
5
5
  // `goodfellow-next update` updates Goodfellow, as `goodfellow update` does for other sites.
6
- // `goodfellow-next due [--since <time>]` says whether a scheduled change has come due, as `goodfellow due` does.
6
+ // `goodfellow-next task [--out <file>]` runs one of a plugin's tasks for the admin panel, as `goodfellow task` does.
7
+ // `goodfellow-next due [--since <time>] [--output <file>]` says whether a scheduled change has come due, as `goodfellow due` does.
7
8
  // `goodfellow-next nightly [--mode <mode>]` says whether tonight's build is needed, as `goodfellow nightly` does.
8
9
  // `goodfellow-next content link|copy [folder]` puts a content repository's files in place, for a site whose
9
10
  // content is kept in a repository of its own.
10
11
  // `goodfellow-next import wordpress <file or address> [--dry-run]` imports a WordPress site, as `goodfellow import` does.
11
12
  // `goodfellow-next export wordpress [file]` writes the site as a WordPress export file, as `goodfellow export` does.
13
+ // `goodfellow-next blocks add <item> [--registry <folder>] [--native <app>]` makes a block of a shadcn item, as
14
+ // `goodfellow blocks add` does, and with --native its version for an Expo app.
12
15
  // `goodfellow-next admin add <address>|check|remove` sets the admin address, and `goodfellow-next broker use <address>|remove`
13
16
  // the token broker, in goodfellow.config.tsx, as `goodfellow admin` and `goodfellow broker` do.
14
17
  import { existsSync, readFileSync } from "node:fs";
15
18
  import { rm } from "node:fs/promises";
16
- import { relative } from "node:path";
19
+ import { relative, resolve } from "node:path";
17
20
  import { parseArgs } from "node:util";
18
21
  import { buildNotes, CLOUDFLARE_REDIRECTS_LIMIT, configAdmin, formatUpdateResult } from "@goodfellow-cms/core";
19
22
  import { reportSiteProblems } from "@goodfellow-cms/core/accessibility";
@@ -38,8 +41,10 @@ import {
38
41
  update,
39
42
  } from "@goodfellow-cms/core/node";
40
43
  import { writePagesSearchIndex, writeSearchIndex } from "@goodfellow-cms/core/search-index";
44
+ import { addShadcnBlock, blockWorks, formatShadcnBlock, ShadcnBlockError } from "@goodfellow-cms/core/shadcn";
41
45
  import { WordPressReadError } from "@goodfellow-cms/core/wordpress";
42
46
  import {
47
+ blockPageRender,
43
48
  buildFileFolders,
44
49
  builtPages,
45
50
  checkPages,
@@ -49,11 +54,14 @@ import {
49
54
  NEXT_SITE_FILES,
50
55
  writeBuildRecordFile,
51
56
  writeCalendarFiles,
57
+ writeContentApi,
52
58
  writeEditorPolicy,
53
59
  writeFeedFiles,
54
60
  writeFontFiles,
55
61
  writePageLanguages,
62
+ writePluginFiles,
56
63
  writeRedirectFiles,
64
+ writeTaskResult,
57
65
  } from "@goodfellow-cms/next/export";
58
66
 
59
67
  const [command, dir = "out"] = process.argv.slice(2);
@@ -77,9 +85,15 @@ if (command === "finish" && dir === "--server") {
77
85
  if (calendars > 0) console.log(`Wrote ${calendars} calendar files, in public/calendars`);
78
86
  const feeds = await writeFeedFiles("public");
79
87
  if (feeds > 0) console.log(`Wrote ${feeds} feeds, in public/feeds`);
88
+ const api = await writeContentApi("public");
89
+ if (api !== undefined) console.log(`Wrote the content API, with ${api} pages, in public/api`);
80
90
  // Before the pages are read, so the search index and the checks see each page's language.
81
91
  const languages = await writePageLanguages(undefined);
82
92
  if (languages > 0) console.log(`Marked ${languages} pages with their language`);
93
+ // Plugins' admin frames and build steps, into public/, which next start serves.
94
+ const plugins = await writePluginFiles("public");
95
+ if (plugins.frames.length > 0)
96
+ console.log(`Wrote the admin frames of ${plugins.frames.join(", ")}, in public/admin/plugins`);
83
97
  const pages = await builtPages();
84
98
  const indexed = await writePagesSearchIndex(pages, "public/pagefind");
85
99
  console.log(
@@ -102,9 +116,30 @@ if (command === "update") {
102
116
  console.log(formatUpdateResult(result));
103
117
  process.exit(result.state === "failed" ? 1 : 0);
104
118
  }
119
+ if (command === "task") {
120
+ const { values } = parseArgs({ args: process.argv.slice(3), options: { out: { type: "string" } } });
121
+ const env = process.env;
122
+ await writeTaskResult(
123
+ ".",
124
+ {
125
+ id: env.GOODFELLOW_TASK_REQUEST ?? "",
126
+ plugin: env.GOODFELLOW_TASK_PLUGIN ?? "",
127
+ task: env.GOODFELLOW_TASK_NAME ?? "",
128
+ input: env.GOODFELLOW_TASK_INPUT ?? "",
129
+ key: env.GOODFELLOW_TASK_KEY ?? "",
130
+ },
131
+ values.out,
132
+ );
133
+ process.exit(0);
134
+ }
105
135
  if (command === "due") {
106
- const { values } = parseArgs({ args: process.argv.slice(3), options: { since: { type: "string" } } });
107
- for (const line of await reportDue(await scheduleDue({ since: values.since }))) console.log(line);
136
+ const { values } = parseArgs({
137
+ args: process.argv.slice(3),
138
+ options: { since: { type: "string" }, output: { type: "string" } },
139
+ });
140
+ const result = await scheduleDue({ since: values.since });
141
+ for (const line of await reportDue(result, process.env, values.output ?? process.env.GITHUB_OUTPUT))
142
+ console.log(line);
108
143
  process.exit(0);
109
144
  }
110
145
  if (command === "nightly") {
@@ -199,6 +234,31 @@ if (command === "export") {
199
234
  process.exit(0);
200
235
  }
201
236
  }
237
+ if (command === "blocks") {
238
+ const { positionals, values } = parseArgs({
239
+ args: process.argv.slice(3),
240
+ allowPositionals: true,
241
+ options: { registry: { type: "string" }, native: { type: "string" } },
242
+ });
243
+ const [action, item] = positionals;
244
+ if (action === "add" && item) {
245
+ try {
246
+ const result = await addShadcnBlock({
247
+ root: process.cwd(),
248
+ item,
249
+ ...(values.registry && { registry: resolve(values.registry) }),
250
+ ...(values.native && { native: resolve(values.native) }),
251
+ render: blockPageRender("."),
252
+ });
253
+ console.log(formatShadcnBlock(result));
254
+ process.exit(blockWorks(result) ? 0 : 1);
255
+ } catch (error) {
256
+ if (!(error instanceof ShadcnBlockError)) throw error;
257
+ console.error(error.message);
258
+ process.exit(1);
259
+ }
260
+ }
261
+ }
202
262
  if (command !== "finish" && command !== "index") {
203
263
  console.error(
204
264
  [
@@ -206,18 +266,20 @@ if (command !== "finish" && command !== "index") {
206
266
  " goodfellow-next finish --server",
207
267
  " goodfellow-next index [folder]",
208
268
  " goodfellow-next update [--to fixes|automatic|latest|<release>] [--publish | --commit]",
209
- " goodfellow-next due [--since <time>]",
269
+ " goodfellow-next due [--since <time>] [--output <file>]",
210
270
  " goodfellow-next nightly [--mode changed|always|off] [--output <file>]",
211
271
  " goodfellow-next content link|copy [folder]",
212
272
  " goodfellow-next import wordpress <file or address> [--dry-run]",
213
273
  " goodfellow-next export wordpress [file]",
274
+ " goodfellow-next blocks add <item> [--registry <folder>] [--native <app>]",
214
275
  " goodfellow-next admin add <address> | check | remove",
215
276
  " goodfellow-next broker use <address> | remove",
216
277
  "",
217
278
  "finish Gets the exported site in folder (default: out) ready to publish: puts the files",
218
279
  " Next.js uses to load pages ahead where browsers look for them, writes calendar files",
219
280
  " for collections of events, collections' feeds and pages that send visitors on from",
220
- " old addresses, indexes it for search, and lists what automated accessibility checks",
281
+ " old addresses, builds plugins' admin frames and runs their build steps, indexes it",
282
+ " for search, and lists what automated accessibility checks",
221
283
  " find on its pages. With --server, for a full-stack site, writes the calendar files",
222
284
  " and feeds into public/calendars and public/feeds and indexes and checks the pages",
223
285
  " next build rendered, with the index in public/pagefind, instead.",
@@ -227,7 +289,7 @@ if (command !== "finish" && command !== "index") {
227
289
  " that runs none of the site's code to publish.",
228
290
  "due Says whether a scheduled change has come due since --since (default: the last",
229
291
  " hour), so the site needs building again, and sets the step's due output on",
230
- " GitHub Actions.",
292
+ " GitHub Actions, or adds it to --output.",
231
293
  "nightly Says whether tonight's build is needed: when the site has changed since the live",
232
294
  " site was built, what it shows by today's date has, or an update is available",
233
295
  " (--mode, or GOODFELLOW_NIGHTLY: changed, the default, always or off), and sets the",
@@ -240,6 +302,11 @@ if (command !== "finish" && command !== "index") {
240
302
  " says what it would bring across and changes nothing.",
241
303
  "export Writes this site as a WordPress export file (default: named after the site),",
242
304
  " which WordPress imports with Tools → Import → WordPress.",
305
+ "blocks Installs a shadcn registry item (button, @acme/hero, or an item's address) with",
306
+ " shadcn's own CLI, makes a block of its component, with fields from its props, and",
307
+ " says what it couldn't do. With --registry, writes the block into the block registry",
308
+ " in folder instead. Needs TypeScript 7 as typescript-7, since Next.js builds with 5:",
309
+ " npm install --save-dev typescript-7@npm:typescript@7.0.2",
243
310
  "admin The admin panel's address of its own, such as admin.example.org, in",
244
311
  " goodfellow.config.tsx: add names it; check checks it once the site has rebuilt with",
245
312
  " it, then starts the admin panel only there; remove stops using it.",
@@ -259,6 +326,8 @@ if (command === "finish") {
259
326
  if (calendars > 0) console.log(`Wrote ${calendars} calendar files`);
260
327
  const feeds = await writeFeedFiles(dir);
261
328
  if (feeds > 0) console.log(`Wrote ${feeds} feeds`);
329
+ const api = await writeContentApi(dir);
330
+ if (api !== undefined) console.log(`Wrote the content API, with ${api} pages, in api/`);
262
331
  const languages = await writePageLanguages(dir);
263
332
  if (languages > 0) console.log(`Marked ${languages} pages with their language`);
264
333
  // Before the redirects' pages, which aren't the site's.
@@ -271,6 +340,9 @@ if (command === "finish") {
271
340
  );
272
341
  }
273
342
  for (const line of buildNotes(await writeFontFiles(dir))) console.log(line);
343
+ // Plugins' admin frames and build steps, once Goodfellow has written the rest, before the index and the guards.
344
+ const plugins = await writePluginFiles(dir);
345
+ if (plugins.frames.length > 0) console.log(`Wrote the admin frames of ${plugins.frames.join(", ")}`);
274
346
  // What the site was built from, and when it stops being current, for the nightly check.
275
347
  const currentUntil = await writeBuildRecordFile(dir);
276
348
  if (currentUntil) {
package/dist/admin.js CHANGED
@@ -58,6 +58,7 @@ function useEditorRebuilds() {
58
58
  * dependencies are.
59
59
  */
60
60
  const HOSTS = {
61
+ "@goodfellow-cms/bitbucket": () => import("@goodfellow-cms/bitbucket"),
61
62
  "@goodfellow-cms/github": () => import("@goodfellow-cms/github"),
62
63
  "@goodfellow-cms/gitlab": () => import("@goodfellow-cms/gitlab")
63
64
  };
package/dist/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CALENDARS_DIR, FEEDS_DIR, allPages, brokerOrigin, configAdmin, configBroker, loadSiteContent, normalizeBase, redirectFrom, redirectProblems, shownContent, vercelAdminHeaders } from "@goodfellow-cms/core";
2
- import { BUILD_DATES_DIR, BUILD_DATES_ENV, DEV_API_PREFIX, contentRepositoryProblem, fileSystemSource, handleDevApi, isLocalOrigin, localFileStore, localRegistryDir } from "@goodfellow-cms/core/node";
2
+ import { BUILD_DATES_DIR, BUILD_DATES_ENV, DEV_API_PREFIX, builtSiteSource, contentRepositoryProblem, handleDevApi, isLocalOrigin, localFileStore, localRegistryDir } from "@goodfellow-cms/core/node";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { rm } from "node:fs/promises";
5
5
  import { createServer } from "node:http";
@@ -41,15 +41,21 @@ function startDevApi(root) {
41
41
  globals[SERVER_KEY] ??= new Promise((resolvePort, reject) => {
42
42
  const store = localFileStore(root);
43
43
  const registryDir = localRegistryDir(root);
44
- const editor = import("./dev-editor-DiPKrIAS.js").then(({ startDevEditor }) => startDevEditor(root)).catch((error) => {
44
+ const editor = import("./dev-editor-CzE9HofK.js").then(({ startDevEditor }) => startDevEditor(root)).catch((error) => {
45
45
  console.error(`The editor's frames can't be served: ${error.message}`);
46
46
  return { serve: async () => false };
47
47
  });
48
+ const tasks = {
49
+ root,
50
+ plugins: async () => (await (await import("./plugins-K5i8XztM.js").then((n) => n.o)).loadSiteConfig(root)).plugins ?? [],
51
+ server: "next-dev"
52
+ };
48
53
  const server = createServer(async (req, res) => {
49
54
  if (await (await editor).serve(req, res)) return;
50
55
  if (!await handleDevApi(store, req, res, {
51
56
  trustOrigin: isLocalOrigin,
52
- registryDir
57
+ registryDir,
58
+ tasks
53
59
  })) res.writeHead(404).end();
54
60
  });
55
61
  server.on("error", reject);
@@ -95,7 +101,7 @@ function redirectSource(path) {
95
101
  * Content with a problem gives none, and the build reports the problem.
96
102
  */
97
103
  async function siteRedirects(root) {
98
- const content = await loadSiteContent(fileSystemSource(root)).then(shownContent, () => void 0);
104
+ const content = await loadSiteContent(builtSiteSource(root)).then(shownContent, () => void 0);
99
105
  if (!content?.redirects) return [];
100
106
  const taken = new Set(allPages(content).map((page) => page.path));
101
107
  const problems = redirectProblems(content.redirects);
@@ -1,4 +1,5 @@
1
- import { DEV_EDITOR_PREFIX, EDITOR_PRELUDE, editorPage } from "@goodfellow-cms/core";
1
+ import { i as loadSiteConfig, t as buildPluginFrame } from "./plugins-K5i8XztM.js";
2
+ import { DEV_EDITOR_PREFIX, EDITOR_PRELUDE, editorPage, pluginId } from "@goodfellow-cms/core";
2
3
  import { existsSync } from "node:fs";
3
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
4
5
  import { join, resolve, sep } from "node:path";
@@ -101,8 +102,53 @@ async function startDevEditor(root) {
101
102
  built();
102
103
  }
103
104
  });
105
+ const pluginsDir = join(root, "node_modules/.goodfellow/next-plugins");
106
+ const pluginsPrefix = DEV_EDITOR_PREFIX.replace(/editor\/$/, "plugins/");
107
+ const pluginBuilds = /* @__PURE__ */ new Map();
108
+ const pluginFrame = (id) => {
109
+ let ready = pluginBuilds.get(id);
110
+ if (!ready) {
111
+ ready = (async () => {
112
+ const plugin = (await loadSiteConfig(root)).plugins?.find((candidate) => pluginId(candidate.name) === id && candidate.admin);
113
+ if (!plugin) {
114
+ pluginBuilds.delete(id);
115
+ return false;
116
+ }
117
+ await new Promise((done) => {
118
+ buildPluginFrame(plugin, {
119
+ root,
120
+ dir: join(pluginsDir, id),
121
+ watch: { onBuilt(error) {
122
+ if (error) console.error(`${plugin.title}'s admin frame couldn't be built: ${error.message}`);
123
+ done();
124
+ } }
125
+ }).catch((error) => {
126
+ console.error(`${plugin.title}'s admin frame couldn't be built: ${error.message}`);
127
+ done();
128
+ });
129
+ });
130
+ return true;
131
+ })();
132
+ pluginBuilds.set(id, ready);
133
+ }
134
+ return ready;
135
+ };
104
136
  return { async serve(req, res) {
105
137
  const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
138
+ if (pathname.startsWith(pluginsPrefix) && (req.method === "GET" || req.method === "HEAD")) {
139
+ const [id = "", ...rest] = decodeURIComponent(pathname.slice(pluginsPrefix.length)).split("/");
140
+ const file = resolve(pluginsDir, id, rest.join("/") || "index.html");
141
+ if (!file.startsWith(`${pluginsDir}${sep}`) || !await pluginFrame(id)) return false;
142
+ try {
143
+ const data = await readFile(file);
144
+ res.setHeader("Content-Type", TYPES[file.slice(file.lastIndexOf(".") + 1)] ?? "application/octet-stream");
145
+ res.setHeader("Cache-Control", "no-store");
146
+ res.end(req.method === "HEAD" ? void 0 : data);
147
+ } catch {
148
+ res.writeHead(404).end();
149
+ }
150
+ return true;
151
+ }
106
152
  const page = pathname === DEV_EDITOR_PREFIX.slice(0, -1);
107
153
  if (!(page || pathname.startsWith(DEV_EDITOR_PREFIX)) || req.method !== "GET" && req.method !== "HEAD") return false;
108
154
  await ready;
package/dist/export.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { SelfHostedResult, SiteProblem } from "@goodfellow-cms/core";
1
+ import { PluginTaskRequest, SelfHostedResult, SiteProblem, TaskResultFile } from "@goodfellow-cms/core";
2
2
  import { SiteFile } from "@goodfellow-cms/core/node";
3
+ import { PageRender } from "@goodfellow-cms/core/shadcn";
3
4
  //#region src/site-files.d.ts
4
5
  /**
5
6
  * The Next.js starter's files that `goodfellow-next update` keeps up to date
@@ -20,12 +21,35 @@ export declare function writeCalendarFiles(outDir: string, root?: string): Promi
20
21
  * build` does. Returns how many it wrote: none without the site's address.
21
22
  */
22
23
  export declare function writeFeedFiles(outDir: string, root?: string): Promise<number>;
24
+ /**
25
+ * Writes the content API (`/api/`) into the export, as `goodfellow build`
26
+ * does: the site's content as JSON, each page as it's shown, prepared in Node
27
+ * with the site's config as `goodfellow build` prepares it, so both write the
28
+ * same files. Returns how many pages it has, or `undefined` where the
29
+ * config's `api` is `false`.
30
+ */
31
+ export declare function writeContentApi(outDir: string, root?: string): Promise<number | undefined>;
23
32
  /**
24
33
  * The folders the calendar files and feeds go in: `calendars/` and `feeds/`,
25
34
  * and on a site in several languages, the same under each other language's
26
35
  * code, such as `es/feeds/`.
27
36
  */
28
37
  export declare function buildFileFolders(root?: string): Promise<string[]>;
38
+ /** What `writePluginFiles()` wrote and ran. */
39
+ export interface PluginFilesWritten {
40
+ /** The plugins whose admin frames were written. */
41
+ frames: string[];
42
+ /** The build steps that ran, as `plugin: step`. */
43
+ steps: string[];
44
+ }
45
+ /**
46
+ * Plugins' parts that builds make: each plugin's admin frame, beside the
47
+ * editor's frames at `admin/plugins/<id>/`, and its build steps, each in a
48
+ * process of its own held to what the plugin declared. Into a static export
49
+ * (`outDir`), or a full-stack site's `public/`, where `next start` serves
50
+ * them. The config is loaded outside Next.js only when it lists plugins.
51
+ */
52
+ export declare function writePluginFiles(outDir: string, root?: string): Promise<PluginFilesWritten>;
29
53
  /** What `writeFontFiles()` did, for `buildNotes()`. */
30
54
  export interface FontsWritten {
31
55
  fonts?: SelfHostedResult;
@@ -118,4 +142,17 @@ export declare function exportedPages(outDir: string): Promise<BuiltPage[]>;
118
142
  * Pages that aren't the site's, such as Next.js's own, are left out.
119
143
  */
120
144
  export declare function checkPages(pages: readonly BuiltPage[], root?: string): Promise<SiteProblem[]>;
145
+ /**
146
+ * Runs one of a plugin's tasks for the admin panel, as `goodfellow task`
147
+ * does for other sites, with the site's config loaded in Node, and writes its
148
+ * answer sealed to the request's key, even when the task fails. Secrets are
149
+ * taken out of the environment before the config loads.
150
+ */
151
+ export declare function writeTaskResult(root: string, request: PluginTaskRequest, out?: string): Promise<TaskResultFile>;
152
+ /**
153
+ * Renders a page with a block from its own file as well as the site's config's,
154
+ * for `goodfellow-next blocks add`'s check that a block it made renders: both
155
+ * loaded in Node, as `goodfellow-next finish` loads the config.
156
+ */
157
+ export declare function blockPageRender(root: string): PageRender;
121
158
  //#endregion
package/dist/export.js CHANGED
@@ -1,10 +1,12 @@
1
+ import { a as loadSiteModule, i as loadSiteConfig, n as buildPluginFrames, r as configHasPlugins } from "./plugins-K5i8XztM.js";
1
2
  import { r as NEXT_SITE_FILES } from "./site-files-BeLnLxCl.js";
2
- import { CALENDARS_DIR, CLOUDFLARE_REDIRECTS_PATH, EDITOR_PRELUDE, FEEDS_DIR, SITE_DESCRIPTION_PATH, absoluteUrl, allPages, calendarFiles, cloudflareRedirects, editorPolicy, feedFiles, hasLanguages, loadSiteContent, parseSiteDescription, pathLanguage, redirectFiles, rootRobotsUrl, shownContent, siteLanguages } from "@goodfellow-cms/core";
3
- import { BUILD_DATES_DIR, fileSystemSource, guardBuiltSite, reportedDate, selfHostFonts, writeBuildRecord } from "@goodfellow-cms/core/node";
3
+ import { CALENDARS_DIR, CLOUDFLARE_REDIRECTS_PATH, EDITOR_PRELUDE, FEEDS_DIR, SITE_DESCRIPTION_PATH, TASK_RESULT_FILE, absoluteUrl, allPages, calendarFiles, cloudflareRedirects, editorPolicy, feedFiles, hasLanguages, loadSiteContent, parseSiteDescription, pathLanguage, pluginId, redirectFiles, rootRobotsUrl, sealTaskAnswer, shownContent, siteLanguages, sitePages, taskRequestProblem, todayIn } from "@goodfellow-cms/core";
4
+ import { BUILD_DATES_DIR, builtSiteSource, guardBuiltSite, reportedDate, runPluginBuildSteps, runPluginTask, selfHostFonts, taskSecretsFrom, writeBuildRecord } from "@goodfellow-cms/core/node";
4
5
  import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
5
6
  import { dirname, join, relative, resolve } from "node:path";
6
7
  import { createHash } from "node:crypto";
7
8
  import { checkBuiltPages } from "@goodfellow-cms/core/accessibility";
9
+ import { CONTENT_API_DIR } from "@goodfellow-cms/core/content-api";
8
10
  //#region src/export.ts
9
11
  /**
10
12
  * Writes the calendar files of the site's collections of events into the
@@ -12,7 +14,7 @@ import { checkBuiltPages } from "@goodfellow-cms/core/accessibility";
12
14
  * how many it wrote.
13
15
  */
14
16
  async function writeCalendarFiles(outDir, root = process.cwd()) {
15
- const files = calendarFiles(shownContent(await loadSiteContent(fileSystemSource(resolve(root)))));
17
+ const files = calendarFiles(shownContent(await loadSiteContent(builtSiteSource(resolve(root)))));
16
18
  for (const file of files) {
17
19
  const target = join(outDir, file.path.slice(1));
18
20
  await mkdir(dirname(target), { recursive: true });
@@ -25,7 +27,7 @@ async function writeCalendarFiles(outDir, root = process.cwd()) {
25
27
  * build` does. Returns how many it wrote: none without the site's address.
26
28
  */
27
29
  async function writeFeedFiles(outDir, root = process.cwd()) {
28
- const files = feedFiles(shownContent(await loadSiteContent(fileSystemSource(resolve(root)))));
30
+ const files = feedFiles(shownContent(await loadSiteContent(builtSiteSource(resolve(root)))));
29
31
  for (const file of files) {
30
32
  const target = join(outDir, file.path.slice(1));
31
33
  await mkdir(dirname(target), { recursive: true });
@@ -34,12 +36,38 @@ async function writeFeedFiles(outDir, root = process.cwd()) {
34
36
  return files.length;
35
37
  }
36
38
  /**
39
+ * Writes the content API (`/api/`) into the export, as `goodfellow build`
40
+ * does: the site's content as JSON, each page as it's shown, prepared in Node
41
+ * with the site's config as `goodfellow build` prepares it, so both write the
42
+ * same files. Returns how many pages it has, or `undefined` where the
43
+ * config's `api` is `false`.
44
+ */
45
+ async function writeContentApi(outDir, root = process.cwd()) {
46
+ const dir = resolve(root);
47
+ const config = await loadSiteConfig(dir);
48
+ if (config.api === false) return void 0;
49
+ const { contentApi } = await import("@goodfellow-cms/react/server");
50
+ const content = shownContent(await loadSiteContent(builtSiteSource(dir)));
51
+ const pages = sitePages(content, todayIn(content.settings.timeZone), config.blocks);
52
+ const files = await contentApi(config, content, pages, { base: await exportBasePath(dir) });
53
+ await rm(join(outDir, CONTENT_API_DIR), {
54
+ recursive: true,
55
+ force: true
56
+ });
57
+ for (const file of files) {
58
+ const target = join(outDir, file.path.slice(1));
59
+ await mkdir(dirname(target), { recursive: true });
60
+ await writeFile(target, file.content);
61
+ }
62
+ return pages.length;
63
+ }
64
+ /**
37
65
  * The folders the calendar files and feeds go in: `calendars/` and `feeds/`,
38
66
  * and on a site in several languages, the same under each other language's
39
67
  * code, such as `es/feeds/`.
40
68
  */
41
69
  async function buildFileFolders(root = process.cwd()) {
42
- const { settings } = await loadSiteContent(fileSystemSource(resolve(root)));
70
+ const { settings } = await loadSiteContent(builtSiteSource(resolve(root)));
43
71
  return siteLanguages(settings).flatMap((language) => [CALENDARS_DIR, FEEDS_DIR].map((dir) => `${language.prefix.slice(1)}${language.prefix ? "/" : ""}${dir}`));
44
72
  }
45
73
  /** The `basePath` the export was built with, which Next.js records in `.next/routes-manifest.json`. */
@@ -51,6 +79,36 @@ async function exportBasePath(root) {
51
79
  return "";
52
80
  }
53
81
  }
82
+ /**
83
+ * Plugins' parts that builds make: each plugin's admin frame, beside the
84
+ * editor's frames at `admin/plugins/<id>/`, and its build steps, each in a
85
+ * process of its own held to what the plugin declared. Into a static export
86
+ * (`outDir`), or a full-stack site's `public/`, where `next start` serves
87
+ * them. The config is loaded outside Next.js only when it lists plugins.
88
+ */
89
+ async function writePluginFiles(outDir, root = process.cwd()) {
90
+ const site = resolve(root);
91
+ if (!await configHasPlugins(site)) return {
92
+ frames: [],
93
+ steps: []
94
+ };
95
+ const config = await loadSiteConfig(site);
96
+ const base = `${await exportBasePath(site)}/`;
97
+ const dir = resolve(site, outDir);
98
+ return {
99
+ frames: await buildPluginFrames(config.plugins, {
100
+ root: site,
101
+ outDir: dir
102
+ }),
103
+ steps: (await runPluginBuildSteps({
104
+ root: site,
105
+ outDir: dir,
106
+ base,
107
+ plugins: config.plugins ?? [],
108
+ onStep: (step) => console.log(`Running ${step.title}, from ${step.plugin}…`)
109
+ })).map((step) => `${step.plugin}: ${step.title}`)
110
+ };
111
+ }
54
112
  async function exists(path) {
55
113
  return access(path).then(() => true, () => false);
56
114
  }
@@ -61,7 +119,7 @@ async function exists(path) {
61
119
  * subfolder (`rootRobotsUrl()`).
62
120
  */
63
121
  async function writeFontFiles(outDir, root = process.cwd()) {
64
- const { settings } = await loadSiteContent(fileSystemSource(resolve(root)));
122
+ const { settings } = await loadSiteContent(builtSiteSource(resolve(root)));
65
123
  const fonts = await selfHostFonts(outDir, settings.theme, { base: await exportBasePath(root) });
66
124
  const robotsElsewhere = rootRobotsUrl(settings.url);
67
125
  return {
@@ -78,7 +136,7 @@ async function writeFontFiles(outDir, root = process.cwd()) {
78
136
  */
79
137
  async function writeBuildRecordFile(outDir, root = process.cwd()) {
80
138
  const dir = resolve(root);
81
- const content = await loadSiteContent(fileSystemSource(dir));
139
+ const content = await loadSiteContent(builtSiteSource(dir));
82
140
  const date = await reportedDate(join(dir, ".next", BUILD_DATES_DIR));
83
141
  return (await writeBuildRecord(outDir, {
84
142
  root: dir,
@@ -95,7 +153,7 @@ async function writeBuildRecordFile(outDir, root = process.cwd()) {
95
153
  */
96
154
  async function writeRedirectFiles(outDir, root = process.cwd()) {
97
155
  const dir = resolve(root);
98
- const content = shownContent(await loadSiteContent(fileSystemSource(dir)));
156
+ const content = shownContent(await loadSiteContent(builtSiteSource(dir)));
99
157
  const base = await exportBasePath(dir);
100
158
  const files = redirectFiles(content, {
101
159
  taken: allPages(content).map((page) => page.path),
@@ -131,7 +189,7 @@ async function guardExport(outDir, root = process.cwd()) {
131
189
  const admin = parsed?.ok ? parsed.description.admin : void 0;
132
190
  if (!admin) return void 0;
133
191
  const dir = resolve(root);
134
- const content = await loadSiteContent(fileSystemSource(dir));
192
+ const content = await loadSiteContent(builtSiteSource(dir));
135
193
  return guardBuiltSite(outDir, {
136
194
  admin,
137
195
  ...parsed?.ok && parsed.description.broker && { broker: parsed.description.broker },
@@ -215,7 +273,7 @@ function withPageLanguage(html, language) {
215
273
  */
216
274
  async function writePageLanguages(outDir, root = process.cwd()) {
217
275
  const dir = resolve(root);
218
- const { settings } = await loadSiteContent(fileSystemSource(dir));
276
+ const { settings } = await loadSiteContent(builtSiteSource(dir));
219
277
  if (!hasLanguages(settings)) return 0;
220
278
  const base = outDir ?? join(dir, ".next/server/app");
221
279
  const files = (await readdir(base, { recursive: true })).map((file) => file.split("\\").join("/")).filter((file) => file.endsWith(".html") && !/^(admin|_next|pagefind)\//.test(file));
@@ -283,7 +341,7 @@ function contentFile(pages, path) {
283
341
  * Pages that aren't the site's, such as Next.js's own, are left out.
284
342
  */
285
343
  async function checkPages(pages, root = process.cwd()) {
286
- const known = allPages(shownContent(await loadSiteContent(fileSystemSource(resolve(root)))));
344
+ const known = allPages(shownContent(await loadSiteContent(builtSiteSource(resolve(root)))));
287
345
  return checkBuiltPages(pages.flatMap((page) => {
288
346
  const path = page.url.replace(/(.)\/$/, "$1");
289
347
  const file = contentFile(known, path);
@@ -294,5 +352,62 @@ async function checkPages(pages, root = process.cwd()) {
294
352
  }] : [];
295
353
  }));
296
354
  }
355
+ /**
356
+ * Runs one of a plugin's tasks for the admin panel, as `goodfellow task`
357
+ * does for other sites, with the site's config loaded in Node, and writes its
358
+ * answer sealed to the request's key, even when the task fails. Secrets are
359
+ * taken out of the environment before the config loads.
360
+ */
361
+ async function writeTaskResult(root, request, out = TASK_RESULT_FILE) {
362
+ const secrets = taskSecretsFrom(process.env);
363
+ const problem = taskRequestProblem(request);
364
+ if (problem) throw new Error(problem);
365
+ let answer;
366
+ try {
367
+ const plugin = ((await loadSiteConfig(resolve(root))).plugins ?? []).find((candidate) => pluginId(candidate.name) === request.plugin);
368
+ answer = plugin ? await runPluginTask({
369
+ root: resolve(root),
370
+ plugin,
371
+ task: request.task,
372
+ input: request.input,
373
+ secrets
374
+ }) : {
375
+ ok: false,
376
+ error: `The site has no plugin ${request.plugin}.`
377
+ };
378
+ } catch (error) {
379
+ answer = {
380
+ ok: false,
381
+ error: `The site couldn't run the task: ${error instanceof Error ? error.message : error}`
382
+ };
383
+ }
384
+ const sealed = await sealTaskAnswer(answer, request.key);
385
+ await writeFile(resolve(root, out), `${JSON.stringify(sealed)}\n`);
386
+ return sealed;
387
+ }
388
+ /**
389
+ * Renders a page with a block from its own file as well as the site's config's,
390
+ * for `goodfellow-next blocks add`'s check that a block it made renders: both
391
+ * loaded in Node, as `goodfellow-next finish` loads the config.
392
+ */
393
+ function blockPageRender(root) {
394
+ return async (page, block) => {
395
+ const site = resolve(root);
396
+ const config = await loadSiteConfig(site);
397
+ const entry = (await loadSiteModule(site, join(site, block.entry))).default;
398
+ const { createPageRenderer } = await import("@goodfellow-cms/react/server");
399
+ return createPageRenderer({
400
+ ...config,
401
+ blocks: {
402
+ ...config.blocks,
403
+ [block.name]: entry
404
+ }
405
+ })(await loadSiteContent(builtSiteSource(site)), {
406
+ path: "/",
407
+ file: "content/pages/index.json",
408
+ content: page
409
+ });
410
+ };
411
+ }
297
412
  //#endregion
298
- export { EDITOR_PAGES, NEXT_SITE_FILES, buildFileFolders, builtPages, checkPages, exportedPages, fixSegmentFiles, guardExport, writeBuildRecordFile, writeCalendarFiles, writeEditorPolicy, writeFeedFiles, writeFontFiles, writePageLanguages, writeRedirectFiles };
413
+ export { EDITOR_PAGES, NEXT_SITE_FILES, blockPageRender, buildFileFolders, builtPages, checkPages, exportedPages, fixSegmentFiles, guardExport, writeBuildRecordFile, writeCalendarFiles, writeContentApi, writeEditorPolicy, writeFeedFiles, writeFontFiles, writePageLanguages, writePluginFiles, writeRedirectFiles, writeTaskResult };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ByLanguage, CurrentMenuLinks, NextImage, NextLink, PageLanguage } from "./components.js";
2
- import { absoluteUrl, colorSchemeMeta, colorSchemeScript, contentForPage, demoContent, feedLinks, findRedirect, fontPreloads, googleFontsUrl, hasLanguages, languageMap, loadSiteContent, normalizePagePath, pageHead, shownContent, siteLanguages, sitePages, sitemapPages, themeToCss, todayIn, withBase } from "@goodfellow-cms/core";
3
- import { BUILD_DATES_ENV, fileSystemSource, fontStandIns, readMediaSizes, readStylesheet, reportDatesTo } from "@goodfellow-cms/core/node";
2
+ import { absoluteUrl, adminSiteForCopy, colorSchemeMeta, colorSchemeScript, contentForPage, demoContent, describedForCopy, feedLinks, findRedirect, fontPreloads, googleFontsUrl, hasLanguages, languageMap, loadSiteContent, normalizePagePath, pageHead, shownContent, siteCopyFrom, siteLanguages, sitePages, sitemapPages, storedMediaSizes, themeToCss, todayIn, withBase } from "@goodfellow-cms/core";
3
+ import { BUILD_DATES_ENV, builtSiteSource, fileSystemSource, fontStandIns, readMediaSizes, readStylesheet, reportDatesTo } from "@goodfellow-cms/core/node";
4
4
  import { BodyCode, HeadCode, PageBody, PageContent, PageFooter, PageHeader, adminSite, applyPageEntry, createPuckConfigs, describeSite, prepareLayout, preparePage } from "@goodfellow-cms/react";
5
5
  import { notFound, redirect } from "next/navigation";
6
6
  import { cache } from "react";
@@ -24,7 +24,7 @@ function configuredBasePath() {
24
24
  * for the length of one request, so a page and its metadata read it once, and
25
25
  * content edits show on reload in development.
26
26
  */
27
- const loadSite = cache(async (root = process.cwd()) => shownContent(await loadSiteContent(fileSystemSource(root))));
27
+ const loadSite = cache(async (root = process.cwd()) => shownContent(await loadSiteContent(builtSiteSource(root))));
28
28
  function pathOf(segments) {
29
29
  try {
30
30
  return normalizePagePath(`/${(segments ?? []).map((segment) => decodeURIComponent(segment)).join("/")}`);
@@ -87,7 +87,10 @@ function goodfellowPages(config, options = {}) {
87
87
  const renderOptions = async () => ({
88
88
  base: basePath(),
89
89
  components,
90
- media: await readMediaSizes(root)
90
+ media: {
91
+ ...await readMediaSizes(root),
92
+ ...storedMediaSizes((await site()).mediaStorage)
93
+ }
91
94
  });
92
95
  /** The site's theme and fonts, which React moves into the document's head. themeToCss escapes its output. */
93
96
  async function Theme({ content }) {
@@ -134,7 +137,7 @@ function goodfellowPages(config, options = {}) {
134
137
  * editor's frames, at `app/admin/editor/page.tsx`.
135
138
  */
136
139
  admin() {
137
- return adminSite(config);
140
+ return adminSiteForCopy(adminSite(config), siteCopyFrom(process.env));
138
141
  },
139
142
  /** For `app/admin/layout.tsx`: the admin panel's title, kept out of search engines, with the site's icon. */
140
143
  async adminMetadata() {
@@ -268,7 +271,7 @@ function goodfellowPages(config, options = {}) {
268
271
  * edit its content without its code, such as the Claude connector.
269
272
  */
270
273
  siteDescription() {
271
- return Response.json(describeSite(config));
274
+ return Response.json(describedForCopy(describeSite(config), siteCopyFrom(process.env)));
272
275
  },
273
276
  /** For `app/robots.ts`: lets search engines in, and points them at the sitemap once the site's address is set. */
274
277
  async robots() {
@@ -0,0 +1,197 @@
1
+ import { EDITOR_PRELUDE, INSTALLED_PLUGINS_INDEX, PLUGINS_PATH, framedPlugins, pluginFrameEntry, pluginFramePage, pluginId } from "@goodfellow-cms/core";
2
+ import { LICENSES_FILE, licensesPlugin } from "@goodfellow-cms/core/node";
3
+ import { existsSync } from "node:fs";
4
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { isAbsolute, join, resolve } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ //#region \0rolldown/runtime.js
8
+ var __defProp = Object.defineProperty;
9
+ var __exportAll = (all, no_symbols) => {
10
+ let target = {};
11
+ for (var name in all) __defProp(target, name, {
12
+ get: all[name],
13
+ enumerable: true
14
+ });
15
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
16
+ return target;
17
+ };
18
+ //#endregion
19
+ //#region src/plugins.ts
20
+ /**
21
+ * Plugins on Next.js sites: their admin frames, bundled with Rolldown as
22
+ * `next dev` bundles the editor's frames, and their build steps, both run by
23
+ * `goodfellow-next finish` from the site's config, which Next.js alone
24
+ * bundles otherwise. Node only.
25
+ */
26
+ var plugins_exports = /* @__PURE__ */ __exportAll({
27
+ buildPluginFrame: () => buildPluginFrame,
28
+ buildPluginFrames: () => buildPluginFrames,
29
+ configHasPlugins: () => configHasPlugins,
30
+ findConfigFile: () => findConfigFile,
31
+ loadSiteConfig: () => loadSiteConfig,
32
+ loadSiteModule: () => loadSiteModule
33
+ });
34
+ const CONFIG_FILES = [
35
+ "goodfellow.config.tsx",
36
+ "goodfellow.config.ts",
37
+ "goodfellow.config.jsx",
38
+ "goodfellow.config.js"
39
+ ];
40
+ const FRAME_ENTRY = "\0goodfellow:plugin-frame";
41
+ function findConfigFile(root) {
42
+ return CONFIG_FILES.map((name) => join(root, name)).find((file) => existsSync(file));
43
+ }
44
+ /**
45
+ * Whether the site's config lists plugins, read from its text, so builds of
46
+ * sites without any never load the config outside Next.js: plugins of its
47
+ * own, or any installed from the admin panel, which `plugins/installed/`
48
+ * lists.
49
+ */
50
+ async function configHasPlugins(root) {
51
+ const file = findConfigFile(root);
52
+ const listed = file && /\bplugins\s*:\s*([^\n]*)/.exec(await readFile(file, "utf8"))?.[1];
53
+ if (!listed) return false;
54
+ if (!/^installedPlugins\s*,?$/.test(listed.trim())) return true;
55
+ const index = join(root, INSTALLED_PLUGINS_INDEX);
56
+ return existsSync(index) && /\binstalledPlugin\(/.test(await readFile(index, "utf8"));
57
+ }
58
+ /**
59
+ * A module of the site, loaded in Node: bundled with Rolldown, with the
60
+ * packages it imports left for Node to load and stylesheets left out, then
61
+ * imported.
62
+ */
63
+ async function loadSiteModule(root, input) {
64
+ const { build } = await import("rolldown");
65
+ const outDir = join(root, "node_modules/.goodfellow");
66
+ await mkdir(outDir, { recursive: true });
67
+ const file = join(outDir, `module-${process.pid}-${Date.now()}.mjs`);
68
+ await build({
69
+ input,
70
+ cwd: root,
71
+ platform: "node",
72
+ tsconfig: existsSync(join(root, "tsconfig.json")) ? join(root, "tsconfig.json") : false,
73
+ transform: { jsx: { runtime: "automatic" } },
74
+ external: (id, _importer, resolved) => resolved ? /[\\/]node_modules[\\/]/.test(id) : !id.startsWith(".") && !isAbsolute(id) && !id.startsWith("@/"),
75
+ plugins: [{
76
+ name: "goodfellow:no-styles",
77
+ load: (id) => id.endsWith(".css") ? {
78
+ code: "",
79
+ moduleType: "js"
80
+ } : void 0
81
+ }],
82
+ onLog(level, log, handler) {
83
+ if (log.code === "MODULE_LEVEL_DIRECTIVE") return;
84
+ handler(level, log);
85
+ },
86
+ output: {
87
+ file,
88
+ format: "esm",
89
+ codeSplitting: false
90
+ }
91
+ });
92
+ try {
93
+ return await import(pathToFileURL(file).href);
94
+ } finally {
95
+ await rm(file, { force: true });
96
+ }
97
+ }
98
+ /** The site's config, loaded in Node with `loadSiteModule()`. Only what plugins and checks need is read from it. */
99
+ async function loadSiteConfig(root) {
100
+ const configFile = findConfigFile(root);
101
+ if (!configFile) throw new Error(`No goodfellow.config.tsx in ${root}.`);
102
+ return (await loadSiteModule(root, configFile)).default;
103
+ }
104
+ /** A plugin's admin module as the bundler imports it: a package's export, or a path from the site's root. */
105
+ function adminModule(root, specifier) {
106
+ return specifier.startsWith("./") || specifier.startsWith("../") ? resolve(root, specifier) : specifier;
107
+ }
108
+ /**
109
+ * Bundles a plugin's admin module into its frame, at `admin/plugins/<id>/`
110
+ * under `outDir`: one classic script starting with `EDITOR_PRELUDE`, its
111
+ * stylesheets in one file, `pluginFramePage()`, whose Content Security
112
+ * Policy reaches only the addresses the plugin declared, and the notices of
113
+ * what it bundles (`LICENSES_FILE`). Answers a function that stops rebuilding.
114
+ */
115
+ async function buildPluginFrame(plugin, { root, dir, watch }) {
116
+ if (!plugin.admin) throw new Error(`${plugin.name} has no admin module.`);
117
+ const rolldown = await import("rolldown");
118
+ await mkdir(dir, { recursive: true });
119
+ const stylesheets = /* @__PURE__ */ new Set();
120
+ const writePage = async () => {
121
+ const css = await Promise.all([...stylesheets].map((file) => readFile(file, "utf8")));
122
+ await writeFile(join(dir, "plugin.css"), css.join("\n"));
123
+ await writeFile(join(dir, "index.html"), pluginFramePage({
124
+ permissions: plugin.permissions ?? {},
125
+ scripts: ["plugin.js"],
126
+ stylesheets: ["plugin.css"],
127
+ licenses: LICENSES_FILE,
128
+ editor: Boolean(plugin.editor)
129
+ }));
130
+ };
131
+ const options = {
132
+ input: { plugin: FRAME_ENTRY },
133
+ cwd: root,
134
+ platform: "browser",
135
+ tsconfig: existsSync(join(root, "tsconfig.json")) ? join(root, "tsconfig.json") : false,
136
+ transform: {
137
+ jsx: { runtime: "automatic" },
138
+ define: { "process.env.NODE_ENV": JSON.stringify(watch ? "development" : "production") }
139
+ },
140
+ plugins: [{
141
+ name: "goodfellow:plugin-frame",
142
+ resolveId: (id) => id === FRAME_ENTRY ? id : void 0,
143
+ load(id) {
144
+ if (id === FRAME_ENTRY) return pluginFrameEntry(adminModule(root, plugin.admin ?? ""));
145
+ if (id.endsWith(".css")) {
146
+ stylesheets.add(id);
147
+ return {
148
+ code: "",
149
+ moduleType: "js"
150
+ };
151
+ }
152
+ }
153
+ }, licensesPlugin(plugin.title, LICENSES_FILE)],
154
+ onLog(level, log, handler) {
155
+ if (log.code === "MODULE_LEVEL_DIRECTIVE") return;
156
+ handler(level, log);
157
+ },
158
+ output: {
159
+ dir,
160
+ format: "iife",
161
+ codeSplitting: false,
162
+ entryFileNames: "plugin.js",
163
+ banner: EDITOR_PRELUDE,
164
+ ...watch ? { sourcemap: true } : { minify: true }
165
+ }
166
+ };
167
+ if (!watch) {
168
+ await rolldown.build(options);
169
+ await writePage();
170
+ return async () => {};
171
+ }
172
+ const watcher = rolldown.watch(options);
173
+ watcher.on("event", (event) => {
174
+ if (event.code === "BUNDLE_END") {
175
+ event.result.close();
176
+ writePage().then(() => watch.onBuilt(), (error) => watch.onBuilt(error));
177
+ } else if (event.code === "ERROR") {
178
+ event.result?.close();
179
+ watch.onBuilt(event.error);
180
+ }
181
+ });
182
+ return () => watcher.close();
183
+ }
184
+ /** Builds every plugin's admin frame into `admin/plugins/<id>/` in `outDir`. Answers the plugins' names. */
185
+ async function buildPluginFrames(plugins, { root, outDir }) {
186
+ const built = [];
187
+ for (const plugin of framedPlugins(plugins)) {
188
+ await buildPluginFrame(plugin, {
189
+ root,
190
+ dir: join(outDir, PLUGINS_PATH, pluginId(plugin.name))
191
+ });
192
+ built.push(plugin.name);
193
+ }
194
+ return built;
195
+ }
196
+ //#endregion
197
+ export { loadSiteModule as a, loadSiteConfig as i, buildPluginFrames as n, plugins_exports as o, configHasPlugins as r, buildPluginFrame as t };
package/dist/server.d.ts CHANGED
@@ -31,6 +31,13 @@ export interface GoodfellowApiOptions {
31
31
  ai?: ServerKeys;
32
32
  /** Told about failures editors only hear about as "something went wrong". Defaults to `console.error`. */
33
33
  onError?: (error: unknown) => void;
34
+ /**
35
+ * The server modules of the config's plugins, by plugin name, imported in
36
+ * the route's file, such as `{ "goodfellow-plugin-s3": s3Server }` from
37
+ * `import s3Server from "goodfellow-plugin-s3/server"`. Each answers
38
+ * `plugins/<id>/…`, and runs with everything the server has.
39
+ */
40
+ plugins?: AppApiOptions["plugins"];
34
41
  }
35
42
  /** The route parameters of a catch-all route named `[...path]`. */
36
43
  interface RouteContext {
package/dist/server.js CHANGED
@@ -31,7 +31,8 @@ function goodfellowApi(options) {
31
31
  signIn: options.signIn,
32
32
  ...options.signOut && { signOut: options.signOut },
33
33
  ...ai && ai.services.length > 0 && { ai },
34
- ...options.onError && { onError: options.onError }
34
+ ...options.onError && { onError: options.onError },
35
+ ...options.plugins && { plugins: options.plugins }
35
36
  });
36
37
  return api;
37
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfellow-cms/next",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Goodfellow pages and the admin panel in a Next.js site.",
5
5
  "keywords": [
6
6
  "goodfellow",
@@ -63,22 +63,23 @@
63
63
  "dist"
64
64
  ],
65
65
  "dependencies": {
66
- "@goodfellow-cms/ai": "0.5.0",
67
- "@goodfellow-cms/core": "0.5.0",
68
- "@goodfellow-cms/react": "0.5.0",
66
+ "@goodfellow-cms/ai": "0.7.0",
67
+ "@goodfellow-cms/core": "0.7.0",
68
+ "@goodfellow-cms/react": "0.7.0",
69
69
  "rolldown": "1.2.12"
70
70
  },
71
71
  "peerDependencies": {
72
- "@goodfellow-cms/admin": "0.5.0",
72
+ "@goodfellow-cms/admin": "0.7.0",
73
73
  "@puckeditor/core": "0.23.0",
74
74
  "next": "^16.0.0",
75
75
  "react": "^19.0.0",
76
76
  "react-dom": "^19.0.0"
77
77
  },
78
78
  "devDependencies": {
79
- "@goodfellow-cms/admin": "0.5.0",
80
- "@goodfellow-cms/github": "0.5.0",
81
- "@goodfellow-cms/gitlab": "0.5.0",
79
+ "@goodfellow-cms/admin": "0.7.0",
80
+ "@goodfellow-cms/bitbucket": "0.7.0",
81
+ "@goodfellow-cms/github": "0.7.0",
82
+ "@goodfellow-cms/gitlab": "0.7.0",
82
83
  "@puckeditor/core": "0.23.0",
83
84
  "@tailwindcss/browser": "4.3.3",
84
85
  "@types/react": "^19.2.0",