@jant/core 0.6.16 → 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/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +749 -220
- package/bin/commands/reset-password.js +21 -8
- 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 +19 -61
- package/bin/commands/site/snapshot/import.js +23 -24
- package/bin/lib/d1-query.js +92 -18
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +94 -29
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +37 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
- package/dist/client/_assets/client-DAhoqPdr.css +2 -0
- package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
- package/dist/client/_assets/client-knSEyUJO.js +295 -0
- package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
- package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
- package/dist/github-sync-Gw4orAHk.js +8398 -0
- package/dist/index.js +2 -5
- package/dist/node.js +11 -9
- package/package.json +15 -5
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- 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 +390 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +842 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -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__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +86 -22
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +137 -108
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +622 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- 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/bubble-menu.ts +47 -16
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +555 -73
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +601 -119
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +614 -132
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +131 -3
- package/src/lib/__tests__/schemas.test.ts +69 -5
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +439 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +62 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +111 -6
- package/src/lib/schemas.ts +251 -15
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +43 -2
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +159 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +53 -25
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- 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 +75 -0
- package/src/node/index.ts +3 -0
- package/src/node/request-handler.ts +3 -1
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +6 -3
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +161 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +44 -7
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +9 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +714 -111
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +487 -379
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +43 -18
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +228 -45
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +22 -5
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +67 -1
- package/src/runtime/readiness.ts +15 -0
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/collection.test.ts +108 -4
- 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__/language.test.ts +495 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +236 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +120 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +408 -39
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +29 -24
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- 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 +103 -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/feed-post-content.xml +105 -2
- 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/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
- 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 +13 -2
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +880 -72
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +22 -2
- package/src/services/media.ts +22 -7
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +254 -6
- package/src/services/post.ts +1234 -190
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +27 -14
- package/src/services/site.ts +160 -32
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1090 -7216
- package/src/types/app-context.ts +23 -0
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +74 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +277 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/export-Dl5KCiEs.js +0 -5562
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/client/tiptap/exitable-marks.ts +0 -73
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -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,15 +109,96 @@ 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)", () => {
|
|
128
|
+
// Hugo has its own summary rule and would cut somewhere else, so whether the
|
|
129
|
+
// timeline truncates a post is decided here and carried as front matter.
|
|
130
|
+
// Written only when true — a consumer reads its presence.
|
|
131
|
+
it("marks a truncated post in front matter and leaves a short one unmarked", async () => {
|
|
132
|
+
const para = (text: string) => ({
|
|
133
|
+
type: "paragraph",
|
|
134
|
+
content: [{ type: "text", text }],
|
|
135
|
+
});
|
|
136
|
+
const long = makePost({
|
|
137
|
+
id: "post-long",
|
|
138
|
+
slug: "long-read",
|
|
139
|
+
title: "A long read",
|
|
140
|
+
threadId: "post-long",
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
type: "doc",
|
|
143
|
+
content: [
|
|
144
|
+
para("Alpha ".repeat(90).trim()),
|
|
145
|
+
para("Omega ".repeat(90).trim()),
|
|
146
|
+
],
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
const short = makePost({
|
|
150
|
+
id: "post-short",
|
|
151
|
+
slug: "short-note",
|
|
152
|
+
title: null,
|
|
153
|
+
threadId: "post-short",
|
|
154
|
+
body: JSON.stringify({ type: "doc", content: [para("Short enough.")] }),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const files = await createExportService(
|
|
158
|
+
{
|
|
159
|
+
posts: { list: async () => [long, short] },
|
|
160
|
+
paths: {
|
|
161
|
+
getPostSlugMap: async () =>
|
|
162
|
+
new Map([
|
|
163
|
+
["post-long", "long-read"],
|
|
164
|
+
["post-short", "short-note"],
|
|
165
|
+
]),
|
|
166
|
+
listStandalonePaths: async () => [],
|
|
167
|
+
getPostAliases: async () => new Map(),
|
|
168
|
+
getCollectionSlugMap: async () => new Map(),
|
|
169
|
+
},
|
|
170
|
+
collections: {
|
|
171
|
+
list: async () => [],
|
|
172
|
+
listDirectoryData: async () => ({
|
|
173
|
+
collections: [],
|
|
174
|
+
items: [],
|
|
175
|
+
directoryItems: [],
|
|
176
|
+
}),
|
|
177
|
+
getCollectionsByPostIds: async () => new Map(),
|
|
178
|
+
getCollectionEntriesByThreadIds: async () => new Map(),
|
|
179
|
+
},
|
|
180
|
+
media: { getByPostIds: async () => new Map() },
|
|
181
|
+
} as unknown as ServicesArg,
|
|
182
|
+
makeSiteConfig(),
|
|
183
|
+
).generateHugoFiles();
|
|
184
|
+
|
|
185
|
+
const read = async (path: string) => {
|
|
186
|
+
const file = files.find((f) => f.path === path);
|
|
187
|
+
expect(file, `missing ${path}`).toBeDefined();
|
|
188
|
+
const { frontMatter } = await parseFrontMatter(
|
|
189
|
+
typeof file!.content === "string"
|
|
190
|
+
? file!.content
|
|
191
|
+
: new TextDecoder().decode(file!.content),
|
|
192
|
+
);
|
|
193
|
+
return frontMatter;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
expect((await read("content/long-read/_index.md")).truncated).toBe(true);
|
|
197
|
+
expect(
|
|
198
|
+
(await read("content/short-note/_index.md")).truncated,
|
|
199
|
+
).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
102
202
|
it("emits a branch bundle per root post with YAML front matter in stable key order", async () => {
|
|
103
203
|
const root = makePost({
|
|
104
204
|
id: "post-root",
|
|
@@ -251,6 +351,105 @@ describe("createExportService (Hugo)", () => {
|
|
|
251
351
|
);
|
|
252
352
|
});
|
|
253
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
|
+
|
|
254
453
|
it("merges historical root aliases + reply slugs onto the root", async () => {
|
|
255
454
|
const root = makePost({ id: "r", slug: "new-slug", threadId: "r" });
|
|
256
455
|
const reply = makePost({
|
|
@@ -341,6 +540,23 @@ describe("createExportService (Hugo)", () => {
|
|
|
341
540
|
expect(toml).not.toContain("home_default_view");
|
|
342
541
|
});
|
|
343
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
|
+
|
|
344
560
|
it("configures hugo.toml for Atom RSS output with per-section opt-in", async () => {
|
|
345
561
|
// The root feed lives at /index.xml, featured/archive/collections feeds
|
|
346
562
|
// are opted in via per-section `outputs = ["html", "rss"]` front matter.
|
|
@@ -451,6 +667,254 @@ describe("createExportService (Hugo)", () => {
|
|
|
451
667
|
}
|
|
452
668
|
});
|
|
453
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
|
+
|
|
454
918
|
it("resolves the nav RSS link to /featured/index.xml when mainRssFeed=featured", async () => {
|
|
455
919
|
const service = createExportService(
|
|
456
920
|
buildServices({ posts: [] }),
|
|
@@ -496,6 +960,164 @@ describe("createExportService (Hugo)", () => {
|
|
|
496
960
|
expect(data).toContain('url = "/index.xml"');
|
|
497
961
|
});
|
|
498
962
|
|
|
963
|
+
it("resolves the nav Subscribe link to the feed the page would recommend", async () => {
|
|
964
|
+
const service = createExportService(
|
|
965
|
+
buildServices({ posts: [] }),
|
|
966
|
+
makeSiteConfig({
|
|
967
|
+
mainRssFeed: "featured",
|
|
968
|
+
navItems: [
|
|
969
|
+
{
|
|
970
|
+
type: "system",
|
|
971
|
+
systemKey: "subscribe",
|
|
972
|
+
label: "",
|
|
973
|
+
url: "/subscribe",
|
|
974
|
+
position: 0,
|
|
975
|
+
placement: "more",
|
|
976
|
+
},
|
|
977
|
+
],
|
|
978
|
+
}),
|
|
979
|
+
);
|
|
980
|
+
const data = filesToMap(await service.generateHugoFiles()).get(
|
|
981
|
+
"data/jant.toml",
|
|
982
|
+
) as string;
|
|
983
|
+
|
|
984
|
+
// There is no /subscribe page in the exported site to link at.
|
|
985
|
+
expect(data).not.toContain('url = "/subscribe"');
|
|
986
|
+
expect(data).toContain('url = "/featured/index.xml"');
|
|
987
|
+
expect(data).toContain('label = "Subscribe"');
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it("redirects every runtime feed address to its exported counterpart", async () => {
|
|
991
|
+
const service = createExportService(
|
|
992
|
+
buildServices({ posts: [] }),
|
|
993
|
+
makeSiteConfig({ mainRssFeed: "latest" }),
|
|
994
|
+
);
|
|
995
|
+
const redirects = filesToMap(await service.generateHugoFiles()).get(
|
|
996
|
+
"static/_redirects",
|
|
997
|
+
) as string;
|
|
998
|
+
|
|
999
|
+
// Feed readers hold the runtime addresses; nothing in the Hugo tree
|
|
1000
|
+
// answers them without these.
|
|
1001
|
+
const rules = redirects
|
|
1002
|
+
.split("\n")
|
|
1003
|
+
.filter((l) => l.startsWith("/"))
|
|
1004
|
+
.map((l) => l.split(/\s+/));
|
|
1005
|
+
expect(new Map(rules.map((r) => [r[0], r[1]]))).toEqual(
|
|
1006
|
+
new Map([
|
|
1007
|
+
["/feed", "/index.xml"],
|
|
1008
|
+
["/latest/feed", "/index.xml"],
|
|
1009
|
+
["/featured/feed", "/featured/index.xml"],
|
|
1010
|
+
["/archive/feed", "/archive/index.xml"],
|
|
1011
|
+
// The 308 aliases the live site still answers.
|
|
1012
|
+
["/feed/latest", "/index.xml"],
|
|
1013
|
+
["/feed/all", "/index.xml"],
|
|
1014
|
+
["/feed/featured", "/featured/index.xml"],
|
|
1015
|
+
["/feed/atom.xml", "/index.xml"],
|
|
1016
|
+
["/feed/latest/atom.xml", "/index.xml"],
|
|
1017
|
+
["/feed/all/atom.xml", "/index.xml"],
|
|
1018
|
+
["/feed/featured/atom.xml", "/featured/index.xml"],
|
|
1019
|
+
["/:slug/feed", "/:slug/index.xml"],
|
|
1020
|
+
]),
|
|
1021
|
+
);
|
|
1022
|
+
// A feed reader follows a 301 and rewrites its stored address; a 302
|
|
1023
|
+
// would leave it polling the dead one forever.
|
|
1024
|
+
expect(rules.every((r) => r[2] === "301")).toBe(true);
|
|
1025
|
+
// First match wins in both hosts that read this file, so the collection
|
|
1026
|
+
// catch-all must not shadow the fixed sections.
|
|
1027
|
+
expect(rules.at(-1)?.[0]).toBe("/:slug/feed");
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it("points /feed at the featured feed when that is the site's main feed", async () => {
|
|
1031
|
+
const service = createExportService(
|
|
1032
|
+
buildServices({ posts: [] }),
|
|
1033
|
+
makeSiteConfig({ mainRssFeed: "featured" }),
|
|
1034
|
+
);
|
|
1035
|
+
const redirects = filesToMap(await service.generateHugoFiles()).get(
|
|
1036
|
+
"static/_redirects",
|
|
1037
|
+
) as string;
|
|
1038
|
+
|
|
1039
|
+
expect(redirects).toMatch(/^\/feed\s+\/featured\/index\.xml\s+301$/m);
|
|
1040
|
+
expect(redirects).toMatch(
|
|
1041
|
+
/^\/feed\/atom\.xml\s+\/featured\/index\.xml\s+301$/m,
|
|
1042
|
+
);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it("falls back to the latest feed when no featured section was emitted", async () => {
|
|
1046
|
+
// A post owning the `featured` slug takes the section's place, so
|
|
1047
|
+
// /featured/index.xml is never built. Redirecting there would turn a live
|
|
1048
|
+
// subscription into a 404; the latest feed is wider but real.
|
|
1049
|
+
const service = createExportService(
|
|
1050
|
+
buildServices({ posts: [makePost({ id: "post-1", slug: "featured" })] }),
|
|
1051
|
+
makeSiteConfig({ mainRssFeed: "featured" }),
|
|
1052
|
+
);
|
|
1053
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
1054
|
+
const redirects = files.get("static/_redirects") as string;
|
|
1055
|
+
|
|
1056
|
+
// The post took the path the featured section would have used, so the
|
|
1057
|
+
// section — and /featured/index.xml with it — was never built.
|
|
1058
|
+
expect(files.get("content/featured/_index.md") as string).not.toContain(
|
|
1059
|
+
"type: featured",
|
|
1060
|
+
);
|
|
1061
|
+
expect(redirects).not.toContain("/featured/index.xml");
|
|
1062
|
+
expect(redirects).toMatch(/^\/feed\s+\/index\.xml\s+301$/m);
|
|
1063
|
+
expect(redirects).toMatch(/^\/featured\/feed\s+\/index\.xml\s+301$/m);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it("writes no feed redirects when the site publishes no feeds", async () => {
|
|
1067
|
+
const service = createExportService(
|
|
1068
|
+
buildServices({ posts: [] }),
|
|
1069
|
+
makeSiteConfig({ rssFeedsEnabled: false }),
|
|
1070
|
+
);
|
|
1071
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
1072
|
+
|
|
1073
|
+
expect(files.has("static/_redirects")).toBe(false);
|
|
1074
|
+
expect(files.get("README.md") as string).not.toContain("## Feeds");
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it("tells the README reader that feed addresses are the exception to URL continuity", async () => {
|
|
1078
|
+
const service = createExportService(
|
|
1079
|
+
buildServices({ posts: [] }),
|
|
1080
|
+
makeSiteConfig({ mainRssFeed: "latest" }),
|
|
1081
|
+
);
|
|
1082
|
+
const readme = filesToMap(await service.generateHugoFiles()).get(
|
|
1083
|
+
"README.md",
|
|
1084
|
+
) as string;
|
|
1085
|
+
|
|
1086
|
+
// The Notes list advertises that post URLs survive the move; without this
|
|
1087
|
+
// it reads as a promise covering every URL.
|
|
1088
|
+
expect(readme).toContain("## Feeds");
|
|
1089
|
+
expect(readme).toContain("`/latest/feed`");
|
|
1090
|
+
expect(readme).toContain("static/_redirects");
|
|
1091
|
+
expect(readme).toContain("Feed addresses are the exception");
|
|
1092
|
+
// No /subscribe page exists in the export, and the README says where the
|
|
1093
|
+
// nav entry lands instead.
|
|
1094
|
+
expect(readme).toContain("has no `/subscribe` page");
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it("hides system Subscribe navigation when feeds are off", async () => {
|
|
1098
|
+
const service = createExportService(
|
|
1099
|
+
buildServices({ posts: [] }),
|
|
1100
|
+
makeSiteConfig({
|
|
1101
|
+
rssFeedsEnabled: false,
|
|
1102
|
+
navItems: [
|
|
1103
|
+
{
|
|
1104
|
+
type: "system",
|
|
1105
|
+
systemKey: "subscribe",
|
|
1106
|
+
label: "System subscribe",
|
|
1107
|
+
url: "/subscribe",
|
|
1108
|
+
position: 0,
|
|
1109
|
+
placement: "more",
|
|
1110
|
+
},
|
|
1111
|
+
],
|
|
1112
|
+
}),
|
|
1113
|
+
);
|
|
1114
|
+
const data = filesToMap(await service.generateHugoFiles()).get(
|
|
1115
|
+
"data/jant.toml",
|
|
1116
|
+
) as string;
|
|
1117
|
+
|
|
1118
|
+
expect(data).not.toContain('label = "System subscribe"');
|
|
1119
|
+
});
|
|
1120
|
+
|
|
499
1121
|
it("emits last_activity_at on root post front matter so feed <updated> tracks thread bumps", async () => {
|
|
500
1122
|
const post = makePost({
|
|
501
1123
|
id: "post-1",
|
|
@@ -609,6 +1231,12 @@ describe("createExportService (Hugo)", () => {
|
|
|
609
1231
|
"themes/jant/layouts/partials/reply.html",
|
|
610
1232
|
"themes/jant/layouts/partials/featured-thread.html",
|
|
611
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",
|
|
612
1240
|
];
|
|
613
1241
|
for (const path of expectedLayouts) {
|
|
614
1242
|
expect(files.has(path), `missing ${path}`).toBe(true);
|
|
@@ -619,10 +1247,10 @@ describe("createExportService (Hugo)", () => {
|
|
|
619
1247
|
expect(files.has("themes/jant/static/theme.css")).toBe(true);
|
|
620
1248
|
expect(files.has("themes/jant/static/custom.css")).toBe(true);
|
|
621
1249
|
|
|
622
|
-
const
|
|
623
|
-
"themes/jant/layouts/
|
|
1250
|
+
const collectionThreads = files.get(
|
|
1251
|
+
"themes/jant/layouts/partials/collection-threads.html",
|
|
624
1252
|
) as string;
|
|
625
|
-
expect(
|
|
1253
|
+
expect(collectionThreads).toContain(
|
|
626
1254
|
'class="thread thread-full{{ if $hasReplies }} thread-has-replies{{ end }}"',
|
|
627
1255
|
);
|
|
628
1256
|
});
|
|
@@ -663,9 +1291,9 @@ describe("createExportService (Hugo)", () => {
|
|
|
663
1291
|
buildServices({ posts: [makePost()] }),
|
|
664
1292
|
makeSiteConfig(),
|
|
665
1293
|
);
|
|
666
|
-
const zip =
|
|
667
|
-
|
|
668
|
-
|
|
1294
|
+
const zip = new Uint8Array(
|
|
1295
|
+
await new Response(await service.generateHugoSite()).arrayBuffer(),
|
|
1296
|
+
);
|
|
669
1297
|
// ZIP magic: PK\x03\x04
|
|
670
1298
|
expect(zip[0]).toBe(0x50);
|
|
671
1299
|
expect(zip[1]).toBe(0x4b);
|
|
@@ -810,18 +1438,60 @@ describe("createExportService (Hugo)", () => {
|
|
|
810
1438
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
811
1439
|
{ storage: storage as any },
|
|
812
1440
|
);
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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(
|
|
817
1451
|
new Uint8Array([1, 2, 3]),
|
|
818
1452
|
);
|
|
819
|
-
expect(
|
|
1453
|
+
expect(archive["static/media/med-reply.png"]).toEqual(
|
|
820
1454
|
new Uint8Array([9, 9, 9, 9]),
|
|
821
1455
|
);
|
|
822
1456
|
// Sanity: the old per-bundle paths are gone.
|
|
823
|
-
expect(
|
|
824
|
-
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();
|
|
825
1495
|
});
|
|
826
1496
|
|
|
827
1497
|
it("emits poster bytes and poster field for video media with posterKey", async () => {
|
|
@@ -861,12 +1531,11 @@ describe("createExportService (Hugo)", () => {
|
|
|
861
1531
|
{ storage: storage as any },
|
|
862
1532
|
);
|
|
863
1533
|
const files = filesToMap(await service.generateHugoFiles());
|
|
864
|
-
|
|
865
|
-
expect(
|
|
866
|
-
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(
|
|
867
1536
|
new Uint8Array([10, 20, 30]),
|
|
868
1537
|
);
|
|
869
|
-
expect(
|
|
1538
|
+
expect(archive["static/media/med-video-poster.webp"]).toEqual(
|
|
870
1539
|
new Uint8Array([40, 50, 60, 70]),
|
|
871
1540
|
);
|
|
872
1541
|
|
|
@@ -1049,14 +1718,159 @@ describe("createExportService (Hugo)", () => {
|
|
|
1049
1718
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1050
1719
|
{ storage: storage as any, bundleMedia: false },
|
|
1051
1720
|
);
|
|
1052
|
-
const
|
|
1721
|
+
const exportFiles = await service.generateHugoFiles();
|
|
1722
|
+
const files = filesToMap(exportFiles);
|
|
1053
1723
|
// No resolvable URL — bundling is the only way to avoid a broken link.
|
|
1054
|
-
expect(
|
|
1055
|
-
|
|
1056
|
-
);
|
|
1724
|
+
expect(exportFiles.filter(isStoredExportFile)).toEqual([
|
|
1725
|
+
{ path: "static/media/med-1.webp", storageKey: "media/med-1.webp" },
|
|
1726
|
+
]);
|
|
1057
1727
|
const { frontMatter } = await parseFrontMatter(
|
|
1058
1728
|
files.get("content/with-media/_index.md") as string,
|
|
1059
1729
|
);
|
|
1060
1730
|
expect(frontMatter.media![0].src).toBe("/media/med-1.webp");
|
|
1061
1731
|
});
|
|
1062
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
|
+
});
|