@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 +1 -1
- package/bin/goodfellow-next.js +80 -8
- package/dist/admin.js +1 -0
- package/dist/config.js +10 -4
- package/dist/{dev-editor-DiPKrIAS.js → dev-editor-CzE9HofK.js} +47 -1
- package/dist/export.d.ts +38 -1
- package/dist/export.js +127 -12
- package/dist/index.js +9 -6
- package/dist/plugins-K5i8XztM.js +197 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +2 -1
- package/package.json +9 -8
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
|
|
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
|
|
package/bin/goodfellow-next.js
CHANGED
|
@@ -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
|
|
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({
|
|
107
|
-
|
|
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,
|
|
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,
|
|
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-
|
|
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(
|
|
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 {
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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.
|
|
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.
|
|
67
|
-
"@goodfellow-cms/core": "0.
|
|
68
|
-
"@goodfellow-cms/react": "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.
|
|
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.
|
|
80
|
-
"@goodfellow-cms/
|
|
81
|
-
"@goodfellow-cms/
|
|
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",
|