@jant/core 0.7.0 → 0.7.1
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/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +689 -216
- package/bin/commands/setup.js +136 -0
- package/bin/commands/site/export.js +71 -34
- package/bin/commands/site/pull-media.js +2 -6
- package/bin/commands/site/snapshot/export.js +18 -60
- package/bin/commands/site/snapshot/import.js +22 -23
- package/bin/lib/d1-query.js +87 -2
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +10 -1
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +9 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-K_Aa1MMn.js → app-ZcaI1kPN.js} +732 -217
- package/dist/client/.vite/manifest.json +20 -20
- package/dist/client/_assets/chunks/{create-editor-CD3FhrOB.js → create-editor-B-m7X7S5.js} +50 -46
- package/dist/client/_assets/chunks/{sortable-list-CgaL2jCs.js → sortable-list-BJyd-LXE.js} +1 -1
- package/dist/client/_assets/chunks/{unsafe-svg-0QCkP0vZ.js → unsafe-svg-BkscJx69.js} +2 -2
- package/dist/client/_assets/{client-GiYENVw8.css → client-DAhoqPdr.css} +1 -1
- package/dist/client/_assets/{client-auth-k8gJ6QJj.js → client-auth-CAMfTrKW.js} +1 -1
- package/dist/client/_assets/{client-compose-B_kDtWkW.js → client-compose-D6rExKVK.js} +1 -1
- package/dist/client/_assets/{client-VnFxJN7G.js → client-knSEyUJO.js} +1 -1
- package/dist/client/_assets/{client-manage-M90aSTOk.js → client-manage-sVOkqVbH.js} +1 -1
- package/dist/client/_assets/{client-settings-DfYs9n1F.js → client-settings-CGR_nZ78.js} +7 -7
- package/dist/{github-sync-BPAvT999.js → github-sync-Gw4orAHk.js} +799 -110
- package/dist/index.js +2 -2
- package/dist/node.js +9 -5
- package/package.json +13 -3
- package/src/__tests__/dev-scripts.test.ts +203 -0
- package/src/__tests__/export-collection-order.test.ts +276 -0
- package/src/__tests__/export-feed-ids.test.ts +184 -0
- package/src/__tests__/export-feed-order.test.ts +294 -0
- package/src/__tests__/export-hugo-build.test.ts +130 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +611 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/hugo-site.ts +146 -0
- package/src/__tests__/import-site-command.test.ts +487 -1
- package/src/__tests__/mise-config.test.ts +75 -4
- package/src/__tests__/node-dev-tasks.test.ts +264 -0
- package/src/__tests__/site-export-canonical-import.test.ts +149 -0
- package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
- package/src/__tests__/snapshot-settings.test.ts +97 -0
- package/src/__tests__/snapshot-tables.test.ts +219 -0
- package/src/__tests__/sql-export.test.ts +173 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +15 -6
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +1 -1
- package/src/client/components/__tests__/jant-settings-general.test.ts +21 -1
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +4 -7
- package/src/client/components/jant-settings-general.ts +17 -12
- package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
- package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/i18n/locales/settings/en.po +8 -8
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +8 -8
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +8 -8
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/resolve-config.test.ts +67 -0
- package/src/lib/__tests__/schemas.test.ts +27 -1
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/discover.ts +3 -1
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/hugo-markdown.ts +46 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/resolve-config.ts +58 -1
- package/src/lib/schemas.ts +50 -5
- package/src/lib/thread-fold.ts +3 -3
- package/src/lib/timeline.ts +1 -1
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/url.ts +20 -0
- package/src/lib/view.ts +2 -2
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
- package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
- package/src/node/__tests__/runtime.test.ts +38 -0
- package/src/node/index.ts +2 -0
- package/src/node/request-handler.ts +3 -1
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/export.ts +3 -3
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/posts.ts +2 -0
- package/src/routes/api/public/posts.ts +21 -1
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/compose.tsx +4 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/settings.tsx +212 -70
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +17 -11
- package/src/routes/pages/featured.tsx +11 -5
- package/src/routes/pages/page.tsx +53 -31
- package/src/routes/pages/search.tsx +4 -2
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/node.ts +49 -0
- package/src/runtime/readiness.ts +15 -0
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/custom-url.test.ts +34 -0
- package/src/services/__tests__/github-app-installations.test.ts +153 -0
- package/src/services/__tests__/github-sync-push.test.ts +46 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/path.test.ts +56 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +74 -0
- package/src/services/bootstrap.ts +263 -44
- package/src/services/custom-url.ts +2 -2
- package/src/services/export-theme/layouts/_default/alias.html +27 -1
- package/src/services/export-theme/layouts/_default/list.html +2 -63
- package/src/services/export-theme/layouts/_default/rss.xml +27 -38
- package/src/services/export-theme/layouts/collections/list.html +3 -3
- package/src/services/export-theme/layouts/featured/list.html +1 -4
- package/src/services/export-theme/layouts/index.html +40 -26
- package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
- package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
- package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
- package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/head.html +1 -1
- package/src/services/export-theme/layouts/partials/header.html +5 -3
- package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
- package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +1 -1
- package/src/services/export-theme/layouts/post/list.html +1 -1
- package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
- package/src/services/export-theme/styles/main.css +0 -1
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +620 -71
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/mcp.ts +19 -1
- package/src/services/media.ts +8 -1
- package/src/services/path.ts +34 -1
- package/src/services/post.ts +214 -39
- package/src/services/site-admin.ts +3 -7
- package/src/services/site.ts +83 -31
- package/src/styles/ui.css +7 -1
- package/src/types/app-context.ts +16 -0
- package/src/types/bindings.ts +5 -0
- package/src/types/operations.ts +10 -0
- package/src/ui/dash/settings/GeneralContent.tsx +10 -8
- package/src/client/tiptap/exitable-marks.ts +0 -73
|
@@ -8,11 +8,24 @@
|
|
|
8
8
|
* Jant theme, and the `data/*.toml` files consumed by templates.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { describe, expect, it, vi } from "vitest";
|
|
12
12
|
import { unzipSync } from "fflate";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
createExportService,
|
|
15
|
+
deriveWorkerName,
|
|
16
|
+
isStoredExportFile,
|
|
17
|
+
WRANGLER_CONFIG_PATH,
|
|
18
|
+
type ExportFile,
|
|
19
|
+
} from "../services/export.js";
|
|
20
|
+
import { suggestSyncRepoName } from "../lib/github-sync-repo-name.js";
|
|
14
21
|
import { parseFrontMatter } from "../lib/hugo-markdown.js";
|
|
15
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
Collection,
|
|
24
|
+
Media,
|
|
25
|
+
PathRecord,
|
|
26
|
+
Post,
|
|
27
|
+
SmartCollectionDirectoryEntry,
|
|
28
|
+
} from "../types.js";
|
|
16
29
|
import {
|
|
17
30
|
makeCollection,
|
|
18
31
|
makeMedia,
|
|
@@ -40,6 +53,8 @@ interface FixtureOptions {
|
|
|
40
53
|
aliasMap?: Map<string, string[]>;
|
|
41
54
|
collectionSlugMap?: Map<string, string>;
|
|
42
55
|
directoryItems?: unknown[];
|
|
56
|
+
smartCollections?: SmartCollectionDirectoryEntry[];
|
|
57
|
+
standalonePaths?: PathRecord[];
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
function buildServices(opts: FixtureOptions): ServicesArg {
|
|
@@ -53,6 +68,8 @@ function buildServices(opts: FixtureOptions): ServicesArg {
|
|
|
53
68
|
aliasMap = new Map(),
|
|
54
69
|
collectionSlugMap = new Map(collections.map((c) => [c.id, c.slug])),
|
|
55
70
|
directoryItems,
|
|
71
|
+
smartCollections = [],
|
|
72
|
+
standalonePaths = [],
|
|
56
73
|
} = opts;
|
|
57
74
|
|
|
58
75
|
return {
|
|
@@ -61,6 +78,7 @@ function buildServices(opts: FixtureOptions): ServicesArg {
|
|
|
61
78
|
},
|
|
62
79
|
paths: {
|
|
63
80
|
getPostSlugMap: async () => slugMap,
|
|
81
|
+
listStandalonePaths: async () => standalonePaths,
|
|
64
82
|
getPostAliases: async () => aliasMap,
|
|
65
83
|
getCollectionSlugMap: async () => collectionSlugMap,
|
|
66
84
|
},
|
|
@@ -68,6 +86,7 @@ function buildServices(opts: FixtureOptions): ServicesArg {
|
|
|
68
86
|
list: async () => collections,
|
|
69
87
|
listDirectoryData: async () => ({
|
|
70
88
|
collections: [],
|
|
89
|
+
smartCollections,
|
|
71
90
|
items:
|
|
72
91
|
directoryItems ??
|
|
73
92
|
collections.map((collection) => ({
|
|
@@ -90,14 +109,21 @@ function buildServices(opts: FixtureOptions): ServicesArg {
|
|
|
90
109
|
} as unknown as ServicesArg;
|
|
91
110
|
}
|
|
92
111
|
|
|
93
|
-
function filesToMap(
|
|
94
|
-
list: { path: string; content: string | Uint8Array }[],
|
|
95
|
-
): Map<string, string | Uint8Array> {
|
|
112
|
+
function filesToMap(list: ExportFile[]): Map<string, string | Uint8Array> {
|
|
96
113
|
const map = new Map<string, string | Uint8Array>();
|
|
97
|
-
for (const f of list)
|
|
114
|
+
for (const f of list) {
|
|
115
|
+
if (!isStoredExportFile(f)) map.set(f.path, f.content);
|
|
116
|
+
}
|
|
98
117
|
return map;
|
|
99
118
|
}
|
|
100
119
|
|
|
120
|
+
/** Read the streamed archive the way a download receives it. */
|
|
121
|
+
async function readArchive(
|
|
122
|
+
stream: ReadableStream<Uint8Array>,
|
|
123
|
+
): Promise<Record<string, Uint8Array>> {
|
|
124
|
+
return unzipSync(new Uint8Array(await new Response(stream).arrayBuffer()));
|
|
125
|
+
}
|
|
126
|
+
|
|
101
127
|
describe("createExportService (Hugo)", () => {
|
|
102
128
|
// Hugo has its own summary rule and would cut somewhere else, so whether the
|
|
103
129
|
// timeline truncates a post is decided here and carried as front matter.
|
|
@@ -137,6 +163,7 @@ describe("createExportService (Hugo)", () => {
|
|
|
137
163
|
["post-long", "long-read"],
|
|
138
164
|
["post-short", "short-note"],
|
|
139
165
|
]),
|
|
166
|
+
listStandalonePaths: async () => [],
|
|
140
167
|
getPostAliases: async () => new Map(),
|
|
141
168
|
getCollectionSlugMap: async () => new Map(),
|
|
142
169
|
},
|
|
@@ -324,6 +351,105 @@ describe("createExportService (Hugo)", () => {
|
|
|
324
351
|
);
|
|
325
352
|
});
|
|
326
353
|
|
|
354
|
+
it("emits no aliases for a root that is not published", async () => {
|
|
355
|
+
// Hugo writes an alias page for every `aliases:` entry whether or not the
|
|
356
|
+
// page it points at was built. On an unbuilt target `.Permalink` is
|
|
357
|
+
// empty, `<meta http-equiv=refresh content="0; url=">` means "reload this
|
|
358
|
+
// page" to a browser, and the old URL becomes an infinite refresh loop.
|
|
359
|
+
const draftRoot = makePost({
|
|
360
|
+
id: "d",
|
|
361
|
+
slug: "secret-plan",
|
|
362
|
+
threadId: "d",
|
|
363
|
+
status: "draft",
|
|
364
|
+
});
|
|
365
|
+
const reply = makePost({
|
|
366
|
+
id: "d-rep",
|
|
367
|
+
slug: "secret-reply",
|
|
368
|
+
replyToId: "d",
|
|
369
|
+
threadId: "d",
|
|
370
|
+
createdAt: 1773018000,
|
|
371
|
+
publishedAt: 1773018000,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const service = createExportService(
|
|
375
|
+
buildServices({
|
|
376
|
+
posts: [draftRoot, reply],
|
|
377
|
+
aliasMap: new Map([["d", ["/blog/old-plan/"]]]),
|
|
378
|
+
}),
|
|
379
|
+
makeSiteConfig(),
|
|
380
|
+
);
|
|
381
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
382
|
+
const { frontMatter } = await parseFrontMatter(
|
|
383
|
+
files.get("content/secret-plan/_index.md") as string,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
expect(frontMatter.draft).toBe(true);
|
|
387
|
+
expect(frontMatter.aliases).toBeUndefined();
|
|
388
|
+
// The historical slugs still round-trip through import; it is only the
|
|
389
|
+
// Hugo-facing redirect list that an unpublished thread does not get.
|
|
390
|
+
expect(frontMatter.root_aliases).toEqual(["/blog/old-plan/"]);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("emits no aliases for a private root either", async () => {
|
|
394
|
+
const privateRoot = makePost({
|
|
395
|
+
id: "p",
|
|
396
|
+
slug: "for-me",
|
|
397
|
+
threadId: "p",
|
|
398
|
+
visibility: "private",
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const service = createExportService(
|
|
402
|
+
buildServices({
|
|
403
|
+
posts: [privateRoot],
|
|
404
|
+
aliasMap: new Map([["p", ["/blog/for-me/"]]]),
|
|
405
|
+
}),
|
|
406
|
+
makeSiteConfig(),
|
|
407
|
+
);
|
|
408
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
409
|
+
const { frontMatter } = await parseFrontMatter(
|
|
410
|
+
files.get("content/for-me/_index.md") as string,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
expect(frontMatter.draft).toBe(true);
|
|
414
|
+
expect(frontMatter.aliases).toBeUndefined();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("writes the entry ID Jant's feeds give each published root", async () => {
|
|
418
|
+
// `<id>` is the permalink Jant served: the oldest custom path in place of
|
|
419
|
+
// the slug, under the site path prefix, no trailing slash. The theme's
|
|
420
|
+
// feed writes it as it is, so subscribers don't see the post again.
|
|
421
|
+
const plain = makePost({ id: "a", slug: "xta29", threadId: "a" });
|
|
422
|
+
const custom = makePost({ id: "b", slug: "links-4", threadId: "b" });
|
|
423
|
+
const draft = makePost({
|
|
424
|
+
id: "c",
|
|
425
|
+
slug: "unfinished",
|
|
426
|
+
threadId: "c",
|
|
427
|
+
status: "draft",
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const service = createExportService(
|
|
431
|
+
buildServices({
|
|
432
|
+
posts: [plain, custom, draft],
|
|
433
|
+
aliasMap: new Map([["b", ["/blog/links/4", "/links/four"]]]),
|
|
434
|
+
}),
|
|
435
|
+
makeSiteConfig({
|
|
436
|
+
siteUrl: "https://example.com/journal",
|
|
437
|
+
sitePathPrefix: "/journal",
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
441
|
+
const feedId = async (slug: string) =>
|
|
442
|
+
(await parseFrontMatter(files.get(`content/${slug}/_index.md`) as string))
|
|
443
|
+
.frontMatter.feed_id;
|
|
444
|
+
|
|
445
|
+
expect(await feedId("xta29")).toBe("https://example.com/journal/xta29");
|
|
446
|
+
expect(await feedId("links-4")).toBe(
|
|
447
|
+
"https://example.com/journal/blog/links/4",
|
|
448
|
+
);
|
|
449
|
+
// Not in any feed, so there is no ID to keep.
|
|
450
|
+
expect(await feedId("unfinished")).toBeUndefined();
|
|
451
|
+
});
|
|
452
|
+
|
|
327
453
|
it("merges historical root aliases + reply slugs onto the root", async () => {
|
|
328
454
|
const root = makePost({ id: "r", slug: "new-slug", threadId: "r" });
|
|
329
455
|
const reply = makePost({
|
|
@@ -414,6 +540,23 @@ describe("createExportService (Hugo)", () => {
|
|
|
414
540
|
expect(toml).not.toContain("home_default_view");
|
|
415
541
|
});
|
|
416
542
|
|
|
543
|
+
it("omits the site-level paginate key the theme never reads", async () => {
|
|
544
|
+
const service = createExportService(
|
|
545
|
+
buildServices({ posts: [] }),
|
|
546
|
+
makeSiteConfig({ pageSize: 25 }),
|
|
547
|
+
);
|
|
548
|
+
const toml = filesToMap(await service.generateHugoFiles()).get(
|
|
549
|
+
"hugo.toml",
|
|
550
|
+
) as string;
|
|
551
|
+
|
|
552
|
+
// Every paginated template calls `.Paginate` with `params.page_size`, so
|
|
553
|
+
// a site-level pager size would never be read — and `paginate` was
|
|
554
|
+
// removed from Hugo, which fails the build outright rather than warning.
|
|
555
|
+
expect(toml).not.toMatch(/^paginate\b/m);
|
|
556
|
+
expect(toml).not.toContain("pagerSize");
|
|
557
|
+
expect(toml).toContain("page_size = 25");
|
|
558
|
+
});
|
|
559
|
+
|
|
417
560
|
it("configures hugo.toml for Atom RSS output with per-section opt-in", async () => {
|
|
418
561
|
// The root feed lives at /index.xml, featured/archive/collections feeds
|
|
419
562
|
// are opted in via per-section `outputs = ["html", "rss"]` front matter.
|
|
@@ -524,6 +667,254 @@ describe("createExportService (Hugo)", () => {
|
|
|
524
667
|
}
|
|
525
668
|
});
|
|
526
669
|
|
|
670
|
+
// An import back into Jant restores navigation from this file, so it names
|
|
671
|
+
// what the theme doesn't need: the author's own label, the target by slug,
|
|
672
|
+
// and the settings entry the static site skips.
|
|
673
|
+
it("writes what an import needs to rebuild navigation", async () => {
|
|
674
|
+
const collection = makeCollection({
|
|
675
|
+
id: "col-now",
|
|
676
|
+
slug: "now",
|
|
677
|
+
title: "Now",
|
|
678
|
+
});
|
|
679
|
+
const about = makePost({ id: "pst-about", slug: "about", title: "About" });
|
|
680
|
+
const service = createExportService(
|
|
681
|
+
buildServices({ posts: [about], collections: [collection] }),
|
|
682
|
+
makeSiteConfig({
|
|
683
|
+
navItems: [
|
|
684
|
+
{
|
|
685
|
+
type: "system",
|
|
686
|
+
systemKey: "archive",
|
|
687
|
+
label: "All",
|
|
688
|
+
url: "/archive",
|
|
689
|
+
position: 0,
|
|
690
|
+
placement: "header",
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
type: "collection",
|
|
694
|
+
collectionId: "col-now",
|
|
695
|
+
label: "",
|
|
696
|
+
targetTitle: "Now",
|
|
697
|
+
url: "/now",
|
|
698
|
+
position: 1,
|
|
699
|
+
placement: "header",
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
type: "page",
|
|
703
|
+
postId: "pst-about",
|
|
704
|
+
label: "",
|
|
705
|
+
targetTitle: "About",
|
|
706
|
+
url: "/about",
|
|
707
|
+
position: 2,
|
|
708
|
+
placement: "more",
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
type: "system",
|
|
712
|
+
systemKey: "settings",
|
|
713
|
+
label: "",
|
|
714
|
+
url: "/settings",
|
|
715
|
+
position: 3,
|
|
716
|
+
placement: "more",
|
|
717
|
+
},
|
|
718
|
+
],
|
|
719
|
+
}),
|
|
720
|
+
);
|
|
721
|
+
const data = (await service.generateHugoFiles()).find(
|
|
722
|
+
(file) => file.path === "data/jant.toml",
|
|
723
|
+
)?.content as string;
|
|
724
|
+
|
|
725
|
+
expect(data).toContain(
|
|
726
|
+
'system_key = "archive"\nplacement = "header"\ncustom_label = "All"',
|
|
727
|
+
);
|
|
728
|
+
expect(data).toContain('collection_slug = "now"');
|
|
729
|
+
expect(data).toContain('placement = "more"\npost_slug = "about"');
|
|
730
|
+
expect(data).toContain('system_key = "settings"');
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// The conditions travel, not the matches of the day: an import recreates the
|
|
734
|
+
// smart collection, and the theme evaluates the conditions against the posts
|
|
735
|
+
// in the repository. The collection a condition names goes by slug, which is
|
|
736
|
+
// what another site can resolve.
|
|
737
|
+
it("exports each smart collection's conditions as a section page", async () => {
|
|
738
|
+
const ideas = makeCollection({ id: "col-ideas", slug: "ideas" });
|
|
739
|
+
const smartCollection = (
|
|
740
|
+
over: Partial<SmartCollectionDirectoryEntry>,
|
|
741
|
+
): SmartCollectionDirectoryEntry => ({
|
|
742
|
+
id: "smc-thoughts",
|
|
743
|
+
siteId: "site",
|
|
744
|
+
slug: "thoughts",
|
|
745
|
+
title: "Thoughts",
|
|
746
|
+
description: "Short notes.",
|
|
747
|
+
selection: {},
|
|
748
|
+
sort: "newest",
|
|
749
|
+
layout: null,
|
|
750
|
+
createdAt: 1,
|
|
751
|
+
updatedAt: 1,
|
|
752
|
+
threadCount: 4,
|
|
753
|
+
recentActivityAt: 1773020000,
|
|
754
|
+
...over,
|
|
755
|
+
});
|
|
756
|
+
const thoughts = smartCollection({
|
|
757
|
+
selection: { format: "note", title: false },
|
|
758
|
+
});
|
|
759
|
+
const pictures = smartCollection({
|
|
760
|
+
id: "smc-pictures",
|
|
761
|
+
slug: "pictures",
|
|
762
|
+
title: "Pictures",
|
|
763
|
+
description: null,
|
|
764
|
+
selection: {
|
|
765
|
+
collection: ["col-ideas"],
|
|
766
|
+
media: ["image", "video"],
|
|
767
|
+
year: 2025,
|
|
768
|
+
replies: true,
|
|
769
|
+
visibility: "featured",
|
|
770
|
+
},
|
|
771
|
+
sort: "rating_desc",
|
|
772
|
+
layout: "grid",
|
|
773
|
+
});
|
|
774
|
+
const orphaned = smartCollection({
|
|
775
|
+
id: "smc-orphaned",
|
|
776
|
+
slug: "orphaned",
|
|
777
|
+
selection: { collection: ["col-gone"] },
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
const files = filesToMap(
|
|
781
|
+
await createExportService(
|
|
782
|
+
buildServices({
|
|
783
|
+
posts: [],
|
|
784
|
+
collections: [ideas],
|
|
785
|
+
smartCollections: [thoughts, pictures, orphaned],
|
|
786
|
+
directoryItems: [
|
|
787
|
+
{
|
|
788
|
+
id: "dir-1",
|
|
789
|
+
type: "smart_collection",
|
|
790
|
+
smartCollection: thoughts,
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
id: "dir-2",
|
|
794
|
+
type: "smart_collection",
|
|
795
|
+
smartCollection: orphaned,
|
|
796
|
+
},
|
|
797
|
+
],
|
|
798
|
+
}),
|
|
799
|
+
makeSiteConfig({
|
|
800
|
+
navItems: [
|
|
801
|
+
{
|
|
802
|
+
type: "smart_collection",
|
|
803
|
+
smartCollectionId: "smc-pictures",
|
|
804
|
+
label: "",
|
|
805
|
+
targetTitle: "Pictures",
|
|
806
|
+
url: "/pictures",
|
|
807
|
+
position: 0,
|
|
808
|
+
placement: "header",
|
|
809
|
+
},
|
|
810
|
+
],
|
|
811
|
+
}),
|
|
812
|
+
).generateHugoFiles(),
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
const { frontMatter: thoughtsPage } = await parseFrontMatter(
|
|
816
|
+
files.get("content/thoughts/_index.md") as string,
|
|
817
|
+
);
|
|
818
|
+
expect(thoughtsPage).toEqual({
|
|
819
|
+
title: "Thoughts",
|
|
820
|
+
slug: "thoughts",
|
|
821
|
+
type: "smart_collection",
|
|
822
|
+
summary_text: "Short notes.",
|
|
823
|
+
sort_order: "newest",
|
|
824
|
+
selection: { format: "note", title: false },
|
|
825
|
+
outputs: ["html", "rss"],
|
|
826
|
+
});
|
|
827
|
+
const { frontMatter: picturesPage } = await parseFrontMatter(
|
|
828
|
+
files.get("content/pictures/_index.md") as string,
|
|
829
|
+
);
|
|
830
|
+
expect(picturesPage.selection).toEqual({
|
|
831
|
+
collection: "ideas",
|
|
832
|
+
media: ["image", "video"],
|
|
833
|
+
year: 2025,
|
|
834
|
+
replies: true,
|
|
835
|
+
visibility: "featured",
|
|
836
|
+
});
|
|
837
|
+
expect(picturesPage.sort_order).toBe("rating_desc");
|
|
838
|
+
expect(picturesPage.display_layout).toBe("grid");
|
|
839
|
+
expect(files.has("content/orphaned/_index.md")).toBe(false);
|
|
840
|
+
|
|
841
|
+
const { parse } = await import("smol-toml");
|
|
842
|
+
const data = parse(files.get("data/jant.toml") as string) as {
|
|
843
|
+
directory: Record<string, unknown>[];
|
|
844
|
+
nav: Record<string, unknown>[];
|
|
845
|
+
};
|
|
846
|
+
expect(data.directory).toEqual([
|
|
847
|
+
expect.objectContaining({
|
|
848
|
+
type: "smart_collection",
|
|
849
|
+
slug: "thoughts",
|
|
850
|
+
title: "Thoughts",
|
|
851
|
+
entry_count: 4,
|
|
852
|
+
}),
|
|
853
|
+
]);
|
|
854
|
+
expect(data.nav[0]).toMatchObject({
|
|
855
|
+
type: "smart_collection",
|
|
856
|
+
smart_collection_slug: "pictures",
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it("carries redirects and archive URLs that name no post", async () => {
|
|
861
|
+
const record = (over: Partial<PathRecord>): PathRecord => ({
|
|
862
|
+
id: "pth-1",
|
|
863
|
+
siteId: "site",
|
|
864
|
+
path: "",
|
|
865
|
+
kind: "redirect",
|
|
866
|
+
postId: null,
|
|
867
|
+
collectionId: null,
|
|
868
|
+
smartCollectionId: null,
|
|
869
|
+
redirectToPath: null,
|
|
870
|
+
redirectType: null,
|
|
871
|
+
archiveQuery: null,
|
|
872
|
+
createdAt: 1,
|
|
873
|
+
updatedAt: 1,
|
|
874
|
+
...over,
|
|
875
|
+
});
|
|
876
|
+
const files = filesToMap(
|
|
877
|
+
await createExportService(
|
|
878
|
+
buildServices({
|
|
879
|
+
posts: [],
|
|
880
|
+
standalonePaths: [
|
|
881
|
+
record({
|
|
882
|
+
path: "atom.xml",
|
|
883
|
+
redirectToPath: "feed",
|
|
884
|
+
redirectType: 301,
|
|
885
|
+
}),
|
|
886
|
+
record({
|
|
887
|
+
path: "inspires",
|
|
888
|
+
redirectToPath: "inspired",
|
|
889
|
+
redirectType: 302,
|
|
890
|
+
}),
|
|
891
|
+
record({
|
|
892
|
+
path: "links",
|
|
893
|
+
kind: "archive",
|
|
894
|
+
archiveQuery: "format=link",
|
|
895
|
+
}),
|
|
896
|
+
],
|
|
897
|
+
}),
|
|
898
|
+
makeSiteConfig(),
|
|
899
|
+
).generateHugoFiles(),
|
|
900
|
+
);
|
|
901
|
+
const data = files.get("data/jant.toml") as string;
|
|
902
|
+
const redirects = files.get("static/_redirects") as string;
|
|
903
|
+
|
|
904
|
+
expect(data).toContain(
|
|
905
|
+
'[[custom_url]]\npath = "atom.xml"\nkind = "redirect"\nto = "/feed"\nstatus = 301',
|
|
906
|
+
);
|
|
907
|
+
expect(data).toContain(
|
|
908
|
+
'path = "inspires"\nkind = "redirect"\nto = "/inspired"\nstatus = 302',
|
|
909
|
+
);
|
|
910
|
+
expect(data).toContain(
|
|
911
|
+
'path = "links"\nkind = "archive"\narchive_query = "format=link"',
|
|
912
|
+
);
|
|
913
|
+
expect(redirects).toMatch(/^\/atom\.xml\s+\/feed\s+301$/m);
|
|
914
|
+
expect(redirects).toMatch(/^\/inspires\s+\/inspired\s+302$/m);
|
|
915
|
+
expect(redirects).not.toContain("/links");
|
|
916
|
+
});
|
|
917
|
+
|
|
527
918
|
it("resolves the nav RSS link to /featured/index.xml when mainRssFeed=featured", async () => {
|
|
528
919
|
const service = createExportService(
|
|
529
920
|
buildServices({ posts: [] }),
|
|
@@ -840,6 +1231,12 @@ describe("createExportService (Hugo)", () => {
|
|
|
840
1231
|
"themes/jant/layouts/partials/reply.html",
|
|
841
1232
|
"themes/jant/layouts/partials/featured-thread.html",
|
|
842
1233
|
"themes/jant/layouts/partials/feed-post-content.xml",
|
|
1234
|
+
"themes/jant/layouts/partials/collection-threads.html",
|
|
1235
|
+
"themes/jant/layouts/partials/collection-members.html",
|
|
1236
|
+
"themes/jant/layouts/partials/latest-members.html",
|
|
1237
|
+
"themes/jant/layouts/partials/featured-members.html",
|
|
1238
|
+
"themes/jant/layouts/partials/smart-collection-members.html",
|
|
1239
|
+
"themes/jant/layouts/smart_collection/list.html",
|
|
843
1240
|
];
|
|
844
1241
|
for (const path of expectedLayouts) {
|
|
845
1242
|
expect(files.has(path), `missing ${path}`).toBe(true);
|
|
@@ -850,10 +1247,10 @@ describe("createExportService (Hugo)", () => {
|
|
|
850
1247
|
expect(files.has("themes/jant/static/theme.css")).toBe(true);
|
|
851
1248
|
expect(files.has("themes/jant/static/custom.css")).toBe(true);
|
|
852
1249
|
|
|
853
|
-
const
|
|
854
|
-
"themes/jant/layouts/
|
|
1250
|
+
const collectionThreads = files.get(
|
|
1251
|
+
"themes/jant/layouts/partials/collection-threads.html",
|
|
855
1252
|
) as string;
|
|
856
|
-
expect(
|
|
1253
|
+
expect(collectionThreads).toContain(
|
|
857
1254
|
'class="thread thread-full{{ if $hasReplies }} thread-has-replies{{ end }}"',
|
|
858
1255
|
);
|
|
859
1256
|
});
|
|
@@ -894,9 +1291,9 @@ describe("createExportService (Hugo)", () => {
|
|
|
894
1291
|
buildServices({ posts: [makePost()] }),
|
|
895
1292
|
makeSiteConfig(),
|
|
896
1293
|
);
|
|
897
|
-
const zip =
|
|
898
|
-
|
|
899
|
-
|
|
1294
|
+
const zip = new Uint8Array(
|
|
1295
|
+
await new Response(await service.generateHugoSite()).arrayBuffer(),
|
|
1296
|
+
);
|
|
900
1297
|
// ZIP magic: PK\x03\x04
|
|
901
1298
|
expect(zip[0]).toBe(0x50);
|
|
902
1299
|
expect(zip[1]).toBe(0x4b);
|
|
@@ -1041,18 +1438,60 @@ describe("createExportService (Hugo)", () => {
|
|
|
1041
1438
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1042
1439
|
{ storage: storage as any },
|
|
1043
1440
|
);
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1441
|
+
// The file list names the objects; the archive reads them as it goes.
|
|
1442
|
+
const stored = (await service.generateHugoFiles()).filter(
|
|
1443
|
+
isStoredExportFile,
|
|
1444
|
+
);
|
|
1445
|
+
expect(stored).toEqual([
|
|
1446
|
+
{ path: "static/media/med-root.webp", storageKey: "media/med-root.webp" },
|
|
1447
|
+
{ path: "static/media/med-reply.png", storageKey: "media/med-reply.png" },
|
|
1448
|
+
]);
|
|
1449
|
+
const archive = await readArchive(await service.generateHugoSite());
|
|
1450
|
+
expect(archive["static/media/med-root.webp"]).toEqual(
|
|
1048
1451
|
new Uint8Array([1, 2, 3]),
|
|
1049
1452
|
);
|
|
1050
|
-
expect(
|
|
1453
|
+
expect(archive["static/media/med-reply.png"]).toEqual(
|
|
1051
1454
|
new Uint8Array([9, 9, 9, 9]),
|
|
1052
1455
|
);
|
|
1053
1456
|
// Sanity: the old per-bundle paths are gone.
|
|
1054
|
-
expect(
|
|
1055
|
-
expect(
|
|
1457
|
+
expect(archive["content/with-media/med-root.webp"]).toBeUndefined();
|
|
1458
|
+
expect(
|
|
1459
|
+
archive["content/with-media/reply-one/med-reply.png"],
|
|
1460
|
+
).toBeUndefined();
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
it("leaves a missing object out of the archive and says so", async () => {
|
|
1464
|
+
const root = makePost({ id: "post-root", slug: "with-media" });
|
|
1465
|
+
const service = createExportService(
|
|
1466
|
+
buildServices({
|
|
1467
|
+
posts: [root],
|
|
1468
|
+
mediaByPost: new Map([
|
|
1469
|
+
[
|
|
1470
|
+
"post-root",
|
|
1471
|
+
[
|
|
1472
|
+
makeMedia({
|
|
1473
|
+
id: "med-gone",
|
|
1474
|
+
filename: "gone.webp",
|
|
1475
|
+
storageKey: "media/med-gone.webp",
|
|
1476
|
+
}),
|
|
1477
|
+
],
|
|
1478
|
+
],
|
|
1479
|
+
]),
|
|
1480
|
+
}),
|
|
1481
|
+
makeSiteConfig(),
|
|
1482
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1483
|
+
{ storage: { get: async () => null } as any },
|
|
1484
|
+
);
|
|
1485
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1486
|
+
|
|
1487
|
+
const archive = await readArchive(await service.generateHugoSite());
|
|
1488
|
+
|
|
1489
|
+
expect(archive["static/media/med-gone.webp"]).toBeUndefined();
|
|
1490
|
+
expect(archive["hugo.toml"]).toBeDefined();
|
|
1491
|
+
expect(warn).toHaveBeenCalledWith(
|
|
1492
|
+
expect.stringContaining("media/med-gone.webp is missing from storage"),
|
|
1493
|
+
);
|
|
1494
|
+
warn.mockRestore();
|
|
1056
1495
|
});
|
|
1057
1496
|
|
|
1058
1497
|
it("emits poster bytes and poster field for video media with posterKey", async () => {
|
|
@@ -1092,12 +1531,11 @@ describe("createExportService (Hugo)", () => {
|
|
|
1092
1531
|
{ storage: storage as any },
|
|
1093
1532
|
);
|
|
1094
1533
|
const files = filesToMap(await service.generateHugoFiles());
|
|
1095
|
-
|
|
1096
|
-
expect(
|
|
1097
|
-
expect(files.get("static/media/med-video.mp4")).toEqual(
|
|
1534
|
+
const archive = await readArchive(await service.generateHugoSite());
|
|
1535
|
+
expect(archive["static/media/med-video.mp4"]).toEqual(
|
|
1098
1536
|
new Uint8Array([10, 20, 30]),
|
|
1099
1537
|
);
|
|
1100
|
-
expect(
|
|
1538
|
+
expect(archive["static/media/med-video-poster.webp"]).toEqual(
|
|
1101
1539
|
new Uint8Array([40, 50, 60, 70]),
|
|
1102
1540
|
);
|
|
1103
1541
|
|
|
@@ -1280,14 +1718,159 @@ describe("createExportService (Hugo)", () => {
|
|
|
1280
1718
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1281
1719
|
{ storage: storage as any, bundleMedia: false },
|
|
1282
1720
|
);
|
|
1283
|
-
const
|
|
1721
|
+
const exportFiles = await service.generateHugoFiles();
|
|
1722
|
+
const files = filesToMap(exportFiles);
|
|
1284
1723
|
// No resolvable URL — bundling is the only way to avoid a broken link.
|
|
1285
|
-
expect(
|
|
1286
|
-
|
|
1287
|
-
);
|
|
1724
|
+
expect(exportFiles.filter(isStoredExportFile)).toEqual([
|
|
1725
|
+
{ path: "static/media/med-1.webp", storageKey: "media/med-1.webp" },
|
|
1726
|
+
]);
|
|
1288
1727
|
const { frontMatter } = await parseFrontMatter(
|
|
1289
1728
|
files.get("content/with-media/_index.md") as string,
|
|
1290
1729
|
);
|
|
1291
1730
|
expect(frontMatter.media![0].src).toBe("/media/med-1.webp");
|
|
1292
1731
|
});
|
|
1293
1732
|
});
|
|
1733
|
+
|
|
1734
|
+
describe("bundled theme", () => {
|
|
1735
|
+
it("reads site data only through the version-agnostic partial", async () => {
|
|
1736
|
+
const service = createExportService(
|
|
1737
|
+
buildServices({ posts: [] }),
|
|
1738
|
+
makeSiteConfig(),
|
|
1739
|
+
);
|
|
1740
|
+
const layouts = (await service.generateHugoFiles()).filter(
|
|
1741
|
+
(f) =>
|
|
1742
|
+
f.path.startsWith("themes/jant/layouts/") &&
|
|
1743
|
+
typeof f.content === "string",
|
|
1744
|
+
);
|
|
1745
|
+
|
|
1746
|
+
expect(layouts.length).toBeGreaterThan(0);
|
|
1747
|
+
// Hugo has no accessor for `data/` that works on every version: it added
|
|
1748
|
+
// `hugo.Data` in v0.160.0 and deprecated `site.Data` in v0.156.0, so a
|
|
1749
|
+
// template naming either one directly is a build failure on one side of
|
|
1750
|
+
// that line. `partials/jant-data.html` branches on the running version;
|
|
1751
|
+
// everything else goes through it.
|
|
1752
|
+
const offenders = layouts
|
|
1753
|
+
.filter(
|
|
1754
|
+
(f) =>
|
|
1755
|
+
f.path !== "themes/jant/layouts/partials/jant-data.html" &&
|
|
1756
|
+
/\b(?:hugo|site)\.Data\b/.test(f.content as string),
|
|
1757
|
+
)
|
|
1758
|
+
.map((f) => f.path);
|
|
1759
|
+
|
|
1760
|
+
expect(offenders).toEqual([]);
|
|
1761
|
+
});
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
describe("deriveWorkerName", () => {
|
|
1765
|
+
it("uses the repository name when the export has one", () => {
|
|
1766
|
+
// Cloudflare names a Worker imported from a repository after the
|
|
1767
|
+
// repository, and fails the build when `wrangler.jsonc` disagrees — so
|
|
1768
|
+
// the repo name is the default that needs no editing.
|
|
1769
|
+
expect(
|
|
1770
|
+
deriveWorkerName("owenyoung-blog", "https://notes.example.com"),
|
|
1771
|
+
).toBe("owenyoung-blog");
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
it.each([
|
|
1775
|
+
["My_Blog", "my-blog"],
|
|
1776
|
+
["blog.owenyoung.com", "blog-owenyoung-com"],
|
|
1777
|
+
["--site--", "site"],
|
|
1778
|
+
])("normalizes repository name %s into %s", (repoName, expected) => {
|
|
1779
|
+
expect(deriveWorkerName(repoName, "https://example.com")).toBe(expected);
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
it("falls back to the repository name GitHub Sync suggests", () => {
|
|
1783
|
+
// A downloaded export pushed to a repository created with the settings
|
|
1784
|
+
// page's prefilled name then matches the Worker Cloudflare creates.
|
|
1785
|
+
const siteUrl = "https://notes.example.com";
|
|
1786
|
+
|
|
1787
|
+
expect(deriveWorkerName(null, siteUrl)).toBe("notes-jant-sync");
|
|
1788
|
+
expect(deriveWorkerName(undefined, siteUrl)).toBe(
|
|
1789
|
+
suggestSyncRepoName(siteUrl),
|
|
1790
|
+
);
|
|
1791
|
+
expect(deriveWorkerName("___", siteUrl)).toBe("notes-jant-sync");
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
it("skips a leading www in the fallback, as the settings page does", () => {
|
|
1795
|
+
expect(deriveWorkerName(null, "https://www.owenyoung.com")).toBe(
|
|
1796
|
+
"owenyoung-jant-sync",
|
|
1797
|
+
);
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
it("turns the suggestion's underscores into hyphens Cloudflare accepts", () => {
|
|
1801
|
+
expect(deriveWorkerName(null, "https://my_notes.example.com")).toBe(
|
|
1802
|
+
"my-notes-jant-sync",
|
|
1803
|
+
);
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
it("still yields a name when the site URL has no host", () => {
|
|
1807
|
+
expect(deriveWorkerName(null, "")).toBe("jant-site-sync");
|
|
1808
|
+
expect(deriveWorkerName("", "not a url")).toBe("jant-site-sync");
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
it("keeps the name inside Cloudflare's 63-character limit", () => {
|
|
1812
|
+
const name = deriveWorkerName(`${"a".repeat(62)}.long-repo`, "");
|
|
1813
|
+
|
|
1814
|
+
expect(name).toHaveLength(62);
|
|
1815
|
+
expect(name).toMatch(/^[a-z0-9-]+$/);
|
|
1816
|
+
// Cut at 63, the name would end on the hyphen that replaced the dot.
|
|
1817
|
+
expect(name.endsWith("-")).toBe(false);
|
|
1818
|
+
});
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
describe("wrangler.jsonc", () => {
|
|
1822
|
+
it("declares the Worker name, a compatibility date, the Hugo build, and public/ as the asset directory", async () => {
|
|
1823
|
+
const service = createExportService(
|
|
1824
|
+
buildServices({ posts: [] }),
|
|
1825
|
+
makeSiteConfig({ siteUrl: "https://www.owenyoung.com" }),
|
|
1826
|
+
{ repoName: "owenyoung-blog" },
|
|
1827
|
+
);
|
|
1828
|
+
const files = await service.generateHugoFiles();
|
|
1829
|
+
const config = files.find((f) => f.path === WRANGLER_CONFIG_PATH);
|
|
1830
|
+
|
|
1831
|
+
expect(config).toBeDefined();
|
|
1832
|
+
const text = config!.content as string;
|
|
1833
|
+
expect(text).toContain('"name": "owenyoung-blog"');
|
|
1834
|
+
expect(text).toMatch(/"compatibility_date": "\d{4}-\d{2}-\d{2}"/);
|
|
1835
|
+
expect(text).toContain('"directory": "./public"');
|
|
1836
|
+
// Nothing else runs Hugo: Workers Builds detects a framework from a
|
|
1837
|
+
// `package.json`, a Hugo site has none, so the build command it offers
|
|
1838
|
+
// on connect is empty and the deploy uploads a `public/` that was never
|
|
1839
|
+
// built. Wrangler runs this before uploading.
|
|
1840
|
+
expect(text).toContain('"command": "hugo --gc --minify"');
|
|
1841
|
+
// An assets-only Worker needs no script.
|
|
1842
|
+
expect(text).not.toContain('"main"');
|
|
1843
|
+
// The theme emits no 404.html to point it at.
|
|
1844
|
+
expect(text).not.toContain("not_found_handling");
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
it("names the Worker after the suggested repository when there is no repository", async () => {
|
|
1848
|
+
const service = createExportService(
|
|
1849
|
+
buildServices({ posts: [] }),
|
|
1850
|
+
makeSiteConfig({ siteUrl: "https://notes.example.com" }),
|
|
1851
|
+
);
|
|
1852
|
+
const text = (await service.generateHugoFiles()).find(
|
|
1853
|
+
(f) => f.path === WRANGLER_CONFIG_PATH,
|
|
1854
|
+
)!.content as string;
|
|
1855
|
+
|
|
1856
|
+
expect(text).toContain('"name": "notes-jant-sync"');
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
it("is scaffolding the repository owns after the first write", async () => {
|
|
1860
|
+
const service = createExportService(
|
|
1861
|
+
buildServices({ posts: [] }),
|
|
1862
|
+
makeSiteConfig(),
|
|
1863
|
+
);
|
|
1864
|
+
const files = await service.generateHugoFiles();
|
|
1865
|
+
|
|
1866
|
+
// The Worker name is a guess at the site's host; correcting it by hand is
|
|
1867
|
+
// expected, and a later sync must not revert that.
|
|
1868
|
+
expect(
|
|
1869
|
+
files.find((f) => f.path === WRANGLER_CONFIG_PATH)?.scaffoldOnce,
|
|
1870
|
+
).toBe(true);
|
|
1871
|
+
// Nothing else claims the flag — every other file is Jant's to rewrite.
|
|
1872
|
+
expect(files.filter((f) => f.scaffoldOnce).map((f) => f.path)).toEqual([
|
|
1873
|
+
WRANGLER_CONFIG_PATH,
|
|
1874
|
+
]);
|
|
1875
|
+
});
|
|
1876
|
+
});
|