getfilepress 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +178 -4
- package/package.json +89 -7
- package/packages/app/package.json +31 -0
- package/packages/app/src/app.d.ts +12 -0
- package/packages/app/src/app.html +12 -0
- package/packages/app/src/critical-theme.d.ts +4 -0
- package/packages/app/src/lib/content.server.ts +8 -0
- package/packages/app/src/lib/empty-theme.css +1 -0
- package/packages/app/src/lib/genie/GenieHost.svelte +16 -0
- package/packages/app/src/lib/genie/GeniePanel.svelte +429 -0
- package/packages/app/src/lib/genie/ops.ts +237 -0
- package/packages/app/src/lib/genie/store.ts +217 -0
- package/packages/app/src/lib/genie/types.ts +61 -0
- package/packages/app/src/lib/pages.server.ts +7 -0
- package/packages/app/src/lib/site.server.ts +52 -0
- package/packages/app/src/lib/theme-entry.ts +7 -0
- package/packages/app/src/routes/+layout.svelte +29 -0
- package/packages/app/src/routes/+layout.ts +10 -0
- package/packages/app/src/routes/+page.server.ts +28 -0
- package/packages/app/src/routes/+page.svelte +67 -0
- package/packages/app/src/routes/[slug]/+page.server.ts +20 -0
- package/packages/app/src/routes/[slug]/+page.svelte +47 -0
- package/packages/app/src/routes/page/[n]/+page.server.ts +21 -0
- package/packages/app/src/routes/page/[n]/+page.svelte +36 -0
- package/packages/app/src/routes/posts/[slug]/+page.server.ts +24 -0
- package/packages/app/src/routes/posts/[slug]/+page.svelte +95 -0
- package/packages/app/src/routes/robots.txt/+server.ts +11 -0
- package/packages/app/src/routes/rss.xml/+server.ts +13 -0
- package/packages/app/src/routes/sitemap.xml/+server.ts +19 -0
- package/packages/app/src/routes/tags/+page.server.ts +4 -0
- package/packages/app/src/routes/tags/+page.svelte +26 -0
- package/packages/app/src/routes/tags/[tag]/+page.server.ts +15 -0
- package/packages/app/src/routes/tags/[tag]/+page.svelte +23 -0
- package/packages/app/src/routes/topics/+page.server.ts +28 -0
- package/packages/app/src/routes/topics/+page.svelte +47 -0
- package/packages/app/src/routes/writing/+page.server.ts +12 -0
- package/packages/app/src/routes/writing/+page.svelte +38 -0
- package/packages/app/src/site-theme.d.ts +2 -0
- package/packages/app/static/.gitkeep +0 -0
- package/packages/app/tsconfig.json +15 -0
- package/packages/app/vite-plugin-critical-theme.ts +70 -0
- package/packages/app/vite-plugin-genie.ts +114 -0
- package/packages/app/vite.config.ts +141 -0
- package/packages/core/package.json +51 -0
- package/packages/core/src/lib/assets/favicon.svg +1 -0
- package/packages/core/src/lib/components/Newsletter.svelte +13 -0
- package/packages/core/src/lib/components/PostCard.svelte +34 -0
- package/packages/core/src/lib/components/PostIndex.svelte +86 -0
- package/packages/core/src/lib/components/SiteFooter.svelte +15 -0
- package/packages/core/src/lib/components/SiteHeader.svelte +28 -0
- package/packages/core/src/lib/config.ts +154 -0
- package/packages/core/src/lib/content/content.ts +193 -0
- package/packages/core/src/lib/content/feeds.ts +102 -0
- package/packages/core/src/lib/content/markdown.ts +78 -0
- package/packages/core/src/lib/content/pages.ts +103 -0
- package/packages/core/src/lib/content/parse.ts +214 -0
- package/packages/core/src/lib/content/rehype-figure.ts +79 -0
- package/packages/core/src/lib/content/types.ts +88 -0
- package/packages/core/src/lib/format.ts +25 -0
- package/packages/core/src/lib/index.ts +27 -0
- package/packages/core/src/lib/server.ts +33 -0
- package/packages/core/src/lib/styles/fonts.css +72 -0
- package/packages/core/src/lib/styles/theme.css +762 -0
- package/packages/core/src/lib/theme.ts +5 -0
- package/packages/import/package.json +27 -0
- package/packages/import/src/cli.ts +346 -0
- package/packages/import/src/discover.ts +190 -0
- package/packages/import/src/extract.ts +378 -0
- package/packages/import/src/fetch.ts +63 -0
- package/packages/import/src/html-to-md.ts +21 -0
- package/packages/import/src/images.ts +247 -0
- package/packages/import/src/inspire.ts +417 -0
- package/packages/import/src/ir.ts +109 -0
- package/packages/import/src/ollama.ts +145 -0
- package/packages/import/src/stock.ts +218 -0
- package/packages/import/src/theme.ts +478 -0
- package/packages/import/src/write-site.ts +371 -0
- package/packages/import/tsconfig.json +13 -0
- package/pnpm-workspace.yaml +2 -0
- package/scripts/create-site.mjs +260 -0
- package/scripts/filepress.mjs +273 -0
- package/scripts/link-embedded-packages.mjs +40 -0
- package/scripts/postinstall.mjs +56 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
rmSync
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import type { DesignBrief, ImportOptions, SiteIR } from './ir.ts';
|
|
13
|
+
import { fetchBuffer } from './fetch.ts';
|
|
14
|
+
import { fetchChosenImages } from './images.ts';
|
|
15
|
+
import { themeCssFromBrief } from './theme.ts';
|
|
16
|
+
|
|
17
|
+
function writeAttribution(metaDir: string, ir: SiteIR) {
|
|
18
|
+
const stockLines = ir.notes.filter(
|
|
19
|
+
(n) =>
|
|
20
|
+
n.includes('Openverse') ||
|
|
21
|
+
n.startsWith('- **') ||
|
|
22
|
+
n.startsWith('Stock attribution')
|
|
23
|
+
);
|
|
24
|
+
if (!stockLines.length) return;
|
|
25
|
+
writeFileSync(
|
|
26
|
+
join(metaDir, 'IMAGE_ATTRIBUTION.md'),
|
|
27
|
+
`# Image attribution
|
|
28
|
+
|
|
29
|
+
Chrome covers were fetched via [Openverse](https://openverse.org/) (Creative Commons).
|
|
30
|
+
Keep this file with the site if you redistribute.
|
|
31
|
+
|
|
32
|
+
${stockLines.join('\n')}
|
|
33
|
+
`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DEFAULT_FAVICON = resolve(
|
|
38
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
39
|
+
'../../core/src/lib/assets/favicon.svg'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
function ensureScaffold(out: string, opts: ImportOptions, title: string, url: string) {
|
|
43
|
+
const configPath = join(out, 'filepress.config.ts');
|
|
44
|
+
const needsScaffold = !existsSync(configPath) || !existsSync(join(out, 'package.json'));
|
|
45
|
+
if (!needsScaffold) {
|
|
46
|
+
mkdirSync(join(out, 'posts'), { recursive: true });
|
|
47
|
+
mkdirSync(join(out, 'pages'), { recursive: true });
|
|
48
|
+
mkdirSync(join(out, 'static'), { recursive: true });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const createScript = join(opts.engineRoot, 'scripts', 'create-site.mjs');
|
|
53
|
+
const result = spawnSync(
|
|
54
|
+
process.execPath,
|
|
55
|
+
[
|
|
56
|
+
createScript,
|
|
57
|
+
opts.siteName,
|
|
58
|
+
'--external',
|
|
59
|
+
out,
|
|
60
|
+
'--title',
|
|
61
|
+
title,
|
|
62
|
+
'--url',
|
|
63
|
+
url,
|
|
64
|
+
...(opts.force ? ['--force'] : [])
|
|
65
|
+
],
|
|
66
|
+
{ encoding: 'utf8' }
|
|
67
|
+
);
|
|
68
|
+
if (result.status !== 0) {
|
|
69
|
+
throw new Error(`create-site failed:\n${result.stdout}\n${result.stderr}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function yamlQuote(s: string): string {
|
|
74
|
+
return JSON.stringify(s);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function postFilename(date: string, slug: string): string {
|
|
78
|
+
return `${date}-${slug}.md`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function rewriteImageUrls(markdown: string, mapping: Map<string, string>): string {
|
|
82
|
+
let out = markdown;
|
|
83
|
+
for (const [from, to] of mapping) {
|
|
84
|
+
out = out.split(from).join(to);
|
|
85
|
+
// also without origin if relative forms appeared
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function downloadImages(
|
|
91
|
+
urls: string[],
|
|
92
|
+
staticDir: string
|
|
93
|
+
): Promise<Map<string, string>> {
|
|
94
|
+
const map = new Map<string, string>();
|
|
95
|
+
const imgDir = join(staticDir, 'images');
|
|
96
|
+
mkdirSync(imgDir, { recursive: true });
|
|
97
|
+
let i = 0;
|
|
98
|
+
for (const url of urls) {
|
|
99
|
+
try {
|
|
100
|
+
const buf = await fetchBuffer(url);
|
|
101
|
+
const ext = guessExt(url, buf);
|
|
102
|
+
const base = url.split('/').pop()?.split('?')[0] || `img-${++i}`;
|
|
103
|
+
const safe = base.replace(/[^\w.-]+/g, '-').slice(0, 80) || `img-${++i}`;
|
|
104
|
+
const name = safe.includes('.') ? safe : `${safe}${ext}`;
|
|
105
|
+
const dest = join(imgDir, name);
|
|
106
|
+
writeFileSync(dest, buf);
|
|
107
|
+
map.set(url, `/images/${name}`);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.warn(`import: image skip ${url}: ${e instanceof Error ? e.message : e}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return map;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Copy favicons / touch icons to static/ root (preserve basename). */
|
|
116
|
+
async function downloadRootAssets(urls: string[], staticDir: string): Promise<void> {
|
|
117
|
+
mkdirSync(staticDir, { recursive: true });
|
|
118
|
+
for (const url of urls) {
|
|
119
|
+
try {
|
|
120
|
+
const buf = await fetchBuffer(url);
|
|
121
|
+
const name = new URL(url).pathname.split('/').filter(Boolean).pop();
|
|
122
|
+
if (!name || name.includes('..')) continue;
|
|
123
|
+
writeFileSync(join(staticDir, name), buf);
|
|
124
|
+
} catch {
|
|
125
|
+
/* optional asset */
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function guessExt(url: string, buf: Uint8Array): string {
|
|
131
|
+
const path = new URL(url).pathname.toLowerCase();
|
|
132
|
+
if (path.endsWith('.png')) return '.png';
|
|
133
|
+
if (path.endsWith('.jpg') || path.endsWith('.jpeg')) return '.jpg';
|
|
134
|
+
if (path.endsWith('.webp')) return '.webp';
|
|
135
|
+
if (path.endsWith('.gif')) return '.gif';
|
|
136
|
+
if (path.endsWith('.svg')) return '.svg';
|
|
137
|
+
if (buf[0] === 0x89 && buf[1] === 0x50) return '.png';
|
|
138
|
+
if (buf[0] === 0xff && buf[1] === 0xd8) return '.jpg';
|
|
139
|
+
return '.bin';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildReport(ir: SiteIR, opts: ImportOptions, brief: DesignBrief | null): string {
|
|
143
|
+
const imgs = brief?.images;
|
|
144
|
+
const imageSection = imgs
|
|
145
|
+
? `## Chrome images
|
|
146
|
+
|
|
147
|
+
${imgs.hero ? `- Hero cover: \`${imgs.hero}\`` : '- Hero cover: (none — typographic lede)'}
|
|
148
|
+
${imgs.header ? `- Header cover: \`${imgs.header}\`` : '- Header cover: (none)'}
|
|
149
|
+
${imgs.background ? `- Page background: \`${imgs.background}\`` : '- Page background: (none)'}
|
|
150
|
+
${imgs.logo ? `- Logo (config): \`${imgs.logo}\`` : '- Logo: (none — text title)'}
|
|
151
|
+
${imgs.portrait ? `- Portrait file (not a CSS cover): \`${imgs.portrait}\`` : '- Portrait: (none)'}
|
|
152
|
+
|
|
153
|
+
Replace files under \`static/images/\` anytime; theme.css references cover paths only (not portrait).
|
|
154
|
+
`
|
|
155
|
+
: `## Chrome images
|
|
156
|
+
|
|
157
|
+
Not fetched. Re-run with \`--fetch-images\` to download suggested hero/header/background/logo into \`static/images/\`, or drop your own files there and set paths in \`theme.css\` / \`logo\` in config.
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
return `# filepress import report
|
|
161
|
+
|
|
162
|
+
Source: ${ir.source.url}
|
|
163
|
+
Generator guess: ${ir.source.generator ?? 'unknown'}
|
|
164
|
+
Output: ${opts.out}
|
|
165
|
+
Generated: ${new Date().toISOString()}
|
|
166
|
+
|
|
167
|
+
## Identity
|
|
168
|
+
|
|
169
|
+
- Title: ${ir.identity.title}
|
|
170
|
+
- Author: ${ir.identity.author}
|
|
171
|
+
- Canonical URL (config): ${opts.canonicalUrl || ir.identity.canonicalUrl}
|
|
172
|
+
|
|
173
|
+
## Imported
|
|
174
|
+
|
|
175
|
+
- Posts: ${ir.posts.length}
|
|
176
|
+
- Pages: ${ir.pages.length}
|
|
177
|
+
- Topics: ${ir.topics.map((t) => t.tag).join(', ') || '(none)'}
|
|
178
|
+
|
|
179
|
+
## URL remaps
|
|
180
|
+
|
|
181
|
+
Posts move to \`/posts/<slug>\` (filepress default). If you need old paths (e.g. \`/writing/…\`), add Cloudflare Pages redirects.
|
|
182
|
+
|
|
183
|
+
${ir.posts.map((p) => `- \`${p.sourceUrl}\` → \`/posts/${p.slug}\``).join('\n')}
|
|
184
|
+
|
|
185
|
+
${ir.pages.map((p) => `- \`${p.sourceUrl}\` → \`/${p.slug}\``).join('\n')}
|
|
186
|
+
|
|
187
|
+
## Notes
|
|
188
|
+
|
|
189
|
+
${ir.notes.map((n) => `- ${n}`).join('\n')}
|
|
190
|
+
|
|
191
|
+
## Design
|
|
192
|
+
|
|
193
|
+
${brief ? `Mood: ${brief.mood}\nDensity: ${brief.density}\nDo: ${brief.do.join('; ')}\nDon't: ${brief.dont.join('; ')}` : 'No LLM brief (token theme from defaults / source CSS).'}
|
|
194
|
+
|
|
195
|
+
${imageSection}
|
|
196
|
+
## Next
|
|
197
|
+
|
|
198
|
+
\`\`\`bash
|
|
199
|
+
cd ${opts.out}
|
|
200
|
+
pnpm install
|
|
201
|
+
pnpm filepress dev
|
|
202
|
+
\`\`\`
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function writeSite(
|
|
207
|
+
ir: SiteIR,
|
|
208
|
+
opts: ImportOptions,
|
|
209
|
+
brief: DesignBrief | null
|
|
210
|
+
): Promise<{ reportPath: string }> {
|
|
211
|
+
const out = resolve(opts.out);
|
|
212
|
+
if (existsSync(out)) {
|
|
213
|
+
const entries = readdirSync(out).filter((e) => e !== '.git' && e !== 'artifacts');
|
|
214
|
+
if (entries.length && !opts.force) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Refusing non-empty directory ${out} (pass --force to overwrite generated files carefully)`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (opts.force && existsSync(join(out, 'filepress.config.ts'))) {
|
|
220
|
+
// wipe content dirs we own; keep .git
|
|
221
|
+
for (const d of ['posts', 'pages', 'static', '.filepress-import']) {
|
|
222
|
+
rmSync(join(out, d), { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const title = opts.title || ir.identity.title;
|
|
228
|
+
const url = opts.canonicalUrl || ir.identity.canonicalUrl;
|
|
229
|
+
const author = opts.author || ir.identity.author;
|
|
230
|
+
|
|
231
|
+
mkdirSync(out, { recursive: true });
|
|
232
|
+
ensureScaffold(out, opts, title, url);
|
|
233
|
+
|
|
234
|
+
// Remove scaffold sample content; we'll write imported files
|
|
235
|
+
rmSync(join(out, 'posts', 'hello-world.md'), { force: true });
|
|
236
|
+
rmSync(join(out, 'pages', 'about.md'), { force: true });
|
|
237
|
+
// Clear prior import posts/pages when re-running with --force
|
|
238
|
+
if (opts.force) {
|
|
239
|
+
for (const dir of ['posts', 'pages']) {
|
|
240
|
+
const abs = join(out, dir);
|
|
241
|
+
if (!existsSync(abs)) continue;
|
|
242
|
+
for (const f of readdirSync(abs)) {
|
|
243
|
+
if (f.endsWith('.md')) rmSync(join(abs, f), { force: true });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const staticDir = join(out, 'static');
|
|
249
|
+
mkdirSync(staticDir, { recursive: true });
|
|
250
|
+
|
|
251
|
+
let activeBrief = brief;
|
|
252
|
+
if (opts.fetchImages && brief?.images) {
|
|
253
|
+
console.log('import: fetching chrome images …');
|
|
254
|
+
const local = await fetchChosenImages(brief.images, staticDir);
|
|
255
|
+
activeBrief = { ...brief, images: local };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const allImages = [
|
|
259
|
+
...new Set([...ir.posts.flatMap((p) => p.imageUrls), ...ir.pages.flatMap((p) => p.imageUrls)])
|
|
260
|
+
];
|
|
261
|
+
const imageMap = await downloadImages(allImages, staticDir);
|
|
262
|
+
await downloadRootAssets(ir.assets ?? [], staticDir);
|
|
263
|
+
// Layout always links /favicon.svg — guarantee one exists for prerender.
|
|
264
|
+
const faviconDest = join(staticDir, 'favicon.svg');
|
|
265
|
+
if (!existsSync(faviconDest) && existsSync(DEFAULT_FAVICON)) {
|
|
266
|
+
copyFileSync(DEFAULT_FAVICON, faviconDest);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const post of ir.posts) {
|
|
270
|
+
const body = rewriteImageUrls(post.markdown, imageMap);
|
|
271
|
+
const tags =
|
|
272
|
+
post.tags.length > 0 ? `\ntags: [${post.tags.map((t) => yamlQuote(t)).join(', ')}]` : '';
|
|
273
|
+
const desc = post.description ? `\ndescription: ${yamlQuote(post.description)}` : '';
|
|
274
|
+
const md = `---
|
|
275
|
+
title: ${yamlQuote(post.title)}
|
|
276
|
+
date: ${post.date}
|
|
277
|
+
slug: ${yamlQuote(post.slug)}${desc}${tags}
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
${body}
|
|
281
|
+
`;
|
|
282
|
+
writeFileSync(join(out, 'posts', postFilename(post.date, post.slug)), md);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const page of ir.pages) {
|
|
286
|
+
const body = rewriteImageUrls(page.markdown, imageMap);
|
|
287
|
+
const desc = page.description ? `\ndescription: ${yamlQuote(page.description)}` : '';
|
|
288
|
+
const md = `---
|
|
289
|
+
title: ${yamlQuote(page.title)}${desc}
|
|
290
|
+
order: ${page.order}
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
${body}
|
|
294
|
+
`;
|
|
295
|
+
writeFileSync(join(out, 'pages', `${page.slug}.md`), md);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const topicsLit = ir.topics
|
|
299
|
+
.map((t) => `\t\t{ label: ${yamlQuote(t.label)}, tag: ${yamlQuote(t.tag)} }`)
|
|
300
|
+
.join(',\n');
|
|
301
|
+
const navLit = ir.nav
|
|
302
|
+
.map((n) => `\t\t{ label: ${yamlQuote(n.label)}, href: ${yamlQuote(n.href)} }`)
|
|
303
|
+
.join(',\n');
|
|
304
|
+
const ledeLine = ir.lede ? `\n\tlede: ${yamlQuote(ir.lede)},` : '';
|
|
305
|
+
const logo = activeBrief?.images?.logo;
|
|
306
|
+
const logoLine = logo ? `\n\tlogo: ${yamlQuote(logo)},` : '';
|
|
307
|
+
|
|
308
|
+
writeFileSync(
|
|
309
|
+
join(out, 'filepress.config.ts'),
|
|
310
|
+
`import { defineFilepressConfig } from 'getfilepress';
|
|
311
|
+
|
|
312
|
+
export default defineFilepressConfig({
|
|
313
|
+
title: ${yamlQuote(title)},
|
|
314
|
+
description: ${yamlQuote(ir.identity.description)},
|
|
315
|
+
url: ${yamlQuote(url)},
|
|
316
|
+
author: ${yamlQuote(author)},${ledeLine}${logoLine}
|
|
317
|
+
nav: [
|
|
318
|
+
${navLit}
|
|
319
|
+
],
|
|
320
|
+
topics: [
|
|
321
|
+
${topicsLit}
|
|
322
|
+
]
|
|
323
|
+
});
|
|
324
|
+
`
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
writeFileSync(join(out, 'theme.css'), themeCssFromBrief(activeBrief ?? undefined));
|
|
328
|
+
|
|
329
|
+
const metaDir = join(out, '.filepress-import');
|
|
330
|
+
mkdirSync(metaDir, { recursive: true });
|
|
331
|
+
writeFileSync(join(metaDir, 'site-ir.json'), JSON.stringify(ir, null, 2));
|
|
332
|
+
if (activeBrief)
|
|
333
|
+
writeFileSync(join(metaDir, 'design-brief.json'), JSON.stringify(activeBrief, null, 2));
|
|
334
|
+
writeAttribution(metaDir, ir);
|
|
335
|
+
const reportPath = join(metaDir, 'import-report.md');
|
|
336
|
+
writeFileSync(reportPath, buildReport(ir, opts, activeBrief));
|
|
337
|
+
|
|
338
|
+
// gitignore import cache
|
|
339
|
+
const gi = join(out, '.gitignore');
|
|
340
|
+
writeFileSync(
|
|
341
|
+
gi,
|
|
342
|
+
`/build
|
|
343
|
+
/node_modules
|
|
344
|
+
.DS_Store
|
|
345
|
+
Thumbs.db
|
|
346
|
+
.filepress/
|
|
347
|
+
.filepress-genie/
|
|
348
|
+
.filepress-import/crawl-cache/
|
|
349
|
+
`
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
return { reportPath };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function defaultEngineRoot(): string {
|
|
356
|
+
// packages/import/src → repo root
|
|
357
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function defaultOutPath(engineRoot: string, siteName: string): string {
|
|
361
|
+
return resolve(engineRoot, '..', siteName);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function siteNameFromUrl(source: string): string {
|
|
365
|
+
try {
|
|
366
|
+
const host = new URL(source).hostname.replace(/^www\./, '');
|
|
367
|
+
return host.split('.')[0].toLowerCase().replace(/[^a-z0-9-]+/g, '-') || 'imported-site';
|
|
368
|
+
} catch {
|
|
369
|
+
return 'imported-site';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Scaffold a content-only filepress site.
|
|
3
|
+
//
|
|
4
|
+
// Monorepo (default):
|
|
5
|
+
// node scripts/create-site.mjs my-site --title "My Site" --url https://my.site
|
|
6
|
+
// → sites/my-site/
|
|
7
|
+
//
|
|
8
|
+
// External sibling repo:
|
|
9
|
+
// node scripts/create-site.mjs example-site --external ../example-site \
|
|
10
|
+
// --title "…" --url https://…
|
|
11
|
+
// → writes package.json with "getfilepress": "link:<rel-to-engine>"
|
|
12
|
+
//
|
|
13
|
+
// Refuses a non-empty target (edge case 18), unless --force is passed (still
|
|
14
|
+
// refuses to overwrite filepress.config.ts / package.json if present).
|
|
15
|
+
|
|
16
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const repoRoot = resolve(scriptDir, '..');
|
|
22
|
+
|
|
23
|
+
function fail(msg) {
|
|
24
|
+
console.error(`create-site: ${msg}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const args = { _: [], force: false };
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i];
|
|
32
|
+
if (a === '--title' || a === '--url' || a === '--external') args[a.slice(2)] = argv[++i];
|
|
33
|
+
else if (a === '--force') args.force = true;
|
|
34
|
+
else args._.push(a);
|
|
35
|
+
}
|
|
36
|
+
return args;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const args = parseArgs(process.argv.slice(2));
|
|
40
|
+
const name = args._[0];
|
|
41
|
+
|
|
42
|
+
if (!name) {
|
|
43
|
+
fail(
|
|
44
|
+
'missing site name.\n' +
|
|
45
|
+
' Usage: node scripts/create-site.mjs <name> [--title ..] [--url ..] [--external <path>]'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
49
|
+
fail(`invalid site name "${name}". Use lowercase letters, numbers, and hyphens.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const external = Boolean(args.external);
|
|
53
|
+
const target = external ? resolve(repoRoot, args.external) : join(repoRoot, 'sites', name);
|
|
54
|
+
|
|
55
|
+
if (existsSync(target)) {
|
|
56
|
+
const entries = readdirSync(target).filter((e) => e !== '.git' && e !== 'artifacts');
|
|
57
|
+
if (entries.length > 0 && !args.force) {
|
|
58
|
+
fail(`refusing to overwrite non-empty directory: ${target} (pass --force to merge carefully)`);
|
|
59
|
+
}
|
|
60
|
+
if (existsSync(join(target, 'filepress.config.ts'))) {
|
|
61
|
+
fail(`filepress.config.ts already exists in ${target}`);
|
|
62
|
+
}
|
|
63
|
+
if (external && existsSync(join(target, 'package.json'))) {
|
|
64
|
+
fail(`package.json already exists in ${target}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const title = args.title || name;
|
|
69
|
+
const url = args.url || `https://${name}.example.com`;
|
|
70
|
+
|
|
71
|
+
mkdirSync(join(target, 'posts'), { recursive: true });
|
|
72
|
+
mkdirSync(join(target, 'pages'), { recursive: true });
|
|
73
|
+
mkdirSync(join(target, 'static'), { recursive: true });
|
|
74
|
+
|
|
75
|
+
const defaultFavicon = join(repoRoot, 'packages', 'core', 'src', 'lib', 'assets', 'favicon.svg');
|
|
76
|
+
if (existsSync(defaultFavicon)) {
|
|
77
|
+
copyFileSync(defaultFavicon, join(target, 'static', 'favicon.svg'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const configImport = external
|
|
81
|
+
? `import { defineFilepressConfig } from 'getfilepress';`
|
|
82
|
+
: `import { defineFilepressConfig } from 'getfilepress';`;
|
|
83
|
+
|
|
84
|
+
writeFileSync(
|
|
85
|
+
join(target, 'filepress.config.ts'),
|
|
86
|
+
`${configImport}
|
|
87
|
+
|
|
88
|
+
export default defineFilepressConfig({
|
|
89
|
+
title: ${JSON.stringify(title)},
|
|
90
|
+
description: 'A filepress site.',
|
|
91
|
+
url: ${JSON.stringify(url)},
|
|
92
|
+
author: ${JSON.stringify(title)},
|
|
93
|
+
topics: []
|
|
94
|
+
});
|
|
95
|
+
`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
writeFileSync(
|
|
99
|
+
join(target, '.gitignore'),
|
|
100
|
+
`/build
|
|
101
|
+
/node_modules
|
|
102
|
+
.DS_Store
|
|
103
|
+
Thumbs.db
|
|
104
|
+
.filepress/
|
|
105
|
+
.filepress-genie/
|
|
106
|
+
.filepress-import/crawl-cache/
|
|
107
|
+
`
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
111
|
+
writeFileSync(
|
|
112
|
+
join(target, 'posts', 'hello-world.md'),
|
|
113
|
+
`---
|
|
114
|
+
title: "Hello, world"
|
|
115
|
+
date: ${today}
|
|
116
|
+
description: The first post on ${title}.
|
|
117
|
+
tags: [meta]
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
This is your first post. Edit or replace \`posts/hello-world.md\`, then push.
|
|
121
|
+
`
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
writeFileSync(
|
|
125
|
+
join(target, 'pages', 'about.md'),
|
|
126
|
+
`---
|
|
127
|
+
title: About
|
|
128
|
+
description: About ${title}.
|
|
129
|
+
order: 1
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
Tell visitors who you are. This page lives at \`/about\` — edit \`pages/about.md\`.
|
|
133
|
+
`
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (external) {
|
|
137
|
+
const relToEngine = relative(target, repoRoot).replace(/\\/g, '/') || '..';
|
|
138
|
+
writeFileSync(
|
|
139
|
+
join(target, 'package.json'),
|
|
140
|
+
JSON.stringify(
|
|
141
|
+
{
|
|
142
|
+
name,
|
|
143
|
+
private: true,
|
|
144
|
+
version: '0.0.1',
|
|
145
|
+
type: 'module',
|
|
146
|
+
scripts: {
|
|
147
|
+
dev: 'filepress dev',
|
|
148
|
+
build: 'filepress build',
|
|
149
|
+
preview: 'filepress preview',
|
|
150
|
+
check: 'filepress check'
|
|
151
|
+
},
|
|
152
|
+
devDependencies: {
|
|
153
|
+
// link: uses the live engine tree (with its workspace node_modules).
|
|
154
|
+
// Alternatives after publish / push:
|
|
155
|
+
// "getfilepress": "^0.1.0"
|
|
156
|
+
// "getfilepress": "github:Catalyst-Forge-LLC/filepress#v0.1.0"
|
|
157
|
+
getfilepress: `link:${relToEngine}`
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
null,
|
|
161
|
+
'\t'
|
|
162
|
+
) + '\n'
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
writeFileSync(
|
|
166
|
+
join(target, 'tsconfig.json'),
|
|
167
|
+
`{
|
|
168
|
+
"compilerOptions": {
|
|
169
|
+
"module": "esnext",
|
|
170
|
+
"moduleResolution": "bundler",
|
|
171
|
+
"target": "esnext",
|
|
172
|
+
"strict": true,
|
|
173
|
+
"skipLibCheck": true,
|
|
174
|
+
"noEmit": true,
|
|
175
|
+
"allowImportingTsExtensions": true
|
|
176
|
+
},
|
|
177
|
+
"include": ["filepress.config.ts"]
|
|
178
|
+
}
|
|
179
|
+
`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
writeFileSync(
|
|
183
|
+
join(target, 'README.md'),
|
|
184
|
+
`# ${title}
|
|
185
|
+
|
|
186
|
+
Content-only filepress site. Local engine via \`link:${relToEngine}\`.
|
|
187
|
+
|
|
188
|
+
\`\`\`bash
|
|
189
|
+
# once in the engine repo
|
|
190
|
+
cd ${relToEngine} && pnpm install
|
|
191
|
+
|
|
192
|
+
# in this site
|
|
193
|
+
pnpm install
|
|
194
|
+
pnpm dev
|
|
195
|
+
pnpm build # → build/
|
|
196
|
+
\`\`\`
|
|
197
|
+
|
|
198
|
+
Optional: add \`theme.css\` next to \`filepress.config.ts\` to
|
|
199
|
+
override the default Essay theme.
|
|
200
|
+
|
|
201
|
+
## Deploy (Cloudflare Pages)
|
|
202
|
+
|
|
203
|
+
\`link:\` only works on your machine. For CI/hosting, switch to npm or a git pin:
|
|
204
|
+
|
|
205
|
+
\`\`\`json
|
|
206
|
+
"getfilepress": "^0.1.0"
|
|
207
|
+
\`\`\`
|
|
208
|
+
|
|
209
|
+
or:
|
|
210
|
+
|
|
211
|
+
\`\`\`json
|
|
212
|
+
"getfilepress": "github:Catalyst-Forge-LLC/filepress#v0.1.0"
|
|
213
|
+
\`\`\`
|
|
214
|
+
|
|
215
|
+
| Setting | Value |
|
|
216
|
+
| --- | --- |
|
|
217
|
+
| Build command | \`pnpm install && pnpm build\` |
|
|
218
|
+
| Output directory | \`build\` |
|
|
219
|
+
`
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
console.log(`Created external site at ${target}`);
|
|
223
|
+
console.log(`Next:`);
|
|
224
|
+
console.log(` cd ${relToEngine} && pnpm install # if not already`);
|
|
225
|
+
console.log(` cd ${target} && pnpm install && pnpm dev`);
|
|
226
|
+
} else {
|
|
227
|
+
writeFileSync(
|
|
228
|
+
join(target, 'tsconfig.json'),
|
|
229
|
+
`{
|
|
230
|
+
"compilerOptions": {
|
|
231
|
+
"module": "esnext",
|
|
232
|
+
"moduleResolution": "bundler",
|
|
233
|
+
"target": "esnext",
|
|
234
|
+
"strict": true,
|
|
235
|
+
"skipLibCheck": true,
|
|
236
|
+
"noEmit": true,
|
|
237
|
+
"allowImportingTsExtensions": true
|
|
238
|
+
},
|
|
239
|
+
"include": ["filepress.config.ts"]
|
|
240
|
+
}
|
|
241
|
+
`
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
writeFileSync(
|
|
245
|
+
join(target, 'README.md'),
|
|
246
|
+
`# ${title}
|
|
247
|
+
|
|
248
|
+
Content-only filepress site (monorepo). Edit [\`filepress.config.ts\`](filepress.config.ts)
|
|
249
|
+
and [\`posts/\`](posts/). Optional [\`theme.css\`](theme.css) overrides the Essay theme.
|
|
250
|
+
|
|
251
|
+
\`\`\`bash
|
|
252
|
+
pnpm filepress dev --site ${name}
|
|
253
|
+
pnpm filepress build --site ${name} # → build/
|
|
254
|
+
\`\`\`
|
|
255
|
+
`
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
console.log(`Created sites/${name} (content-only)`);
|
|
259
|
+
console.log(`Next: pnpm filepress dev --site ${name}`);
|
|
260
|
+
}
|