create-shapio 0.5.6 → 0.5.8
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/dist/index.js +10 -313
- package/dist/index.js.map +1 -1
- package/dist/main-CJjrw1fB.js +320 -0
- package/dist/main-CJjrw1fB.js.map +1 -0
- package/package.json +1 -1
- package/templates/astro/README.md +1 -2
- package/templates/astro/package.json +2 -2
- package/templates/astro/scripts/seed.ts +54 -10
- package/templates/next/README.md +1 -2
- package/templates/next/package.json +3 -3
- package/templates/next/scripts/seed.ts +54 -10
- package/templates/sveltekit/README.md +1 -2
- package/templates/sveltekit/package.json +2 -2
- package/templates/sveltekit/scripts/seed.ts +54 -10
package/dist/index.js
CHANGED
|
@@ -1,319 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { randomBytes } from "node:crypto";
|
|
7
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
8
|
-
//#region src/templates.ts
|
|
9
|
-
const packageJson = ({ projectName, shapioSpec }) => `${JSON.stringify({
|
|
10
|
-
name: projectName,
|
|
11
|
-
private: true,
|
|
12
|
-
type: "module",
|
|
13
|
-
engines: { node: ">=24" },
|
|
14
|
-
scripts: {
|
|
15
|
-
start: "shapio start",
|
|
16
|
-
migrate: "shapio migrate",
|
|
17
|
-
worker: "shapio worker",
|
|
18
|
-
status: "shapio status"
|
|
19
|
-
},
|
|
20
|
-
dependencies: { "@shapio/cms": shapioSpec }
|
|
21
|
-
}, null, 2)}\n`;
|
|
22
|
-
const env = ({ databaseUrl, sessionSecret }) => [
|
|
23
|
-
"# Shapio configuration. Every setting is an environment variable; the `shapio` command loads this file.",
|
|
24
|
-
"# Real environment variables override values here.",
|
|
25
|
-
"NODE_ENV=production",
|
|
26
|
-
"HOST=127.0.0.1",
|
|
27
|
-
"PORT=4300",
|
|
28
|
-
"LOG_LEVEL=info",
|
|
29
|
-
"",
|
|
30
|
-
"# The origin people reach Shapio at; every absolute URL is built from it. Change it when you deploy.",
|
|
31
|
-
"PUBLIC_URL=http://localhost:4300",
|
|
32
|
-
"# BASE_PATH=/cms",
|
|
33
|
-
"",
|
|
34
|
-
"# HTTPS without a reverse proxy: set PUBLIC_URL=https://your.domain and PORT=443, and point these at your",
|
|
35
|
-
"# certificate and key (PEM). Renewed files are picked up without a restart. HTTP_PORT=80 redirects to HTTPS.",
|
|
36
|
-
"# TLS_CERT_FILE=/etc/shapio/tls/fullchain.pem",
|
|
37
|
-
"# TLS_KEY_FILE=/etc/shapio/tls/privkey.pem",
|
|
38
|
-
"# HTTP_PORT=80",
|
|
39
|
-
"",
|
|
40
|
-
"# PostgreSQL >= 16, or sqlite:<path> for a single-process install",
|
|
41
|
-
`DATABASE_URL=${databaseUrl}`,
|
|
42
|
-
"MIGRATE_ON_START=true",
|
|
43
|
-
"",
|
|
44
|
-
"# Signs preview links, private-media URLs and app-user tokens, and encrypts stored secrets.",
|
|
45
|
-
"# Generated for this project; keep it secret, and back it up with the database.",
|
|
46
|
-
`SESSION_SECRET=${sessionSecret}`,
|
|
47
|
-
"",
|
|
48
|
-
"# SETUP_REQUIRE_TOKEN=true # require the logged one-time token for first-run setup (for installs exposed before setup)",
|
|
49
|
-
"",
|
|
50
|
-
"# inline: jobs run inside the server process. dedicated: run `npm run worker` as a second process.",
|
|
51
|
-
"WORKER_MODE=inline",
|
|
52
|
-
"",
|
|
53
|
-
"MEDIA_PATH=./media",
|
|
54
|
-
""
|
|
55
|
-
].join("\n");
|
|
56
|
-
const shapioConfig = () => `// Project configuration for Shapio: code extensions (hooks, custom routes, services, jobs) and custom
|
|
57
|
-
// field editors. Content models are NOT configured here; they are created live in the admin UI and synced
|
|
58
|
-
// with \`shapio schema pull\` / \`shapio schema apply\`. This file is code: changing it, or anything in
|
|
59
|
-
// ./extensions, needs a restart (never a rebuild). Check it with \`npx shapio extensions check\`.
|
|
60
|
-
// TypeScript works as is (erasable syntax only: no enums or namespaces). Guide: documentation/extensions.md in Shapio's repository.
|
|
61
|
-
import { defineConfig } from '@shapio/cms/config';
|
|
62
|
-
|
|
63
|
-
export const config = defineConfig({
|
|
64
|
-
// Lifecycle hooks by model API ID ('*' = every model). before* hooks run inside the write's transaction
|
|
65
|
-
// and may reject it (422 HOOK_REJECTED); after* hooks run after commit, as retried jobs.
|
|
66
|
-
hooks: {
|
|
67
|
-
// article: {
|
|
68
|
-
// beforePublish: ({ data, reject }) => {
|
|
69
|
-
// if (!data?.cover) reject('An article needs a cover image');
|
|
70
|
-
// },
|
|
71
|
-
// },
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
// Custom React field editors: file names of ES modules built into ./extensions/editors/, e.g.
|
|
75
|
-
// ['star-rating.js']. The admin loads them at runtime; installing one is a file copy plus a restart.
|
|
76
|
-
editors: [],
|
|
77
|
-
|
|
78
|
-
// Admin colour themes, listed after the built-in ones. Each variant sets every UI token; see
|
|
79
|
-
// documentation/extensions.md (Admin themes) and examples/extension/extensions/sepiaTheme.ts.
|
|
80
|
-
// themes: [{ key: 'acme', name: 'Acme', dark: { background: '#101014', foreground: '#f4f4f5', ... } }],
|
|
81
|
-
|
|
82
|
-
// Custom Fastify routes, mounted at /api/ext/<prefix>. Mutating routes must declare config.audit.
|
|
83
|
-
// routes: [{ prefix: 'acme', plugin: async (app) => { app.get('/ping', async () => ({ ok: true })); } }],
|
|
84
|
-
|
|
85
|
-
// Services constructed once at startup and shared by hooks, routes and jobs.
|
|
86
|
-
// services: { stats: ({ services }) => ({ articles: () => services.content.count('article') }) },
|
|
87
|
-
|
|
88
|
-
// Background jobs, registered as ext.<name>; enqueue with services.jobs.enqueue('<name>', payload).
|
|
89
|
-
// jobs: { nightlyReport: async ({ services, logger }) => { logger.info('report'); } },
|
|
90
|
-
});
|
|
91
|
-
`;
|
|
92
|
-
const gitignore = () => `node_modules/
|
|
93
|
-
.env
|
|
94
|
-
media/*
|
|
95
|
-
!media/.gitkeep
|
|
96
|
-
`;
|
|
97
|
-
const pm2Ecosystem = ({ projectName }) => `// PM2 process file: pm2 start ecosystem.config.cjs
|
|
98
|
-
// One instance in fork mode, the simplest setup. Several instances on one database are supported too: use the
|
|
99
|
-
// cluster variant below.
|
|
100
|
-
module.exports = {
|
|
101
|
-
apps: [
|
|
102
|
-
{
|
|
103
|
-
name: ${JSON.stringify(projectName)},
|
|
104
|
-
script: 'node_modules/@shapio/cms/dist/cli.js',
|
|
105
|
-
args: 'start',
|
|
106
|
-
exec_mode: 'fork',
|
|
107
|
-
instances: 1,
|
|
108
|
-
kill_timeout: 20000,
|
|
109
|
-
env: { NODE_ENV: 'production' },
|
|
110
|
-
},
|
|
111
|
-
// Cluster variant (replace the app above): two processes sharing the port, each with its inline worker.
|
|
112
|
-
// { name: ${JSON.stringify(projectName)}, script: 'node_modules/@shapio/cms/dist/cli.js', args: 'start', exec_mode: 'cluster', instances: 2, kill_timeout: 20000, env: { NODE_ENV: 'production' } },
|
|
113
|
-
],
|
|
114
|
-
};
|
|
115
|
-
`;
|
|
116
|
-
const readme = ({ projectName }) => `# ${projectName}
|
|
117
|
-
|
|
118
|
-
A [Shapio](https://github.com/mybrokengnome/shapio) project.
|
|
119
|
-
|
|
120
|
-
1. Set \`DATABASE_URL\` in \`.env\` to a PostgreSQL (>= 16) database, or to \`sqlite:<path>\` for a
|
|
121
|
-
single-process install.
|
|
122
|
-
2. \`npm run start\`. Migrations run on start. The first boot logs where to create the owner account: open
|
|
123
|
-
\`/admin/\`; the first person to complete Setup becomes the owner.
|
|
124
|
-
3. Under PM2: \`pm2 start ecosystem.config.cjs\`.
|
|
125
|
-
|
|
126
|
-
Content models are edited live in the admin; no restart is needed for modelling. Changing \`shapio.config.ts\`
|
|
127
|
-
or \`extensions/\` (code) needs a restart; \`npx shapio extensions check\` validates them.
|
|
128
|
-
`;
|
|
129
|
-
/** Files to write, relative to the project directory. */
|
|
130
|
-
const renderTemplates = (input) => [
|
|
131
|
-
["package.json", packageJson(input)],
|
|
132
|
-
[".env", env(input)],
|
|
133
|
-
[".env.example", env({
|
|
134
|
-
...input,
|
|
135
|
-
databaseUrl: "postgres://user:password@localhost:5432/shapio",
|
|
136
|
-
sessionSecret: ""
|
|
137
|
-
})],
|
|
138
|
-
["shapio.config.ts", shapioConfig()],
|
|
139
|
-
[".gitignore", gitignore()],
|
|
140
|
-
["ecosystem.config.cjs", pm2Ecosystem(input)],
|
|
141
|
-
["README.md", readme(input)],
|
|
142
|
-
["extensions/.gitkeep", ""],
|
|
143
|
-
["extensions/editors/.gitkeep", ""],
|
|
144
|
-
["media/.gitkeep", ""]
|
|
145
|
-
];
|
|
146
|
-
//#endregion
|
|
147
|
-
//#region src/scaffold.ts
|
|
148
|
-
var ScaffoldError = class extends Error {
|
|
149
|
-
constructor(message) {
|
|
150
|
-
super(message);
|
|
151
|
-
this.name = "ScaffoldError";
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
/** npm package names: lowercase, URL-safe. Derived from the directory name. */
|
|
155
|
-
const toPackageName = (directory) => basename(resolve(directory)).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[-.]+$/g, "") || "shapio-project";
|
|
156
|
-
/** Writes the project files. Refuses to touch a non-empty directory. Returns the absolute project path. */
|
|
157
|
-
const scaffoldProject = ({ directory, shapioSpec, databaseUrl }) => {
|
|
158
|
-
const root = resolve(directory);
|
|
159
|
-
if (existsSync(root) && readdirSync(root).length > 0) throw new ScaffoldError(`${root} exists and is not empty`);
|
|
160
|
-
const projectName = toPackageName(root);
|
|
161
|
-
const files = renderTemplates({
|
|
162
|
-
projectName,
|
|
163
|
-
shapioSpec,
|
|
164
|
-
databaseUrl: databaseUrl ?? `postgres://postgres:postgres@localhost:5432/${projectName.replace(/[^a-z0-9_]/g, "_")}`,
|
|
165
|
-
sessionSecret: randomBytes(32).toString("base64url")
|
|
166
|
-
});
|
|
167
|
-
for (const [path, content] of files) {
|
|
168
|
-
const target = join(root, path);
|
|
169
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
170
|
-
writeFileSync(target, content, { flag: "wx" });
|
|
171
|
-
}
|
|
172
|
-
return root;
|
|
173
|
-
};
|
|
174
|
-
//#endregion
|
|
175
|
-
//#region src/starters.ts
|
|
176
|
-
/** The site starters `create-shapio --site` scaffolds (examples/<name> in the repository). */
|
|
177
|
-
const SITE_STARTERS = [
|
|
178
|
-
"astro",
|
|
179
|
-
"next",
|
|
180
|
-
"sveltekit"
|
|
181
|
-
];
|
|
182
|
-
const isSiteStarter = (value) => SITE_STARTERS.includes(value);
|
|
183
|
-
/** npm leaves `.gitignore` out of published packages, so templates carry it under this name. */
|
|
184
|
-
const PACKED_GITIGNORE = "gitignore";
|
|
185
|
-
//#endregion
|
|
186
|
-
//#region src/site.ts
|
|
187
|
-
/**
|
|
188
|
-
* Writes a standalone site project from a starter template: the framework app, the blog's model files in
|
|
189
|
-
* shapio/, and the seed and smoke scripts. Refuses to touch a non-empty directory. Returns the project path.
|
|
190
|
-
*/
|
|
191
|
-
const scaffoldSite = ({ directory, site, templatesDir }) => {
|
|
192
|
-
const root = resolve(directory);
|
|
193
|
-
if (existsSync(root) && readdirSync(root).length > 0) throw new ScaffoldError(`${root} exists and is not empty`);
|
|
194
|
-
const template = join(templatesDir, site);
|
|
195
|
-
if (!existsSync(join(template, "package.json"))) throw new ScaffoldError(`The ${site} template is missing from ${templatesDir}; build create-shapio first`);
|
|
196
|
-
cpSync(template, root, { recursive: true });
|
|
197
|
-
if (existsSync(join(root, "gitignore"))) renameSync(join(root, PACKED_GITIGNORE), join(root, ".gitignore"));
|
|
198
|
-
const packagePath = join(root, "package.json");
|
|
199
|
-
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
200
|
-
writeFileSync(packagePath, `${JSON.stringify({
|
|
201
|
-
...packageJson,
|
|
202
|
-
name: toPackageName(root),
|
|
203
|
-
version: "0.1.0"
|
|
204
|
-
}, null, 2)}\n`);
|
|
205
|
-
return root;
|
|
206
|
-
};
|
|
207
|
-
//#endregion
|
|
208
|
-
//#region src/main.ts
|
|
209
|
-
const USAGE = `Usage: npx create-shapio <directory> [options]
|
|
210
|
-
|
|
211
|
-
Without --site: a Shapio project (the CMS server). With --site: a website that reads content from Shapio.
|
|
212
|
-
|
|
213
|
-
Options:
|
|
214
|
-
--site <${SITE_STARTERS.join("|")}> Create a site starter instead (a blog: pages, articles, authors)
|
|
215
|
-
--database-url <url> Database URL to put in .env (PostgreSQL, or sqlite:<path> for a single-process
|
|
216
|
-
install; CMS project only)
|
|
217
|
-
--shapio-spec <spec> Dependency spec for shapio (default: ^<this version>; a tarball path works too)
|
|
218
|
-
--no-install Skip npm install
|
|
219
|
-
-h, --help Show this help
|
|
220
|
-
`;
|
|
221
|
-
/** Packed from the repository's examples/ when create-shapio is built (templates/ beside dist/). */
|
|
222
|
-
const TEMPLATES_DIR = fileURLToPath(new URL("../templates/", import.meta.url));
|
|
223
|
-
const ownVersion = () => {
|
|
224
|
-
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
225
|
-
};
|
|
226
|
-
/** Returns the exit code. */
|
|
227
|
-
const main = (argv, { out, err }) => {
|
|
228
|
-
const { values, positionals } = parseArgs({
|
|
229
|
-
args: [...argv],
|
|
230
|
-
allowPositionals: true,
|
|
231
|
-
allowNegative: true,
|
|
232
|
-
options: {
|
|
233
|
-
"database-url": { type: "string" },
|
|
234
|
-
"shapio-spec": { type: "string" },
|
|
235
|
-
site: { type: "string" },
|
|
236
|
-
install: {
|
|
237
|
-
type: "boolean",
|
|
238
|
-
default: true
|
|
239
|
-
},
|
|
240
|
-
help: {
|
|
241
|
-
type: "boolean",
|
|
242
|
-
short: "h"
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
const [directory] = positionals;
|
|
247
|
-
if (values.help || !directory) {
|
|
248
|
-
(directory ? out : err)(USAGE);
|
|
249
|
-
return values.help ? 0 : 1;
|
|
250
|
-
}
|
|
251
|
-
const site = values.site;
|
|
252
|
-
if (site !== void 0 && !isSiteStarter(site)) {
|
|
253
|
-
err(`create-shapio: --site must be one of ${SITE_STARTERS.join(", ")}\n`);
|
|
254
|
-
return 1;
|
|
255
|
-
}
|
|
256
|
-
if (site !== void 0 && (values["database-url"] || values["shapio-spec"])) {
|
|
257
|
-
err("create-shapio: --database-url and --shapio-spec are for a CMS project, not --site\n");
|
|
258
|
-
return 1;
|
|
259
|
-
}
|
|
260
|
-
let root;
|
|
261
|
-
try {
|
|
262
|
-
root = site ? scaffoldSite({
|
|
263
|
-
directory,
|
|
264
|
-
site,
|
|
265
|
-
templatesDir: TEMPLATES_DIR
|
|
266
|
-
}) : scaffoldProject({
|
|
267
|
-
directory,
|
|
268
|
-
shapioSpec: values["shapio-spec"] ?? `^${ownVersion()}`,
|
|
269
|
-
...values["database-url"] ? { databaseUrl: values["database-url"] } : {}
|
|
270
|
-
});
|
|
271
|
-
} catch (error) {
|
|
272
|
-
if (error instanceof ScaffoldError) {
|
|
273
|
-
err(`create-shapio: ${error.message}\n`);
|
|
274
|
-
return 1;
|
|
275
|
-
}
|
|
276
|
-
throw error;
|
|
277
|
-
}
|
|
278
|
-
out(`Created a ${site ? `${site} site` : "Shapio project"} in ${root}\n`);
|
|
279
|
-
if (values.install) {
|
|
280
|
-
out("Installing dependencies with npm...\n");
|
|
281
|
-
if (spawnSync("npm", ["install"], {
|
|
282
|
-
cwd: root,
|
|
283
|
-
stdio: "inherit"
|
|
284
|
-
}).status !== 0) {
|
|
285
|
-
err("create-shapio: npm install failed; run it yourself in the project directory\n");
|
|
286
|
-
return 1;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
out(site ? siteNextSteps(directory, site) : projectNextSteps(directory, values["database-url"] !== void 0));
|
|
290
|
-
return 0;
|
|
291
|
-
};
|
|
292
|
-
const SERVE_COMMAND = {
|
|
293
|
-
astro: "npm run preview",
|
|
294
|
-
next: "npm run start",
|
|
295
|
-
sveltekit: "npm run preview"
|
|
296
|
-
};
|
|
297
|
-
/** Points at DATABASE_URL only when --database-url did not already set it. */
|
|
298
|
-
const projectNextSteps = (directory, databaseUrlGiven) => [
|
|
299
|
-
"",
|
|
300
|
-
"Next:",
|
|
301
|
-
` cd ${directory}`,
|
|
302
|
-
...databaseUrlGiven ? [] : [" edit .env (DATABASE_URL)"],
|
|
303
|
-
" npm run start",
|
|
304
|
-
""
|
|
305
|
-
].join("\n");
|
|
306
|
-
const siteNextSteps = (directory, site) => [
|
|
307
|
-
"",
|
|
308
|
-
"Next (with a running Shapio; README.md has the details):",
|
|
309
|
-
` cd ${directory}`,
|
|
310
|
-
" SHAPIO_URL=http://localhost:4300 SHAPIO_ADMIN_EMAIL=you@example.com SHAPIO_ADMIN_PASSWORD=... npm run seed",
|
|
311
|
-
" npm run build",
|
|
312
|
-
` ${SERVE_COMMAND[site]}`,
|
|
313
|
-
""
|
|
314
|
-
].join("\n");
|
|
2
|
+
/** True when `version` (as `process.versions.node` gives it, e.g. `24.1.0`) is new enough. */
|
|
3
|
+
const isSupportedNode = (version) => Number.parseInt(version.split(".")[0] ?? "", 10) >= 24;
|
|
4
|
+
/** The one-line refusal shown when Node.js is too old, or undefined when `version` is supported. */
|
|
5
|
+
const unsupportedNodeMessage = (version) => isSupportedNode(version) ? void 0 : `create-shapio needs Node.js 24 or newer, and this is v${version}. Install a newer Node.js from https://nodejs.org/ or with your version manager, then run the command again.`;
|
|
315
6
|
//#endregion
|
|
316
7
|
//#region src/index.ts
|
|
8
|
+
const refusal = unsupportedNodeMessage(process.versions.node);
|
|
9
|
+
if (refusal) {
|
|
10
|
+
process.stderr.write(`${refusal}\n`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const { main } = await import("./main-CJjrw1fB.js");
|
|
317
14
|
process.exitCode = main(process.argv.slice(2), {
|
|
318
15
|
out: (text) => process.stdout.write(text),
|
|
319
16
|
err: (text) => process.stderr.write(text)
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/templates.ts","../src/scaffold.ts","../src/starters.ts","../src/site.ts","../src/main.ts","../src/index.ts"],"sourcesContent":["export type TemplateInput = {\n projectName: string;\n shapioSpec: string;\n databaseUrl: string;\n sessionSecret: string;\n};\n\nconst packageJson = ({ projectName, shapioSpec }: TemplateInput) =>\n `${JSON.stringify(\n {\n name: projectName,\n private: true,\n type: 'module',\n engines: { node: '>=24' },\n scripts: {\n start: 'shapio start',\n migrate: 'shapio migrate',\n worker: 'shapio worker',\n status: 'shapio status',\n },\n dependencies: { '@shapio/cms': shapioSpec },\n },\n null,\n 2,\n )}\\n`;\n\nconst env = ({ databaseUrl, sessionSecret }: TemplateInput) =>\n [\n '# Shapio configuration. Every setting is an environment variable; the `shapio` command loads this file.',\n '# Real environment variables override values here.',\n 'NODE_ENV=production',\n 'HOST=127.0.0.1',\n 'PORT=4300',\n 'LOG_LEVEL=info',\n '',\n '# The origin people reach Shapio at; every absolute URL is built from it. Change it when you deploy.',\n 'PUBLIC_URL=http://localhost:4300',\n '# BASE_PATH=/cms',\n '',\n '# HTTPS without a reverse proxy: set PUBLIC_URL=https://your.domain and PORT=443, and point these at your',\n '# certificate and key (PEM). Renewed files are picked up without a restart. HTTP_PORT=80 redirects to HTTPS.',\n '# TLS_CERT_FILE=/etc/shapio/tls/fullchain.pem',\n '# TLS_KEY_FILE=/etc/shapio/tls/privkey.pem',\n '# HTTP_PORT=80',\n '',\n '# PostgreSQL >= 16, or sqlite:<path> for a single-process install',\n `DATABASE_URL=${databaseUrl}`,\n 'MIGRATE_ON_START=true',\n '',\n '# Signs preview links, private-media URLs and app-user tokens, and encrypts stored secrets.',\n '# Generated for this project; keep it secret, and back it up with the database.',\n `SESSION_SECRET=${sessionSecret}`,\n '',\n '# SETUP_REQUIRE_TOKEN=true # require the logged one-time token for first-run setup (for installs exposed before setup)',\n '',\n '# inline: jobs run inside the server process. dedicated: run `npm run worker` as a second process.',\n 'WORKER_MODE=inline',\n '',\n 'MEDIA_PATH=./media',\n '',\n ].join('\\n');\n\nconst shapioConfig =\n () => `// Project configuration for Shapio: code extensions (hooks, custom routes, services, jobs) and custom\n// field editors. Content models are NOT configured here; they are created live in the admin UI and synced\n// with \\`shapio schema pull\\` / \\`shapio schema apply\\`. This file is code: changing it, or anything in\n// ./extensions, needs a restart (never a rebuild). Check it with \\`npx shapio extensions check\\`.\n// TypeScript works as is (erasable syntax only: no enums or namespaces). Guide: documentation/extensions.md in Shapio's repository.\nimport { defineConfig } from '@shapio/cms/config';\n\nexport const config = defineConfig({\n // Lifecycle hooks by model API ID ('*' = every model). before* hooks run inside the write's transaction\n // and may reject it (422 HOOK_REJECTED); after* hooks run after commit, as retried jobs.\n hooks: {\n // article: {\n // beforePublish: ({ data, reject }) => {\n // if (!data?.cover) reject('An article needs a cover image');\n // },\n // },\n },\n\n // Custom React field editors: file names of ES modules built into ./extensions/editors/, e.g.\n // ['star-rating.js']. The admin loads them at runtime; installing one is a file copy plus a restart.\n editors: [],\n\n // Admin colour themes, listed after the built-in ones. Each variant sets every UI token; see\n // documentation/extensions.md (Admin themes) and examples/extension/extensions/sepiaTheme.ts.\n // themes: [{ key: 'acme', name: 'Acme', dark: { background: '#101014', foreground: '#f4f4f5', ... } }],\n\n // Custom Fastify routes, mounted at /api/ext/<prefix>. Mutating routes must declare config.audit.\n // routes: [{ prefix: 'acme', plugin: async (app) => { app.get('/ping', async () => ({ ok: true })); } }],\n\n // Services constructed once at startup and shared by hooks, routes and jobs.\n // services: { stats: ({ services }) => ({ articles: () => services.content.count('article') }) },\n\n // Background jobs, registered as ext.<name>; enqueue with services.jobs.enqueue('<name>', payload).\n // jobs: { nightlyReport: async ({ services, logger }) => { logger.info('report'); } },\n});\n`;\n\nconst gitignore = () => `node_modules/\n.env\nmedia/*\n!media/.gitkeep\n`;\n\nconst pm2Ecosystem = ({ projectName }: TemplateInput) => `// PM2 process file: pm2 start ecosystem.config.cjs\n// One instance in fork mode, the simplest setup. Several instances on one database are supported too: use the\n// cluster variant below.\nmodule.exports = {\n apps: [\n {\n name: ${JSON.stringify(projectName)},\n script: 'node_modules/@shapio/cms/dist/cli.js',\n args: 'start',\n exec_mode: 'fork',\n instances: 1,\n kill_timeout: 20000,\n env: { NODE_ENV: 'production' },\n },\n // Cluster variant (replace the app above): two processes sharing the port, each with its inline worker.\n // { name: ${JSON.stringify(projectName)}, script: 'node_modules/@shapio/cms/dist/cli.js', args: 'start', exec_mode: 'cluster', instances: 2, kill_timeout: 20000, env: { NODE_ENV: 'production' } },\n ],\n};\n`;\n\nconst readme = ({ projectName }: TemplateInput) =>\n `# ${projectName}\n\nA [Shapio](https://github.com/mybrokengnome/shapio) project.\n\n1. Set \\`DATABASE_URL\\` in \\`.env\\` to a PostgreSQL (>= 16) database, or to \\`sqlite:<path>\\` for a\n single-process install.\n2. \\`npm run start\\`. Migrations run on start. The first boot logs where to create the owner account: open\n \\`/admin/\\`; the first person to complete Setup becomes the owner.\n3. Under PM2: \\`pm2 start ecosystem.config.cjs\\`.\n\nContent models are edited live in the admin; no restart is needed for modelling. Changing \\`shapio.config.ts\\`\nor \\`extensions/\\` (code) needs a restart; \\`npx shapio extensions check\\` validates them.\n`;\n\n/** Files to write, relative to the project directory. */\nexport const renderTemplates = (input: TemplateInput): ReadonlyArray<[path: string, content: string]> => [\n ['package.json', packageJson(input)],\n ['.env', env(input)],\n [\n '.env.example',\n env({ ...input, databaseUrl: 'postgres://user:password@localhost:5432/shapio', sessionSecret: '' }),\n ],\n ['shapio.config.ts', shapioConfig()],\n ['.gitignore', gitignore()],\n ['ecosystem.config.cjs', pm2Ecosystem(input)],\n ['README.md', readme(input)],\n ['extensions/.gitkeep', ''],\n ['extensions/editors/.gitkeep', ''],\n ['media/.gitkeep', ''],\n];\n","import { randomBytes } from 'node:crypto';\nimport { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';\nimport { basename, dirname, join, resolve } from 'node:path';\nimport { renderTemplates } from './templates.js';\n\nexport type ScaffoldOptions = {\n directory: string;\n shapioSpec: string;\n databaseUrl?: string;\n};\n\nexport class ScaffoldError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ScaffoldError';\n }\n}\n\n/** npm package names: lowercase, URL-safe. Derived from the directory name. */\nexport const toPackageName = (directory: string): string =>\n basename(resolve(directory))\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, '-')\n .replace(/^[._-]+|[-.]+$/g, '') || 'shapio-project';\n\n/** Writes the project files. Refuses to touch a non-empty directory. Returns the absolute project path. */\nexport const scaffoldProject = ({ directory, shapioSpec, databaseUrl }: ScaffoldOptions): string => {\n const root = resolve(directory);\n if (existsSync(root) && readdirSync(root).length > 0) {\n throw new ScaffoldError(`${root} exists and is not empty`);\n }\n const projectName = toPackageName(root);\n const files = renderTemplates({\n projectName,\n shapioSpec,\n databaseUrl:\n databaseUrl ?? `postgres://postgres:postgres@localhost:5432/${projectName.replace(/[^a-z0-9_]/g, '_')}`,\n sessionSecret: randomBytes(32).toString('base64url'),\n });\n for (const [path, content] of files) {\n const target = join(root, path);\n mkdirSync(dirname(target), { recursive: true });\n writeFileSync(target, content, { flag: 'wx' });\n }\n return root;\n};\n","/** The site starters `create-shapio --site` scaffolds (examples/<name> in the repository). */\nexport const SITE_STARTERS = ['astro', 'next', 'sveltekit'] as const;\n\nexport type SiteStarter = (typeof SITE_STARTERS)[number];\n\nexport const isSiteStarter = (value: string): value is SiteStarter =>\n (SITE_STARTERS as readonly string[]).includes(value);\n\n/** npm leaves `.gitignore` out of published packages, so templates carry it under this name. */\nexport const PACKED_GITIGNORE = 'gitignore';\n","import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { ScaffoldError, toPackageName } from './scaffold.js';\nimport { PACKED_GITIGNORE, type SiteStarter } from './starters.js';\n\nexport type ScaffoldSiteOptions = {\n directory: string;\n site: SiteStarter;\n /** The packed templates (templates/ next to dist/ in the published package). */\n templatesDir: string;\n};\n\n/**\n * Writes a standalone site project from a starter template: the framework app, the blog's model files in\n * shapio/, and the seed and smoke scripts. Refuses to touch a non-empty directory. Returns the project path.\n */\nexport const scaffoldSite = ({ directory, site, templatesDir }: ScaffoldSiteOptions): string => {\n const root = resolve(directory);\n if (existsSync(root) && readdirSync(root).length > 0) {\n throw new ScaffoldError(`${root} exists and is not empty`);\n }\n const template = join(templatesDir, site);\n if (!existsSync(join(template, 'package.json'))) {\n throw new ScaffoldError(\n `The ${site} template is missing from ${templatesDir}; build create-shapio first`,\n );\n }\n cpSync(template, root, { recursive: true });\n if (existsSync(join(root, PACKED_GITIGNORE))) {\n renameSync(join(root, PACKED_GITIGNORE), join(root, '.gitignore'));\n }\n const packagePath = join(root, 'package.json');\n const packageJson = JSON.parse(readFileSync(packagePath, 'utf8')) as Record<string, unknown>;\n writeFileSync(\n packagePath,\n `${JSON.stringify({ ...packageJson, name: toPackageName(root), version: '0.1.0' }, null, 2)}\\n`,\n );\n return root;\n};\n","import { spawnSync } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { parseArgs } from 'node:util';\nimport { scaffoldProject, ScaffoldError } from './scaffold.js';\nimport { scaffoldSite } from './site.js';\nimport { isSiteStarter, SITE_STARTERS, type SiteStarter } from './starters.js';\n\nconst USAGE = `Usage: npx create-shapio <directory> [options]\n\nWithout --site: a Shapio project (the CMS server). With --site: a website that reads content from Shapio.\n\nOptions:\n --site <${SITE_STARTERS.join('|')}> Create a site starter instead (a blog: pages, articles, authors)\n --database-url <url> Database URL to put in .env (PostgreSQL, or sqlite:<path> for a single-process\n install; CMS project only)\n --shapio-spec <spec> Dependency spec for shapio (default: ^<this version>; a tarball path works too)\n --no-install Skip npm install\n -h, --help Show this help\n`;\n\n/** Packed from the repository's examples/ when create-shapio is built (templates/ beside dist/). */\nconst TEMPLATES_DIR = fileURLToPath(new URL('../templates/', import.meta.url));\n\nconst ownVersion = (): string => {\n const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {\n version: string;\n };\n return packageJson.version;\n};\n\ntype Output = { out: (text: string) => void; err: (text: string) => void };\n\n/** Returns the exit code. */\nexport const main = (argv: readonly string[], { out, err }: Output): number => {\n const { values, positionals } = parseArgs({\n args: [...argv],\n allowPositionals: true,\n allowNegative: true,\n options: {\n 'database-url': { type: 'string' },\n 'shapio-spec': { type: 'string' },\n site: { type: 'string' },\n install: { type: 'boolean', default: true },\n help: { type: 'boolean', short: 'h' },\n },\n });\n const [directory] = positionals;\n if (values.help || !directory) {\n (directory ? out : err)(USAGE);\n return values.help ? 0 : 1;\n }\n const site = values.site;\n if (site !== undefined && !isSiteStarter(site)) {\n err(`create-shapio: --site must be one of ${SITE_STARTERS.join(', ')}\\n`);\n return 1;\n }\n if (site !== undefined && (values['database-url'] || values['shapio-spec'])) {\n err('create-shapio: --database-url and --shapio-spec are for a CMS project, not --site\\n');\n return 1;\n }\n let root: string;\n try {\n root = site\n ? scaffoldSite({ directory, site, templatesDir: TEMPLATES_DIR })\n : scaffoldProject({\n directory,\n shapioSpec: values['shapio-spec'] ?? `^${ownVersion()}`,\n ...(values['database-url'] ? { databaseUrl: values['database-url'] } : {}),\n });\n } catch (error) {\n if (error instanceof ScaffoldError) {\n err(`create-shapio: ${error.message}\\n`);\n return 1;\n }\n throw error;\n }\n out(`Created a ${site ? `${site} site` : 'Shapio project'} in ${root}\\n`);\n if (values.install) {\n out('Installing dependencies with npm...\\n');\n const result = spawnSync('npm', ['install'], { cwd: root, stdio: 'inherit' });\n if (result.status !== 0) {\n err('create-shapio: npm install failed; run it yourself in the project directory\\n');\n return 1;\n }\n }\n out(\n site ? siteNextSteps(directory, site) : projectNextSteps(directory, values['database-url'] !== undefined),\n );\n return 0;\n};\n\nconst SERVE_COMMAND: Record<SiteStarter, string> = {\n astro: 'npm run preview',\n next: 'npm run start',\n sveltekit: 'npm run preview',\n};\n\n/** Points at DATABASE_URL only when --database-url did not already set it. */\nconst projectNextSteps = (directory: string, databaseUrlGiven: boolean) =>\n [\n '',\n 'Next:',\n ` cd ${directory}`,\n ...(databaseUrlGiven ? [] : [' edit .env (DATABASE_URL)']),\n ' npm run start',\n '',\n ].join('\\n');\n\nconst siteNextSteps = (directory: string, site: SiteStarter) =>\n [\n '',\n 'Next (with a running Shapio; README.md has the details):',\n ` cd ${directory}`,\n ' SHAPIO_URL=http://localhost:4300 SHAPIO_ADMIN_EMAIL=you@example.com SHAPIO_ADMIN_PASSWORD=... npm run seed',\n ' npm run build',\n ` ${SERVE_COMMAND[site]}`,\n '',\n ].join('\\n');\n","#!/usr/bin/env node\nimport { main } from './main.js';\n\nprocess.exitCode = main(process.argv.slice(2), {\n out: (text) => process.stdout.write(text),\n err: (text) => process.stderr.write(text),\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/nodeVersion.ts","../src/index.ts"],"sourcesContent":["// Duplicates apps/api/src/helpers/nodeVersion.ts on purpose: create-shapio has no runtime dependencies.\n/** The oldest Node.js major version Shapio runs on (`engines.node` in package.json). */\nexport const MIN_NODE_MAJOR = 24;\n\n/** True when `version` (as `process.versions.node` gives it, e.g. `24.1.0`) is new enough. */\nexport const isSupportedNode = (version: string): boolean =>\n Number.parseInt(version.split('.')[0] ?? '', 10) >= MIN_NODE_MAJOR;\n\n/** The one-line refusal shown when Node.js is too old, or undefined when `version` is supported. */\nexport const unsupportedNodeMessage = (version: string): string | undefined =>\n isSupportedNode(version)\n ? undefined\n : `create-shapio needs Node.js ${MIN_NODE_MAJOR} or newer, and this is v${version}. ` +\n 'Install a newer Node.js from https://nodejs.org/ or with your version manager, then run the command again.';\n","#!/usr/bin/env node\nimport { unsupportedNodeMessage } from './nodeVersion.js';\n\n// Checked before the scaffolder loads, so nothing is written or installed on an unsupported Node.js.\nconst refusal = unsupportedNodeMessage(process.versions.node);\nif (refusal) {\n process.stderr.write(`${refusal}\\n`);\n process.exit(1);\n}\nconst { main } = await import('./main.js');\nprocess.exitCode = main(process.argv.slice(2), {\n out: (text) => process.stdout.write(text),\n err: (text) => process.stderr.write(text),\n});\n"],"mappings":";;AAKA,MAAa,mBAAmB,YAC9B,OAAO,SAAS,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,EAAE,KAAA;;AAGjD,MAAa,0BAA0B,YACrC,gBAAgB,OAAO,IACnB,KAAA,IACA,yDAAwE,QAAQ;;;ACRtF,MAAM,UAAU,uBAAuB,QAAQ,SAAS,IAAI;AAC5D,IAAI,SAAS;CACX,QAAQ,OAAO,MAAM,GAAG,QAAQ,GAAG;CACnC,QAAQ,KAAK,CAAC;AAChB;AACA,MAAM,EAAE,SAAS,MAAM,OAAO;AAC9B,QAAQ,WAAW,KAAK,QAAQ,KAAK,MAAM,CAAC,GAAG;CAC7C,MAAM,SAAS,QAAQ,OAAO,MAAM,IAAI;CACxC,MAAM,SAAS,QAAQ,OAAO,MAAM,IAAI;AAC1C,CAAC"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
//#region src/templates.ts
|
|
8
|
+
const packageJson = ({ projectName, shapioSpec }) => `${JSON.stringify({
|
|
9
|
+
name: projectName,
|
|
10
|
+
private: true,
|
|
11
|
+
type: "module",
|
|
12
|
+
engines: { node: ">=24" },
|
|
13
|
+
scripts: {
|
|
14
|
+
start: "shapio start",
|
|
15
|
+
migrate: "shapio migrate",
|
|
16
|
+
worker: "shapio worker",
|
|
17
|
+
status: "shapio status"
|
|
18
|
+
},
|
|
19
|
+
dependencies: { "@shapio/cms": shapioSpec }
|
|
20
|
+
}, null, 2)}\n`;
|
|
21
|
+
const env = ({ databaseUrl, sessionSecret }) => [
|
|
22
|
+
"# Shapio configuration. Every setting is an environment variable; the `shapio` command loads this file.",
|
|
23
|
+
"# Real environment variables override values here.",
|
|
24
|
+
"NODE_ENV=production",
|
|
25
|
+
"HOST=127.0.0.1",
|
|
26
|
+
"PORT=4300",
|
|
27
|
+
"LOG_LEVEL=info",
|
|
28
|
+
"",
|
|
29
|
+
"# The origin people reach Shapio at; every absolute URL is built from it. Change it when you deploy.",
|
|
30
|
+
"PUBLIC_URL=http://localhost:4300",
|
|
31
|
+
"# BASE_PATH=/cms",
|
|
32
|
+
"",
|
|
33
|
+
"# HTTPS without a reverse proxy: set PUBLIC_URL=https://your.domain and PORT=443, and point these at your",
|
|
34
|
+
"# certificate and key (PEM). Renewed files are picked up without a restart. HTTP_PORT=80 redirects to HTTPS.",
|
|
35
|
+
"# TLS_CERT_FILE=/etc/shapio/tls/fullchain.pem",
|
|
36
|
+
"# TLS_KEY_FILE=/etc/shapio/tls/privkey.pem",
|
|
37
|
+
"# HTTP_PORT=80",
|
|
38
|
+
"",
|
|
39
|
+
"# PostgreSQL >= 16, or sqlite:<path> for a single-process install",
|
|
40
|
+
`DATABASE_URL=${databaseUrl}`,
|
|
41
|
+
"MIGRATE_ON_START=true",
|
|
42
|
+
"",
|
|
43
|
+
"# Signs preview links, private-media URLs and app-user tokens, and encrypts stored secrets.",
|
|
44
|
+
"# Generated for this project; keep it secret, and back it up with the database.",
|
|
45
|
+
`SESSION_SECRET=${sessionSecret}`,
|
|
46
|
+
"",
|
|
47
|
+
"# SETUP_REQUIRE_TOKEN=true # require the logged one-time token for first-run setup (for installs exposed before setup)",
|
|
48
|
+
"",
|
|
49
|
+
"# inline: jobs run inside the server process. dedicated: run `npm run worker` as a second process.",
|
|
50
|
+
"WORKER_MODE=inline",
|
|
51
|
+
"",
|
|
52
|
+
"MEDIA_PATH=./media",
|
|
53
|
+
""
|
|
54
|
+
].join("\n");
|
|
55
|
+
const shapioConfig = () => `// Project configuration for Shapio: code extensions (hooks, custom routes, services, jobs) and custom
|
|
56
|
+
// field editors. Content models are NOT configured here; they are created live in the admin UI and synced
|
|
57
|
+
// with \`shapio schema pull\` / \`shapio schema apply\`. This file is code: changing it, or anything in
|
|
58
|
+
// ./extensions, needs a restart (never a rebuild). Check it with \`npx shapio extensions check\`.
|
|
59
|
+
// TypeScript works as is (erasable syntax only: no enums or namespaces). Guide: documentation/extensions.md in Shapio's repository.
|
|
60
|
+
import { defineConfig } from '@shapio/cms/config';
|
|
61
|
+
|
|
62
|
+
export const config = defineConfig({
|
|
63
|
+
// Lifecycle hooks by model API ID ('*' = every model). before* hooks run inside the write's transaction
|
|
64
|
+
// and may reject it (422 HOOK_REJECTED); after* hooks run after commit, as retried jobs.
|
|
65
|
+
hooks: {
|
|
66
|
+
// article: {
|
|
67
|
+
// beforePublish: ({ data, reject }) => {
|
|
68
|
+
// if (!data?.cover) reject('An article needs a cover image');
|
|
69
|
+
// },
|
|
70
|
+
// },
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Custom React field editors: file names of ES modules built into ./extensions/editors/, e.g.
|
|
74
|
+
// ['star-rating.js']. The admin loads them at runtime; installing one is a file copy plus a restart.
|
|
75
|
+
editors: [],
|
|
76
|
+
|
|
77
|
+
// Admin colour themes, listed after the built-in ones. Each variant sets every UI token; see
|
|
78
|
+
// documentation/extensions.md (Admin themes) and examples/extension/extensions/sepiaTheme.ts.
|
|
79
|
+
// themes: [{ key: 'acme', name: 'Acme', dark: { background: '#101014', foreground: '#f4f4f5', ... } }],
|
|
80
|
+
|
|
81
|
+
// Custom Fastify routes, mounted at /api/ext/<prefix>. Mutating routes must declare config.audit.
|
|
82
|
+
// routes: [{ prefix: 'acme', plugin: async (app) => { app.get('/ping', async () => ({ ok: true })); } }],
|
|
83
|
+
|
|
84
|
+
// Services constructed once at startup and shared by hooks, routes and jobs.
|
|
85
|
+
// services: { stats: ({ services }) => ({ articles: () => services.content.count('article') }) },
|
|
86
|
+
|
|
87
|
+
// Background jobs, registered as ext.<name>; enqueue with services.jobs.enqueue('<name>', payload).
|
|
88
|
+
// jobs: { nightlyReport: async ({ services, logger }) => { logger.info('report'); } },
|
|
89
|
+
});
|
|
90
|
+
`;
|
|
91
|
+
const gitignore = () => `node_modules/
|
|
92
|
+
.env
|
|
93
|
+
media/*
|
|
94
|
+
!media/.gitkeep
|
|
95
|
+
`;
|
|
96
|
+
const pm2Ecosystem = ({ projectName }) => `// PM2 process file: pm2 start ecosystem.config.cjs
|
|
97
|
+
// One instance in fork mode, the simplest setup. Several instances on one database are supported too: use the
|
|
98
|
+
// cluster variant below.
|
|
99
|
+
module.exports = {
|
|
100
|
+
apps: [
|
|
101
|
+
{
|
|
102
|
+
name: ${JSON.stringify(projectName)},
|
|
103
|
+
script: 'node_modules/@shapio/cms/dist/cli.js',
|
|
104
|
+
args: 'start',
|
|
105
|
+
exec_mode: 'fork',
|
|
106
|
+
instances: 1,
|
|
107
|
+
kill_timeout: 20000,
|
|
108
|
+
env: { NODE_ENV: 'production' },
|
|
109
|
+
},
|
|
110
|
+
// Cluster variant (replace the app above): two processes sharing the port, each with its inline worker.
|
|
111
|
+
// { name: ${JSON.stringify(projectName)}, script: 'node_modules/@shapio/cms/dist/cli.js', args: 'start', exec_mode: 'cluster', instances: 2, kill_timeout: 20000, env: { NODE_ENV: 'production' } },
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
`;
|
|
115
|
+
const readme = ({ projectName }) => `# ${projectName}
|
|
116
|
+
|
|
117
|
+
A [Shapio](https://github.com/mybrokengnome/shapio) project.
|
|
118
|
+
|
|
119
|
+
1. Set \`DATABASE_URL\` in \`.env\` to a PostgreSQL (>= 16) database, or to \`sqlite:<path>\` for a
|
|
120
|
+
single-process install.
|
|
121
|
+
2. \`npm run start\`. Migrations run on start. The first boot logs where to create the owner account: open
|
|
122
|
+
\`/admin/\`; the first person to complete Setup becomes the owner.
|
|
123
|
+
3. Under PM2: \`pm2 start ecosystem.config.cjs\`.
|
|
124
|
+
|
|
125
|
+
Content models are edited live in the admin; no restart is needed for modelling. Changing \`shapio.config.ts\`
|
|
126
|
+
or \`extensions/\` (code) needs a restart; \`npx shapio extensions check\` validates them.
|
|
127
|
+
`;
|
|
128
|
+
/** Files to write, relative to the project directory. */
|
|
129
|
+
const renderTemplates = (input) => [
|
|
130
|
+
["package.json", packageJson(input)],
|
|
131
|
+
[".env", env(input)],
|
|
132
|
+
[".env.example", env({
|
|
133
|
+
...input,
|
|
134
|
+
databaseUrl: "postgres://user:password@localhost:5432/shapio",
|
|
135
|
+
sessionSecret: ""
|
|
136
|
+
})],
|
|
137
|
+
["shapio.config.ts", shapioConfig()],
|
|
138
|
+
[".gitignore", gitignore()],
|
|
139
|
+
["ecosystem.config.cjs", pm2Ecosystem(input)],
|
|
140
|
+
["README.md", readme(input)],
|
|
141
|
+
["extensions/.gitkeep", ""],
|
|
142
|
+
["extensions/editors/.gitkeep", ""],
|
|
143
|
+
["media/.gitkeep", ""]
|
|
144
|
+
];
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/scaffold.ts
|
|
147
|
+
var ScaffoldError = class extends Error {
|
|
148
|
+
constructor(message) {
|
|
149
|
+
super(message);
|
|
150
|
+
this.name = "ScaffoldError";
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
/** npm package names: lowercase, URL-safe. Derived from the directory name. */
|
|
154
|
+
const toPackageName = (directory) => basename(resolve(directory)).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[-.]+$/g, "") || "shapio-project";
|
|
155
|
+
/** Writes the project files. Refuses to touch a non-empty directory. Returns the absolute project path. */
|
|
156
|
+
const scaffoldProject = ({ directory, shapioSpec, databaseUrl }) => {
|
|
157
|
+
const root = resolve(directory);
|
|
158
|
+
if (existsSync(root) && readdirSync(root).length > 0) throw new ScaffoldError(`${root} exists and is not empty`);
|
|
159
|
+
const projectName = toPackageName(root);
|
|
160
|
+
const files = renderTemplates({
|
|
161
|
+
projectName,
|
|
162
|
+
shapioSpec,
|
|
163
|
+
databaseUrl: databaseUrl ?? `postgres://postgres:postgres@localhost:5432/${projectName.replace(/[^a-z0-9_]/g, "_")}`,
|
|
164
|
+
sessionSecret: randomBytes(32).toString("base64url")
|
|
165
|
+
});
|
|
166
|
+
for (const [path, content] of files) {
|
|
167
|
+
const target = join(root, path);
|
|
168
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
169
|
+
writeFileSync(target, content, {
|
|
170
|
+
flag: "wx",
|
|
171
|
+
...path === ".env" ? { mode: 384 } : {}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return root;
|
|
175
|
+
};
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/starters.ts
|
|
178
|
+
/** The site starters `create-shapio --site` scaffolds (examples/<name> in the repository). */
|
|
179
|
+
const SITE_STARTERS = [
|
|
180
|
+
"astro",
|
|
181
|
+
"next",
|
|
182
|
+
"sveltekit"
|
|
183
|
+
];
|
|
184
|
+
const isSiteStarter = (value) => SITE_STARTERS.includes(value);
|
|
185
|
+
/** npm leaves `.gitignore` out of published packages, so templates carry it under this name. */
|
|
186
|
+
const PACKED_GITIGNORE = "gitignore";
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/site.ts
|
|
189
|
+
/**
|
|
190
|
+
* Writes a standalone site project from a starter template: the framework app, the blog's model files in
|
|
191
|
+
* shapio/, and the seed and smoke scripts. Refuses to touch a non-empty directory. Returns the project path.
|
|
192
|
+
*/
|
|
193
|
+
const scaffoldSite = ({ directory, site, templatesDir }) => {
|
|
194
|
+
const root = resolve(directory);
|
|
195
|
+
if (existsSync(root) && readdirSync(root).length > 0) throw new ScaffoldError(`${root} exists and is not empty`);
|
|
196
|
+
const template = join(templatesDir, site);
|
|
197
|
+
if (!existsSync(join(template, "package.json"))) throw new ScaffoldError(`The ${site} template is missing from ${templatesDir}; build create-shapio first`);
|
|
198
|
+
cpSync(template, root, { recursive: true });
|
|
199
|
+
if (existsSync(join(root, "gitignore"))) renameSync(join(root, PACKED_GITIGNORE), join(root, ".gitignore"));
|
|
200
|
+
const packagePath = join(root, "package.json");
|
|
201
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
202
|
+
writeFileSync(packagePath, `${JSON.stringify({
|
|
203
|
+
...packageJson,
|
|
204
|
+
name: toPackageName(root),
|
|
205
|
+
version: "0.1.0"
|
|
206
|
+
}, null, 2)}\n`);
|
|
207
|
+
return root;
|
|
208
|
+
};
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/main.ts
|
|
211
|
+
const USAGE = `Usage: npx create-shapio <directory> [options]
|
|
212
|
+
|
|
213
|
+
Without --site: a Shapio project (the CMS server). With --site: a website that reads content from Shapio.
|
|
214
|
+
|
|
215
|
+
Options:
|
|
216
|
+
--site <${SITE_STARTERS.join("|")}> Create a site starter instead (a blog: pages, articles, authors)
|
|
217
|
+
--database-url <url> Database URL to put in .env (PostgreSQL, or sqlite:<path> for a single-process
|
|
218
|
+
install; CMS project only)
|
|
219
|
+
--shapio-spec <spec> Dependency spec for shapio (default: ^<this version>; a tarball path works too)
|
|
220
|
+
--no-install Skip npm install
|
|
221
|
+
-h, --help Show this help
|
|
222
|
+
`;
|
|
223
|
+
/** Packed from the repository's examples/ when create-shapio is built (templates/ beside dist/). */
|
|
224
|
+
const TEMPLATES_DIR = fileURLToPath(new URL("../templates/", import.meta.url));
|
|
225
|
+
const ownVersion = () => {
|
|
226
|
+
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
227
|
+
};
|
|
228
|
+
/** Returns the exit code. */
|
|
229
|
+
const main = (argv, { out, err }) => {
|
|
230
|
+
const { values, positionals } = parseArgs({
|
|
231
|
+
args: [...argv],
|
|
232
|
+
allowPositionals: true,
|
|
233
|
+
allowNegative: true,
|
|
234
|
+
options: {
|
|
235
|
+
"database-url": { type: "string" },
|
|
236
|
+
"shapio-spec": { type: "string" },
|
|
237
|
+
site: { type: "string" },
|
|
238
|
+
install: {
|
|
239
|
+
type: "boolean",
|
|
240
|
+
default: true
|
|
241
|
+
},
|
|
242
|
+
help: {
|
|
243
|
+
type: "boolean",
|
|
244
|
+
short: "h"
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
const [directory] = positionals;
|
|
249
|
+
if (values.help || !directory) {
|
|
250
|
+
(directory ? out : err)(USAGE);
|
|
251
|
+
return values.help ? 0 : 1;
|
|
252
|
+
}
|
|
253
|
+
const site = values.site;
|
|
254
|
+
if (site !== void 0 && !isSiteStarter(site)) {
|
|
255
|
+
err(`create-shapio: --site must be one of ${SITE_STARTERS.join(", ")}\n`);
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
if (site !== void 0 && (values["database-url"] || values["shapio-spec"])) {
|
|
259
|
+
err("create-shapio: --database-url and --shapio-spec are for a CMS project, not --site\n");
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
let root;
|
|
263
|
+
try {
|
|
264
|
+
root = site ? scaffoldSite({
|
|
265
|
+
directory,
|
|
266
|
+
site,
|
|
267
|
+
templatesDir: TEMPLATES_DIR
|
|
268
|
+
}) : scaffoldProject({
|
|
269
|
+
directory,
|
|
270
|
+
shapioSpec: values["shapio-spec"] ?? `^${ownVersion()}`,
|
|
271
|
+
...values["database-url"] ? { databaseUrl: values["database-url"] } : {}
|
|
272
|
+
});
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error instanceof ScaffoldError) {
|
|
275
|
+
err(`create-shapio: ${error.message}\n`);
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
out(`Created a ${site ? `${site} site` : "Shapio project"} in ${root}\n`);
|
|
281
|
+
if (values.install) {
|
|
282
|
+
out("Installing dependencies with npm...\n");
|
|
283
|
+
if (spawnSync("npm", ["install"], {
|
|
284
|
+
cwd: root,
|
|
285
|
+
stdio: "inherit"
|
|
286
|
+
}).status !== 0) {
|
|
287
|
+
err("create-shapio: npm install failed; run it yourself in the project directory\n");
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
out(site ? siteNextSteps(directory, site) : projectNextSteps(directory, values["database-url"] !== void 0));
|
|
292
|
+
return 0;
|
|
293
|
+
};
|
|
294
|
+
const SERVE_COMMAND = {
|
|
295
|
+
astro: "npm run preview",
|
|
296
|
+
next: "npm run start",
|
|
297
|
+
sveltekit: "npm run preview"
|
|
298
|
+
};
|
|
299
|
+
/** Points at DATABASE_URL only when --database-url did not already set it. */
|
|
300
|
+
const projectNextSteps = (directory, databaseUrlGiven) => [
|
|
301
|
+
"",
|
|
302
|
+
"Next:",
|
|
303
|
+
` cd ${directory}`,
|
|
304
|
+
...databaseUrlGiven ? [] : [" edit .env (DATABASE_URL)"],
|
|
305
|
+
" npm run start",
|
|
306
|
+
""
|
|
307
|
+
].join("\n");
|
|
308
|
+
const siteNextSteps = (directory, site) => [
|
|
309
|
+
"",
|
|
310
|
+
"Next (with a running Shapio; README.md has the details):",
|
|
311
|
+
` cd ${directory}`,
|
|
312
|
+
" SHAPIO_URL=http://localhost:4300 SHAPIO_ADMIN_EMAIL=you@example.com SHAPIO_ADMIN_PASSWORD=... npm run seed",
|
|
313
|
+
" npm run build",
|
|
314
|
+
` ${SERVE_COMMAND[site]}`,
|
|
315
|
+
""
|
|
316
|
+
].join("\n");
|
|
317
|
+
//#endregion
|
|
318
|
+
export { main };
|
|
319
|
+
|
|
320
|
+
//# sourceMappingURL=main-CJjrw1fB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main-CJjrw1fB.js","names":[],"sources":["../src/templates.ts","../src/scaffold.ts","../src/starters.ts","../src/site.ts","../src/main.ts"],"sourcesContent":["export type TemplateInput = {\n projectName: string;\n shapioSpec: string;\n databaseUrl: string;\n sessionSecret: string;\n};\n\nconst packageJson = ({ projectName, shapioSpec }: TemplateInput) =>\n `${JSON.stringify(\n {\n name: projectName,\n private: true,\n type: 'module',\n engines: { node: '>=24' },\n scripts: {\n start: 'shapio start',\n migrate: 'shapio migrate',\n worker: 'shapio worker',\n status: 'shapio status',\n },\n dependencies: { '@shapio/cms': shapioSpec },\n },\n null,\n 2,\n )}\\n`;\n\nconst env = ({ databaseUrl, sessionSecret }: TemplateInput) =>\n [\n '# Shapio configuration. Every setting is an environment variable; the `shapio` command loads this file.',\n '# Real environment variables override values here.',\n 'NODE_ENV=production',\n 'HOST=127.0.0.1',\n 'PORT=4300',\n 'LOG_LEVEL=info',\n '',\n '# The origin people reach Shapio at; every absolute URL is built from it. Change it when you deploy.',\n 'PUBLIC_URL=http://localhost:4300',\n '# BASE_PATH=/cms',\n '',\n '# HTTPS without a reverse proxy: set PUBLIC_URL=https://your.domain and PORT=443, and point these at your',\n '# certificate and key (PEM). Renewed files are picked up without a restart. HTTP_PORT=80 redirects to HTTPS.',\n '# TLS_CERT_FILE=/etc/shapio/tls/fullchain.pem',\n '# TLS_KEY_FILE=/etc/shapio/tls/privkey.pem',\n '# HTTP_PORT=80',\n '',\n '# PostgreSQL >= 16, or sqlite:<path> for a single-process install',\n `DATABASE_URL=${databaseUrl}`,\n 'MIGRATE_ON_START=true',\n '',\n '# Signs preview links, private-media URLs and app-user tokens, and encrypts stored secrets.',\n '# Generated for this project; keep it secret, and back it up with the database.',\n `SESSION_SECRET=${sessionSecret}`,\n '',\n '# SETUP_REQUIRE_TOKEN=true # require the logged one-time token for first-run setup (for installs exposed before setup)',\n '',\n '# inline: jobs run inside the server process. dedicated: run `npm run worker` as a second process.',\n 'WORKER_MODE=inline',\n '',\n 'MEDIA_PATH=./media',\n '',\n ].join('\\n');\n\nconst shapioConfig =\n () => `// Project configuration for Shapio: code extensions (hooks, custom routes, services, jobs) and custom\n// field editors. Content models are NOT configured here; they are created live in the admin UI and synced\n// with \\`shapio schema pull\\` / \\`shapio schema apply\\`. This file is code: changing it, or anything in\n// ./extensions, needs a restart (never a rebuild). Check it with \\`npx shapio extensions check\\`.\n// TypeScript works as is (erasable syntax only: no enums or namespaces). Guide: documentation/extensions.md in Shapio's repository.\nimport { defineConfig } from '@shapio/cms/config';\n\nexport const config = defineConfig({\n // Lifecycle hooks by model API ID ('*' = every model). before* hooks run inside the write's transaction\n // and may reject it (422 HOOK_REJECTED); after* hooks run after commit, as retried jobs.\n hooks: {\n // article: {\n // beforePublish: ({ data, reject }) => {\n // if (!data?.cover) reject('An article needs a cover image');\n // },\n // },\n },\n\n // Custom React field editors: file names of ES modules built into ./extensions/editors/, e.g.\n // ['star-rating.js']. The admin loads them at runtime; installing one is a file copy plus a restart.\n editors: [],\n\n // Admin colour themes, listed after the built-in ones. Each variant sets every UI token; see\n // documentation/extensions.md (Admin themes) and examples/extension/extensions/sepiaTheme.ts.\n // themes: [{ key: 'acme', name: 'Acme', dark: { background: '#101014', foreground: '#f4f4f5', ... } }],\n\n // Custom Fastify routes, mounted at /api/ext/<prefix>. Mutating routes must declare config.audit.\n // routes: [{ prefix: 'acme', plugin: async (app) => { app.get('/ping', async () => ({ ok: true })); } }],\n\n // Services constructed once at startup and shared by hooks, routes and jobs.\n // services: { stats: ({ services }) => ({ articles: () => services.content.count('article') }) },\n\n // Background jobs, registered as ext.<name>; enqueue with services.jobs.enqueue('<name>', payload).\n // jobs: { nightlyReport: async ({ services, logger }) => { logger.info('report'); } },\n});\n`;\n\nconst gitignore = () => `node_modules/\n.env\nmedia/*\n!media/.gitkeep\n`;\n\nconst pm2Ecosystem = ({ projectName }: TemplateInput) => `// PM2 process file: pm2 start ecosystem.config.cjs\n// One instance in fork mode, the simplest setup. Several instances on one database are supported too: use the\n// cluster variant below.\nmodule.exports = {\n apps: [\n {\n name: ${JSON.stringify(projectName)},\n script: 'node_modules/@shapio/cms/dist/cli.js',\n args: 'start',\n exec_mode: 'fork',\n instances: 1,\n kill_timeout: 20000,\n env: { NODE_ENV: 'production' },\n },\n // Cluster variant (replace the app above): two processes sharing the port, each with its inline worker.\n // { name: ${JSON.stringify(projectName)}, script: 'node_modules/@shapio/cms/dist/cli.js', args: 'start', exec_mode: 'cluster', instances: 2, kill_timeout: 20000, env: { NODE_ENV: 'production' } },\n ],\n};\n`;\n\nconst readme = ({ projectName }: TemplateInput) =>\n `# ${projectName}\n\nA [Shapio](https://github.com/mybrokengnome/shapio) project.\n\n1. Set \\`DATABASE_URL\\` in \\`.env\\` to a PostgreSQL (>= 16) database, or to \\`sqlite:<path>\\` for a\n single-process install.\n2. \\`npm run start\\`. Migrations run on start. The first boot logs where to create the owner account: open\n \\`/admin/\\`; the first person to complete Setup becomes the owner.\n3. Under PM2: \\`pm2 start ecosystem.config.cjs\\`.\n\nContent models are edited live in the admin; no restart is needed for modelling. Changing \\`shapio.config.ts\\`\nor \\`extensions/\\` (code) needs a restart; \\`npx shapio extensions check\\` validates them.\n`;\n\n/** Files to write, relative to the project directory. */\nexport const renderTemplates = (input: TemplateInput): ReadonlyArray<[path: string, content: string]> => [\n ['package.json', packageJson(input)],\n ['.env', env(input)],\n [\n '.env.example',\n env({ ...input, databaseUrl: 'postgres://user:password@localhost:5432/shapio', sessionSecret: '' }),\n ],\n ['shapio.config.ts', shapioConfig()],\n ['.gitignore', gitignore()],\n ['ecosystem.config.cjs', pm2Ecosystem(input)],\n ['README.md', readme(input)],\n ['extensions/.gitkeep', ''],\n ['extensions/editors/.gitkeep', ''],\n ['media/.gitkeep', ''],\n];\n","import { randomBytes } from 'node:crypto';\nimport { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';\nimport { basename, dirname, join, resolve } from 'node:path';\nimport { renderTemplates } from './templates.js';\n\nexport type ScaffoldOptions = {\n directory: string;\n shapioSpec: string;\n databaseUrl?: string;\n};\n\nexport class ScaffoldError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ScaffoldError';\n }\n}\n\n/** npm package names: lowercase, URL-safe. Derived from the directory name. */\nexport const toPackageName = (directory: string): string =>\n basename(resolve(directory))\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, '-')\n .replace(/^[._-]+|[-.]+$/g, '') || 'shapio-project';\n\n/** Writes the project files. Refuses to touch a non-empty directory. Returns the absolute project path. */\nexport const scaffoldProject = ({ directory, shapioSpec, databaseUrl }: ScaffoldOptions): string => {\n const root = resolve(directory);\n if (existsSync(root) && readdirSync(root).length > 0) {\n throw new ScaffoldError(`${root} exists and is not empty`);\n }\n const projectName = toPackageName(root);\n const files = renderTemplates({\n projectName,\n shapioSpec,\n databaseUrl:\n databaseUrl ?? `postgres://postgres:postgres@localhost:5432/${projectName.replace(/[^a-z0-9_]/g, '_')}`,\n sessionSecret: randomBytes(32).toString('base64url'),\n });\n for (const [path, content] of files) {\n const target = join(root, path);\n mkdirSync(dirname(target), { recursive: true });\n // `.env` holds SESSION_SECRET and the database password: readable by the owner only.\n writeFileSync(target, content, { flag: 'wx', ...(path === '.env' ? { mode: 0o600 } : {}) });\n }\n return root;\n};\n","/** The site starters `create-shapio --site` scaffolds (examples/<name> in the repository). */\nexport const SITE_STARTERS = ['astro', 'next', 'sveltekit'] as const;\n\nexport type SiteStarter = (typeof SITE_STARTERS)[number];\n\nexport const isSiteStarter = (value: string): value is SiteStarter =>\n (SITE_STARTERS as readonly string[]).includes(value);\n\n/** npm leaves `.gitignore` out of published packages, so templates carry it under this name. */\nexport const PACKED_GITIGNORE = 'gitignore';\n","import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { ScaffoldError, toPackageName } from './scaffold.js';\nimport { PACKED_GITIGNORE, type SiteStarter } from './starters.js';\n\nexport type ScaffoldSiteOptions = {\n directory: string;\n site: SiteStarter;\n /** The packed templates (templates/ next to dist/ in the published package). */\n templatesDir: string;\n};\n\n/**\n * Writes a standalone site project from a starter template: the framework app, the blog's model files in\n * shapio/, and the seed and smoke scripts. Refuses to touch a non-empty directory. Returns the project path.\n */\nexport const scaffoldSite = ({ directory, site, templatesDir }: ScaffoldSiteOptions): string => {\n const root = resolve(directory);\n if (existsSync(root) && readdirSync(root).length > 0) {\n throw new ScaffoldError(`${root} exists and is not empty`);\n }\n const template = join(templatesDir, site);\n if (!existsSync(join(template, 'package.json'))) {\n throw new ScaffoldError(\n `The ${site} template is missing from ${templatesDir}; build create-shapio first`,\n );\n }\n cpSync(template, root, { recursive: true });\n if (existsSync(join(root, PACKED_GITIGNORE))) {\n renameSync(join(root, PACKED_GITIGNORE), join(root, '.gitignore'));\n }\n const packagePath = join(root, 'package.json');\n const packageJson = JSON.parse(readFileSync(packagePath, 'utf8')) as Record<string, unknown>;\n writeFileSync(\n packagePath,\n `${JSON.stringify({ ...packageJson, name: toPackageName(root), version: '0.1.0' }, null, 2)}\\n`,\n );\n return root;\n};\n","import { spawnSync } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { parseArgs } from 'node:util';\nimport { scaffoldProject, ScaffoldError } from './scaffold.js';\nimport { scaffoldSite } from './site.js';\nimport { isSiteStarter, SITE_STARTERS, type SiteStarter } from './starters.js';\n\nconst USAGE = `Usage: npx create-shapio <directory> [options]\n\nWithout --site: a Shapio project (the CMS server). With --site: a website that reads content from Shapio.\n\nOptions:\n --site <${SITE_STARTERS.join('|')}> Create a site starter instead (a blog: pages, articles, authors)\n --database-url <url> Database URL to put in .env (PostgreSQL, or sqlite:<path> for a single-process\n install; CMS project only)\n --shapio-spec <spec> Dependency spec for shapio (default: ^<this version>; a tarball path works too)\n --no-install Skip npm install\n -h, --help Show this help\n`;\n\n/** Packed from the repository's examples/ when create-shapio is built (templates/ beside dist/). */\nconst TEMPLATES_DIR = fileURLToPath(new URL('../templates/', import.meta.url));\n\nconst ownVersion = (): string => {\n const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {\n version: string;\n };\n return packageJson.version;\n};\n\ntype Output = { out: (text: string) => void; err: (text: string) => void };\n\n/** Returns the exit code. */\nexport const main = (argv: readonly string[], { out, err }: Output): number => {\n const { values, positionals } = parseArgs({\n args: [...argv],\n allowPositionals: true,\n allowNegative: true,\n options: {\n 'database-url': { type: 'string' },\n 'shapio-spec': { type: 'string' },\n site: { type: 'string' },\n install: { type: 'boolean', default: true },\n help: { type: 'boolean', short: 'h' },\n },\n });\n const [directory] = positionals;\n if (values.help || !directory) {\n (directory ? out : err)(USAGE);\n return values.help ? 0 : 1;\n }\n const site = values.site;\n if (site !== undefined && !isSiteStarter(site)) {\n err(`create-shapio: --site must be one of ${SITE_STARTERS.join(', ')}\\n`);\n return 1;\n }\n if (site !== undefined && (values['database-url'] || values['shapio-spec'])) {\n err('create-shapio: --database-url and --shapio-spec are for a CMS project, not --site\\n');\n return 1;\n }\n let root: string;\n try {\n root = site\n ? scaffoldSite({ directory, site, templatesDir: TEMPLATES_DIR })\n : scaffoldProject({\n directory,\n shapioSpec: values['shapio-spec'] ?? `^${ownVersion()}`,\n ...(values['database-url'] ? { databaseUrl: values['database-url'] } : {}),\n });\n } catch (error) {\n if (error instanceof ScaffoldError) {\n err(`create-shapio: ${error.message}\\n`);\n return 1;\n }\n throw error;\n }\n out(`Created a ${site ? `${site} site` : 'Shapio project'} in ${root}\\n`);\n if (values.install) {\n out('Installing dependencies with npm...\\n');\n const result = spawnSync('npm', ['install'], { cwd: root, stdio: 'inherit' });\n if (result.status !== 0) {\n err('create-shapio: npm install failed; run it yourself in the project directory\\n');\n return 1;\n }\n }\n out(\n site ? siteNextSteps(directory, site) : projectNextSteps(directory, values['database-url'] !== undefined),\n );\n return 0;\n};\n\nconst SERVE_COMMAND: Record<SiteStarter, string> = {\n astro: 'npm run preview',\n next: 'npm run start',\n sveltekit: 'npm run preview',\n};\n\n/** Points at DATABASE_URL only when --database-url did not already set it. */\nconst projectNextSteps = (directory: string, databaseUrlGiven: boolean) =>\n [\n '',\n 'Next:',\n ` cd ${directory}`,\n ...(databaseUrlGiven ? [] : [' edit .env (DATABASE_URL)']),\n ' npm run start',\n '',\n ].join('\\n');\n\nconst siteNextSteps = (directory: string, site: SiteStarter) =>\n [\n '',\n 'Next (with a running Shapio; README.md has the details):',\n ` cd ${directory}`,\n ' SHAPIO_URL=http://localhost:4300 SHAPIO_ADMIN_EMAIL=you@example.com SHAPIO_ADMIN_PASSWORD=... npm run seed',\n ' npm run build',\n ` ${SERVE_COMMAND[site]}`,\n '',\n ].join('\\n');\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -45,5 +45,4 @@ Shapio may frame it) and writes it to `dist/_headers`.
|
|
|
45
45
|
|
|
46
46
|
Guides: [site starters](https://github.com/mybrokengnome/shapio/blob/main/documentation/starters.md) and the
|
|
47
47
|
[walkthrough](https://github.com/mybrokengnome/shapio/blob/main/documentation/example-site.md) (model, preview,
|
|
48
|
-
deploy to Cloudflare Pages, build callbacks).
|
|
49
|
-
`pnpm --filter example-astro <script>` after `pnpm build`.
|
|
48
|
+
deploy to Cloudflare Pages, build callbacks).
|
|
@@ -28,8 +28,9 @@ import { createPng } from './lib/png.js';
|
|
|
28
28
|
* `npm run seed`: prepares a Shapio instance for this starter, idempotently (run it again to reset the
|
|
29
29
|
* content to the seed):
|
|
30
30
|
* 1. the `fr` locale;
|
|
31
|
-
* 2. the models and components in shapio/
|
|
32
|
-
*
|
|
31
|
+
* 2. the models and components in shapio/ (models/ and components/, plus sites/<key>/models/ and
|
|
32
|
+
* sites/<key>/components/ once `shapio schema pull` has run) through the schema apply API (live, no
|
|
33
|
+
* restart; the same planner as `shapio schema apply` and the admin), as the site's own (SHAPIO_SITE, else the token's site, else the
|
|
33
34
|
* primary): a starter's schema belongs to the first site it is seeded on;
|
|
34
35
|
* The built-in SEO component (components/seo.json) is the exception: it is always shared with every site;
|
|
35
36
|
* 3. placeholder images, an author, the site settings, pages and articles in English and French, published
|
|
@@ -46,6 +47,8 @@ import { createPng } from './lib/png.js';
|
|
|
46
47
|
*/
|
|
47
48
|
const SCHEMA_DIR = resolve(import.meta.dirname, '..', 'shapio');
|
|
48
49
|
const SCHEMA_KINDS = ['models', 'components'] as const;
|
|
50
|
+
/** A pulled tree keeps each site's own definitions in sites/<siteKey>/models and sites/<siteKey>/components. */
|
|
51
|
+
const SITES_DIR = 'sites';
|
|
49
52
|
const SITE_MODELS = ['page', 'article', 'author', 'siteSettings'];
|
|
50
53
|
const DELIVERY_ROLE_KEY = 'starter-delivery';
|
|
51
54
|
const DELIVERY_TOKEN_NAME = 'starter build';
|
|
@@ -92,15 +95,59 @@ const ensureLocale = async (client: ShapioClient) => {
|
|
|
92
95
|
}
|
|
93
96
|
};
|
|
94
97
|
|
|
95
|
-
/**
|
|
98
|
+
/** A refusal the seed explains itself: printed without a stack. */
|
|
99
|
+
class SeedRefusedError extends Error {}
|
|
100
|
+
|
|
101
|
+
/** A folder's entries, or none when it is missing (a fresh starter has no sites/; a pulled one may lack models/). */
|
|
102
|
+
const listFolder = async (path: string): Promise<string[]> => {
|
|
103
|
+
try {
|
|
104
|
+
return (await readdir(path)).sort();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// ENOTDIR: a stray file under sites/ (.DS_Store) is not a site folder.
|
|
107
|
+
const { code } = error as NodeJS.ErrnoException;
|
|
108
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The folders that may hold definition files, in apply order: every models folder, then every components
|
|
117
|
+
* folder; within each kind the shared folder first, then `sites/<key>/` in key order. The site key is ignored
|
|
118
|
+
* (the seed decides the scope, see applySchema).
|
|
119
|
+
*/
|
|
120
|
+
const definitionFolders = async (): Promise<string[]> => {
|
|
121
|
+
const siteFolders = (await listFolder(join(SCHEMA_DIR, SITES_DIR))).map((key) =>
|
|
122
|
+
join(SCHEMA_DIR, SITES_DIR, key),
|
|
123
|
+
);
|
|
124
|
+
return SCHEMA_KINDS.flatMap((kind) => [SCHEMA_DIR, ...siteFolders].map((folder) => join(folder, kind)));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The checked-in definition files (`shapio schema pull` format), models first: the shared `models/` and
|
|
129
|
+
* `components/` plus every `sites/<key>/models/` and `sites/<key>/components/`. A definition whose ID appears
|
|
130
|
+
* twice is read once (the first wins); files without an ID are kept.
|
|
131
|
+
*/
|
|
96
132
|
const readDefinitions = async (): Promise<unknown[]> => {
|
|
97
133
|
const definitions: unknown[] = [];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (const name of
|
|
101
|
-
|
|
134
|
+
const seenIds = new Set<string>();
|
|
135
|
+
for (const folder of await definitionFolders()) {
|
|
136
|
+
for (const name of (await listFolder(folder)).filter((entry) => entry.endsWith('.json'))) {
|
|
137
|
+
const definition: unknown = JSON.parse(await readFile(join(folder, name), 'utf8'));
|
|
138
|
+
const id = (definition as { id?: unknown }).id;
|
|
139
|
+
if (typeof id === 'string') {
|
|
140
|
+
if (seenIds.has(id)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
seenIds.add(id);
|
|
144
|
+
}
|
|
145
|
+
definitions.push(definition);
|
|
102
146
|
}
|
|
103
147
|
}
|
|
148
|
+
if (definitions.length === 0) {
|
|
149
|
+
throw new SeedRefusedError(`No model or component files found in ${SCHEMA_DIR}.`);
|
|
150
|
+
}
|
|
104
151
|
return definitions;
|
|
105
152
|
};
|
|
106
153
|
|
|
@@ -122,9 +169,6 @@ const waitForChange = async (client: ShapioClient, item: SchemaSyncResult) => {
|
|
|
122
169
|
}
|
|
123
170
|
};
|
|
124
171
|
|
|
125
|
-
/** A refusal the seed explains itself: printed without a stack. */
|
|
126
|
-
class SeedRefusedError extends Error {}
|
|
127
|
-
|
|
128
172
|
/** The schema files' IDs already belong to another site's definitions (the starter was seeded there first). */
|
|
129
173
|
const isSeededElsewhere = (error: unknown) =>
|
|
130
174
|
error instanceof ShapioApiError &&
|
package/templates/next/README.md
CHANGED
|
@@ -91,5 +91,4 @@ slugs are a 404. A build pinned with `SHAPIO_SNAPSHOT` keeps showing that snapsh
|
|
|
91
91
|
reason and revalidates nothing. The snapshot is held per `next start` process, so run one process per copy of
|
|
92
92
|
the site, each with its own webhook. In Shapio, Publishing → Webhooks shows every delivery and can send a test.
|
|
93
93
|
|
|
94
|
-
Guide: [site starters](https://github.com/mybrokengnome/shapio/blob/main/documentation/starters.md).
|
|
95
|
-
Shapio repository, run the scripts with `pnpm --filter example-next <script>` after `pnpm build`.
|
|
94
|
+
Guide: [site starters](https://github.com/mybrokengnome/shapio/blob/main/documentation/starters.md).
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@shapio/client": "^0.5.
|
|
22
|
-
"@shapio/local": "0.5.
|
|
23
|
-
"@shapio/visual": "^0.5.
|
|
21
|
+
"@shapio/client": "^0.5.8",
|
|
22
|
+
"@shapio/local": "0.5.8",
|
|
23
|
+
"@shapio/visual": "^0.5.8",
|
|
24
24
|
"next": "16.3.8",
|
|
25
25
|
"react": "19.3.0",
|
|
26
26
|
"react-dom": "19.3.0"
|
|
@@ -28,8 +28,9 @@ import { createPng } from './lib/png.js';
|
|
|
28
28
|
* `npm run seed`: prepares a Shapio instance for this starter, idempotently (run it again to reset the
|
|
29
29
|
* content to the seed):
|
|
30
30
|
* 1. the `fr` locale;
|
|
31
|
-
* 2. the models and components in shapio/
|
|
32
|
-
*
|
|
31
|
+
* 2. the models and components in shapio/ (models/ and components/, plus sites/<key>/models/ and
|
|
32
|
+
* sites/<key>/components/ once `shapio schema pull` has run) through the schema apply API (live, no
|
|
33
|
+
* restart; the same planner as `shapio schema apply` and the admin), as the site's own (SHAPIO_SITE, else the token's site, else the
|
|
33
34
|
* primary): a starter's schema belongs to the first site it is seeded on;
|
|
34
35
|
* The built-in SEO component (components/seo.json) is the exception: it is always shared with every site;
|
|
35
36
|
* 3. placeholder images, an author, the site settings, pages and articles in English and French, published
|
|
@@ -46,6 +47,8 @@ import { createPng } from './lib/png.js';
|
|
|
46
47
|
*/
|
|
47
48
|
const SCHEMA_DIR = resolve(import.meta.dirname, '..', 'shapio');
|
|
48
49
|
const SCHEMA_KINDS = ['models', 'components'] as const;
|
|
50
|
+
/** A pulled tree keeps each site's own definitions in sites/<siteKey>/models and sites/<siteKey>/components. */
|
|
51
|
+
const SITES_DIR = 'sites';
|
|
49
52
|
const SITE_MODELS = ['page', 'article', 'author', 'siteSettings'];
|
|
50
53
|
const DELIVERY_ROLE_KEY = 'starter-delivery';
|
|
51
54
|
const DELIVERY_TOKEN_NAME = 'starter build';
|
|
@@ -92,15 +95,59 @@ const ensureLocale = async (client: ShapioClient) => {
|
|
|
92
95
|
}
|
|
93
96
|
};
|
|
94
97
|
|
|
95
|
-
/**
|
|
98
|
+
/** A refusal the seed explains itself: printed without a stack. */
|
|
99
|
+
class SeedRefusedError extends Error {}
|
|
100
|
+
|
|
101
|
+
/** A folder's entries, or none when it is missing (a fresh starter has no sites/; a pulled one may lack models/). */
|
|
102
|
+
const listFolder = async (path: string): Promise<string[]> => {
|
|
103
|
+
try {
|
|
104
|
+
return (await readdir(path)).sort();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// ENOTDIR: a stray file under sites/ (.DS_Store) is not a site folder.
|
|
107
|
+
const { code } = error as NodeJS.ErrnoException;
|
|
108
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The folders that may hold definition files, in apply order: every models folder, then every components
|
|
117
|
+
* folder; within each kind the shared folder first, then `sites/<key>/` in key order. The site key is ignored
|
|
118
|
+
* (the seed decides the scope, see applySchema).
|
|
119
|
+
*/
|
|
120
|
+
const definitionFolders = async (): Promise<string[]> => {
|
|
121
|
+
const siteFolders = (await listFolder(join(SCHEMA_DIR, SITES_DIR))).map((key) =>
|
|
122
|
+
join(SCHEMA_DIR, SITES_DIR, key),
|
|
123
|
+
);
|
|
124
|
+
return SCHEMA_KINDS.flatMap((kind) => [SCHEMA_DIR, ...siteFolders].map((folder) => join(folder, kind)));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The checked-in definition files (`shapio schema pull` format), models first: the shared `models/` and
|
|
129
|
+
* `components/` plus every `sites/<key>/models/` and `sites/<key>/components/`. A definition whose ID appears
|
|
130
|
+
* twice is read once (the first wins); files without an ID are kept.
|
|
131
|
+
*/
|
|
96
132
|
const readDefinitions = async (): Promise<unknown[]> => {
|
|
97
133
|
const definitions: unknown[] = [];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (const name of
|
|
101
|
-
|
|
134
|
+
const seenIds = new Set<string>();
|
|
135
|
+
for (const folder of await definitionFolders()) {
|
|
136
|
+
for (const name of (await listFolder(folder)).filter((entry) => entry.endsWith('.json'))) {
|
|
137
|
+
const definition: unknown = JSON.parse(await readFile(join(folder, name), 'utf8'));
|
|
138
|
+
const id = (definition as { id?: unknown }).id;
|
|
139
|
+
if (typeof id === 'string') {
|
|
140
|
+
if (seenIds.has(id)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
seenIds.add(id);
|
|
144
|
+
}
|
|
145
|
+
definitions.push(definition);
|
|
102
146
|
}
|
|
103
147
|
}
|
|
148
|
+
if (definitions.length === 0) {
|
|
149
|
+
throw new SeedRefusedError(`No model or component files found in ${SCHEMA_DIR}.`);
|
|
150
|
+
}
|
|
104
151
|
return definitions;
|
|
105
152
|
};
|
|
106
153
|
|
|
@@ -122,9 +169,6 @@ const waitForChange = async (client: ShapioClient, item: SchemaSyncResult) => {
|
|
|
122
169
|
}
|
|
123
170
|
};
|
|
124
171
|
|
|
125
|
-
/** A refusal the seed explains itself: printed without a stack. */
|
|
126
|
-
class SeedRefusedError extends Error {}
|
|
127
|
-
|
|
128
172
|
/** The schema files' IDs already belong to another site's definitions (the starter was seeded there first). */
|
|
129
173
|
const isSeededElsewhere = (error: unknown) =>
|
|
130
174
|
error instanceof ShapioApiError &&
|
|
@@ -43,5 +43,4 @@ origin to Shapio's `CORS_ORIGINS`. In Shapio's preview pane, clicking the title,
|
|
|
43
43
|
field and saves re-render the draft ([visual editing](https://github.com/mybrokengnome/shapio/blob/main/documentation/visual-editing.md)). `PUBLIC_SHAPIO_URL` sets the Shapio URL the
|
|
44
44
|
browser calls, when it differs from `SHAPIO_URL`.
|
|
45
45
|
|
|
46
|
-
Guide: [site starters](https://github.com/mybrokengnome/shapio/blob/main/documentation/starters.md).
|
|
47
|
-
Shapio repository, run the scripts with `pnpm --filter example-sveltekit <script>` after `pnpm build`.
|
|
46
|
+
Guide: [site starters](https://github.com/mybrokengnome/shapio/blob/main/documentation/starters.md).
|
|
@@ -28,8 +28,9 @@ import { createPng } from './lib/png.js';
|
|
|
28
28
|
* `npm run seed`: prepares a Shapio instance for this starter, idempotently (run it again to reset the
|
|
29
29
|
* content to the seed):
|
|
30
30
|
* 1. the `fr` locale;
|
|
31
|
-
* 2. the models and components in shapio/
|
|
32
|
-
*
|
|
31
|
+
* 2. the models and components in shapio/ (models/ and components/, plus sites/<key>/models/ and
|
|
32
|
+
* sites/<key>/components/ once `shapio schema pull` has run) through the schema apply API (live, no
|
|
33
|
+
* restart; the same planner as `shapio schema apply` and the admin), as the site's own (SHAPIO_SITE, else the token's site, else the
|
|
33
34
|
* primary): a starter's schema belongs to the first site it is seeded on;
|
|
34
35
|
* The built-in SEO component (components/seo.json) is the exception: it is always shared with every site;
|
|
35
36
|
* 3. placeholder images, an author, the site settings, pages and articles in English and French, published
|
|
@@ -46,6 +47,8 @@ import { createPng } from './lib/png.js';
|
|
|
46
47
|
*/
|
|
47
48
|
const SCHEMA_DIR = resolve(import.meta.dirname, '..', 'shapio');
|
|
48
49
|
const SCHEMA_KINDS = ['models', 'components'] as const;
|
|
50
|
+
/** A pulled tree keeps each site's own definitions in sites/<siteKey>/models and sites/<siteKey>/components. */
|
|
51
|
+
const SITES_DIR = 'sites';
|
|
49
52
|
const SITE_MODELS = ['page', 'article', 'author', 'siteSettings'];
|
|
50
53
|
const DELIVERY_ROLE_KEY = 'starter-delivery';
|
|
51
54
|
const DELIVERY_TOKEN_NAME = 'starter build';
|
|
@@ -92,15 +95,59 @@ const ensureLocale = async (client: ShapioClient) => {
|
|
|
92
95
|
}
|
|
93
96
|
};
|
|
94
97
|
|
|
95
|
-
/**
|
|
98
|
+
/** A refusal the seed explains itself: printed without a stack. */
|
|
99
|
+
class SeedRefusedError extends Error {}
|
|
100
|
+
|
|
101
|
+
/** A folder's entries, or none when it is missing (a fresh starter has no sites/; a pulled one may lack models/). */
|
|
102
|
+
const listFolder = async (path: string): Promise<string[]> => {
|
|
103
|
+
try {
|
|
104
|
+
return (await readdir(path)).sort();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// ENOTDIR: a stray file under sites/ (.DS_Store) is not a site folder.
|
|
107
|
+
const { code } = error as NodeJS.ErrnoException;
|
|
108
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The folders that may hold definition files, in apply order: every models folder, then every components
|
|
117
|
+
* folder; within each kind the shared folder first, then `sites/<key>/` in key order. The site key is ignored
|
|
118
|
+
* (the seed decides the scope, see applySchema).
|
|
119
|
+
*/
|
|
120
|
+
const definitionFolders = async (): Promise<string[]> => {
|
|
121
|
+
const siteFolders = (await listFolder(join(SCHEMA_DIR, SITES_DIR))).map((key) =>
|
|
122
|
+
join(SCHEMA_DIR, SITES_DIR, key),
|
|
123
|
+
);
|
|
124
|
+
return SCHEMA_KINDS.flatMap((kind) => [SCHEMA_DIR, ...siteFolders].map((folder) => join(folder, kind)));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The checked-in definition files (`shapio schema pull` format), models first: the shared `models/` and
|
|
129
|
+
* `components/` plus every `sites/<key>/models/` and `sites/<key>/components/`. A definition whose ID appears
|
|
130
|
+
* twice is read once (the first wins); files without an ID are kept.
|
|
131
|
+
*/
|
|
96
132
|
const readDefinitions = async (): Promise<unknown[]> => {
|
|
97
133
|
const definitions: unknown[] = [];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (const name of
|
|
101
|
-
|
|
134
|
+
const seenIds = new Set<string>();
|
|
135
|
+
for (const folder of await definitionFolders()) {
|
|
136
|
+
for (const name of (await listFolder(folder)).filter((entry) => entry.endsWith('.json'))) {
|
|
137
|
+
const definition: unknown = JSON.parse(await readFile(join(folder, name), 'utf8'));
|
|
138
|
+
const id = (definition as { id?: unknown }).id;
|
|
139
|
+
if (typeof id === 'string') {
|
|
140
|
+
if (seenIds.has(id)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
seenIds.add(id);
|
|
144
|
+
}
|
|
145
|
+
definitions.push(definition);
|
|
102
146
|
}
|
|
103
147
|
}
|
|
148
|
+
if (definitions.length === 0) {
|
|
149
|
+
throw new SeedRefusedError(`No model or component files found in ${SCHEMA_DIR}.`);
|
|
150
|
+
}
|
|
104
151
|
return definitions;
|
|
105
152
|
};
|
|
106
153
|
|
|
@@ -122,9 +169,6 @@ const waitForChange = async (client: ShapioClient, item: SchemaSyncResult) => {
|
|
|
122
169
|
}
|
|
123
170
|
};
|
|
124
171
|
|
|
125
|
-
/** A refusal the seed explains itself: printed without a stack. */
|
|
126
|
-
class SeedRefusedError extends Error {}
|
|
127
|
-
|
|
128
172
|
/** The schema files' IDs already belong to another site's definitions (the starter was seeded there first). */
|
|
129
173
|
const isSeededElsewhere = (error: unknown) =>
|
|
130
174
|
error instanceof ShapioApiError &&
|