create-krantho 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,472 @@
1
+ import { KRANTHO_RANGE } from '../version.js';
2
+ export function generateSveltekitFiles(options) {
3
+ const files = new Map();
4
+ files.set('package.json', packageJson(options));
5
+ files.set('tsconfig.json', tsconfig());
6
+ files.set('svelte.config.js', svelteConfig());
7
+ files.set('vite.config.ts', viteConfig(options));
8
+ files.set('krantho.config.ts', kranthoConfig(options));
9
+ files.set('krantho.collections.ts', kranthoCollections(options));
10
+ files.set('src/app.html', appHtml(options));
11
+ files.set('src/app.css', appCss(options));
12
+ files.set('src/lib/cms.ts', cmsInit());
13
+ files.set('src/routes/api/[...path]/+server.ts', apiHandler());
14
+ files.set('src/routes/studio/[...path]/+server.ts', studioHandler());
15
+ files.set('src/routes/sitemap.xml/+server.ts', seoRoute('/api/seo/sitemap'));
16
+ files.set('src/routes/robots.txt/+server.ts', seoRoute('/api/seo/robots.txt'));
17
+ files.set('src/routes/+layout.server.ts', layoutServer());
18
+ files.set('src/routes/+layout.svelte', layoutSvelte(options));
19
+ files.set('src/routes/+page.server.ts', homePageServer());
20
+ files.set('src/routes/+page.svelte', homePage());
21
+ files.set('src/routes/[slug]/+page.server.ts', dynamicPageServer());
22
+ files.set('src/routes/[slug]/+page.svelte', dynamicPage());
23
+ return files;
24
+ }
25
+ function packageJson(options) {
26
+ // `@krantho/sveltekit` is the one-install umbrella — it bundles
27
+ // server-sveltekit, ui-svelte, studio, media-local, and seo so a single
28
+ // dependency provisions the full stack. The packages the generated source
29
+ // imports *by name* (server-sveltekit, core, schema, persistence-sqlite) are
30
+ // also listed explicitly so the project resolves under a strict, non-hoisted
31
+ // node_modules layout where transitive deps are not directly importable.
32
+ const deps = {
33
+ '@krantho/config': KRANTHO_RANGE,
34
+ '@krantho/sveltekit': KRANTHO_RANGE,
35
+ '@krantho/server-sveltekit': KRANTHO_RANGE,
36
+ '@krantho/core': KRANTHO_RANGE,
37
+ '@krantho/schema': KRANTHO_RANGE,
38
+ '@krantho/persistence-sqlite': KRANTHO_RANGE,
39
+ '@sveltejs/adapter-auto': '^7.0.1',
40
+ '@sveltejs/kit': '^2.55.0',
41
+ svelte: '^5.55.0',
42
+ zod: '^4.4.3',
43
+ };
44
+ if (options.ui === 'tailwind') {
45
+ deps['tailwindcss'] = '^4.2.2';
46
+ deps['@tailwindcss/vite'] = '^4.2.2';
47
+ }
48
+ const devDeps = {
49
+ '@types/node': '^25.5.0',
50
+ typescript: '^6.0.2',
51
+ vite: '^8.0.3',
52
+ };
53
+ const pkg = {
54
+ name: options.name,
55
+ version: '0.1.0',
56
+ private: true,
57
+ type: 'module',
58
+ scripts: {
59
+ // Vite does not load .env into process.env for server code; the
60
+ // config resolver reads KRANTHO_* from process.env, so load it here.
61
+ dev: `node --env-file-if-exists=.env node_modules/vite/bin/vite.js dev --port ${options.port}`,
62
+ build: 'vite build',
63
+ preview: 'vite preview',
64
+ },
65
+ dependencies: sortObject(deps),
66
+ devDependencies: sortObject(devDeps),
67
+ };
68
+ return JSON.stringify(pkg, null, 2) + '\n';
69
+ }
70
+ function tsconfig() {
71
+ return (JSON.stringify({
72
+ extends: './.svelte-kit/tsconfig.json',
73
+ compilerOptions: {
74
+ allowJs: true,
75
+ checkJs: true,
76
+ esModuleInterop: true,
77
+ forceConsistentCasingInFileNames: true,
78
+ resolveJsonModule: true,
79
+ skipLibCheck: true,
80
+ sourceMap: true,
81
+ strict: true,
82
+ moduleResolution: 'bundler',
83
+ },
84
+ }, null, 2) + '\n');
85
+ }
86
+ function svelteConfig() {
87
+ return `import adapter from '@sveltejs/adapter-auto'
88
+
89
+ export default {
90
+ kit: { adapter: adapter() }
91
+ }
92
+ `;
93
+ }
94
+ function viteConfig(options) {
95
+ if (options.ui === 'tailwind') {
96
+ return `import { sveltekit } from '@sveltejs/kit/vite'
97
+ import tailwindcss from '@tailwindcss/vite'
98
+ import { defineConfig } from 'vite'
99
+
100
+ export default defineConfig({
101
+ plugins: [tailwindcss(), sveltekit()]
102
+ })
103
+ `;
104
+ }
105
+ return `import { sveltekit } from '@sveltejs/kit/vite'
106
+ import { defineConfig } from 'vite'
107
+
108
+ export default defineConfig({
109
+ plugins: [sveltekit()]
110
+ })
111
+ `;
112
+ }
113
+ function appHtml(options) {
114
+ return `<!doctype html>
115
+ <html lang="en">
116
+ <head>
117
+ <meta charset="utf-8" />
118
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
119
+ <title>${options.name}</title>
120
+ %sveltekit.head%
121
+ </head>
122
+ <body data-sveltekit-preload-data="hover">
123
+ <div style="display: contents">%sveltekit.body%</div>
124
+ </body>
125
+ </html>
126
+ `;
127
+ }
128
+ function appCss(options) {
129
+ if (options.ui === 'tailwind') {
130
+ return `@import 'tailwindcss';
131
+
132
+ :root {
133
+ --font-sans: system-ui, -apple-system, sans-serif;
134
+ --color-primary: #2563eb;
135
+ --color-text: #1a1a2e;
136
+ --color-muted: #6b7280;
137
+ }
138
+
139
+ body {
140
+ font-family: var(--font-sans);
141
+ color: var(--color-text);
142
+ margin: 0;
143
+ line-height: 1.6;
144
+ }
145
+ `;
146
+ }
147
+ return `:root {
148
+ --font-sans: system-ui, -apple-system, sans-serif;
149
+ --color-primary: #2563eb;
150
+ --color-text: #1a1a2e;
151
+ --color-muted: #6b7280;
152
+ --color-border: #e5e7eb;
153
+ --max-width: 48rem;
154
+ }
155
+
156
+ * { box-sizing: border-box; margin: 0; padding: 0; }
157
+
158
+ body {
159
+ font-family: var(--font-sans);
160
+ color: var(--color-text);
161
+ line-height: 1.6;
162
+ }
163
+
164
+ header {
165
+ border-bottom: 1px solid var(--color-border);
166
+ padding: 1rem 1.5rem;
167
+ }
168
+
169
+ header nav {
170
+ max-width: var(--max-width);
171
+ margin: 0 auto;
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: space-between;
175
+ }
176
+
177
+ header a { color: inherit; text-decoration: none; }
178
+ header nav .links { display: flex; gap: 1rem; }
179
+ header nav .links a:hover { color: var(--color-primary); }
180
+
181
+ main {
182
+ max-width: var(--max-width);
183
+ margin: 0 auto;
184
+ padding: 3rem 1.5rem;
185
+ }
186
+
187
+ h1 { font-size: 1.875rem; font-weight: 700; margin-bottom: 1rem; }
188
+ h2 { font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; }
189
+
190
+ .intro { font-size: 1.125rem; color: var(--color-muted); margin-bottom: 2rem; }
191
+
192
+ footer {
193
+ border-top: 1px solid var(--color-border);
194
+ padding: 2rem 1.5rem;
195
+ text-align: center;
196
+ font-size: 0.875rem;
197
+ color: var(--color-muted);
198
+ }
199
+ `;
200
+ }
201
+ function cmsInit() {
202
+ return `import { renderContent } from '@krantho/core'
203
+ import { createContentLoader } from '@krantho/server-sveltekit'
204
+ import config from '../../krantho.config'
205
+ import { collections } from '../../krantho.collections'
206
+
207
+ // Pages read the live (published) version of each document from the database
208
+ // the API route opens: both resolve this app's configuration.
209
+ const loader = createContentLoader({ config, collections })
210
+
211
+ /** A published document, rendered for a page */
212
+ export interface PageContent {
213
+ fields: Record<string, unknown>
214
+ html: string
215
+ }
216
+
217
+ /** The published version of a document; null when it has none */
218
+ export async function loadContent(collection: string, slug: string): Promise<PageContent | null> {
219
+ const doc = await loader.getPublished({ collection, slug })
220
+ if (!doc) return null
221
+ const { html } = await renderContent(doc.fields, doc.body)
222
+ return { fields: doc.fields, html }
223
+ }
224
+ `;
225
+ }
226
+ /** With SEO chosen, a page's own structured data is a group of its metadata, edited in Data mode */
227
+ function kranthoCollections(options) {
228
+ const seoSlot = options.seo ? ', seo: { archetype: structuredDataMetadata }' : '';
229
+ const imports = options.seo
230
+ ? `import {
231
+ defineCollection,
232
+ defineDocumentArchetype,
233
+ sharingMetadata,
234
+ structuredDataMetadata,
235
+ } from '@krantho/schema'`
236
+ : "import { defineCollection, defineDocumentArchetype, sharingMetadata } from '@krantho/schema'";
237
+ return `import { join } from 'path'
238
+ ${imports}
239
+ import { z } from 'zod'
240
+
241
+ // The collections this site serves — shared by the API route and the page loader,
242
+ // so whichever a fresh deployment serves first imports the same content.
243
+
244
+ const pages = defineCollection({
245
+ name: 'pages',
246
+ source: 'filesystem',
247
+ directory: join(process.cwd(), 'content', 'pages'),
248
+ archetype: defineDocumentArchetype({
249
+ id: 'site/pages', label: 'Page',
250
+ fields: z.object({
251
+ title: z.string(),
252
+ description: z.string(),
253
+
254
+ }),
255
+ metadata: { sharing: { archetype: sharingMetadata }${seoSlot} },
256
+ }),
257
+ })
258
+
259
+ const globals = defineCollection({
260
+ name: 'globals',
261
+ source: 'filesystem',
262
+ directory: join(process.cwd(), 'content', 'globals'),
263
+ archetype: defineDocumentArchetype({
264
+ id: 'site/globals', label: 'Shared content',
265
+ fields: z.object({
266
+ 'logo-name': z.string().optional(),
267
+ 'logo-subtitle': z.string().optional(),
268
+ 'nav-home': z.string().optional(),
269
+ 'nav-about': z.string().optional(),
270
+ 'footer-title': z.string().optional(),
271
+ 'footer-description': z.string().optional(),
272
+ 'footer-copyright-text': z.string().optional(),
273
+ }),
274
+ metadata: { sharing: { archetype: sharingMetadata } },
275
+ }),
276
+ })
277
+
278
+ export const collections = { pages, globals }
279
+ `;
280
+ }
281
+ /**
282
+ * A root route search engines look for (/sitemap.xml, /robots.txt), answered
283
+ * by the krantho API, which builds it from the published content and site.baseUrl
284
+ */
285
+ function seoRoute(apiPath) {
286
+ return `import type { RequestHandler } from '@sveltejs/kit'
287
+
288
+ // Search engines look for this file at the site root; the krantho API builds it.
289
+ export const GET: RequestHandler = ({ fetch }) => fetch('${apiPath}')
290
+ `;
291
+ }
292
+ function apiHandler() {
293
+ return `import { createApiHandlers } from '@krantho/server-sveltekit'
294
+ import config from '../../../../krantho.config'
295
+ import { collections } from '../../../../krantho.collections'
296
+
297
+ export const { GET, POST, PUT, DELETE } = createApiHandlers({
298
+ config,
299
+ collections,
300
+ })
301
+ `;
302
+ }
303
+ /** Studio, which the project tells its owner to open at /studio */
304
+ function studioHandler() {
305
+ return `import { createStudioHandler } from '@krantho/server-sveltekit'
306
+ import config from '../../../../krantho.config'
307
+
308
+ export const { GET } = createStudioHandler({ config })
309
+ `;
310
+ }
311
+ function kranthoConfig(options) {
312
+ const authLines = options.auth
313
+ ? `
314
+ // Token mode — the secret comes from KRANTHO_API_TOKEN in the environment.
315
+ auth: { mode: 'token' },`
316
+ : '';
317
+ return `import { defineKranthoConfig } from '@krantho/config'
318
+
319
+ /**
320
+ * Site configuration — data only. Every value here can be overridden from the
321
+ * environment; \`KRANTHO_SITE_BASE_URL\` overrides \`site.baseUrl\`, and so on.
322
+ * Collections live in krantho.collections.ts; plugins are wiring in the route
323
+ * file. The media file store is chosen by \`media.provider\`.
324
+ */
325
+ export default defineKranthoConfig({
326
+ site: {
327
+ domain: 'localhost',
328
+ name: '${options.name}',
329
+ language: 'en',
330
+ baseUrl: 'http://localhost:${options.port}',
331
+ supportedLocales: ['en'],
332
+ defaultLocale: 'en',
333
+ },
334
+ // Uploads go to static/uploads, which SvelteKit serves at /uploads
335
+ media: { provider: 'local', local: { dir: 'static/uploads' } },${authLines}
336
+ })
337
+ `;
338
+ }
339
+ function layoutServer() {
340
+ return `import { loadContent } from '$lib/cms'
341
+
342
+ export async function load() {
343
+ const nav = await loadContent('globals', 'navigation')
344
+ const footer = await loadContent('globals', 'footer')
345
+ return {
346
+ nav: nav?.fields ?? {},
347
+ footer: footer?.fields ?? {},
348
+ }
349
+ }
350
+ `;
351
+ }
352
+ function layoutSvelte(options) {
353
+ const tailwindImport = options.ui === 'tailwind' ? ` import '../app.css'\n` : ` import '../app.css'\n`;
354
+ return `<script lang="ts">
355
+ ${tailwindImport} import type { Snippet } from 'svelte'
356
+
357
+ interface Props {
358
+ data: {
359
+ nav: Record<string, unknown>
360
+ footer: Record<string, unknown>
361
+ }
362
+ children: Snippet
363
+ }
364
+
365
+ let { data, children }: Props = $props()
366
+ const nav = $derived(data.nav)
367
+ const footer = $derived(data.footer)
368
+ </script>
369
+
370
+ <header>
371
+ <nav>
372
+ <a href="/">
373
+ <strong>{nav['logo-name'] ?? '${options.name}'}</strong>
374
+ {#if nav['logo-subtitle']}
375
+ <span> &mdash; {nav['logo-subtitle']}</span>
376
+ {/if}
377
+ </a>
378
+ <div class="links">
379
+ <a href="/">{nav['nav-home'] ?? 'Home'}</a>
380
+ <a href="/about">{nav['nav-about'] ?? 'About'}</a>
381
+ </div>
382
+ </nav>
383
+ </header>
384
+
385
+ <main>
386
+ {@render children()}
387
+ </main>
388
+
389
+ <footer>
390
+ <p>{footer['footer-title'] ?? '${options.name}'}</p>
391
+ {#if footer['footer-description']}
392
+ <p>{footer['footer-description']}</p>
393
+ {/if}
394
+ <p>{footer['footer-copyright-text'] ?? ''}</p>
395
+ </footer>
396
+ `;
397
+ }
398
+ function homePageServer() {
399
+ return `import { loadContent } from '$lib/cms'
400
+ import { error } from '@sveltejs/kit'
401
+
402
+ export async function load() {
403
+ const page = await loadContent('pages', 'home')
404
+ if (!page) error(404, 'Page not found')
405
+ return {
406
+ fields: page.fields,
407
+ html: page.html,
408
+ }
409
+ }
410
+ `;
411
+ }
412
+ function homePage() {
413
+ return `<script lang="ts">
414
+ interface Props {
415
+ data: {
416
+ fields: Record<string, unknown>
417
+ html: string
418
+ }
419
+ }
420
+
421
+ let { data }: Props = $props()
422
+ const fm = $derived(data.fields)
423
+ </script>
424
+
425
+ <article>
426
+ <h1>{fm['title'] ?? 'Home'}</h1>
427
+ <div class="prose">{@html data.html}</div>
428
+ </article>
429
+ `;
430
+ }
431
+ function dynamicPageServer() {
432
+ return `import { loadContent } from '$lib/cms'
433
+ import { error } from '@sveltejs/kit'
434
+
435
+ export async function load({ params }: { params: { slug: string } }) {
436
+ const page = await loadContent('pages', params.slug)
437
+ if (!page) error(404, 'Page not found')
438
+ return {
439
+ fields: page.fields,
440
+ html: page.html,
441
+ }
442
+ }
443
+ `;
444
+ }
445
+ function dynamicPage() {
446
+ return `<script lang="ts">
447
+ interface Props {
448
+ data: {
449
+ fields: Record<string, unknown>
450
+ html: string
451
+ }
452
+ }
453
+
454
+ let { data }: Props = $props()
455
+ const fm = $derived(data.fields)
456
+ </script>
457
+
458
+ <article>
459
+ <h1>{fm['title'] ?? 'Page'}</h1>
460
+ <div class="prose">{@html data.html}</div>
461
+ </article>
462
+ `;
463
+ }
464
+ function sortObject(obj) {
465
+ const sorted = {};
466
+ for (const key of Object.keys(obj).sort()) {
467
+ const value = obj[key];
468
+ if (value !== undefined)
469
+ sorted[key] = value;
470
+ }
471
+ return sorted;
472
+ }
@@ -0,0 +1,4 @@
1
+ /** The krantho release this scaffolder belongs to; every `@krantho/*` package carries the same version */
2
+ export declare const KRANTHO_VERSION: string;
3
+ /** The range a new project depends on: this release and the compatible ones after it */
4
+ export declare const KRANTHO_RANGE: string;
@@ -0,0 +1,5 @@
1
+ import { readFileSync } from 'node:fs';
2
+ /** The krantho release this scaffolder belongs to; every `@krantho/*` package carries the same version */
3
+ export const KRANTHO_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version;
4
+ /** The range a new project depends on: this release and the compatible ones after it */
5
+ export const KRANTHO_RANGE = `^${KRANTHO_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "create-krantho",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "Scaffold a new krantho project",
5
+ "license": "MIT",
6
+ "homepage": "https://krantho.com",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/descoped/krantho.git",
10
+ "directory": "packages/create-krantho"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/descoped/krantho/issues"
14
+ },
15
+ "author": "oranheim",
16
+ "type": "module",
17
+ "bin": {
18
+ "create-krantho": "./dist/index.js"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "dependencies": {
24
+ "prompts": "^2.4.2"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^25.9.9",
28
+ "@types/prompts": "^2.4.9",
29
+ "typescript": "^6.0.3"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "typecheck": "tsc --noEmit"
34
+ }
6
35
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.