cosmolo 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -11
- package/dist/cli/index.js +6 -2
- package/dist/cli/init.js +173 -21
- package/dist/cli/migrate/drizzle-setup.js +158 -10
- package/dist/cli/setup/r2.d.ts +1 -0
- package/dist/cli/setup/r2.js +90 -0
- package/dist/markdown.js +7 -3
- package/dist/types.d.ts +0 -3
- package/package.json +2 -2
- package/templates/shared/config/site.json +1 -2
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ bun generate:article
|
|
|
54
54
|
bun dev
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
`cosmolo init` asks two questions — which mode (
|
|
57
|
+
`cosmolo init` asks two questions — which mode (Full or Slim) and which adapter (SSG, Cloudflare, or Serverless) — then copies the appropriate route files into your project.
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
@@ -273,6 +273,49 @@ Prompts for key (slug), label, and description. Appends the new entry to `config
|
|
|
273
273
|
|
|
274
274
|
---
|
|
275
275
|
|
|
276
|
+
## Cloudflare
|
|
277
|
+
|
|
278
|
+
Cosmolo works with any SvelteKit-compatible deployment platform, but it is purpose-built
|
|
279
|
+
around the Cloudflare stack. SvelteKit and Cloudflare Workers are an unusually good fit —
|
|
280
|
+
edge-native rendering, zero cold starts, globally distributed infrastructure, and a generous
|
|
281
|
+
free tier. Cosmolo's CLI removes the usual setup friction so you can go from `init` to
|
|
282
|
+
deployed in minutes.
|
|
283
|
+
|
|
284
|
+
### One-command Cloudflare setup
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
bunx cosmolo init # choose "Cloudflare" when prompted for adapter
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
This single command generates everything needed to deploy:
|
|
291
|
+
|
|
292
|
+
| Generated file | Purpose |
|
|
293
|
+
|---|---|
|
|
294
|
+
| `svelte.config.js` | Pre-configured with `adapter-cloudflare` |
|
|
295
|
+
| `wrangler.toml` | Project name, `nodejs_compat`, D1 template commented out |
|
|
296
|
+
| `src/app.d.ts` | `App.Platform` with `Env`, `CfProperties`, `ExecutionContext` |
|
|
297
|
+
| `.github/workflows/deploy.yml` | Optional — push-to-`main` deploy via `wrangler-action` |
|
|
298
|
+
|
|
299
|
+
After init, two commands to go live:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
bun install && bun add -D @sveltejs/adapter-cloudflare @cloudflare/workers-types
|
|
303
|
+
bun run deploy # bun run build + wrangler pages deploy
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
If you opted in to GitHub Actions during init, pushing to `main` triggers the deploy automatically.
|
|
307
|
+
|
|
308
|
+
### Cloudflare services
|
|
309
|
+
|
|
310
|
+
| Command | What it sets up |
|
|
311
|
+
|---|---|
|
|
312
|
+
| `cosmolo migrate:db` → option 3 | **D1** — Drizzle schema, CRUD helpers (`getArticlesByCategory`, `getArticlesByTag`, …), D1-backed `+page.server.ts` route files |
|
|
313
|
+
| `cosmolo setup:r2` | **R2** — `wrangler.toml` binding, `src/lib/r2.ts` helper, `/assets/[...key]` edge serving route |
|
|
314
|
+
|
|
315
|
+
Each command is self-contained — run only the ones you need.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
276
319
|
## Database Migration
|
|
277
320
|
|
|
278
321
|
When a file-based Cosmolo site outgrows Markdown — multiple writers, mobile editing,
|
|
@@ -305,12 +348,22 @@ The command is interactive and offers three paths:
|
|
|
305
348
|
After a preflight check (drizzle installed, wrangler.toml, table conflicts), the following files are generated:
|
|
306
349
|
|
|
307
350
|
```
|
|
308
|
-
drizzle/schema.ts
|
|
309
|
-
src/lib/db/articles.ts
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
.
|
|
351
|
+
drizzle/schema.ts ← Drizzle schema for articles and categories tables
|
|
352
|
+
src/lib/db/articles.ts ← getArticles, getArticlesByCategory, getArticlesByTag,
|
|
353
|
+
getArticle, parseArticle, createArticle, updateArticle, deleteArticle
|
|
354
|
+
src/lib/db/categories.ts ← getCategories, getCategory, createCategory, updateCategory, deleteCategory
|
|
355
|
+
wrangler.toml ← [[d1_databases]] binding added (merged if file exists)
|
|
356
|
+
drizzle.config.ts ← drizzle-kit config (dialect: sqlite)
|
|
357
|
+
.dev.vars.example ← Cloudflare environment variable reference
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Optionally (prompted during setup), the existing `+page.server.ts` route files are replaced with D1-backed versions that read from `platform.env.DB` instead of the Cosmolo virtual module:
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
src/routes/+page.server.ts ← Home page — getArticles + getCategories from D1
|
|
364
|
+
src/routes/articles/[slug]/+page.server.ts ← Article — getArticle from D1, Markdown rendered with marked
|
|
365
|
+
src/routes/categories/[slug]/+page.server.ts ← Category — getArticlesByCategory from D1
|
|
366
|
+
src/routes/tags/[tag]/+page.server.ts ← Tag — getArticlesByTag from D1 (json_each query)
|
|
314
367
|
```
|
|
315
368
|
|
|
316
369
|
The command prints step-by-step instructions after generation:
|
|
@@ -338,6 +391,42 @@ The upside: content edits take effect immediately — no rebuild or redeploy nee
|
|
|
338
391
|
|
|
339
392
|
---
|
|
340
393
|
|
|
394
|
+
## R2 Asset Storage
|
|
395
|
+
|
|
396
|
+
Add Cloudflare R2 object storage for article images and other binary assets:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
bunx cosmolo setup:r2
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The command asks for a bucket name and binding name, then generates:
|
|
403
|
+
|
|
404
|
+
```
|
|
405
|
+
src/lib/r2.ts ← getR2Asset(bucket, key) helper
|
|
406
|
+
src/routes/assets/[...key]/+server.ts ← Edge route — serves files directly from R2
|
|
407
|
+
wrangler.toml ← [[r2_buckets]] binding appended
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
After setup:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
# 1. Create the bucket
|
|
414
|
+
bunx wrangler r2 bucket create <bucket-name>
|
|
415
|
+
|
|
416
|
+
# 2. Upload an asset
|
|
417
|
+
bunx wrangler r2 object put <bucket-name>/images/photo.jpg --file ./static/images/photo.jpg
|
|
418
|
+
|
|
419
|
+
# 3. Reference it in templates as /assets/images/photo.jpg
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Add the binding type to `src/app.d.ts` (one line):
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
interface Platform { env: { ASSETS: R2Bucket } }
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
341
430
|
## Headless CMS
|
|
342
431
|
|
|
343
432
|
Cosmolo can expose your content as static JSON endpoints, making it usable as a
|
|
@@ -391,7 +480,7 @@ bun add -D vite @sveltejs/kit
|
|
|
391
480
|
**1. Create `cosmolo.config.ts`** in your project root
|
|
392
481
|
|
|
393
482
|
```typescript
|
|
394
|
-
import { resolveConfig } from 'cosmolo';
|
|
483
|
+
import { resolveConfig } from 'cosmolo/plugin';
|
|
395
484
|
|
|
396
485
|
export default resolveConfig({
|
|
397
486
|
articlesDir: 'src/content/articles', // default
|
|
@@ -439,10 +528,11 @@ The command asks two questions:
|
|
|
439
528
|
|
|
440
529
|
**Adapter**
|
|
441
530
|
|
|
442
|
-
| Adapter |
|
|
531
|
+
| Adapter | Generated files |
|
|
443
532
|
|---|---|
|
|
444
|
-
| **SSG** (`adapter-static`) |
|
|
445
|
-
| **
|
|
533
|
+
| **SSG** (`adapter-static`) | `svelte.config.js` (adapter-static) + `src/routes/+layout.ts` (`prerender = true`) |
|
|
534
|
+
| **Cloudflare** (`adapter-cloudflare`) | `svelte.config.js` (adapter-cloudflare) + `wrangler.toml` + `src/app.d.ts` (`Platform` type). Optionally `.github/workflows/deploy.yml`. |
|
|
535
|
+
| **Serverless** | No extra files — bring your own adapter (Vercel, Node, etc.) |
|
|
446
536
|
|
|
447
537
|
If any target file already exists, the command lists every conflict and exits without
|
|
448
538
|
writing anything.
|
package/dist/cli/index.js
CHANGED
|
@@ -10,15 +10,19 @@ switch (cmd) {
|
|
|
10
10
|
case 'migrate:db':
|
|
11
11
|
await (await import('./migrate.js')).main();
|
|
12
12
|
break;
|
|
13
|
+
case 'setup:r2':
|
|
14
|
+
await (await import('./setup/r2.js')).main();
|
|
15
|
+
break;
|
|
13
16
|
default: {
|
|
14
17
|
const isUnknown = Boolean(cmd);
|
|
15
18
|
if (isUnknown)
|
|
16
19
|
console.error(`Unknown command: ${cmd}\n`);
|
|
17
20
|
console.log('Usage: cosmolo <command>\n');
|
|
18
21
|
console.log('Commands:');
|
|
19
|
-
console.log(' init Scaffold routes into
|
|
22
|
+
console.log(' init Scaffold routes and config into a SvelteKit project');
|
|
20
23
|
console.log(' generate [article|page|category] Create content files');
|
|
21
|
-
console.log(' migrate:db Migrate content to a database');
|
|
24
|
+
console.log(' migrate:db Migrate file-based content to a database (D1)');
|
|
25
|
+
console.log(' setup:r2 Add Cloudflare R2 bucket for asset storage');
|
|
22
26
|
process.exit(isUnknown ? 1 : 0);
|
|
23
27
|
}
|
|
24
28
|
}
|
package/dist/cli/init.js
CHANGED
|
@@ -36,7 +36,21 @@ function destPath(relativePath, projectRoot) {
|
|
|
36
36
|
.replace(/^lib\//, 'src/lib/');
|
|
37
37
|
return path.join(projectRoot, mapped);
|
|
38
38
|
}
|
|
39
|
-
function
|
|
39
|
+
function readProjectName(projectRoot) {
|
|
40
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
41
|
+
if (fs.existsSync(pkgPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
44
|
+
if (typeof pkg.name === 'string' && pkg.name)
|
|
45
|
+
return pkg.name;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// fall through
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return 'my-cosmolo-site';
|
|
52
|
+
}
|
|
53
|
+
function injectPackageScripts(projectRoot, adapter) {
|
|
40
54
|
const pkgPath = path.join(projectRoot, 'package.json');
|
|
41
55
|
if (!fs.existsSync(pkgPath))
|
|
42
56
|
return;
|
|
@@ -49,6 +63,9 @@ function injectPackageScripts(projectRoot) {
|
|
|
49
63
|
'generate:page': 'cosmolo generate page',
|
|
50
64
|
'generate:category': 'cosmolo generate category',
|
|
51
65
|
};
|
|
66
|
+
if (adapter === 'cloudflare') {
|
|
67
|
+
scripts['deploy'] = 'bun run build && bunx wrangler pages deploy .svelte-kit/cloudflare';
|
|
68
|
+
}
|
|
52
69
|
for (const [key, val] of Object.entries(scripts)) {
|
|
53
70
|
if (!pkg.scripts[key]) {
|
|
54
71
|
pkg.scripts[key] = val;
|
|
@@ -64,6 +81,40 @@ function injectPackageScripts(projectRoot) {
|
|
|
64
81
|
console.log(' updated package.json (added cosmolo dependency + generate:* scripts)');
|
|
65
82
|
}
|
|
66
83
|
}
|
|
84
|
+
function svelteConfigContent(adapter) {
|
|
85
|
+
const pkg = adapter === 'ssg' ? '@sveltejs/adapter-static' : '@sveltejs/adapter-cloudflare';
|
|
86
|
+
return (`import adapter from '${pkg}';\n` +
|
|
87
|
+
`import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';\n\n` +
|
|
88
|
+
`/** @type {import('@sveltejs/kit').Config} */\n` +
|
|
89
|
+
`const config = {\n` +
|
|
90
|
+
`\tpreprocess: vitePreprocess(),\n` +
|
|
91
|
+
`\tkit: {\n` +
|
|
92
|
+
`\t\tadapter: adapter(),\n` +
|
|
93
|
+
`\t},\n` +
|
|
94
|
+
`};\n\n` +
|
|
95
|
+
`export default config;\n`);
|
|
96
|
+
}
|
|
97
|
+
function githubActionsContent(projectName) {
|
|
98
|
+
return (`name: Deploy to Cloudflare Pages\n\n` +
|
|
99
|
+
`on:\n` +
|
|
100
|
+
` push:\n` +
|
|
101
|
+
` branches: [main]\n\n` +
|
|
102
|
+
`jobs:\n` +
|
|
103
|
+
` deploy:\n` +
|
|
104
|
+
` runs-on: ubuntu-latest\n` +
|
|
105
|
+
` permissions:\n` +
|
|
106
|
+
` contents: read\n` +
|
|
107
|
+
` deployments: write\n` +
|
|
108
|
+
` steps:\n` +
|
|
109
|
+
` - uses: actions/checkout@v4\n` +
|
|
110
|
+
` - uses: oven-sh/setup-bun@v2\n` +
|
|
111
|
+
` - run: bun install --frozen-lockfile\n` +
|
|
112
|
+
` - run: bun run build\n` +
|
|
113
|
+
` - uses: cloudflare/wrangler-action@v3\n` +
|
|
114
|
+
` with:\n` +
|
|
115
|
+
` apiToken: \${{ secrets.CF_API_TOKEN }}\n` +
|
|
116
|
+
` command: pages deploy .svelte-kit/cloudflare --project-name=${projectName}\n`);
|
|
117
|
+
}
|
|
67
118
|
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
68
119
|
export async function main() {
|
|
69
120
|
const PROJECT_ROOT = process.cwd();
|
|
@@ -82,15 +133,16 @@ export async function main() {
|
|
|
82
133
|
const mode = modeRaw === '1' ? 'full' : 'slim';
|
|
83
134
|
// ── Adapter selection ───────────────────────────────────────────────────
|
|
84
135
|
console.log('\nChoose your deployment adapter:\n');
|
|
85
|
-
console.log(' 1) SSG
|
|
86
|
-
console.log(' 2)
|
|
136
|
+
console.log(' 1) SSG — @sveltejs/adapter-static (GitHub Pages, Cloudflare Pages static, etc.)');
|
|
137
|
+
console.log(' 2) Cloudflare — @sveltejs/adapter-cloudflare (Workers / Pages SSR)');
|
|
138
|
+
console.log(' 3) Serverless — Vercel, Node, etc.\n');
|
|
87
139
|
let adapterRaw = '';
|
|
88
|
-
while (!['1', '2'].includes(adapterRaw)) {
|
|
89
|
-
adapterRaw = (await ask(rl, 'Adapter [1/2]: ')).trim();
|
|
90
|
-
if (!['1', '2'].includes(adapterRaw))
|
|
91
|
-
console.log(' Please enter 1 or
|
|
140
|
+
while (!['1', '2', '3'].includes(adapterRaw)) {
|
|
141
|
+
adapterRaw = (await ask(rl, 'Adapter [1/2/3]: ')).trim();
|
|
142
|
+
if (!['1', '2', '3'].includes(adapterRaw))
|
|
143
|
+
console.log(' Please enter 1, 2, or 3.');
|
|
92
144
|
}
|
|
93
|
-
const
|
|
145
|
+
const adapter = adapterRaw === '1' ? 'ssg' : adapterRaw === '2' ? 'cloudflare' : 'serverless';
|
|
94
146
|
// ── Collect files ───────────────────────────────────────────────────────
|
|
95
147
|
const sharedFiles = collectFiles(path.join(TEMPLATE_DIR, 'shared'));
|
|
96
148
|
const fullFiles = mode === 'full' ? collectFiles(path.join(TEMPLATE_DIR, 'full')) : [];
|
|
@@ -100,6 +152,16 @@ export async function main() {
|
|
|
100
152
|
];
|
|
101
153
|
const layoutTsPath = path.join(PROJECT_ROOT, 'src/routes/+layout.ts');
|
|
102
154
|
const layoutTsContent = 'export const prerender = true;\n';
|
|
155
|
+
const wranglerTomlPath = path.join(PROJECT_ROOT, 'wrangler.toml');
|
|
156
|
+
const appDtsPath = path.join(PROJECT_ROOT, 'src/app.d.ts');
|
|
157
|
+
const svelteConfigPath = path.join(PROJECT_ROOT, 'svelte.config.js');
|
|
158
|
+
const ghaWorkflowPath = path.join(PROJECT_ROOT, '.github', 'workflows', 'deploy.yml');
|
|
159
|
+
// ── GitHub Actions prompt (Cloudflare only, before conflict detection) ──
|
|
160
|
+
let generateGha = false;
|
|
161
|
+
if (adapter === 'cloudflare') {
|
|
162
|
+
const ans = (await ask(rl, '\nGenerate GitHub Actions deploy workflow? [y/N]: ')).toLowerCase();
|
|
163
|
+
generateGha = ans === 'y';
|
|
164
|
+
}
|
|
103
165
|
// ── Conflict detection ──────────────────────────────────────────────────
|
|
104
166
|
const conflicts = [];
|
|
105
167
|
for (const [, rel] of allFiles) {
|
|
@@ -107,8 +169,21 @@ export async function main() {
|
|
|
107
169
|
if (fs.existsSync(dest))
|
|
108
170
|
conflicts.push(path.relative(PROJECT_ROOT, dest));
|
|
109
171
|
}
|
|
110
|
-
if (
|
|
111
|
-
|
|
172
|
+
if (adapter === 'ssg') {
|
|
173
|
+
if (fs.existsSync(layoutTsPath))
|
|
174
|
+
conflicts.push(path.relative(PROJECT_ROOT, layoutTsPath));
|
|
175
|
+
if (fs.existsSync(svelteConfigPath))
|
|
176
|
+
conflicts.push('svelte.config.js');
|
|
177
|
+
}
|
|
178
|
+
if (adapter === 'cloudflare') {
|
|
179
|
+
if (fs.existsSync(wranglerTomlPath))
|
|
180
|
+
conflicts.push('wrangler.toml');
|
|
181
|
+
if (fs.existsSync(appDtsPath))
|
|
182
|
+
conflicts.push('src/app.d.ts');
|
|
183
|
+
if (fs.existsSync(svelteConfigPath))
|
|
184
|
+
conflicts.push('svelte.config.js');
|
|
185
|
+
if (generateGha && fs.existsSync(ghaWorkflowPath))
|
|
186
|
+
conflicts.push('.github/workflows/deploy.yml');
|
|
112
187
|
}
|
|
113
188
|
if (conflicts.length > 0) {
|
|
114
189
|
console.log('\nThe following files already exist:\n');
|
|
@@ -133,27 +208,104 @@ export async function main() {
|
|
|
133
208
|
copyFile(src, dest);
|
|
134
209
|
console.log(` created ${path.relative(PROJECT_ROOT, dest)}`);
|
|
135
210
|
}
|
|
136
|
-
if (
|
|
211
|
+
if (adapter === 'ssg') {
|
|
137
212
|
writeFile(layoutTsPath, layoutTsContent);
|
|
138
213
|
console.log(` created src/routes/+layout.ts`);
|
|
214
|
+
writeFile(svelteConfigPath, svelteConfigContent('ssg'));
|
|
215
|
+
console.log(` created svelte.config.js`);
|
|
139
216
|
}
|
|
140
|
-
|
|
217
|
+
if (adapter === 'cloudflare') {
|
|
218
|
+
writeFile(svelteConfigPath, svelteConfigContent('cloudflare'));
|
|
219
|
+
console.log(` created svelte.config.js`);
|
|
220
|
+
const projectName = readProjectName(PROJECT_ROOT);
|
|
221
|
+
const wranglerToml = [
|
|
222
|
+
`name = "${projectName}"`,
|
|
223
|
+
`compatibility_date = "${new Date().toISOString().slice(0, 10)}"`,
|
|
224
|
+
`compatibility_flags = ["nodejs_compat"]`,
|
|
225
|
+
``,
|
|
226
|
+
`# Uncomment to add Cloudflare D1 (run: bunx cosmolo migrate:db)`,
|
|
227
|
+
`# [[d1_databases]]`,
|
|
228
|
+
`# binding = "DB"`,
|
|
229
|
+
`# database_name = "${projectName}-db"`,
|
|
230
|
+
`# database_id = "" # fill in after: bunx wrangler d1 create ${projectName}-db`,
|
|
231
|
+
].join('\n') + '\n';
|
|
232
|
+
writeFile(wranglerTomlPath, wranglerToml);
|
|
233
|
+
console.log(` created wrangler.toml`);
|
|
234
|
+
const appDts = [
|
|
235
|
+
`// See https://svelte.dev/docs/kit/types#app.d.ts`,
|
|
236
|
+
`// Install @cloudflare/workers-types for full type support:`,
|
|
237
|
+
`// bun add -D @cloudflare/workers-types`,
|
|
238
|
+
`declare global {`,
|
|
239
|
+
`\tnamespace App {`,
|
|
240
|
+
`\t\tinterface Platform {`,
|
|
241
|
+
`\t\t\tenv: Env;`,
|
|
242
|
+
`\t\t\tcf: CfProperties;`,
|
|
243
|
+
`\t\t\tctx: ExecutionContext;`,
|
|
244
|
+
`\t\t}`,
|
|
245
|
+
`\t}`,
|
|
246
|
+
`}`,
|
|
247
|
+
``,
|
|
248
|
+
`export {};`,
|
|
249
|
+
].join('\n') + '\n';
|
|
250
|
+
writeFile(appDtsPath, appDts);
|
|
251
|
+
console.log(` created src/app.d.ts`);
|
|
252
|
+
if (generateGha) {
|
|
253
|
+
writeFile(ghaWorkflowPath, githubActionsContent(projectName));
|
|
254
|
+
console.log(` created .github/workflows/deploy.yml`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
injectPackageScripts(PROJECT_ROOT, adapter);
|
|
141
258
|
// ── Next steps ──────────────────────────────────────────────────────────
|
|
142
259
|
console.log('\nDone! Next steps:\n');
|
|
143
260
|
console.log(' 1. Run: bun install');
|
|
144
|
-
if (
|
|
261
|
+
if (adapter === 'ssg') {
|
|
145
262
|
console.log(' 2. Install adapter: bun add -D @sveltejs/adapter-static');
|
|
263
|
+
if (mode === 'full') {
|
|
264
|
+
console.log(' 3. Install sass: bun add -D sass (SCSS used in Svelte templates)');
|
|
265
|
+
console.log(' 4. Run: bun dev');
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
console.log(' 3. Add your own +page.svelte files for each route.');
|
|
269
|
+
console.log(' 4. Run: bun dev');
|
|
270
|
+
}
|
|
146
271
|
}
|
|
147
|
-
else {
|
|
148
|
-
console.log(' 2. Install adapter: bun add -D @sveltejs/adapter-cloudflare
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
272
|
+
else if (adapter === 'cloudflare') {
|
|
273
|
+
console.log(' 2. Install adapter: bun add -D @sveltejs/adapter-cloudflare');
|
|
274
|
+
console.log(' 3. Install types: bun add -D @cloudflare/workers-types');
|
|
275
|
+
if (generateGha) {
|
|
276
|
+
console.log(' 4. Add secret to GitHub repo: CF_API_TOKEN (Cloudflare API token)');
|
|
277
|
+
if (mode === 'full') {
|
|
278
|
+
console.log(' 5. Install sass: bun add -D sass (SCSS used in Svelte templates)');
|
|
279
|
+
console.log(' 6. Push to main — GitHub Actions will build and deploy automatically.');
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
console.log(' 5. Add your own +page.svelte files for each route.');
|
|
283
|
+
console.log(' 6. Push to main — GitHub Actions will build and deploy automatically.');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
if (mode === 'full') {
|
|
288
|
+
console.log(' 4. Install sass: bun add -D sass (SCSS used in Svelte templates)');
|
|
289
|
+
console.log(' 5. Run: bunx wrangler dev (or bun dev for local Vite)');
|
|
290
|
+
console.log(' 6. Deploy: bun run deploy');
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
console.log(' 4. Add your own +page.svelte files for each route.');
|
|
294
|
+
console.log(' 5. Run: bunx wrangler dev (or bun dev for local Vite)');
|
|
295
|
+
console.log(' 6. Deploy: bun run deploy');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
153
298
|
}
|
|
154
299
|
else {
|
|
155
|
-
console.log('
|
|
156
|
-
|
|
300
|
+
console.log(' 2. Install adapter: bun add -D @sveltejs/adapter-vercel (or your adapter)');
|
|
301
|
+
if (mode === 'full') {
|
|
302
|
+
console.log(' 3. Install sass: bun add -D sass (SCSS used in Svelte templates)');
|
|
303
|
+
console.log(' 4. Run: bun dev');
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
console.log(' 3. Add your own +page.svelte files for each route.');
|
|
307
|
+
console.log(' 4. Run: bun dev');
|
|
308
|
+
}
|
|
157
309
|
}
|
|
158
310
|
console.log('\n See https://github.com/alcogy/cosmolo for full documentation.\n');
|
|
159
311
|
}
|
|
@@ -38,13 +38,21 @@ function generateDrizzleSchema() {
|
|
|
38
38
|
// ─── CRUD file generation ─────────────────────────────────────────────────────
|
|
39
39
|
function generateArticlesCrud() {
|
|
40
40
|
return `import { drizzle } from 'drizzle-orm/d1';
|
|
41
|
-
import { desc, eq } from 'drizzle-orm';
|
|
41
|
+
import { and, desc, eq, sql } from 'drizzle-orm';
|
|
42
42
|
import { articles } from '../../drizzle/schema';
|
|
43
43
|
|
|
44
44
|
export function createDb(d1: D1Database) {
|
|
45
45
|
return drizzle(d1);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function parseArticle<T extends { tags: string | null; related: string | null }>(row: T) {
|
|
49
|
+
return {
|
|
50
|
+
...row,
|
|
51
|
+
tags: JSON.parse(row.tags ?? '[]') as string[],
|
|
52
|
+
related: JSON.parse(row.related ?? '[]') as string[],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
export async function getArticles(d1: D1Database) {
|
|
49
57
|
return createDb(d1)
|
|
50
58
|
.select()
|
|
@@ -53,6 +61,27 @@ export async function getArticles(d1: D1Database) {
|
|
|
53
61
|
.orderBy(desc(articles.sort));
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
export async function getArticlesByCategory(d1: D1Database, category: string) {
|
|
65
|
+
return createDb(d1)
|
|
66
|
+
.select()
|
|
67
|
+
.from(articles)
|
|
68
|
+
.where(and(eq(articles.draft, 0), eq(articles.category, category)))
|
|
69
|
+
.orderBy(desc(articles.sort));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function getArticlesByTag(d1: D1Database, tag: string) {
|
|
73
|
+
return createDb(d1)
|
|
74
|
+
.select()
|
|
75
|
+
.from(articles)
|
|
76
|
+
.where(
|
|
77
|
+
and(
|
|
78
|
+
eq(articles.draft, 0),
|
|
79
|
+
sql\`EXISTS (SELECT 1 FROM json_each(\${articles.tags}) WHERE value = \${tag})\`
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
.orderBy(desc(articles.sort));
|
|
83
|
+
}
|
|
84
|
+
|
|
56
85
|
export async function getArticle(d1: D1Database, slug: string) {
|
|
57
86
|
const [row] = await createDb(d1).select().from(articles).where(eq(articles.slug, slug));
|
|
58
87
|
return row ?? null;
|
|
@@ -110,6 +139,81 @@ export async function deleteCategory(d1: D1Database, key: string) {
|
|
|
110
139
|
}
|
|
111
140
|
`;
|
|
112
141
|
}
|
|
142
|
+
// ─── Route file generation ────────────────────────────────────────────────────
|
|
143
|
+
function generateHomeRoute() {
|
|
144
|
+
return `import type { PageServerLoad } from './$types';
|
|
145
|
+
import { getArticles, parseArticle } from '$lib/db/articles';
|
|
146
|
+
import { getCategories } from '$lib/db/categories';
|
|
147
|
+
import siteConfig from '../../config/site.json';
|
|
148
|
+
|
|
149
|
+
export const load: PageServerLoad = async ({ platform }) => {
|
|
150
|
+
const db = platform!.env.DB;
|
|
151
|
+
const [rawArticles, categories] = await Promise.all([getArticles(db), getCategories(db)]);
|
|
152
|
+
return {
|
|
153
|
+
articles: rawArticles.map(parseArticle),
|
|
154
|
+
categories,
|
|
155
|
+
articlesPerPage: siteConfig.articlesPerPage ?? 10,
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
function generateArticleRoute() {
|
|
161
|
+
return `import type { PageServerLoad } from './$types';
|
|
162
|
+
import { error } from '@sveltejs/kit';
|
|
163
|
+
import { marked } from 'marked';
|
|
164
|
+
import { getArticle, parseArticle } from '$lib/db/articles';
|
|
165
|
+
import { getCategories } from '$lib/db/categories';
|
|
166
|
+
|
|
167
|
+
export const load: PageServerLoad = async ({ params, platform }) => {
|
|
168
|
+
const db = platform!.env.DB;
|
|
169
|
+
const [raw, categories] = await Promise.all([getArticle(db, params.slug), getCategories(db)]);
|
|
170
|
+
if (!raw) error(404, 'Article not found');
|
|
171
|
+
const article = {
|
|
172
|
+
...parseArticle(raw),
|
|
173
|
+
body: await marked(raw.body ?? ''),
|
|
174
|
+
};
|
|
175
|
+
return { article, categories };
|
|
176
|
+
};
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
function generateCategoryRoute() {
|
|
180
|
+
return `import type { PageServerLoad } from './$types';
|
|
181
|
+
import { error } from '@sveltejs/kit';
|
|
182
|
+
import { getArticlesByCategory, parseArticle } from '$lib/db/articles';
|
|
183
|
+
import { getCategory } from '$lib/db/categories';
|
|
184
|
+
import siteConfig from '../../../../config/site.json';
|
|
185
|
+
|
|
186
|
+
export const load: PageServerLoad = async ({ params, platform }) => {
|
|
187
|
+
const db = platform!.env.DB;
|
|
188
|
+
const [rawArticles, category] = await Promise.all([
|
|
189
|
+
getArticlesByCategory(db, params.slug),
|
|
190
|
+
getCategory(db, params.slug),
|
|
191
|
+
]);
|
|
192
|
+
if (!category) error(404, 'Category not found');
|
|
193
|
+
return {
|
|
194
|
+
articles: rawArticles.map(parseArticle),
|
|
195
|
+
category,
|
|
196
|
+
articlesPerPage: siteConfig.articlesPerPage ?? 10,
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
function generateTagRoute() {
|
|
202
|
+
return `import type { PageServerLoad } from './$types';
|
|
203
|
+
import { getArticlesByTag, parseArticle } from '$lib/db/articles';
|
|
204
|
+
import siteConfig from '../../../../config/site.json';
|
|
205
|
+
|
|
206
|
+
export const load: PageServerLoad = async ({ params, platform }) => {
|
|
207
|
+
const db = platform!.env.DB;
|
|
208
|
+
const rawArticles = await getArticlesByTag(db, params.tag);
|
|
209
|
+
return {
|
|
210
|
+
articles: rawArticles.map(parseArticle),
|
|
211
|
+
tag: params.tag,
|
|
212
|
+
articlesPerPage: siteConfig.articlesPerPage ?? 10,
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
113
217
|
function generateDrizzleConfig() {
|
|
114
218
|
return `import type { Config } from 'drizzle-kit';
|
|
115
219
|
|
|
@@ -208,7 +312,25 @@ async function preflight(root, rl) {
|
|
|
208
312
|
}
|
|
209
313
|
// D1 database name
|
|
210
314
|
const dbName = await ask(rl, 'D1 database name', 'cosmolo');
|
|
211
|
-
|
|
315
|
+
// Generate D1-backed routes?
|
|
316
|
+
const routePaths = [
|
|
317
|
+
path.join('src', 'routes', '+page.server.ts'),
|
|
318
|
+
path.join('src', 'routes', 'articles', '[slug]', '+page.server.ts'),
|
|
319
|
+
path.join('src', 'routes', 'categories', '[slug]', '+page.server.ts'),
|
|
320
|
+
path.join('src', 'routes', 'tags', '[tag]', '+page.server.ts'),
|
|
321
|
+
];
|
|
322
|
+
const existingRoutes = routePaths.filter((p) => fs.existsSync(path.join(root, p)));
|
|
323
|
+
let generateRoutes = true;
|
|
324
|
+
if (existingRoutes.length > 0) {
|
|
325
|
+
console.log(`\n The following route files will be replaced with D1-backed versions:`);
|
|
326
|
+
for (const p of existingRoutes)
|
|
327
|
+
console.log(` ${p}`);
|
|
328
|
+
generateRoutes = await confirm(rl, 'Replace with D1-backed route files?');
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
generateRoutes = await confirm(rl, 'Generate D1-backed route files?');
|
|
332
|
+
}
|
|
333
|
+
return { dbName, generateRoutes };
|
|
212
334
|
}
|
|
213
335
|
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
214
336
|
export async function drizzleSetup(_config) {
|
|
@@ -218,7 +340,7 @@ export async function drizzleSetup(_config) {
|
|
|
218
340
|
rl.close();
|
|
219
341
|
if (!result)
|
|
220
342
|
return;
|
|
221
|
-
const { dbName } = result;
|
|
343
|
+
const { dbName, generateRoutes } = result;
|
|
222
344
|
// Generate drizzle/schema.ts
|
|
223
345
|
const drizzleDir = path.join(root, 'drizzle');
|
|
224
346
|
fs.mkdirSync(drizzleDir, { recursive: true });
|
|
@@ -236,6 +358,20 @@ export async function drizzleSetup(_config) {
|
|
|
236
358
|
}
|
|
237
359
|
// wrangler.toml
|
|
238
360
|
const wranglerAction = updateWranglerToml(root, dbName);
|
|
361
|
+
// D1-backed route files
|
|
362
|
+
if (generateRoutes) {
|
|
363
|
+
const routesDir = path.join(root, 'src', 'routes');
|
|
364
|
+
const routeFiles = [
|
|
365
|
+
[path.join(routesDir, '+page.server.ts'), generateHomeRoute()],
|
|
366
|
+
[path.join(routesDir, 'articles', '[slug]', '+page.server.ts'), generateArticleRoute()],
|
|
367
|
+
[path.join(routesDir, 'categories', '[slug]', '+page.server.ts'), generateCategoryRoute()],
|
|
368
|
+
[path.join(routesDir, 'tags', '[tag]', '+page.server.ts'), generateTagRoute()],
|
|
369
|
+
];
|
|
370
|
+
for (const [filePath, content] of routeFiles) {
|
|
371
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
372
|
+
fs.writeFileSync(filePath, content);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
239
375
|
console.log('\n✓ Files generated:');
|
|
240
376
|
console.log(' drizzle/schema.ts');
|
|
241
377
|
console.log(' src/lib/db/articles.ts');
|
|
@@ -243,21 +379,33 @@ export async function drizzleSetup(_config) {
|
|
|
243
379
|
console.log(' drizzle.config.ts');
|
|
244
380
|
console.log(' .dev.vars.example');
|
|
245
381
|
console.log(` wrangler.toml (${wranglerAction})`);
|
|
382
|
+
if (generateRoutes) {
|
|
383
|
+
console.log(' src/routes/+page.server.ts');
|
|
384
|
+
console.log(' src/routes/articles/[slug]/+page.server.ts');
|
|
385
|
+
console.log(' src/routes/categories/[slug]/+page.server.ts');
|
|
386
|
+
console.log(' src/routes/tags/[tag]/+page.server.ts');
|
|
387
|
+
}
|
|
388
|
+
let step = 1;
|
|
246
389
|
console.log('\nNext steps:\n');
|
|
247
|
-
console.log(`
|
|
390
|
+
console.log(` ${step++}. Create the D1 database (if not done yet):`);
|
|
248
391
|
console.log(` bunx wrangler d1 create ${dbName}`);
|
|
249
392
|
console.log(` Copy the database_id into wrangler.toml.\n`);
|
|
250
|
-
console.log(`
|
|
393
|
+
console.log(` ${step++}. Generate migration files:`);
|
|
251
394
|
console.log(` bunx drizzle-kit generate\n`);
|
|
252
|
-
console.log(`
|
|
395
|
+
console.log(` ${step++}. Apply migrations locally:`);
|
|
253
396
|
console.log(` bunx wrangler d1 migrations apply ${dbName} --local\n`);
|
|
254
|
-
console.log(`
|
|
397
|
+
console.log(` ${step++}. Seed content from Markdown files:`);
|
|
255
398
|
console.log(` bunx cosmolo migrate:db → choose Option 1 to export SQL`);
|
|
256
399
|
console.log(` bunx wrangler d1 execute ${dbName} --local --file=cosmolo-migration/002_seed_categories.sql`);
|
|
257
400
|
console.log(` bunx wrangler d1 execute ${dbName} --local --file=cosmolo-migration/003_seed_articles.sql\n`);
|
|
258
|
-
|
|
401
|
+
if (generateRoutes) {
|
|
402
|
+
console.log(` ${step++}. Install marked for Markdown rendering in routes:`);
|
|
403
|
+
console.log(` bun add marked\n`);
|
|
404
|
+
}
|
|
405
|
+
console.log(` ${step++}. Install Cloudflare Workers types for TypeScript:`);
|
|
259
406
|
console.log(` bun add -d @cloudflare/workers-types\n`);
|
|
260
|
-
console.log(`
|
|
407
|
+
console.log(` ${step++}. Ensure src/app.d.ts declares the DB binding:`);
|
|
261
408
|
console.log(` interface Platform { env: { DB: D1Database } }`);
|
|
262
|
-
console.log(
|
|
409
|
+
console.log(` (cosmolo init --cloudflare does this automatically)\n`);
|
|
410
|
+
console.log(` See docs/DB_MIGRATION.md for full details.`);
|
|
263
411
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(): Promise<void>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
|
+
function ask(rl, question, fallback = '') {
|
|
5
|
+
const hint = fallback ? ` [${fallback}]` : '';
|
|
6
|
+
return new Promise((resolve) => rl.question(` ${question}${hint}: `, (ans) => resolve(ans.trim() || fallback)));
|
|
7
|
+
}
|
|
8
|
+
function r2HelperContent() {
|
|
9
|
+
return (`// Helper for serving assets stored in Cloudflare R2.\n` +
|
|
10
|
+
`// Usage: import in a +server.ts route and call getR2Asset.\n\n` +
|
|
11
|
+
`export async function getR2Asset(bucket: R2Bucket, key: string): Promise<Response> {\n` +
|
|
12
|
+
` const obj = await bucket.get(key);\n` +
|
|
13
|
+
` if (!obj) return new Response('Not found', { status: 404 });\n` +
|
|
14
|
+
` const headers = new Headers();\n` +
|
|
15
|
+
` obj.writeHttpMetadata(headers);\n` +
|
|
16
|
+
` headers.set('etag', obj.httpEtag);\n` +
|
|
17
|
+
` headers.set('cache-control', 'public, max-age=31536000, immutable');\n` +
|
|
18
|
+
` return new Response(obj.body as ReadableStream, { headers });\n` +
|
|
19
|
+
`}\n`);
|
|
20
|
+
}
|
|
21
|
+
function r2RouteContent(binding) {
|
|
22
|
+
return (`import type { RequestHandler } from './$types';\n` +
|
|
23
|
+
`import { getR2Asset } from '$lib/r2';\n\n` +
|
|
24
|
+
`export const GET: RequestHandler = async ({ params, platform }) => {\n` +
|
|
25
|
+
` return getR2Asset(platform!.env.${binding}, params.key);\n` +
|
|
26
|
+
`};\n`);
|
|
27
|
+
}
|
|
28
|
+
function appendR2Binding(wranglerPath, binding, bucketName) {
|
|
29
|
+
const existing = fs.readFileSync(wranglerPath, 'utf-8');
|
|
30
|
+
const section = `\n[[r2_buckets]]\n` +
|
|
31
|
+
`binding = "${binding}"\n` +
|
|
32
|
+
`bucket_name = "${bucketName}"\n`;
|
|
33
|
+
fs.writeFileSync(wranglerPath, existing + section);
|
|
34
|
+
}
|
|
35
|
+
export async function main() {
|
|
36
|
+
const root = process.cwd();
|
|
37
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
38
|
+
console.log('\ncosmolo setup:r2\n');
|
|
39
|
+
// wrangler.toml check
|
|
40
|
+
const wranglerPath = path.join(root, 'wrangler.toml');
|
|
41
|
+
if (!fs.existsSync(wranglerPath)) {
|
|
42
|
+
console.error(' wrangler.toml not found.\n' +
|
|
43
|
+
' Run `cosmolo init` with the Cloudflare adapter first.\n');
|
|
44
|
+
rl.close();
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const bucketName = await ask(rl, 'R2 bucket name', 'assets');
|
|
48
|
+
const binding = await ask(rl, 'Binding name (used in platform.env.*)', 'ASSETS');
|
|
49
|
+
// Conflict checks
|
|
50
|
+
const helperPath = path.join(root, 'src', 'lib', 'r2.ts');
|
|
51
|
+
const routePath = path.join(root, 'src', 'routes', 'assets', '[...key]', '+server.ts');
|
|
52
|
+
const conflicts = [];
|
|
53
|
+
if (fs.existsSync(helperPath))
|
|
54
|
+
conflicts.push('src/lib/r2.ts');
|
|
55
|
+
if (fs.existsSync(routePath))
|
|
56
|
+
conflicts.push('src/routes/assets/[...key]/+server.ts');
|
|
57
|
+
if (conflicts.length > 0) {
|
|
58
|
+
console.log('\n The following files already exist:');
|
|
59
|
+
for (const f of conflicts)
|
|
60
|
+
console.log(` ${f}`);
|
|
61
|
+
const ans = await new Promise((resolve) => rl.question('\n Overwrite? [y/N]: ', (a) => resolve(a.trim().toLowerCase() || 'n')));
|
|
62
|
+
if (ans !== 'y') {
|
|
63
|
+
console.log('\n Aborted. No files were written.\n');
|
|
64
|
+
rl.close();
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
rl.close();
|
|
69
|
+
// Write helper
|
|
70
|
+
fs.mkdirSync(path.dirname(helperPath), { recursive: true });
|
|
71
|
+
fs.writeFileSync(helperPath, r2HelperContent());
|
|
72
|
+
// Write route
|
|
73
|
+
fs.mkdirSync(path.dirname(routePath), { recursive: true });
|
|
74
|
+
fs.writeFileSync(routePath, r2RouteContent(binding));
|
|
75
|
+
// Update wrangler.toml
|
|
76
|
+
appendR2Binding(wranglerPath, binding, bucketName);
|
|
77
|
+
console.log('\n✓ Files generated:');
|
|
78
|
+
console.log(' src/lib/r2.ts');
|
|
79
|
+
console.log(' src/routes/assets/[...key]/+server.ts');
|
|
80
|
+
console.log(' wrangler.toml (r2_buckets appended)');
|
|
81
|
+
console.log('\nNext steps:\n');
|
|
82
|
+
console.log(` 1. Create the R2 bucket (if not done yet):`);
|
|
83
|
+
console.log(` bunx wrangler r2 bucket create ${bucketName}\n`);
|
|
84
|
+
console.log(` 2. Add the binding to src/app.d.ts:`);
|
|
85
|
+
console.log(` interface Platform { env: { ${binding}: R2Bucket } }\n`);
|
|
86
|
+
console.log(` 3. Upload assets (e.g. from static/images/):`);
|
|
87
|
+
console.log(` bunx wrangler r2 object put ${bucketName}/<key> --file <path>\n`);
|
|
88
|
+
console.log(` 4. Reference assets via /assets/<key> in your templates.\n`);
|
|
89
|
+
console.log(` See https://developers.cloudflare.com/r2/ for full R2 docs.`);
|
|
90
|
+
}
|
package/dist/markdown.js
CHANGED
|
@@ -7,6 +7,9 @@ function slugifyHeading(text) {
|
|
|
7
7
|
.trim()
|
|
8
8
|
.replace(/\s+/g, '-');
|
|
9
9
|
}
|
|
10
|
+
function escapeAttr(val) {
|
|
11
|
+
return val.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
12
|
+
}
|
|
10
13
|
marked.use({
|
|
11
14
|
extensions: [
|
|
12
15
|
{
|
|
@@ -22,7 +25,8 @@ marked.use({
|
|
|
22
25
|
},
|
|
23
26
|
renderer(token) {
|
|
24
27
|
const { videoId } = token;
|
|
25
|
-
|
|
28
|
+
const safeId = videoId.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
29
|
+
return `<div class="youtube-embed"><iframe src="https://www.youtube.com/embed/${safeId}" title="YouTube video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>\n`;
|
|
26
30
|
},
|
|
27
31
|
},
|
|
28
32
|
],
|
|
@@ -32,8 +36,8 @@ marked.use({
|
|
|
32
36
|
link({ href, title, text }) {
|
|
33
37
|
const isExternal = /^https?:\/\//.test(href ?? '');
|
|
34
38
|
const rel = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
|
|
35
|
-
const titleAttr = title ? ` title="${title}"` : '';
|
|
36
|
-
return `<a href="${href}"${titleAttr}${rel}>${text}</a>`;
|
|
39
|
+
const titleAttr = title ? ` title="${escapeAttr(title)}"` : '';
|
|
40
|
+
return `<a href="${escapeAttr(href ?? '')}"${titleAttr}${rel}>${text}</a>`;
|
|
37
41
|
},
|
|
38
42
|
heading({ text, depth }) {
|
|
39
43
|
const id = slugifyHeading(text);
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED