create-shapio 0.5.9 → 0.5.11
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 +1 -1
- package/dist/{main-CJjrw1fB.js → main-9HZdnJkv.js} +3 -3
- package/dist/main-9HZdnJkv.js.map +1 -0
- package/package.json +1 -1
- package/templates/astro/package.json +2 -2
- package/templates/astro/scripts/incrementalCheck.ts +1 -1
- package/templates/astro/scripts/lib/content.ts +7 -7
- package/templates/astro/scripts/lib/png.ts +2 -2
- package/templates/astro/scripts/seed.ts +1 -1
- package/templates/astro/scripts/smoke.ts +4 -4
- package/templates/astro/scripts/smokeDrafts.ts +1 -1
- package/templates/astro/scripts/smokeHttp.ts +4 -4
- package/templates/astro/src/lib/render.ts +1 -1
- package/templates/astro/src/lib/types.ts +1 -1
- package/templates/next/package.json +3 -3
- package/templates/next/scripts/lib/content.ts +7 -7
- package/templates/next/scripts/lib/png.ts +2 -2
- package/templates/next/scripts/seed.ts +1 -1
- package/templates/next/scripts/smokeDrafts.ts +1 -1
- package/templates/next/scripts/smokeHttp.ts +4 -4
- package/templates/next/src/components/RichText/index.tsx +1 -1
- package/templates/next/src/lib/types.ts +1 -1
- package/templates/sveltekit/package.json +2 -2
- package/templates/sveltekit/scripts/lib/content.ts +7 -7
- package/templates/sveltekit/scripts/lib/png.ts +2 -2
- package/templates/sveltekit/scripts/seed.ts +1 -1
- package/templates/sveltekit/scripts/smokeDrafts.ts +1 -1
- package/templates/sveltekit/scripts/smokeHttp.ts +4 -4
- package/templates/sveltekit/src/lib/components/RichText.svelte +1 -1
- package/templates/sveltekit/src/lib/types.ts +1 -1
- package/dist/main-CJjrw1fB.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ if (refusal) {
|
|
|
10
10
|
process.stderr.write(`${refusal}\n`);
|
|
11
11
|
process.exit(1);
|
|
12
12
|
}
|
|
13
|
-
const { main } = await import("./main-
|
|
13
|
+
const { main } = await import("./main-9HZdnJkv.js");
|
|
14
14
|
process.exitCode = main(process.argv.slice(2), {
|
|
15
15
|
out: (text) => process.stdout.write(text),
|
|
16
16
|
err: (text) => process.stderr.write(text)
|
|
@@ -74,7 +74,7 @@ export const config = defineConfig({
|
|
|
74
74
|
// ['star-rating.js']. The admin loads them at runtime; installing one is a file copy plus a restart.
|
|
75
75
|
editors: [],
|
|
76
76
|
|
|
77
|
-
// Admin
|
|
77
|
+
// Admin color themes, listed after the built-in ones. Each variant sets every UI token; see
|
|
78
78
|
// documentation/extensions.md (Admin themes) and examples/extension/extensions/sepiaTheme.ts.
|
|
79
79
|
// themes: [{ key: 'acme', name: 'Acme', dark: { background: '#101014', foreground: '#f4f4f5', ... } }],
|
|
80
80
|
|
|
@@ -122,7 +122,7 @@ A [Shapio](https://github.com/mybrokengnome/shapio) project.
|
|
|
122
122
|
\`/admin/\`; the first person to complete Setup becomes the owner.
|
|
123
123
|
3. Under PM2: \`pm2 start ecosystem.config.cjs\`.
|
|
124
124
|
|
|
125
|
-
Content models are edited live in the admin; no restart is needed for
|
|
125
|
+
Content models are edited live in the admin; no restart is needed for modeling. Changing \`shapio.config.ts\`
|
|
126
126
|
or \`extensions/\` (code) needs a restart; \`npx shapio extensions check\` validates them.
|
|
127
127
|
`;
|
|
128
128
|
/** Files to write, relative to the project directory. */
|
|
@@ -317,4 +317,4 @@ const siteNextSteps = (directory, site) => [
|
|
|
317
317
|
//#endregion
|
|
318
318
|
export { main };
|
|
319
319
|
|
|
320
|
-
//# sourceMappingURL=main-
|
|
320
|
+
//# sourceMappingURL=main-9HZdnJkv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main-9HZdnJkv.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 color 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 modeling. 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
|
@@ -23,7 +23,7 @@ const ROOT = resolve(import.meta.dirname, '..');
|
|
|
23
23
|
const DIST = join(ROOT, 'dist');
|
|
24
24
|
const CACHE_DIR = join(ROOT, 'node_modules', '.astro');
|
|
25
25
|
const MANIFEST = join(CACHE_DIR, 'incremental-build.json');
|
|
26
|
-
const ARTICLE = '
|
|
26
|
+
const ARTICLE = 'modeling-without-a-deploy';
|
|
27
27
|
const EDITED_PAGE = `en/articles/${ARTICLE}/index.html`;
|
|
28
28
|
/** Always rendered, and carries the build time: the only file two builds of one snapshot may differ in. */
|
|
29
29
|
const TIMESTAMPED = new Set(['build.json']);
|
|
@@ -49,7 +49,7 @@ export const IMAGES = {
|
|
|
49
49
|
to: [234, 88, 12],
|
|
50
50
|
},
|
|
51
51
|
coverOne: {
|
|
52
|
-
filename: 'cover-
|
|
52
|
+
filename: 'cover-modeling.png',
|
|
53
53
|
alt: 'Blocks arranged into a grid',
|
|
54
54
|
width: 1600,
|
|
55
55
|
height: 900,
|
|
@@ -229,7 +229,7 @@ const about = (): Localized<Record<string, unknown>> => ({
|
|
|
229
229
|
{
|
|
230
230
|
__component: 'callToAction',
|
|
231
231
|
heading: 'Read our journal',
|
|
232
|
-
text: 'Notes on
|
|
232
|
+
text: 'Notes on modeling, publishing and building.',
|
|
233
233
|
buttonLabel: 'Open the journal',
|
|
234
234
|
buttonUrl: '/en/articles/',
|
|
235
235
|
},
|
|
@@ -261,10 +261,10 @@ const about = (): Localized<Record<string, unknown>> => ({
|
|
|
261
261
|
},
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
-
const
|
|
264
|
+
const modeling = (media: MediaIds, authorId: string): Localized<Record<string, unknown>> => ({
|
|
265
265
|
en: {
|
|
266
|
-
title: '
|
|
267
|
-
slug: '
|
|
266
|
+
title: 'Modeling content without a deploy',
|
|
267
|
+
slug: 'modeling-without-a-deploy',
|
|
268
268
|
excerpt: 'We added a field to our article model on a Tuesday afternoon. Nothing restarted.',
|
|
269
269
|
publishedOn: '2026-09-01',
|
|
270
270
|
cover: media.coverOne,
|
|
@@ -468,8 +468,8 @@ export const entries = (media: MediaIds, authorId: string): EntrySpec[] => [
|
|
|
468
468
|
{ model: 'page', slug: 'about', content: withSeo(about()), publish: true },
|
|
469
469
|
{
|
|
470
470
|
model: 'article',
|
|
471
|
-
slug: '
|
|
472
|
-
content: withSeo(
|
|
471
|
+
slug: 'modeling-without-a-deploy',
|
|
472
|
+
content: withSeo(modeling(media, authorId)),
|
|
473
473
|
publish: true,
|
|
474
474
|
},
|
|
475
475
|
{
|
|
@@ -2,7 +2,7 @@ import { crc32, deflateSync } from 'node:zlib';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A dependency-free PNG encoder for the seed's placeholder images: an RGB diagonal gradient between two
|
|
5
|
-
*
|
|
5
|
+
* colors with a soft circle, so variants and galleries have something to show.
|
|
6
6
|
*/
|
|
7
7
|
export type Rgb = readonly [number, number, number];
|
|
8
8
|
|
|
@@ -22,7 +22,7 @@ export const createPng = (width: number, height: number, from: Rgb, to: Rgb): Bu
|
|
|
22
22
|
header.writeUInt32BE(width, 0);
|
|
23
23
|
header.writeUInt32BE(height, 4);
|
|
24
24
|
header[8] = 8; // bit depth
|
|
25
|
-
header[9] = 2; //
|
|
25
|
+
header[9] = 2; // color type: RGB
|
|
26
26
|
const stride = width * 3 + 1;
|
|
27
27
|
const pixels = Buffer.alloc(stride * height);
|
|
28
28
|
const radius = Math.min(width, height) / 4;
|
|
@@ -159,7 +159,7 @@ const waitForChange = async (client: ShapioClient, item: SchemaSyncResult) => {
|
|
|
159
159
|
if (change.status === 'activated') {
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
|
-
if (change.status === 'failed' || change.status === '
|
|
162
|
+
if (change.status === 'failed' || change.status === 'canceled') {
|
|
163
163
|
throw new Error(`The ${item.apiKey} change ${change.status}: ${JSON.stringify(change.error)}`);
|
|
164
164
|
}
|
|
165
165
|
if (Date.now() > deadline) {
|
|
@@ -56,14 +56,14 @@ const checkBuild = async () => {
|
|
|
56
56
|
);
|
|
57
57
|
check(homeFr.includes('Un contenu qui évolue aussi vite que votre produit'), 'FR home: hero heading');
|
|
58
58
|
check(homeFr.includes('<html lang="fr"'), 'FR home: lang="fr"');
|
|
59
|
-
const articleEn = await html('en/articles/
|
|
60
|
-
const articleFr = await html('fr/articles/
|
|
61
|
-
check(articleEn.includes('
|
|
59
|
+
const articleEn = await html('en/articles/modeling-without-a-deploy/index.html');
|
|
60
|
+
const articleFr = await html('fr/articles/modeling-without-a-deploy/index.html');
|
|
61
|
+
check(articleEn.includes('Modeling content without a deploy'), 'EN article: title');
|
|
62
62
|
check(articleEn.includes('<table>') && articleEn.includes('With Shapio'), 'EN article: rich-text table');
|
|
63
63
|
check(articleEn.includes('Ada Moreau'), 'EN article: populated author');
|
|
64
64
|
check(articleFr.includes('Modéliser du contenu sans déploiement'), 'FR article: title');
|
|
65
65
|
check(
|
|
66
|
-
articleEn.includes('<title>
|
|
66
|
+
articleEn.includes('<title>Modeling content without a deploy · Northwind Studio</title>'),
|
|
67
67
|
'EN article: SEO title through the site template',
|
|
68
68
|
);
|
|
69
69
|
check(
|
|
@@ -11,7 +11,7 @@ import { connectAdmin } from './lib/admin.js';
|
|
|
11
11
|
* `--url` is the running dev server's origin (default: SMOKE_URL, then http://localhost:3000).
|
|
12
12
|
*/
|
|
13
13
|
const DRAFT_TITLE = 'Coming soon: our winter projects';
|
|
14
|
-
const ARTICLE = '
|
|
14
|
+
const ARTICLE = 'modeling-without-a-deploy';
|
|
15
15
|
const BADGE = 'data-shapio-drafts';
|
|
16
16
|
/** How long a saved draft may take to show: the dev server reads fresh, so this only absorbs a slow render. */
|
|
17
17
|
const SHOW_WITHIN_MS = 30_000;
|
|
@@ -21,7 +21,7 @@ import { parseArgs } from 'node:util';
|
|
|
21
21
|
* `diff -r`, e.g. the Next.js starter reading over HTTP and in process.
|
|
22
22
|
*/
|
|
23
23
|
const DRAFT_TITLE = 'Coming soon: our winter projects';
|
|
24
|
-
const ARTICLE = '
|
|
24
|
+
const ARTICLE = 'modeling-without-a-deploy';
|
|
25
25
|
|
|
26
26
|
type PageCheck = { path: string; expect: Array<[needle: string, label: string]> };
|
|
27
27
|
|
|
@@ -52,7 +52,7 @@ const PAGES: readonly PageCheck[] = [
|
|
|
52
52
|
{
|
|
53
53
|
path: '/en/articles/',
|
|
54
54
|
expect: [
|
|
55
|
-
['
|
|
55
|
+
['Modeling content without a deploy', 'article list'],
|
|
56
56
|
['Why our builds pin a snapshot', 'second article'],
|
|
57
57
|
['<title>Journal · Northwind Studio</title>', 'a page without an entry: title through the template'],
|
|
58
58
|
],
|
|
@@ -60,14 +60,14 @@ const PAGES: readonly PageCheck[] = [
|
|
|
60
60
|
{
|
|
61
61
|
path: `/en/articles/${ARTICLE}/`,
|
|
62
62
|
expect: [
|
|
63
|
-
['
|
|
63
|
+
['Modeling content without a deploy', 'title'],
|
|
64
64
|
['<table', 'rich-text table'],
|
|
65
65
|
['With Shapio', 'rich-text table cell'],
|
|
66
66
|
['Ada Moreau', 'populated author'],
|
|
67
67
|
['data-shapio-entry="', 'visual-editing entry attribute'],
|
|
68
68
|
['data-shapio-path="title"', 'visual-editing attribute on the title'],
|
|
69
69
|
['data-shapio-path="body"', 'visual-editing attribute on the body'],
|
|
70
|
-
['<title>
|
|
70
|
+
['<title>Modeling content without a deploy · Northwind Studio</title>', 'SEO title (resolved)'],
|
|
71
71
|
['property="og:type" content="article"', 'og:type article'],
|
|
72
72
|
['Nothing restarted.', 'SEO description from the entry'],
|
|
73
73
|
['name="twitter:card" content="summary_large_image"', 'twitter:card with the cover image'],
|
|
@@ -5,7 +5,7 @@ import type { Article, Author, Media, Section } from './types.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* HTML for the site's content, as plain functions: the static pages render with them at build time and the
|
|
7
7
|
* preview page renders drafts with the same functions in the browser, so a preview looks exactly like the
|
|
8
|
-
* published page. Every string from the CMS is escaped; rich text arrives as Shapio's
|
|
8
|
+
* published page. Every string from the CMS is escaped; rich text arrives as Shapio's sanitized `html`.
|
|
9
9
|
*/
|
|
10
10
|
export type RenderContext = { locale: string; strings: Strings };
|
|
11
11
|
|
|
@@ -3,7 +3,7 @@ import type { SeoResolved } from '@shapio/client';
|
|
|
3
3
|
/**
|
|
4
4
|
* Content as the Shapio delivery API returns it for this site's models (shapio/). Delivery
|
|
5
5
|
* entries are flat: system attributes next to the fields, keyed by API key. Media fields are asset views,
|
|
6
|
-
* rich text carries the JSON document plus a
|
|
6
|
+
* rich text carries the JSON document plus a sanitized `html` rendering, populated relations are entries.
|
|
7
7
|
*/
|
|
8
8
|
export type MediaVariant = {
|
|
9
9
|
name: string;
|
|
@@ -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.11",
|
|
22
|
+
"@shapio/local": "0.5.11",
|
|
23
|
+
"@shapio/visual": "^0.5.11",
|
|
24
24
|
"next": "16.3.8",
|
|
25
25
|
"react": "19.3.0",
|
|
26
26
|
"react-dom": "19.3.0"
|
|
@@ -49,7 +49,7 @@ export const IMAGES = {
|
|
|
49
49
|
to: [234, 88, 12],
|
|
50
50
|
},
|
|
51
51
|
coverOne: {
|
|
52
|
-
filename: 'cover-
|
|
52
|
+
filename: 'cover-modeling.png',
|
|
53
53
|
alt: 'Blocks arranged into a grid',
|
|
54
54
|
width: 1600,
|
|
55
55
|
height: 900,
|
|
@@ -229,7 +229,7 @@ const about = (): Localized<Record<string, unknown>> => ({
|
|
|
229
229
|
{
|
|
230
230
|
__component: 'callToAction',
|
|
231
231
|
heading: 'Read our journal',
|
|
232
|
-
text: 'Notes on
|
|
232
|
+
text: 'Notes on modeling, publishing and building.',
|
|
233
233
|
buttonLabel: 'Open the journal',
|
|
234
234
|
buttonUrl: '/en/articles/',
|
|
235
235
|
},
|
|
@@ -261,10 +261,10 @@ const about = (): Localized<Record<string, unknown>> => ({
|
|
|
261
261
|
},
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
-
const
|
|
264
|
+
const modeling = (media: MediaIds, authorId: string): Localized<Record<string, unknown>> => ({
|
|
265
265
|
en: {
|
|
266
|
-
title: '
|
|
267
|
-
slug: '
|
|
266
|
+
title: 'Modeling content without a deploy',
|
|
267
|
+
slug: 'modeling-without-a-deploy',
|
|
268
268
|
excerpt: 'We added a field to our article model on a Tuesday afternoon. Nothing restarted.',
|
|
269
269
|
publishedOn: '2026-09-01',
|
|
270
270
|
cover: media.coverOne,
|
|
@@ -468,8 +468,8 @@ export const entries = (media: MediaIds, authorId: string): EntrySpec[] => [
|
|
|
468
468
|
{ model: 'page', slug: 'about', content: withSeo(about()), publish: true },
|
|
469
469
|
{
|
|
470
470
|
model: 'article',
|
|
471
|
-
slug: '
|
|
472
|
-
content: withSeo(
|
|
471
|
+
slug: 'modeling-without-a-deploy',
|
|
472
|
+
content: withSeo(modeling(media, authorId)),
|
|
473
473
|
publish: true,
|
|
474
474
|
},
|
|
475
475
|
{
|
|
@@ -2,7 +2,7 @@ import { crc32, deflateSync } from 'node:zlib';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A dependency-free PNG encoder for the seed's placeholder images: an RGB diagonal gradient between two
|
|
5
|
-
*
|
|
5
|
+
* colors with a soft circle, so variants and galleries have something to show.
|
|
6
6
|
*/
|
|
7
7
|
export type Rgb = readonly [number, number, number];
|
|
8
8
|
|
|
@@ -22,7 +22,7 @@ export const createPng = (width: number, height: number, from: Rgb, to: Rgb): Bu
|
|
|
22
22
|
header.writeUInt32BE(width, 0);
|
|
23
23
|
header.writeUInt32BE(height, 4);
|
|
24
24
|
header[8] = 8; // bit depth
|
|
25
|
-
header[9] = 2; //
|
|
25
|
+
header[9] = 2; // color type: RGB
|
|
26
26
|
const stride = width * 3 + 1;
|
|
27
27
|
const pixels = Buffer.alloc(stride * height);
|
|
28
28
|
const radius = Math.min(width, height) / 4;
|
|
@@ -159,7 +159,7 @@ const waitForChange = async (client: ShapioClient, item: SchemaSyncResult) => {
|
|
|
159
159
|
if (change.status === 'activated') {
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
|
-
if (change.status === 'failed' || change.status === '
|
|
162
|
+
if (change.status === 'failed' || change.status === 'canceled') {
|
|
163
163
|
throw new Error(`The ${item.apiKey} change ${change.status}: ${JSON.stringify(change.error)}`);
|
|
164
164
|
}
|
|
165
165
|
if (Date.now() > deadline) {
|
|
@@ -11,7 +11,7 @@ import { connectAdmin } from './lib/admin.js';
|
|
|
11
11
|
* `--url` is the running dev server's origin (default: SMOKE_URL, then http://localhost:3000).
|
|
12
12
|
*/
|
|
13
13
|
const DRAFT_TITLE = 'Coming soon: our winter projects';
|
|
14
|
-
const ARTICLE = '
|
|
14
|
+
const ARTICLE = 'modeling-without-a-deploy';
|
|
15
15
|
const BADGE = 'data-shapio-drafts';
|
|
16
16
|
/** How long a saved draft may take to show: the dev server reads fresh, so this only absorbs a slow render. */
|
|
17
17
|
const SHOW_WITHIN_MS = 30_000;
|
|
@@ -21,7 +21,7 @@ import { parseArgs } from 'node:util';
|
|
|
21
21
|
* `diff -r`, e.g. the Next.js starter reading over HTTP and in process.
|
|
22
22
|
*/
|
|
23
23
|
const DRAFT_TITLE = 'Coming soon: our winter projects';
|
|
24
|
-
const ARTICLE = '
|
|
24
|
+
const ARTICLE = 'modeling-without-a-deploy';
|
|
25
25
|
|
|
26
26
|
type PageCheck = { path: string; expect: Array<[needle: string, label: string]> };
|
|
27
27
|
|
|
@@ -52,7 +52,7 @@ const PAGES: readonly PageCheck[] = [
|
|
|
52
52
|
{
|
|
53
53
|
path: '/en/articles/',
|
|
54
54
|
expect: [
|
|
55
|
-
['
|
|
55
|
+
['Modeling content without a deploy', 'article list'],
|
|
56
56
|
['Why our builds pin a snapshot', 'second article'],
|
|
57
57
|
['<title>Journal · Northwind Studio</title>', 'a page without an entry: title through the template'],
|
|
58
58
|
],
|
|
@@ -60,14 +60,14 @@ const PAGES: readonly PageCheck[] = [
|
|
|
60
60
|
{
|
|
61
61
|
path: `/en/articles/${ARTICLE}/`,
|
|
62
62
|
expect: [
|
|
63
|
-
['
|
|
63
|
+
['Modeling content without a deploy', 'title'],
|
|
64
64
|
['<table', 'rich-text table'],
|
|
65
65
|
['With Shapio', 'rich-text table cell'],
|
|
66
66
|
['Ada Moreau', 'populated author'],
|
|
67
67
|
['data-shapio-entry="', 'visual-editing entry attribute'],
|
|
68
68
|
['data-shapio-path="title"', 'visual-editing attribute on the title'],
|
|
69
69
|
['data-shapio-path="body"', 'visual-editing attribute on the body'],
|
|
70
|
-
['<title>
|
|
70
|
+
['<title>Modeling content without a deploy · Northwind Studio</title>', 'SEO title (resolved)'],
|
|
71
71
|
['property="og:type" content="article"', 'og:type article'],
|
|
72
72
|
['Nothing restarted.', 'SEO description from the entry'],
|
|
73
73
|
['name="twitter:card" content="summary_large_image"', 'twitter:card with the cover image'],
|
|
@@ -8,6 +8,6 @@ type RichTextProps = {
|
|
|
8
8
|
visual?: ShapioAttributes;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
/** Shapio's rich text arrives with a
|
|
11
|
+
/** Shapio's rich text arrives with a sanitized `html` rendering next to the JSON document. */
|
|
12
12
|
export const RichText = ({ value, className = 'prose', visual }: RichTextProps) =>
|
|
13
13
|
value ? <div className={className} {...visual} dangerouslySetInnerHTML={{ __html: value.html }} /> : null;
|
|
@@ -3,7 +3,7 @@ import type { SeoResolved } from '@shapio/client';
|
|
|
3
3
|
/**
|
|
4
4
|
* Content as the Shapio delivery API returns it for the starter's models (shapio/). Delivery
|
|
5
5
|
* entries are flat: system attributes next to the fields, keyed by API key. Media fields are asset views,
|
|
6
|
-
* rich text carries the JSON document plus a
|
|
6
|
+
* rich text carries the JSON document plus a sanitized `html` rendering, populated relations are entries.
|
|
7
7
|
*/
|
|
8
8
|
export type MediaVariant = {
|
|
9
9
|
name: string;
|
|
@@ -49,7 +49,7 @@ export const IMAGES = {
|
|
|
49
49
|
to: [234, 88, 12],
|
|
50
50
|
},
|
|
51
51
|
coverOne: {
|
|
52
|
-
filename: 'cover-
|
|
52
|
+
filename: 'cover-modeling.png',
|
|
53
53
|
alt: 'Blocks arranged into a grid',
|
|
54
54
|
width: 1600,
|
|
55
55
|
height: 900,
|
|
@@ -229,7 +229,7 @@ const about = (): Localized<Record<string, unknown>> => ({
|
|
|
229
229
|
{
|
|
230
230
|
__component: 'callToAction',
|
|
231
231
|
heading: 'Read our journal',
|
|
232
|
-
text: 'Notes on
|
|
232
|
+
text: 'Notes on modeling, publishing and building.',
|
|
233
233
|
buttonLabel: 'Open the journal',
|
|
234
234
|
buttonUrl: '/en/articles/',
|
|
235
235
|
},
|
|
@@ -261,10 +261,10 @@ const about = (): Localized<Record<string, unknown>> => ({
|
|
|
261
261
|
},
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
-
const
|
|
264
|
+
const modeling = (media: MediaIds, authorId: string): Localized<Record<string, unknown>> => ({
|
|
265
265
|
en: {
|
|
266
|
-
title: '
|
|
267
|
-
slug: '
|
|
266
|
+
title: 'Modeling content without a deploy',
|
|
267
|
+
slug: 'modeling-without-a-deploy',
|
|
268
268
|
excerpt: 'We added a field to our article model on a Tuesday afternoon. Nothing restarted.',
|
|
269
269
|
publishedOn: '2026-09-01',
|
|
270
270
|
cover: media.coverOne,
|
|
@@ -468,8 +468,8 @@ export const entries = (media: MediaIds, authorId: string): EntrySpec[] => [
|
|
|
468
468
|
{ model: 'page', slug: 'about', content: withSeo(about()), publish: true },
|
|
469
469
|
{
|
|
470
470
|
model: 'article',
|
|
471
|
-
slug: '
|
|
472
|
-
content: withSeo(
|
|
471
|
+
slug: 'modeling-without-a-deploy',
|
|
472
|
+
content: withSeo(modeling(media, authorId)),
|
|
473
473
|
publish: true,
|
|
474
474
|
},
|
|
475
475
|
{
|
|
@@ -2,7 +2,7 @@ import { crc32, deflateSync } from 'node:zlib';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A dependency-free PNG encoder for the seed's placeholder images: an RGB diagonal gradient between two
|
|
5
|
-
*
|
|
5
|
+
* colors with a soft circle, so variants and galleries have something to show.
|
|
6
6
|
*/
|
|
7
7
|
export type Rgb = readonly [number, number, number];
|
|
8
8
|
|
|
@@ -22,7 +22,7 @@ export const createPng = (width: number, height: number, from: Rgb, to: Rgb): Bu
|
|
|
22
22
|
header.writeUInt32BE(width, 0);
|
|
23
23
|
header.writeUInt32BE(height, 4);
|
|
24
24
|
header[8] = 8; // bit depth
|
|
25
|
-
header[9] = 2; //
|
|
25
|
+
header[9] = 2; // color type: RGB
|
|
26
26
|
const stride = width * 3 + 1;
|
|
27
27
|
const pixels = Buffer.alloc(stride * height);
|
|
28
28
|
const radius = Math.min(width, height) / 4;
|
|
@@ -159,7 +159,7 @@ const waitForChange = async (client: ShapioClient, item: SchemaSyncResult) => {
|
|
|
159
159
|
if (change.status === 'activated') {
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
|
-
if (change.status === 'failed' || change.status === '
|
|
162
|
+
if (change.status === 'failed' || change.status === 'canceled') {
|
|
163
163
|
throw new Error(`The ${item.apiKey} change ${change.status}: ${JSON.stringify(change.error)}`);
|
|
164
164
|
}
|
|
165
165
|
if (Date.now() > deadline) {
|
|
@@ -11,7 +11,7 @@ import { connectAdmin } from './lib/admin.js';
|
|
|
11
11
|
* `--url` is the running dev server's origin (default: SMOKE_URL, then http://localhost:3000).
|
|
12
12
|
*/
|
|
13
13
|
const DRAFT_TITLE = 'Coming soon: our winter projects';
|
|
14
|
-
const ARTICLE = '
|
|
14
|
+
const ARTICLE = 'modeling-without-a-deploy';
|
|
15
15
|
const BADGE = 'data-shapio-drafts';
|
|
16
16
|
/** How long a saved draft may take to show: the dev server reads fresh, so this only absorbs a slow render. */
|
|
17
17
|
const SHOW_WITHIN_MS = 30_000;
|
|
@@ -21,7 +21,7 @@ import { parseArgs } from 'node:util';
|
|
|
21
21
|
* `diff -r`, e.g. the Next.js starter reading over HTTP and in process.
|
|
22
22
|
*/
|
|
23
23
|
const DRAFT_TITLE = 'Coming soon: our winter projects';
|
|
24
|
-
const ARTICLE = '
|
|
24
|
+
const ARTICLE = 'modeling-without-a-deploy';
|
|
25
25
|
|
|
26
26
|
type PageCheck = { path: string; expect: Array<[needle: string, label: string]> };
|
|
27
27
|
|
|
@@ -52,7 +52,7 @@ const PAGES: readonly PageCheck[] = [
|
|
|
52
52
|
{
|
|
53
53
|
path: '/en/articles/',
|
|
54
54
|
expect: [
|
|
55
|
-
['
|
|
55
|
+
['Modeling content without a deploy', 'article list'],
|
|
56
56
|
['Why our builds pin a snapshot', 'second article'],
|
|
57
57
|
['<title>Journal · Northwind Studio</title>', 'a page without an entry: title through the template'],
|
|
58
58
|
],
|
|
@@ -60,14 +60,14 @@ const PAGES: readonly PageCheck[] = [
|
|
|
60
60
|
{
|
|
61
61
|
path: `/en/articles/${ARTICLE}/`,
|
|
62
62
|
expect: [
|
|
63
|
-
['
|
|
63
|
+
['Modeling content without a deploy', 'title'],
|
|
64
64
|
['<table', 'rich-text table'],
|
|
65
65
|
['With Shapio', 'rich-text table cell'],
|
|
66
66
|
['Ada Moreau', 'populated author'],
|
|
67
67
|
['data-shapio-entry="', 'visual-editing entry attribute'],
|
|
68
68
|
['data-shapio-path="title"', 'visual-editing attribute on the title'],
|
|
69
69
|
['data-shapio-path="body"', 'visual-editing attribute on the body'],
|
|
70
|
-
['<title>
|
|
70
|
+
['<title>Modeling content without a deploy · Northwind Studio</title>', 'SEO title (resolved)'],
|
|
71
71
|
['property="og:type" content="article"', 'og:type article'],
|
|
72
72
|
['Nothing restarted.', 'SEO description from the entry'],
|
|
73
73
|
['name="twitter:card" content="summary_large_image"', 'twitter:card with the cover image'],
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
let { value, visual }: Props = $props();
|
|
8
8
|
</script>
|
|
9
9
|
|
|
10
|
-
<!-- Shapio's rich text arrives with a
|
|
10
|
+
<!-- Shapio's rich text arrives with a sanitized `html` rendering next to the JSON document. -->
|
|
11
11
|
{#if value}
|
|
12
12
|
<div class="prose" {...visual}>{@html value.html}</div>
|
|
13
13
|
{/if}
|
|
@@ -3,7 +3,7 @@ import type { SeoResolved } from '@shapio/client';
|
|
|
3
3
|
/**
|
|
4
4
|
* Content as the Shapio delivery API returns it for the starter's models (shapio/). Delivery
|
|
5
5
|
* entries are flat: system attributes next to the fields, keyed by API key. Media fields are asset views,
|
|
6
|
-
* rich text carries the JSON document plus a
|
|
6
|
+
* rich text carries the JSON document plus a sanitized `html` rendering, populated relations are entries.
|
|
7
7
|
*/
|
|
8
8
|
export type MediaVariant = {
|
|
9
9
|
name: string;
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|