create-goodfellow 0.1.0 → 0.3.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 +2 -2
- package/dist/cli.js +36 -3
- package/dist/index.d.ts +6 -79
- package/dist/index.js +2 -2
- package/dist/scaffold-BGxzPdN1.js +150 -0
- package/package.json +8 -8
- package/templates/next/.github/workflows/deploy.yml +37 -2
- package/templates/next/.gitlab-ci.yml +12 -2
- package/templates/next/README.md +14 -7
- package/templates/next/app/admin/layout.tsx +2 -2
- package/templates/next/app/goodfellow-site.json/route.ts +5 -0
- package/templates/next/app/layout.tsx +2 -1
- package/templates/next/content/site.json +3 -1
- package/templates/parish/.github/workflows/deploy.yml +37 -2
- package/templates/parish/.gitlab-ci.yml +12 -2
- package/templates/parish/README.md +14 -7
- package/templates/parish/content/collections/events/_collection.json +19 -20
- package/templates/parish/content/collections/events/advent-penance-service.json +5 -3
- package/templates/parish/content/collections/events/all-souls.json +5 -3
- package/templates/parish/content/collections/events/christmas-concert.json +5 -3
- package/templates/parish/content/collections/events/fall-festival.json +5 -3
- package/templates/parish/content/collections/events/youth-group.json +23 -0
- package/templates/parish/content/collections/news/_collection.json +24 -0
- package/templates/parish/content/collections/news/bell-tower-repairs.json +13 -0
- package/templates/parish/content/collections/news/choir-new-voices.json +14 -0
- package/templates/parish/content/collections/news/food-drive.json +4 -1
- package/templates/parish/content/collections/news/new-website.json +4 -1
- package/templates/parish/content/collections/news/winter-coats.json +13 -0
- package/templates/parish/content/collections/news/youth-group.json +4 -1
- package/templates/parish/content/collections/news/youth-service-day.json +14 -0
- package/templates/parish/content/menus.json +4 -0
- package/templates/parish/content/pages/about.json +48 -5
- package/templates/parish/content/pages/calendar.json +59 -0
- package/templates/parish/content/pages/events.json +27 -11
- package/templates/parish/content/pages/index.json +4 -37
- package/templates/parish/content/pages/news.json +14 -1
- package/templates/parish/content/site.json +4 -1
- package/templates/parish/vercel.ts +65 -0
- package/templates/registry/custom-html.json +1 -1
- package/templates/registry/registry.json +11 -11
- package/templates/registry/shadcn-call-to-action.json +1 -1
- package/templates/registry/shadcn-cards.json +1 -1
- package/templates/registry/shadcn-carousel.json +1 -1
- package/templates/registry/shadcn-faq.json +1 -1
- package/templates/registry/shadcn-hero.json +1 -1
- package/templates/registry/shadcn-notice.json +1 -1
- package/templates/registry/shadcn-pricing.json +1 -1
- package/templates/registry/shadcn-tabs.json +1 -1
- package/templates/registry/shadcn-testimonials.json +1 -1
- package/templates/starter/.github/workflows/deploy.yml +37 -2
- package/templates/starter/.gitlab-ci.yml +12 -2
- package/templates/starter/README.md +14 -7
- package/templates/starter/content/site.json +3 -1
- package/templates/starter/vercel.ts +65 -0
- package/templates/versions.json +12 -11
- package/dist/scaffold-4nOF_fOJ.js +0 -207
- package/templates/next/tsconfig.tsbuildinfo +0 -1
- package/templates/parish/vercel.json +0 -7
- package/templates/starter/vercel.json +0 -7
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Vercel's settings for this site, which Vercel reads when it builds the site.
|
|
2
|
+
//
|
|
3
|
+
// Vercel also sends the redirects from Site Settings → Redirects (content/redirects.json) itself,
|
|
4
|
+
// so visitors go straight to the new address. Like the pages the build writes at old addresses
|
|
5
|
+
// for other hosts, a redirect from an address a page has is left out: pages always win.
|
|
6
|
+
// This file uses only Node's own modules, since Vercel may read it before installing packages.
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
export const config = {
|
|
11
|
+
framework: null,
|
|
12
|
+
installCommand: "npm ci",
|
|
13
|
+
buildCommand: "npx goodfellow build",
|
|
14
|
+
outputDirectory: "dist",
|
|
15
|
+
redirects: redirects(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface Redirect {
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJson(file: string): Record<string, unknown> | undefined {
|
|
24
|
+
return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Every file in a folder and its folders, as paths inside it. */
|
|
28
|
+
function filesIn(dir: string): string[] {
|
|
29
|
+
if (!existsSync(dir)) return [];
|
|
30
|
+
return readdirSync(dir, { recursive: true, encoding: "utf8" }).map((file) => file.split("\\").join("/"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The addresses that have pages: `content/pages/about.json` is /about, and each collection item has its own. */
|
|
34
|
+
function pageAddresses(): Set<string> {
|
|
35
|
+
const addresses = new Set<string>();
|
|
36
|
+
for (const file of filesIn("content/pages")) {
|
|
37
|
+
if (file.endsWith(".json")) addresses.add(`/${file.slice(0, -".json".length)}`.replace(/\/?index$/, "") || "/");
|
|
38
|
+
}
|
|
39
|
+
for (const id of filesIn("content/collections").filter((file) => file.endsWith("/_collection.json"))) {
|
|
40
|
+
const folder = id.slice(0, -"/_collection.json".length);
|
|
41
|
+
const pattern = readJson(join("content/collections", id))?.path;
|
|
42
|
+
if (typeof pattern !== "string") continue;
|
|
43
|
+
for (const file of readdirSync(join("content/collections", folder))) {
|
|
44
|
+
const slug = /^(.+)\.(json|md)$/.exec(file)?.[1];
|
|
45
|
+
if (slug && file !== "_collection.json") addresses.add(pattern.replace("{slug}", slug).replace(/\/+$/, ""));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return addresses;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An address as Vercel matches it, with the characters its patterns use escaped. */
|
|
52
|
+
function source(path: string): string {
|
|
53
|
+
return encodeURI(path).replace(/[()[\]{}+*?:!]/g, "\\$&");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function redirects() {
|
|
57
|
+
const list = (readJson("content/redirects.json")?.redirects ?? []) as Redirect[];
|
|
58
|
+
const pages = pageAddresses();
|
|
59
|
+
return list.flatMap(({ from, to }) => {
|
|
60
|
+
const path = from.replace(/\/+$/, "");
|
|
61
|
+
if (pages.has(path)) return [];
|
|
62
|
+
// With and without a trailing slash, as the other hosts serve the page there.
|
|
63
|
+
return [path, `${path}/`].map((address) => ({ source: source(address), destination: to, permanent: true }));
|
|
64
|
+
});
|
|
65
|
+
}
|
package/templates/versions.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@goodfellow-cms/admin": "0.
|
|
3
|
-
"@goodfellow-cms/ai": "0.
|
|
4
|
-
"@goodfellow-cms/blocks": "0.
|
|
5
|
-
"goodfellow": "0.
|
|
6
|
-
"@goodfellow-cms/core": "0.
|
|
7
|
-
"create-goodfellow": "0.
|
|
8
|
-
"@goodfellow-cms/github": "0.
|
|
9
|
-
"@goodfellow-cms/gitlab": "0.
|
|
10
|
-
"@goodfellow-cms/
|
|
11
|
-
"@goodfellow-cms/
|
|
12
|
-
"@goodfellow-cms/
|
|
2
|
+
"@goodfellow-cms/admin": "0.3.0",
|
|
3
|
+
"@goodfellow-cms/ai": "0.3.0",
|
|
4
|
+
"@goodfellow-cms/blocks": "0.3.0",
|
|
5
|
+
"goodfellow": "0.3.0",
|
|
6
|
+
"@goodfellow-cms/core": "0.3.0",
|
|
7
|
+
"create-goodfellow": "0.3.0",
|
|
8
|
+
"@goodfellow-cms/github": "0.3.0",
|
|
9
|
+
"@goodfellow-cms/gitlab": "0.3.0",
|
|
10
|
+
"@goodfellow-cms/mcp": "0.3.0",
|
|
11
|
+
"@goodfellow-cms/next": "0.3.0",
|
|
12
|
+
"@goodfellow-cms/react": "0.3.0",
|
|
13
|
+
"@goodfellow-cms/registry": "0.3.0"
|
|
13
14
|
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { cp, mkdir, readFile, readdir, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { EMPTY_RECORD, GOODFELLOW_REGISTRY, availableBlocks, planInstall } from "@goodfellow-cms/core";
|
|
4
|
-
//#region src/scaffold.ts
|
|
5
|
-
/** The sites a new site can start from. The keys are what `--template` takes. */
|
|
6
|
-
const TEMPLATES = {
|
|
7
|
-
starter: {
|
|
8
|
-
label: "Starter",
|
|
9
|
-
description: "A simple site with a few pages and a news section"
|
|
10
|
-
},
|
|
11
|
-
parish: {
|
|
12
|
-
label: "Parish example",
|
|
13
|
-
description: "A made-up parish with Mass times, events, news, bulletins and staff, and blocks of its own"
|
|
14
|
-
},
|
|
15
|
-
next: {
|
|
16
|
-
label: "Starter for Next.js",
|
|
17
|
-
description: "The starter as a Next.js site, for developers who want their own Next.js pages beside it"
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
/** Where the built site is served from. Each has its own setup file in the template. */
|
|
21
|
-
const HOSTS = {
|
|
22
|
-
"github-pages": {
|
|
23
|
-
label: "GitHub Pages",
|
|
24
|
-
files: [".github/workflows/deploy.yml"],
|
|
25
|
-
note: "Free for public repositories. Not allowed for online businesses or shops."
|
|
26
|
-
},
|
|
27
|
-
"gitlab-pages": {
|
|
28
|
-
label: "GitLab Pages",
|
|
29
|
-
files: [".gitlab-ci.yml"],
|
|
30
|
-
note: "Free, including for private projects. No rule against business sites that we know of."
|
|
31
|
-
},
|
|
32
|
-
vercel: {
|
|
33
|
-
label: "Vercel",
|
|
34
|
-
files: ["vercel.json"],
|
|
35
|
-
note: "Works with GitHub and GitLab. The free plan is for non-commercial sites only."
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
/** The hosts that can serve a site stored with `backend`: GitHub Pages only builds GitHub repositories, and so on. */
|
|
39
|
-
function hostsFor(backend) {
|
|
40
|
-
if (backend === "github") return ["github-pages", "vercel"];
|
|
41
|
-
if (backend === "gitlab") return ["gitlab-pages", "vercel"];
|
|
42
|
-
return [
|
|
43
|
-
"github-pages",
|
|
44
|
-
"gitlab-pages",
|
|
45
|
-
"vercel"
|
|
46
|
-
];
|
|
47
|
-
}
|
|
48
|
-
/** A problem the person creating the site can fix, described in plain words. */
|
|
49
|
-
var ScaffoldError = class extends Error {
|
|
50
|
-
name = "ScaffoldError";
|
|
51
|
-
};
|
|
52
|
-
/** Never copied, wherever they are: installed packages and build caches. */
|
|
53
|
-
const SKIPPED = /* @__PURE__ */ new Set(["node_modules", ".turbo"]);
|
|
54
|
-
/** Never copied from the template's own folder: build output, and files only the Goodfellow repository uses. */
|
|
55
|
-
const SKIPPED_AT_ROOT = /* @__PURE__ */ new Set([
|
|
56
|
-
"dist",
|
|
57
|
-
"out",
|
|
58
|
-
".next",
|
|
59
|
-
"next-env.d.ts",
|
|
60
|
-
"turbo.json"
|
|
61
|
-
]);
|
|
62
|
-
/**
|
|
63
|
-
* npm leaves `.gitignore` files out of published packages, so templates are
|
|
64
|
-
* bundled with `_gitignore` instead, and it's renamed back in the new site.
|
|
65
|
-
*/
|
|
66
|
-
const BUNDLED_GITIGNORE = "_gitignore";
|
|
67
|
-
/**
|
|
68
|
-
* Copies a template's files, leaving out installed packages and build output.
|
|
69
|
-
* `bundling` renames `.gitignore` files for publishing; otherwise they're renamed back.
|
|
70
|
-
*/
|
|
71
|
-
async function copyTemplate(from, to, { bundling = false } = {}) {
|
|
72
|
-
await cp(from, to, {
|
|
73
|
-
recursive: true,
|
|
74
|
-
filter: (source) => {
|
|
75
|
-
const path = relative(from, source).split(sep);
|
|
76
|
-
return !path.some((part) => SKIPPED.has(part)) && !SKIPPED_AT_ROOT.has(path[0] ?? "");
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
const [find, replace] = bundling ? [".gitignore", BUNDLED_GITIGNORE] : [BUNDLED_GITIGNORE, ".gitignore"];
|
|
80
|
-
for (const file of await readdir(to, { recursive: true })) if (basename(file) === find) await rename(join(to, file), join(to, file.slice(0, -find.length) + replace));
|
|
81
|
-
}
|
|
82
|
-
/** A valid npm package name from a folder name, such as `My Parish` → `my-parish`. */
|
|
83
|
-
function packageName(folder) {
|
|
84
|
-
return basename(resolve(folder)).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[-]+$/g, "").slice(0, 214) || "my-site";
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Reads a repository from what someone typed: `owner/name`, or its address on
|
|
88
|
-
* GitHub or GitLab. Returns `undefined` if it isn't one.
|
|
89
|
-
*/
|
|
90
|
-
function parseRepo(host, input) {
|
|
91
|
-
let path = input.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
92
|
-
const prefix = host === "github" ? /^(?:https?:\/\/)?(?:www\.)?github\.com\//i : /^(?:https?:\/\/)?gitlab\.com\//i;
|
|
93
|
-
path = path.replace(prefix, "");
|
|
94
|
-
return (host === "github" ? /^[A-Za-z0-9-]+\/[A-Za-z0-9._-]+$/ : /^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9._-]+)+$/).test(path) ? path : void 0;
|
|
95
|
-
}
|
|
96
|
-
/** The template's `package.json`, named for the new site and with workspace links replaced by published versions. */
|
|
97
|
-
function sitePackageJson(source, name, versions) {
|
|
98
|
-
const pkg = JSON.parse(source);
|
|
99
|
-
const result = {
|
|
100
|
-
...pkg,
|
|
101
|
-
name
|
|
102
|
-
};
|
|
103
|
-
for (const key of ["dependencies", "devDependencies"]) {
|
|
104
|
-
const deps = pkg[key];
|
|
105
|
-
if (!deps) continue;
|
|
106
|
-
result[key] = Object.fromEntries(Object.entries(deps).map(([dep, range]) => {
|
|
107
|
-
if (!range.startsWith("workspace:")) return [dep, range];
|
|
108
|
-
const version = versions[dep];
|
|
109
|
-
if (!version) throw new Error(`No version of ${dep} to use in the new site.`);
|
|
110
|
-
return [dep, `^${version}`];
|
|
111
|
-
}));
|
|
112
|
-
}
|
|
113
|
-
return `${JSON.stringify(result, null, 2)}\n`;
|
|
114
|
-
}
|
|
115
|
-
/** Turns on one of the commented-out `backend` lines in a template's `goodfellow.config.tsx`. */
|
|
116
|
-
function configureBackend(config, backend) {
|
|
117
|
-
const importLine = `// import { ${backend.host} } from "@goodfellow-cms/${backend.host}";`;
|
|
118
|
-
const backendLine = new RegExp(`^([ \\t]*)// backend: ${backend.host}\\(.*$`, "m");
|
|
119
|
-
if (!config.includes(importLine) || !backendLine.test(config)) throw new Error(`The template's goodfellow.config.tsx has no ${backend.host} line to turn on.`);
|
|
120
|
-
const call = backend.host === "github" ? `github({ repo: ${JSON.stringify(backend.repo)} })` : `gitlab({ project: ${JSON.stringify(backend.repo)} })`;
|
|
121
|
-
return config.replace(importLine, importLine.slice(3)).replace(backendLine, `$1backend: ${call},`);
|
|
122
|
-
}
|
|
123
|
-
/** Notes in a site's README for people reading it in the Goodfellow repository, which new sites don't need. */
|
|
124
|
-
const REPOSITORY_NOTE = /<!-- goodfellow-repository -->[\s\S]*?<!-- \/goodfellow-repository -->\n*/g;
|
|
125
|
-
/** A site's README without the notes for people reading it in the Goodfellow repository. */
|
|
126
|
-
function withoutRepositoryNotes(readme) {
|
|
127
|
-
return readme.replace(REPOSITORY_NOTE, "");
|
|
128
|
-
}
|
|
129
|
-
/** Which blocks a new site starts with, besides the built-in ones. */
|
|
130
|
-
const BLOCK_CHOICES = {
|
|
131
|
-
recommended: {
|
|
132
|
-
label: "Recommended",
|
|
133
|
-
description: "Adds the recommended blocks, such as Hero, Cards and FAQ. Others can be added in the admin panel."
|
|
134
|
-
},
|
|
135
|
-
"built-in": {
|
|
136
|
-
label: "Built-in blocks only",
|
|
137
|
-
description: "Starts with Goodfellow's own blocks. More can be added in the admin panel."
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
/** Reads a built registry from disk, as the admin panel would read it from its address. */
|
|
141
|
-
function localRegistry(dir) {
|
|
142
|
-
const base = "https://registry.invalid/r/";
|
|
143
|
-
return {
|
|
144
|
-
registries: { [GOODFELLOW_REGISTRY]: `${base}{name}.json` },
|
|
145
|
-
fetchJson: async (url) => JSON.parse(await readFile(join(dir, url.slice(27)), "utf8"))
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Installs the registry's recommended blocks into a new site, as the admin
|
|
150
|
-
* panel's Blocks screen would, so the site can add and remove them there later.
|
|
151
|
-
*/
|
|
152
|
-
async function installRecommendedBlocks(target, registryDir) {
|
|
153
|
-
const { registries, fetchJson } = localRegistry(registryDir);
|
|
154
|
-
const recommended = (await availableBlocks(registries, fetchJson)).filter((block) => block.recommended);
|
|
155
|
-
const readSiteFile = (path) => readFile(join(target, path), "utf8").catch(() => void 0);
|
|
156
|
-
let record = EMPTY_RECORD;
|
|
157
|
-
for (const block of recommended) {
|
|
158
|
-
const plan = await planInstall({
|
|
159
|
-
ref: block.ref,
|
|
160
|
-
registries,
|
|
161
|
-
fetchJson,
|
|
162
|
-
readFile: readSiteFile,
|
|
163
|
-
record,
|
|
164
|
-
blocks: []
|
|
165
|
-
});
|
|
166
|
-
for (const change of plan.changes) {
|
|
167
|
-
if (!("content" in change)) continue;
|
|
168
|
-
await mkdir(dirname(join(target, change.path)), { recursive: true });
|
|
169
|
-
await writeFile(join(target, change.path), change.content);
|
|
170
|
-
}
|
|
171
|
-
record = plan.record;
|
|
172
|
-
}
|
|
173
|
-
return recommended.map((block) => block.title);
|
|
174
|
-
}
|
|
175
|
-
async function isEmptyFolder(path) {
|
|
176
|
-
try {
|
|
177
|
-
return (await readdir(path)).length === 0;
|
|
178
|
-
} catch (error) {
|
|
179
|
-
if (error.code === "ENOENT") return true;
|
|
180
|
-
throw error;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
/** Creates a new site in `target` from a template. */
|
|
184
|
-
async function scaffold({ template, target, versions, backend, host, registryDir }) {
|
|
185
|
-
if (!await isEmptyFolder(target)) throw new ScaffoldError(`The folder ${target} already has files in it. Choose a new folder for the site.`);
|
|
186
|
-
await mkdir(target, { recursive: true });
|
|
187
|
-
await copyTemplate(template, target);
|
|
188
|
-
const pkgPath = join(target, "package.json");
|
|
189
|
-
await writeFile(pkgPath, sitePackageJson(await readFile(pkgPath, "utf8"), packageName(target), versions));
|
|
190
|
-
const readmePath = join(target, "README.md");
|
|
191
|
-
const readme = await readFile(readmePath, "utf8").catch(() => void 0);
|
|
192
|
-
if (readme !== void 0) await writeFile(readmePath, withoutRepositoryNotes(readme));
|
|
193
|
-
if (registryDir) await installRecommendedBlocks(target, registryDir);
|
|
194
|
-
if (backend) {
|
|
195
|
-
const configPath = join(target, "goodfellow.config.tsx");
|
|
196
|
-
await writeFile(configPath, configureBackend(await readFile(configPath, "utf8"), backend));
|
|
197
|
-
}
|
|
198
|
-
if (host) {
|
|
199
|
-
for (const [name, { files }] of Object.entries(HOSTS)) {
|
|
200
|
-
if (name === host) continue;
|
|
201
|
-
for (const file of files) await rm(join(target, file), { force: true });
|
|
202
|
-
}
|
|
203
|
-
for (const dir of [".github/workflows", ".github"]) if (await isEmptyFolder(join(target, dir))) await rmdir(join(target, dir)).catch(() => void 0);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
//#endregion
|
|
207
|
-
export { TEMPLATES as a, hostsFor as c, parseRepo as d, scaffold as f, ScaffoldError as i, installRecommendedBlocks as l, withoutRepositoryNotes as m, BUNDLED_GITIGNORE as n, configureBackend as o, sitePackageJson as p, HOSTS as r, copyTemplate as s, BLOCK_CHOICES as t, packageName as u };
|