@goodfellow-cms/next 0.4.0 → 0.6.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.
@@ -0,0 +1,191 @@
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
+ });
33
+ const CONFIG_FILES = [
34
+ "goodfellow.config.tsx",
35
+ "goodfellow.config.ts",
36
+ "goodfellow.config.jsx",
37
+ "goodfellow.config.js"
38
+ ];
39
+ const FRAME_ENTRY = "\0goodfellow:plugin-frame";
40
+ function findConfigFile(root) {
41
+ return CONFIG_FILES.map((name) => join(root, name)).find((file) => existsSync(file));
42
+ }
43
+ /**
44
+ * Whether the site's config lists plugins, read from its text, so builds of
45
+ * sites without any never load the config outside Next.js: plugins of its
46
+ * own, or any installed from the admin panel, which `plugins/installed/`
47
+ * lists.
48
+ */
49
+ async function configHasPlugins(root) {
50
+ const file = findConfigFile(root);
51
+ const listed = file && /\bplugins\s*:\s*([^\n]*)/.exec(await readFile(file, "utf8"))?.[1];
52
+ if (!listed) return false;
53
+ if (!/^installedPlugins\s*,?$/.test(listed.trim())) return true;
54
+ const index = join(root, INSTALLED_PLUGINS_INDEX);
55
+ return existsSync(index) && /\binstalledPlugin\(/.test(await readFile(index, "utf8"));
56
+ }
57
+ /**
58
+ * The site's config, loaded in Node: bundled with Rolldown, with the
59
+ * packages it imports left for Node to load and stylesheets left out, then
60
+ * imported. Only what plugins need is read from it.
61
+ */
62
+ async function loadSiteConfig(root) {
63
+ const configFile = findConfigFile(root);
64
+ if (!configFile) throw new Error(`No goodfellow.config.tsx in ${root}.`);
65
+ const { build } = await import("rolldown");
66
+ const outDir = join(root, "node_modules/.goodfellow");
67
+ await mkdir(outDir, { recursive: true });
68
+ const file = join(outDir, `config-${process.pid}-${Date.now()}.mjs`);
69
+ await build({
70
+ input: configFile,
71
+ cwd: root,
72
+ platform: "node",
73
+ tsconfig: existsSync(join(root, "tsconfig.json")) ? join(root, "tsconfig.json") : false,
74
+ transform: { jsx: { runtime: "automatic" } },
75
+ external: (id, _importer, resolved) => resolved ? /[\\/]node_modules[\\/]/.test(id) : !id.startsWith(".") && !isAbsolute(id) && !id.startsWith("@/"),
76
+ plugins: [{
77
+ name: "goodfellow:no-styles",
78
+ load: (id) => id.endsWith(".css") ? {
79
+ code: "",
80
+ moduleType: "js"
81
+ } : void 0
82
+ }],
83
+ onLog(level, log, handler) {
84
+ if (log.code === "MODULE_LEVEL_DIRECTIVE") return;
85
+ handler(level, log);
86
+ },
87
+ output: {
88
+ file,
89
+ format: "esm",
90
+ codeSplitting: false
91
+ }
92
+ });
93
+ try {
94
+ return (await import(pathToFileURL(file).href)).default;
95
+ } finally {
96
+ await rm(file, { force: true });
97
+ }
98
+ }
99
+ /** A plugin's admin module as the bundler imports it: a package's export, or a path from the site's root. */
100
+ function adminModule(root, specifier) {
101
+ return specifier.startsWith("./") || specifier.startsWith("../") ? resolve(root, specifier) : specifier;
102
+ }
103
+ /**
104
+ * Bundles a plugin's admin module into its frame, at `admin/plugins/<id>/`
105
+ * under `outDir`: one classic script starting with `EDITOR_PRELUDE`, its
106
+ * stylesheets in one file, `pluginFramePage()`, whose Content Security
107
+ * Policy reaches only the addresses the plugin declared, and the notices of
108
+ * what it bundles (`LICENSES_FILE`). Answers a function that stops rebuilding.
109
+ */
110
+ async function buildPluginFrame(plugin, { root, dir, watch }) {
111
+ if (!plugin.admin) throw new Error(`${plugin.name} has no admin module.`);
112
+ const rolldown = await import("rolldown");
113
+ await mkdir(dir, { recursive: true });
114
+ const stylesheets = /* @__PURE__ */ new Set();
115
+ const writePage = async () => {
116
+ const css = await Promise.all([...stylesheets].map((file) => readFile(file, "utf8")));
117
+ await writeFile(join(dir, "plugin.css"), css.join("\n"));
118
+ await writeFile(join(dir, "index.html"), pluginFramePage({
119
+ permissions: plugin.permissions ?? {},
120
+ scripts: ["plugin.js"],
121
+ stylesheets: ["plugin.css"],
122
+ licenses: LICENSES_FILE
123
+ }));
124
+ };
125
+ const options = {
126
+ input: { plugin: FRAME_ENTRY },
127
+ cwd: root,
128
+ platform: "browser",
129
+ tsconfig: existsSync(join(root, "tsconfig.json")) ? join(root, "tsconfig.json") : false,
130
+ transform: {
131
+ jsx: { runtime: "automatic" },
132
+ define: { "process.env.NODE_ENV": JSON.stringify(watch ? "development" : "production") }
133
+ },
134
+ plugins: [{
135
+ name: "goodfellow:plugin-frame",
136
+ resolveId: (id) => id === FRAME_ENTRY ? id : void 0,
137
+ load(id) {
138
+ if (id === FRAME_ENTRY) return pluginFrameEntry(adminModule(root, plugin.admin ?? ""));
139
+ if (id.endsWith(".css")) {
140
+ stylesheets.add(id);
141
+ return {
142
+ code: "",
143
+ moduleType: "js"
144
+ };
145
+ }
146
+ }
147
+ }, licensesPlugin(plugin.title, LICENSES_FILE)],
148
+ onLog(level, log, handler) {
149
+ if (log.code === "MODULE_LEVEL_DIRECTIVE") return;
150
+ handler(level, log);
151
+ },
152
+ output: {
153
+ dir,
154
+ format: "iife",
155
+ codeSplitting: false,
156
+ entryFileNames: "plugin.js",
157
+ banner: EDITOR_PRELUDE,
158
+ ...watch ? { sourcemap: true } : { minify: true }
159
+ }
160
+ };
161
+ if (!watch) {
162
+ await rolldown.build(options);
163
+ await writePage();
164
+ return async () => {};
165
+ }
166
+ const watcher = rolldown.watch(options);
167
+ watcher.on("event", (event) => {
168
+ if (event.code === "BUNDLE_END") {
169
+ event.result.close();
170
+ writePage().then(() => watch.onBuilt(), (error) => watch.onBuilt(error));
171
+ } else if (event.code === "ERROR") {
172
+ event.result?.close();
173
+ watch.onBuilt(event.error);
174
+ }
175
+ });
176
+ return () => watcher.close();
177
+ }
178
+ /** Builds every plugin's admin frame into `admin/plugins/<id>/` in `outDir`. Answers the plugins' names. */
179
+ async function buildPluginFrames(plugins, { root, outDir }) {
180
+ const built = [];
181
+ for (const plugin of framedPlugins(plugins)) {
182
+ await buildPluginFrame(plugin, {
183
+ root,
184
+ dir: join(outDir, PLUGINS_PATH, pluginId(plugin.name))
185
+ });
186
+ built.push(plugin.name);
187
+ }
188
+ return built;
189
+ }
190
+ //#endregion
191
+ export { plugins_exports as a, loadSiteConfig as i, buildPluginFrames as n, 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
  }
@@ -0,0 +1,50 @@
1
+ //#region src/site-files.ts
2
+ /** The release whose Next.js sites have the editor's sandboxed frames at `app/admin/editor/`. */
3
+ const FRAMES_RELEASE = "0.5.0";
4
+ /** `app/admin/page.tsx` as every release before the frames wrote it, loading the site's config itself. */
5
+ const CLIENT_ADMIN_PAGE = `"use client";
6
+
7
+ import { GoodfellowAdmin } from "@goodfellow-cms/next/admin";
8
+ import config from "@/goodfellow.config";
9
+
10
+ export default function AdminPage() {
11
+ return <GoodfellowAdmin config={config} />;
12
+ }
13
+ `;
14
+ const ADMIN_PAGE = `import { GoodfellowAdmin } from "@goodfellow-cms/next/admin";
15
+ import { site } from "@/lib/site";
16
+
17
+ // The admin panel's page never loads the site's config: it's told about the site's blocks here, on the server,
18
+ // and the blocks themselves run in the editor's frames, at /admin/editor/.
19
+ export default function AdminPage() {
20
+ return <GoodfellowAdmin site={site.admin()} />;
21
+ }
22
+ `;
23
+ const EDITOR_PAGE = `"use client";
24
+
25
+ import { GoodfellowEditor } from "@goodfellow-cms/next/editor";
26
+ import config from "@/goodfellow.config";
27
+
28
+ // The editor's frames: Puck and the site's blocks, sandboxed away from the admin panel's page.
29
+ export default function EditorPage() {
30
+ return <GoodfellowEditor config={config} />;
31
+ }
32
+ `;
33
+ /**
34
+ * The Next.js starter's files that `goodfellow-next update` keeps up to date
35
+ * where nobody has changed them, as the starter now has them. A test checks
36
+ * they match `templates/next`.
37
+ */
38
+ const NEXT_SITE_FILES = [{
39
+ path: "app/admin/page.tsx",
40
+ since: FRAMES_RELEASE,
41
+ content: ADMIN_PAGE,
42
+ earlier: [CLIENT_ADMIN_PAGE]
43
+ }, {
44
+ path: "app/admin/editor/page.tsx",
45
+ since: FRAMES_RELEASE,
46
+ content: EDITOR_PAGE,
47
+ earlier: []
48
+ }];
49
+ //#endregion
50
+ export { EDITOR_PAGE as n, NEXT_SITE_FILES as r, ADMIN_PAGE as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfellow-cms/next",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Goodfellow pages and the admin panel in a Next.js site.",
5
5
  "keywords": [
6
6
  "goodfellow",
@@ -38,6 +38,11 @@
38
38
  "import": "./dist/admin.js",
39
39
  "default": "./dist/admin.js"
40
40
  },
41
+ "./editor": {
42
+ "types": "./dist/editor.d.ts",
43
+ "import": "./dist/editor.js",
44
+ "default": "./dist/editor.js"
45
+ },
41
46
  "./export": {
42
47
  "types": "./dist/export.d.ts",
43
48
  "import": "./dist/export.js",
@@ -58,31 +63,33 @@
58
63
  "dist"
59
64
  ],
60
65
  "dependencies": {
61
- "@goodfellow-cms/ai": "0.4.0",
62
- "@goodfellow-cms/react": "0.4.0",
63
- "@goodfellow-cms/core": "0.4.0"
66
+ "@goodfellow-cms/ai": "0.6.0",
67
+ "@goodfellow-cms/core": "0.6.0",
68
+ "@goodfellow-cms/react": "0.6.0",
69
+ "rolldown": "1.2.12"
64
70
  },
65
71
  "peerDependencies": {
72
+ "@goodfellow-cms/admin": "0.6.0",
66
73
  "@puckeditor/core": "0.23.0",
67
74
  "next": "^16.0.0",
68
75
  "react": "^19.0.0",
69
- "react-dom": "^19.0.0",
70
- "@goodfellow-cms/admin": "0.4.0"
76
+ "react-dom": "^19.0.0"
71
77
  },
72
78
  "devDependencies": {
79
+ "@goodfellow-cms/admin": "0.6.0",
80
+ "@goodfellow-cms/bitbucket": "0.6.0",
81
+ "@goodfellow-cms/github": "0.6.0",
82
+ "@goodfellow-cms/gitlab": "0.6.0",
73
83
  "@puckeditor/core": "0.23.0",
74
84
  "@tailwindcss/browser": "4.3.3",
75
85
  "@types/react": "^19.2.0",
76
86
  "@types/react-dom": "^19.2.0",
77
- "next": "16.3.8",
87
+ "next": "16.4.3",
78
88
  "react": "19.3.0",
79
- "react-dom": "19.3.0",
80
- "@goodfellow-cms/admin": "0.4.0",
81
- "@goodfellow-cms/github": "0.4.0",
82
- "@goodfellow-cms/gitlab": "0.4.0"
89
+ "react-dom": "19.3.0"
83
90
  },
84
91
  "engines": {
85
- "node": ">=22"
92
+ "node": "^22.23.4 || ^24.22.1 || >=26.12.1"
86
93
  },
87
94
  "publishConfig": {
88
95
  "access": "public"