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,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PageData } from './$types';
|
|
3
|
+
import { Newsletter, absoluteUrl, formatDate, ogImageUrl } from '@filepress/core';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
let { data }: { data: PageData } = $props();
|
|
7
|
+
|
|
8
|
+
const post = $derived(data.post);
|
|
9
|
+
const pageTitle = $derived(`${post.title} — ${config.title}`);
|
|
10
|
+
const canonical = $derived(absoluteUrl(config, `/posts/${post.slug}`));
|
|
11
|
+
const ogImage = $derived(ogImageUrl(config));
|
|
12
|
+
const showUpdated = $derived(post.updated && post.updated !== post.date);
|
|
13
|
+
const adjacent = $derived(data.adjacent);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<svelte:head>
|
|
17
|
+
<title>{pageTitle}</title>
|
|
18
|
+
{#if post.description}
|
|
19
|
+
<meta name="description" content={post.description} />
|
|
20
|
+
{/if}
|
|
21
|
+
<link rel="canonical" href={canonical} />
|
|
22
|
+
<meta property="og:type" content="article" />
|
|
23
|
+
<meta property="og:title" content={post.title} />
|
|
24
|
+
{#if post.description}
|
|
25
|
+
<meta property="og:description" content={post.description} />
|
|
26
|
+
{/if}
|
|
27
|
+
<meta property="og:url" content={canonical} />
|
|
28
|
+
{#if ogImage}
|
|
29
|
+
<meta property="og:image" content={ogImage} />
|
|
30
|
+
<meta name="twitter:card" content="summary" />
|
|
31
|
+
<meta name="twitter:image" content={ogImage} />
|
|
32
|
+
{/if}
|
|
33
|
+
{#if data.isDraft}
|
|
34
|
+
<meta name="robots" content="noindex" />
|
|
35
|
+
{/if}
|
|
36
|
+
</svelte:head>
|
|
37
|
+
|
|
38
|
+
<article>
|
|
39
|
+
<header class="post-header">
|
|
40
|
+
{#if data.isDraft}
|
|
41
|
+
<p class="draft-banner">
|
|
42
|
+
Draft — shown in local listings; excluded from production index, feeds, and sitemap.
|
|
43
|
+
</p>
|
|
44
|
+
{/if}
|
|
45
|
+
<h1>{post.title}</h1>
|
|
46
|
+
<p class="meta">
|
|
47
|
+
<time datetime={post.date}>{formatDate(post.date)}</time>
|
|
48
|
+
{#if post.author}
|
|
49
|
+
· <span class="byline">{post.author}</span>
|
|
50
|
+
{/if}
|
|
51
|
+
{#if showUpdated}
|
|
52
|
+
· updated <time datetime={post.updated}>{formatDate(post.updated!)}</time>
|
|
53
|
+
{/if}
|
|
54
|
+
</p>
|
|
55
|
+
{#if data.tags.length}
|
|
56
|
+
<ul class="tag-list" style="margin-top:0.75rem">
|
|
57
|
+
{#each data.tags as { tag, hasPage } (tag)}
|
|
58
|
+
<li>
|
|
59
|
+
{#if hasPage}
|
|
60
|
+
<a href="/tags/{tag}">{tag}</a>
|
|
61
|
+
{:else}
|
|
62
|
+
<span class="tag">{tag}</span>
|
|
63
|
+
{/if}
|
|
64
|
+
</li>
|
|
65
|
+
{/each}
|
|
66
|
+
</ul>
|
|
67
|
+
{/if}
|
|
68
|
+
</header>
|
|
69
|
+
|
|
70
|
+
<!-- Body HTML is compiled at build time from the owner's own Markdown (trusted). -->
|
|
71
|
+
<div class="prose">{@html post.html}</div>
|
|
72
|
+
|
|
73
|
+
{#if adjacent.older || adjacent.newer}
|
|
74
|
+
<nav class="post-nav" aria-label="More posts">
|
|
75
|
+
{#if adjacent.older}
|
|
76
|
+
<a class="older" href="/posts/{adjacent.older.slug}">
|
|
77
|
+
<span class="post-nav-label">← Older</span>
|
|
78
|
+
<span class="post-nav-title">{adjacent.older.title}</span>
|
|
79
|
+
</a>
|
|
80
|
+
{:else}
|
|
81
|
+
<span></span>
|
|
82
|
+
{/if}
|
|
83
|
+
{#if adjacent.newer}
|
|
84
|
+
<a class="newer" href="/posts/{adjacent.newer.slug}">
|
|
85
|
+
<span class="post-nav-label">Newer →</span>
|
|
86
|
+
<span class="post-nav-title">{adjacent.newer.title}</span>
|
|
87
|
+
</a>
|
|
88
|
+
{:else}
|
|
89
|
+
<span></span>
|
|
90
|
+
{/if}
|
|
91
|
+
</nav>
|
|
92
|
+
{/if}
|
|
93
|
+
</article>
|
|
94
|
+
|
|
95
|
+
<Newsletter newsletter={config.newsletter} />
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RequestHandler } from './$types';
|
|
2
|
+
import { buildRobotsTxt } from '@filepress/core/server';
|
|
3
|
+
import config from '$site-config';
|
|
4
|
+
|
|
5
|
+
export const prerender = true;
|
|
6
|
+
|
|
7
|
+
export const GET: RequestHandler = () => {
|
|
8
|
+
return new Response(buildRobotsTxt(config), {
|
|
9
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
|
|
10
|
+
});
|
|
11
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RequestHandler } from './$types';
|
|
2
|
+
import { buildRssXml } from '@filepress/core/server';
|
|
3
|
+
import { content } from '$lib/content.server';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
export const prerender = true;
|
|
7
|
+
|
|
8
|
+
export const GET: RequestHandler = () => {
|
|
9
|
+
const xml = buildRssXml(config, content.getPublishedPosts());
|
|
10
|
+
return new Response(xml, {
|
|
11
|
+
headers: { 'Content-Type': 'application/xml; charset=utf-8' }
|
|
12
|
+
});
|
|
13
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RequestHandler } from './$types';
|
|
2
|
+
import { buildSitemapXml } from '@filepress/core/server';
|
|
3
|
+
import { content } from '$lib/content.server';
|
|
4
|
+
import { pages } from '$lib/pages.server';
|
|
5
|
+
import config from '$site-config';
|
|
6
|
+
|
|
7
|
+
export const prerender = true;
|
|
8
|
+
|
|
9
|
+
export const GET: RequestHandler = () => {
|
|
10
|
+
const xml = buildSitemapXml(config, {
|
|
11
|
+
posts: content.getPublishedPosts(),
|
|
12
|
+
tags: content.getAllTags(),
|
|
13
|
+
pageCount: content.getIndexPageCount(config.postsPerPage),
|
|
14
|
+
pages: pages.getPublishedPages()
|
|
15
|
+
});
|
|
16
|
+
return new Response(xml, {
|
|
17
|
+
headers: { 'Content-Type': 'application/xml; charset=utf-8' }
|
|
18
|
+
});
|
|
19
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PageData } from './$types';
|
|
3
|
+
import config from '$site-config';
|
|
4
|
+
|
|
5
|
+
let { data }: { data: PageData } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<svelte:head>
|
|
9
|
+
<title>Tags — {config.title}</title>
|
|
10
|
+
<meta name="description" content="All tags on {config.title}." />
|
|
11
|
+
</svelte:head>
|
|
12
|
+
|
|
13
|
+
<header class="post-header">
|
|
14
|
+
<p class="eyebrow">Index</p>
|
|
15
|
+
<h1>Tags</h1>
|
|
16
|
+
</header>
|
|
17
|
+
|
|
18
|
+
{#if data.tags.length === 0}
|
|
19
|
+
<p class="empty-state">No tags yet.</p>
|
|
20
|
+
{:else}
|
|
21
|
+
<ul class="tag-list">
|
|
22
|
+
{#each data.tags as { tag, count } (tag)}
|
|
23
|
+
<li><a href="/tags/{tag}">{tag} ({count})</a></li>
|
|
24
|
+
{/each}
|
|
25
|
+
</ul>
|
|
26
|
+
{/if}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EntryGenerator, PageServerLoad } from './$types';
|
|
2
|
+
import { error } from '@sveltejs/kit';
|
|
3
|
+
import { content } from '$lib/content.server';
|
|
4
|
+
|
|
5
|
+
export const entries: EntryGenerator = () => {
|
|
6
|
+
return content.getAllTags().map(({ tag }) => ({ tag }));
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const load: PageServerLoad = ({ params }) => {
|
|
10
|
+
const posts = content.getPostsByTag(params.tag);
|
|
11
|
+
if (posts.length === 0) {
|
|
12
|
+
error(404, `No posts tagged "${params.tag}".`);
|
|
13
|
+
}
|
|
14
|
+
return { tag: params.tag, posts };
|
|
15
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PageData } from './$types';
|
|
3
|
+
import { PostCard } from '@filepress/core';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
let { data }: { data: PageData } = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<svelte:head>
|
|
10
|
+
<title>{data.tag} — {config.title}</title>
|
|
11
|
+
<meta name="description" content="Posts tagged {data.tag} on {config.title}." />
|
|
12
|
+
</svelte:head>
|
|
13
|
+
|
|
14
|
+
<header class="post-header">
|
|
15
|
+
<p class="eyebrow">Tag</p>
|
|
16
|
+
<h1>{data.tag}</h1>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
<ul class="post-list">
|
|
20
|
+
{#each data.posts as post (post.slug)}
|
|
21
|
+
<li><PostCard {post} showTags={false} /></li>
|
|
22
|
+
{/each}
|
|
23
|
+
</ul>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PageServerLoad } from './$types';
|
|
2
|
+
import type { Topic } from '@filepress/core';
|
|
3
|
+
import { content } from '$lib/content.server';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
const PREVIEW = 6;
|
|
7
|
+
|
|
8
|
+
export const load: PageServerLoad = () => {
|
|
9
|
+
// Curated topics if the site defines them (Stratechery-style Explore);
|
|
10
|
+
// otherwise fall back to every tag, alphabetically.
|
|
11
|
+
const source: Topic[] = config.topics.length
|
|
12
|
+
? config.topics.map((t: Topic) => ({ label: t.label, tag: t.tag }))
|
|
13
|
+
: content.getAllTags().map((t) => ({ label: t.tag, tag: t.tag }));
|
|
14
|
+
|
|
15
|
+
const groups = source
|
|
16
|
+
.map((topic: Topic) => {
|
|
17
|
+
const posts = content.getPostsByTag(topic.tag);
|
|
18
|
+
return {
|
|
19
|
+
label: topic.label,
|
|
20
|
+
tag: topic.tag,
|
|
21
|
+
count: posts.length,
|
|
22
|
+
posts: posts.slice(0, PREVIEW)
|
|
23
|
+
};
|
|
24
|
+
})
|
|
25
|
+
.filter((g: { count: number }) => g.count > 0);
|
|
26
|
+
|
|
27
|
+
return { groups, curated: config.topics.length > 0 };
|
|
28
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PageData } from './$types';
|
|
3
|
+
import { formatDate } from '@filepress/core';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
let { data }: { data: PageData } = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<svelte:head>
|
|
10
|
+
<title>Topics — {config.title}</title>
|
|
11
|
+
<meta name="description" content="Browse {config.title} by topic." />
|
|
12
|
+
</svelte:head>
|
|
13
|
+
|
|
14
|
+
<header class="post-header">
|
|
15
|
+
<p class="eyebrow">Explore</p>
|
|
16
|
+
<h1>Topics</h1>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
{#if data.groups.length === 0}
|
|
20
|
+
<p class="empty-state">No topics yet.</p>
|
|
21
|
+
{:else}
|
|
22
|
+
{#each data.groups as group (group.tag)}
|
|
23
|
+
<section class="topic-group">
|
|
24
|
+
<h2>
|
|
25
|
+
<a href="/tags/{group.tag}">{group.label}</a>
|
|
26
|
+
<span class="count">{group.count} post{group.count === 1 ? '' : 's'}</span>
|
|
27
|
+
</h2>
|
|
28
|
+
<ul class="topic-posts">
|
|
29
|
+
{#each group.posts as post (post.slug)}
|
|
30
|
+
<li>
|
|
31
|
+
<span class="date">{formatDate(post.date)}</span>
|
|
32
|
+
<a href="/posts/{post.slug}">{post.title}</a>
|
|
33
|
+
</li>
|
|
34
|
+
{/each}
|
|
35
|
+
</ul>
|
|
36
|
+
{#if group.count > group.posts.length}
|
|
37
|
+
<p class="meta" style="margin-top:0.6rem">
|
|
38
|
+
<a href="/tags/{group.tag}">All {group.count} →</a>
|
|
39
|
+
</p>
|
|
40
|
+
{/if}
|
|
41
|
+
</section>
|
|
42
|
+
{/each}
|
|
43
|
+
|
|
44
|
+
<p class="meta" style="margin-top:2rem">
|
|
45
|
+
<a href="/tags">See every tag →</a>
|
|
46
|
+
</p>
|
|
47
|
+
{/if}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { redirect } from '@sveltejs/kit';
|
|
2
|
+
import type { PageServerLoad } from './$types';
|
|
3
|
+
import { content } from '$lib/content.server';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
export const load: PageServerLoad = () => {
|
|
7
|
+
// Without homePage, the post index stays at `/` — keep /writing as an alias.
|
|
8
|
+
if (!config.homePage) {
|
|
9
|
+
redirect(302, '/');
|
|
10
|
+
}
|
|
11
|
+
return content.getIndexPage(1, config.postsPerPage);
|
|
12
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PageData } from './$types';
|
|
3
|
+
import { PostIndex, absoluteUrl, ogImageUrl, postsIndexPath } from '@filepress/core';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
let { data }: { data: PageData } = $props();
|
|
7
|
+
|
|
8
|
+
const pageTitle = $derived(`Posts — ${config.title}`);
|
|
9
|
+
const canonical = $derived(absoluteUrl(config, postsIndexPath(config)));
|
|
10
|
+
const ogImage = $derived(ogImageUrl(config));
|
|
11
|
+
const description = $derived(
|
|
12
|
+
`Posts from ${config.title}. ${config.description}`.trim()
|
|
13
|
+
);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<svelte:head>
|
|
17
|
+
<title>{pageTitle}</title>
|
|
18
|
+
<meta name="description" content={description} />
|
|
19
|
+
<link rel="canonical" href={canonical} />
|
|
20
|
+
<meta property="og:type" content="website" />
|
|
21
|
+
<meta property="og:title" content="Posts — {config.title}" />
|
|
22
|
+
<meta property="og:description" content={description} />
|
|
23
|
+
<meta property="og:url" content={canonical} />
|
|
24
|
+
{#if ogImage}
|
|
25
|
+
<meta property="og:image" content={ogImage} />
|
|
26
|
+
<meta name="twitter:card" content="summary" />
|
|
27
|
+
<meta name="twitter:image" content={ogImage} />
|
|
28
|
+
{/if}
|
|
29
|
+
</svelte:head>
|
|
30
|
+
|
|
31
|
+
<PostIndex
|
|
32
|
+
site={config}
|
|
33
|
+
featured={data.featured}
|
|
34
|
+
posts={data.posts}
|
|
35
|
+
page={data.page}
|
|
36
|
+
totalPages={data.totalPages}
|
|
37
|
+
indexHref={postsIndexPath(config)}
|
|
38
|
+
/>
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rewriteRelativeImportExtensions": true,
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"checkJs": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"moduleResolution": "bundler"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import type { Plugin } from 'vite';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extract early paint tokens from the site theme: `:root` blocks (including
|
|
6
|
+
* those nested in `prefers-color-scheme` media) and top-level `body { ... }`.
|
|
7
|
+
*/
|
|
8
|
+
export function extractCriticalTheme(css: string): string {
|
|
9
|
+
const chunks: string[] = [];
|
|
10
|
+
|
|
11
|
+
// Prefer `:root:root` (higher specificity site tokens) when present.
|
|
12
|
+
for (const m of css.matchAll(/:root(?::root)?\s*\{[^}]*\}/g)) {
|
|
13
|
+
chunks.push(m[0]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const m of css.matchAll(
|
|
17
|
+
/@media\s*\([^)]*prefers-color-scheme[^)]*\)\s*\{\s*:root(?::root)?\s*\{[^}]*\}\s*\}/g
|
|
18
|
+
)) {
|
|
19
|
+
chunks.push(m[0]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const m of css.matchAll(/(?:^|\n)(?:html\s+)?body\s*\{[^}]*\}/g)) {
|
|
23
|
+
chunks.push(m[0].trim());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return chunks.join('\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeCriticalThemeModule(siteThemePath: string, outPath: string): string {
|
|
30
|
+
let css = '';
|
|
31
|
+
if (existsSync(siteThemePath)) {
|
|
32
|
+
try {
|
|
33
|
+
css = readFileSync(siteThemePath, 'utf8');
|
|
34
|
+
} catch {
|
|
35
|
+
css = '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const critical = extractCriticalTheme(css);
|
|
39
|
+
const source = `/** Auto-generated by filepress — do not edit. */\nexport default ${JSON.stringify(critical)};\n`;
|
|
40
|
+
writeFileSync(outPath, source, 'utf8');
|
|
41
|
+
return critical;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Keep `$lib/critical-theme.generated.ts` in sync with the site theme so
|
|
46
|
+
* layout can inline tokens without a Vite virtual module (SSR-safe).
|
|
47
|
+
*/
|
|
48
|
+
export function criticalThemePlugin(siteThemePath: string, outPath: string): Plugin {
|
|
49
|
+
return {
|
|
50
|
+
name: 'filepress-critical-theme',
|
|
51
|
+
buildStart() {
|
|
52
|
+
writeCriticalThemeModule(siteThemePath, outPath);
|
|
53
|
+
},
|
|
54
|
+
configureServer(server) {
|
|
55
|
+
writeCriticalThemeModule(siteThemePath, outPath);
|
|
56
|
+
server.watcher.add(siteThemePath);
|
|
57
|
+
server.watcher.on('change', (file) => {
|
|
58
|
+
const a = file.replace(/\\/g, '/');
|
|
59
|
+
const b = siteThemePath.replace(/\\/g, '/');
|
|
60
|
+
if (a !== b) return;
|
|
61
|
+
writeCriticalThemeModule(siteThemePath, outPath);
|
|
62
|
+
const mod = server.moduleGraph.getModuleById(outPath);
|
|
63
|
+
if (mod) {
|
|
64
|
+
server.moduleGraph.invalidateModule(mod);
|
|
65
|
+
server.ws.send({ type: 'full-reload' });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { Plugin } from 'vite';
|
|
3
|
+
import {
|
|
4
|
+
applySteer,
|
|
5
|
+
doActivate,
|
|
6
|
+
fetchStockCover,
|
|
7
|
+
health,
|
|
8
|
+
receiveUpload
|
|
9
|
+
} from './src/lib/genie/ops.ts';
|
|
10
|
+
import { deleteVersion, listVersions } from './src/lib/genie/store.ts';
|
|
11
|
+
|
|
12
|
+
function readBody(req: IncomingMessage): Promise<string> {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const chunks: Buffer[] = [];
|
|
15
|
+
req.on('data', (c) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
|
|
16
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
17
|
+
req.on('error', reject);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sendJson(res: ServerResponse, status: number, data: unknown) {
|
|
22
|
+
const body = JSON.stringify(data);
|
|
23
|
+
res.statusCode = status;
|
|
24
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
25
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
26
|
+
res.end(body);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Dev-only Genie Mode API. `apply: 'serve'` keeps this out of `vite build`.
|
|
31
|
+
*/
|
|
32
|
+
export function geniePlugin(siteRoot: string): Plugin {
|
|
33
|
+
return {
|
|
34
|
+
name: 'filepress-genie',
|
|
35
|
+
apply: 'serve',
|
|
36
|
+
configureServer(server) {
|
|
37
|
+
server.middlewares.use(async (req, res, next) => {
|
|
38
|
+
const url = req.url || '';
|
|
39
|
+
if (!url.startsWith('/__filepress/genie')) return next();
|
|
40
|
+
|
|
41
|
+
const path = url.split('?')[0];
|
|
42
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
if (method === 'GET' && path === '/__filepress/genie/health') {
|
|
46
|
+
return sendJson(res, 200, await health(siteRoot));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (method === 'GET' && path === '/__filepress/genie/versions') {
|
|
50
|
+
const h = await health(siteRoot);
|
|
51
|
+
return sendJson(res, 200, {
|
|
52
|
+
active: h.active,
|
|
53
|
+
versions: listVersions(siteRoot),
|
|
54
|
+
brief: h.brief
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (method === 'POST' && path === '/__filepress/genie/steer') {
|
|
59
|
+
const body = JSON.parse(await readBody(req));
|
|
60
|
+
return sendJson(res, 200, applySteer(siteRoot, body));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (method === 'POST' && path === '/__filepress/genie/stock') {
|
|
64
|
+
const body = JSON.parse(await readBody(req));
|
|
65
|
+
if (!body.query || typeof body.query !== 'string') {
|
|
66
|
+
return sendJson(res, 400, { error: '`query` string required' });
|
|
67
|
+
}
|
|
68
|
+
return sendJson(res, 200, await fetchStockCover(siteRoot, body));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (method === 'POST' && path === '/__filepress/genie/upload') {
|
|
72
|
+
const body = JSON.parse(await readBody(req));
|
|
73
|
+
if (!body.dataBase64 || !body.role) {
|
|
74
|
+
return sendJson(res, 400, {
|
|
75
|
+
error: '`role` and `dataBase64` required'
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return sendJson(
|
|
79
|
+
res,
|
|
80
|
+
200,
|
|
81
|
+
receiveUpload(siteRoot, {
|
|
82
|
+
role: body.role,
|
|
83
|
+
filename: body.filename || `${body.role}.jpg`,
|
|
84
|
+
dataBase64: body.dataBase64,
|
|
85
|
+
activate: body.activate
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (method === 'POST' && path === '/__filepress/genie/activate') {
|
|
91
|
+
const body = JSON.parse(await readBody(req));
|
|
92
|
+
if (!body.versionId) {
|
|
93
|
+
return sendJson(res, 400, { error: '`versionId` required' });
|
|
94
|
+
}
|
|
95
|
+
return sendJson(res, 200, {
|
|
96
|
+
active: doActivate(siteRoot, body.versionId)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (method === 'POST' && path === '/__filepress/genie/delete') {
|
|
101
|
+
const body = JSON.parse(await readBody(req));
|
|
102
|
+
deleteVersion(siteRoot, body.versionId);
|
|
103
|
+
return sendJson(res, 200, { ok: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return sendJson(res, 404, { error: `Unknown Genie route: ${method} ${path}` });
|
|
107
|
+
} catch (e) {
|
|
108
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
109
|
+
return sendJson(res, 500, { error: message });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|