@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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A smart collection on an exported site lists what it lists on Jant.
|
|
3
|
+
*
|
|
4
|
+
* The export carries a smart collection's conditions, not its matches, and
|
|
5
|
+
* the theme's `smart-collection-members` partial evaluates them against the
|
|
6
|
+
* posts' front matter. That is a second implementation of every condition and
|
|
7
|
+
* every order, in Go templates, so this builds the export with the real Hugo
|
|
8
|
+
* binary and compares each page with the list Jant's own services produce
|
|
9
|
+
* from the same database.
|
|
10
|
+
*
|
|
11
|
+
* Skips when `hugo` is not on PATH, like the other Hugo build tests.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { rm } from "node:fs/promises";
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
16
|
+
import { createTestDatabase, DEFAULT_TEST_SITE_ID } from "./helpers/db.js";
|
|
17
|
+
import { makeSiteConfig } from "./helpers/export-fixtures.js";
|
|
18
|
+
import {
|
|
19
|
+
buildHugoSite,
|
|
20
|
+
hugoAvailable,
|
|
21
|
+
readFeedSlugs,
|
|
22
|
+
readThreadSlugs,
|
|
23
|
+
} from "./helpers/hugo-site.js";
|
|
24
|
+
import { createCollectionService } from "../services/collection.js";
|
|
25
|
+
import { createExportService } from "../services/export.js";
|
|
26
|
+
import { createMediaService } from "../services/media.js";
|
|
27
|
+
import { createPathService } from "../services/path.js";
|
|
28
|
+
import { createPostService } from "../services/post.js";
|
|
29
|
+
import { createSmartCollectionService } from "../services/smart-collection.js";
|
|
30
|
+
import { supportsCollectionRatingSort } from "../lib/collection-sort.js";
|
|
31
|
+
import type { Database } from "../db/index.js";
|
|
32
|
+
import type { CreatePost } from "../types/operations.js";
|
|
33
|
+
import type { MediaKind, SmartCollection } from "../types.js";
|
|
34
|
+
|
|
35
|
+
const SITE_URL = "https://example.com";
|
|
36
|
+
|
|
37
|
+
const at = (iso: string) => Math.floor(Date.parse(iso) / 1000);
|
|
38
|
+
|
|
39
|
+
/** Every condition value and every order, each on its own smart collection. */
|
|
40
|
+
const SMART_COLLECTIONS: {
|
|
41
|
+
slug: string;
|
|
42
|
+
selection: SmartCollection["selection"] | "ideas";
|
|
43
|
+
sort?: SmartCollection["sort"];
|
|
44
|
+
}[] = [
|
|
45
|
+
{ slug: "sc-all-newest", selection: {} },
|
|
46
|
+
{ slug: "sc-all-oldest", selection: {}, sort: "oldest" },
|
|
47
|
+
{ slug: "sc-all-rated", selection: {}, sort: "rating_desc" },
|
|
48
|
+
{ slug: "sc-notes", selection: { format: "note" } },
|
|
49
|
+
// One rated link of two: too few to rank, so the page falls back to newest.
|
|
50
|
+
{ slug: "sc-links", selection: { format: "link" }, sort: "rating_desc" },
|
|
51
|
+
{ slug: "sc-quotes", selection: { format: "quote" } },
|
|
52
|
+
{ slug: "sc-titled", selection: { title: true } },
|
|
53
|
+
{ slug: "sc-untitled", selection: { title: false } },
|
|
54
|
+
{ slug: "sc-2024", selection: { year: 2024 } },
|
|
55
|
+
{ slug: "sc-2025", selection: { year: 2025 } },
|
|
56
|
+
{ slug: "sc-media", selection: { media: "any" } },
|
|
57
|
+
{ slug: "sc-no-media", selection: { media: "none" } },
|
|
58
|
+
{ slug: "sc-images", selection: { media: ["image"] } },
|
|
59
|
+
{ slug: "sc-text-or-video", selection: { media: ["text", "video"] } },
|
|
60
|
+
{ slug: "sc-replies", selection: { replies: true } },
|
|
61
|
+
{ slug: "sc-no-replies", selection: { replies: false } },
|
|
62
|
+
{ slug: "sc-public", selection: { visibility: "public" } },
|
|
63
|
+
{ slug: "sc-hidden", selection: { visibility: "latest_hidden" } },
|
|
64
|
+
{ slug: "sc-featured", selection: { visibility: "featured" } },
|
|
65
|
+
{ slug: "sc-ideas", selection: "ideas" },
|
|
66
|
+
{
|
|
67
|
+
slug: "sc-thoughts",
|
|
68
|
+
selection: { format: "note", title: false },
|
|
69
|
+
sort: "oldest",
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
describe.skipIf(!hugoAvailable)("smart collections on an exported site", () => {
|
|
74
|
+
let siteDir: string;
|
|
75
|
+
const expected = new Map<string, string[]>();
|
|
76
|
+
const built = new Map<string, string[]>();
|
|
77
|
+
const expectedFeed = new Map<string, string[]>();
|
|
78
|
+
const builtFeed = new Map<string, string[]>();
|
|
79
|
+
|
|
80
|
+
beforeAll(async () => {
|
|
81
|
+
const { db: testDb } = createTestDatabase();
|
|
82
|
+
const db = testDb as unknown as Database;
|
|
83
|
+
const siteId = DEFAULT_TEST_SITE_ID;
|
|
84
|
+
const paths = createPathService(db, siteId);
|
|
85
|
+
const posts = createPostService(db, { slugIdLength: 5 }, siteId);
|
|
86
|
+
const smartCollections = createSmartCollectionService(
|
|
87
|
+
db,
|
|
88
|
+
siteId,
|
|
89
|
+
paths,
|
|
90
|
+
posts,
|
|
91
|
+
);
|
|
92
|
+
const collections = createCollectionService(
|
|
93
|
+
db,
|
|
94
|
+
siteId,
|
|
95
|
+
paths,
|
|
96
|
+
undefined,
|
|
97
|
+
undefined,
|
|
98
|
+
smartCollections,
|
|
99
|
+
);
|
|
100
|
+
const media = createMediaService(db, siteId);
|
|
101
|
+
|
|
102
|
+
const ideas = await collections.create({ slug: "ideas", title: "Ideas" });
|
|
103
|
+
|
|
104
|
+
async function post(
|
|
105
|
+
slug: string,
|
|
106
|
+
data: Omit<CreatePost, "slug" | "bodyMarkdown">,
|
|
107
|
+
attachments: MediaKind[] = [],
|
|
108
|
+
) {
|
|
109
|
+
const created = await posts.create({
|
|
110
|
+
slug,
|
|
111
|
+
bodyMarkdown: `Body of ${slug}.`,
|
|
112
|
+
status: "published",
|
|
113
|
+
...data,
|
|
114
|
+
createdAt: data.createdAt ?? data.publishedAt,
|
|
115
|
+
});
|
|
116
|
+
for (const [index, kind] of attachments.entries()) {
|
|
117
|
+
await media.create({
|
|
118
|
+
postId: created.id,
|
|
119
|
+
filename: `${slug}-${index}.bin`,
|
|
120
|
+
originalName: `${slug}-${index}.bin`,
|
|
121
|
+
mimeType: "application/octet-stream",
|
|
122
|
+
size: 1,
|
|
123
|
+
storageKey: `media/${slug}-${index}.bin`,
|
|
124
|
+
mediaKind: kind,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return created;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await post("a-2024-note", {
|
|
131
|
+
format: "note",
|
|
132
|
+
publishedAt: at("2024-03-01T00:00:00Z"),
|
|
133
|
+
rating: 5,
|
|
134
|
+
});
|
|
135
|
+
const titled = await post(
|
|
136
|
+
"b-titled-note",
|
|
137
|
+
{
|
|
138
|
+
format: "note",
|
|
139
|
+
title: "Titled note",
|
|
140
|
+
publishedAt: at("2025-01-10T00:00:00Z"),
|
|
141
|
+
collectionIds: [ideas.id],
|
|
142
|
+
},
|
|
143
|
+
["image"],
|
|
144
|
+
);
|
|
145
|
+
await post("b-reply", {
|
|
146
|
+
format: "note",
|
|
147
|
+
replyToId: titled.id,
|
|
148
|
+
publishedAt: at("2025-06-01T00:00:00Z"),
|
|
149
|
+
});
|
|
150
|
+
await post(
|
|
151
|
+
"c-hidden-link",
|
|
152
|
+
{
|
|
153
|
+
format: "link",
|
|
154
|
+
title: "A link",
|
|
155
|
+
url: "https://example.org/",
|
|
156
|
+
visibility: "latest_hidden",
|
|
157
|
+
publishedAt: at("2025-02-01T00:00:00Z"),
|
|
158
|
+
rating: 3,
|
|
159
|
+
},
|
|
160
|
+
["video"],
|
|
161
|
+
);
|
|
162
|
+
await post("k-newer-link", {
|
|
163
|
+
format: "link",
|
|
164
|
+
title: "Another link",
|
|
165
|
+
url: "https://example.net/",
|
|
166
|
+
publishedAt: at("2025-05-01T00:00:00Z"),
|
|
167
|
+
});
|
|
168
|
+
await post("d-featured-quote", {
|
|
169
|
+
format: "quote",
|
|
170
|
+
title: "Seneca",
|
|
171
|
+
quoteText: "We suffer more in imagination than in reality.",
|
|
172
|
+
publishedAt: at("2023-05-05T00:00:00Z"),
|
|
173
|
+
featuredAt: at("2023-06-01T00:00:00Z"),
|
|
174
|
+
});
|
|
175
|
+
await post(
|
|
176
|
+
"e-untitled-quote",
|
|
177
|
+
{
|
|
178
|
+
format: "quote",
|
|
179
|
+
quoteText: "Unattributed.",
|
|
180
|
+
publishedAt: at("2025-03-01T00:00:00Z"),
|
|
181
|
+
collectionIds: [ideas.id],
|
|
182
|
+
},
|
|
183
|
+
["text"],
|
|
184
|
+
);
|
|
185
|
+
await post("f-private", {
|
|
186
|
+
format: "note",
|
|
187
|
+
visibility: "private",
|
|
188
|
+
publishedAt: at("2025-01-01T00:00:00Z"),
|
|
189
|
+
});
|
|
190
|
+
await post("g-draft", { format: "note", status: "draft" });
|
|
191
|
+
// Two roots published in the same second: the ID decides.
|
|
192
|
+
const tied = await post("h-tied", {
|
|
193
|
+
format: "note",
|
|
194
|
+
publishedAt: at("2025-04-01T12:00:00Z"),
|
|
195
|
+
rating: 4,
|
|
196
|
+
});
|
|
197
|
+
await post("h-draft-reply", {
|
|
198
|
+
format: "note",
|
|
199
|
+
replyToId: tied.id,
|
|
200
|
+
status: "draft",
|
|
201
|
+
});
|
|
202
|
+
await post("i-tied", {
|
|
203
|
+
format: "note",
|
|
204
|
+
publishedAt: at("2025-04-01T12:00:00Z"),
|
|
205
|
+
});
|
|
206
|
+
// The last second of 2024 in UTC, whatever the build machine's zone.
|
|
207
|
+
await post(
|
|
208
|
+
"j-new-years-eve",
|
|
209
|
+
{ format: "note", publishedAt: at("2024-12-31T23:59:59Z") },
|
|
210
|
+
["audio"],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const anonymous = { isAuthenticated: false };
|
|
214
|
+
for (const definition of SMART_COLLECTIONS) {
|
|
215
|
+
const smartCollection = await smartCollections.create({
|
|
216
|
+
slug: definition.slug,
|
|
217
|
+
title: definition.slug,
|
|
218
|
+
selection:
|
|
219
|
+
definition.selection === "ideas"
|
|
220
|
+
? { collection: [ideas.id] }
|
|
221
|
+
: definition.selection,
|
|
222
|
+
sort: definition.sort,
|
|
223
|
+
});
|
|
224
|
+
// What the page shows: the stored order, unless it ranks by rating
|
|
225
|
+
// and too little is rated for that to mean anything.
|
|
226
|
+
const rated = await posts.countUpTo(
|
|
227
|
+
{
|
|
228
|
+
...smartCollections.toPostFilters(smartCollection, anonymous),
|
|
229
|
+
hasRating: true,
|
|
230
|
+
},
|
|
231
|
+
2,
|
|
232
|
+
);
|
|
233
|
+
const sort =
|
|
234
|
+
smartCollection.sort === "rating_desc" &&
|
|
235
|
+
!supportsCollectionRatingSort(rated)
|
|
236
|
+
? "newest"
|
|
237
|
+
: smartCollection.sort;
|
|
238
|
+
const list = await posts.list({
|
|
239
|
+
...smartCollections.toPostFilters(
|
|
240
|
+
{ ...smartCollection, sort },
|
|
241
|
+
anonymous,
|
|
242
|
+
),
|
|
243
|
+
limit: 1000,
|
|
244
|
+
});
|
|
245
|
+
expected.set(
|
|
246
|
+
definition.slug,
|
|
247
|
+
list.map((item) => item.slug),
|
|
248
|
+
);
|
|
249
|
+
// The feed keeps the stored order, fallback or not.
|
|
250
|
+
const feedList = await posts.list({
|
|
251
|
+
...smartCollections.toPostFilters(smartCollection, anonymous),
|
|
252
|
+
limit: 50,
|
|
253
|
+
});
|
|
254
|
+
expectedFeed.set(
|
|
255
|
+
definition.slug,
|
|
256
|
+
feedList.map((item) => item.slug),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// A small page size, so the pages are read across pagination.
|
|
261
|
+
siteDir = await buildHugoSite(
|
|
262
|
+
await createExportService(
|
|
263
|
+
{ posts, paths, collections, media },
|
|
264
|
+
makeSiteConfig({ siteUrl: SITE_URL, pageSize: 4 }),
|
|
265
|
+
{ storage: null, bundleMedia: false },
|
|
266
|
+
).generateHugoFiles(),
|
|
267
|
+
);
|
|
268
|
+
for (const { slug } of SMART_COLLECTIONS) {
|
|
269
|
+
built.set(slug, await readThreadSlugs(siteDir, slug));
|
|
270
|
+
builtFeed.set(slug, await readFeedSlugs(siteDir, slug, SITE_URL));
|
|
271
|
+
}
|
|
272
|
+
}, 60_000);
|
|
273
|
+
|
|
274
|
+
afterAll(async () => {
|
|
275
|
+
if (siteDir) await rm(siteDir, { recursive: true, force: true });
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it.each(SMART_COLLECTIONS.map(({ slug }) => slug))(
|
|
279
|
+
"%s lists the posts Jant lists, in its order",
|
|
280
|
+
(slug) => {
|
|
281
|
+
expect(built.get(slug)).toEqual(expected.get(slug));
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Equal lists prove nothing if the fixture leaves them empty or alike.
|
|
286
|
+
it("runs on a fixture that tells the conditions apart", () => {
|
|
287
|
+
expect(expected.get("sc-thoughts")).toEqual([
|
|
288
|
+
"a-2024-note",
|
|
289
|
+
"j-new-years-eve",
|
|
290
|
+
"h-tied",
|
|
291
|
+
"i-tied",
|
|
292
|
+
]);
|
|
293
|
+
expect(expected.get("sc-all-rated")?.slice(0, 3)).toEqual([
|
|
294
|
+
"a-2024-note",
|
|
295
|
+
"h-tied",
|
|
296
|
+
"c-hidden-link",
|
|
297
|
+
]);
|
|
298
|
+
expect(expected.get("sc-links")).toEqual(["k-newer-link", "c-hidden-link"]);
|
|
299
|
+
// The page falls back to newest with one rated link; the feed doesn't.
|
|
300
|
+
expect(expectedFeed.get("sc-links")).toEqual([
|
|
301
|
+
"c-hidden-link",
|
|
302
|
+
"k-newer-link",
|
|
303
|
+
]);
|
|
304
|
+
expect(expected.get("sc-titled")).toEqual([
|
|
305
|
+
"b-titled-note",
|
|
306
|
+
"k-newer-link",
|
|
307
|
+
"c-hidden-link",
|
|
308
|
+
"d-featured-quote",
|
|
309
|
+
]);
|
|
310
|
+
expect(expected.get("sc-2024")).toEqual(["j-new-years-eve", "a-2024-note"]);
|
|
311
|
+
expect(expected.get("sc-replies")).toEqual(["b-titled-note"]);
|
|
312
|
+
expect(expected.get("sc-text-or-video")).toEqual([
|
|
313
|
+
"e-untitled-quote",
|
|
314
|
+
"c-hidden-link",
|
|
315
|
+
]);
|
|
316
|
+
expect(expected.get("sc-all-newest")).not.toContain("f-private");
|
|
317
|
+
expect(expected.get("sc-all-newest")).not.toContain("g-draft");
|
|
318
|
+
for (const { slug } of SMART_COLLECTIONS) {
|
|
319
|
+
expect(expected.get(slug)?.length, slug).toBeGreaterThan(0);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it.each(SMART_COLLECTIONS.map(({ slug }) => slug))(
|
|
324
|
+
"%s feed carries what Jant's feed carries, in its order",
|
|
325
|
+
(slug) => {
|
|
326
|
+
expect(builtFeed.get(slug)).toEqual(expectedFeed.get(slug));
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `src/styles`, resolved from the working directory rather than
|
|
6
|
+
* `import.meta.url`: under the `happy-dom` environment `import.meta.url` is an
|
|
7
|
+
* `http:` URL that `readFileSync` rejects, and Vitest runs with the package
|
|
8
|
+
* root as its working directory in every environment.
|
|
9
|
+
*/
|
|
10
|
+
const STYLES_DIR = resolve(process.cwd(), "src/styles");
|
|
11
|
+
|
|
12
|
+
/** Hand-written component CSS a signed-out visitor downloads. */
|
|
13
|
+
const READER_SHEETS = ["ui.css", "components.css"] as const;
|
|
14
|
+
|
|
15
|
+
/** Hand-written component CSS that only loads on an authenticated page. */
|
|
16
|
+
const AUTHOR_SHEETS = ["ui-author.css", "components-author.css"] as const;
|
|
17
|
+
|
|
18
|
+
function read(names: readonly string[]): string {
|
|
19
|
+
return names
|
|
20
|
+
.map((name) => readFileSync(resolve(STYLES_DIR, name), "utf8"))
|
|
21
|
+
.join("\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every `@layer components` rule Jant hand-writes, both audiences together.
|
|
26
|
+
*
|
|
27
|
+
* `ui.css`/`components.css` carry what readers need; `ui-author.css` and
|
|
28
|
+
* `components-author.css` carry the composer, editor, settings, config editor
|
|
29
|
+
* and dash chrome that only load once someone is signed in. A test asserting
|
|
30
|
+
* that a rule is declared cares that it exists at all, not which half it
|
|
31
|
+
* landed in — so moving a rule between them must never break a test.
|
|
32
|
+
*
|
|
33
|
+
* @returns The concatenated contents of all four stylesheets.
|
|
34
|
+
* @example
|
|
35
|
+
* expect(readComponentCss()).toMatch(/\.compose-post-meta-panel\s*\{/);
|
|
36
|
+
*/
|
|
37
|
+
export function readComponentCss(): string {
|
|
38
|
+
return read([...READER_SHEETS, ...AUTHOR_SHEETS]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Only the reader halves.
|
|
43
|
+
*
|
|
44
|
+
* Use when the point of the assertion is that a rule ships to signed-out
|
|
45
|
+
* visitors — or, with a negative matcher, that it does not.
|
|
46
|
+
*
|
|
47
|
+
* @returns The concatenated contents of `ui.css` and `components.css`.
|
|
48
|
+
* @example
|
|
49
|
+
* expect(readReaderCss()).not.toMatch(/\.compose-dialog\s*\{/);
|
|
50
|
+
*/
|
|
51
|
+
export function readReaderCss(): string {
|
|
52
|
+
return read(READER_SHEETS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Only the author halves.
|
|
57
|
+
*
|
|
58
|
+
* @returns The concatenated contents of `ui-author.css` and
|
|
59
|
+
* `components-author.css`.
|
|
60
|
+
* @example
|
|
61
|
+
* expect(readAuthorCss()).toMatch(/\.settings-root\s*\{/);
|
|
62
|
+
*/
|
|
63
|
+
export function readAuthorCss(): string {
|
|
64
|
+
return read(AUTHOR_SHEETS);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Every stylesheet Jant hand-writes, each paired with its filename.
|
|
69
|
+
*
|
|
70
|
+
* `readComponentCss()` and friends concatenate, which is right for asking
|
|
71
|
+
* whether a rule exists. This one keeps the files apart so a failure can name
|
|
72
|
+
* the file and line it found the problem on — `tokens.css` and `preset.css`
|
|
73
|
+
* are in here too, because breakpoints live in all six.
|
|
74
|
+
*
|
|
75
|
+
* @returns One entry per stylesheet, in load order.
|
|
76
|
+
* @example
|
|
77
|
+
* for (const { name, css } of readAllStylesheets()) {
|
|
78
|
+
* expect(css, name).not.toMatch(/@media \(max-width: 767px\)/);
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
export function readAllStylesheets(): readonly {
|
|
82
|
+
name: string;
|
|
83
|
+
css: string;
|
|
84
|
+
}[] {
|
|
85
|
+
return [
|
|
86
|
+
{ name: "preset.css", path: resolve(process.cwd(), "src/preset.css") },
|
|
87
|
+
...[...READER_SHEETS, ...AUTHOR_SHEETS, "tokens.css", "site-media.css"].map(
|
|
88
|
+
(name) => ({ name, path: resolve(STYLES_DIR, name) }),
|
|
89
|
+
),
|
|
90
|
+
].map(({ name, path }) => ({ name, css: readFileSync(path, "utf8") }));
|
|
91
|
+
}
|
|
@@ -134,3 +134,36 @@ export function createTestDatabase(options?: { fts?: boolean }) {
|
|
|
134
134
|
|
|
135
135
|
return { db, sqlite };
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
/** The most bound parameters Cloudflare D1 accepts in one statement. */
|
|
139
|
+
export const D1_MAX_BOUND_PARAMETERS = 100;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Make a test database refuse what D1 refuses: a statement with more than
|
|
143
|
+
* {@link D1_MAX_BOUND_PARAMETERS} bound parameters.
|
|
144
|
+
*
|
|
145
|
+
* better-sqlite3 accepts tens of thousands, so a query that puts every id in
|
|
146
|
+
* one `IN (…)` list passes every test and fails in production once the list
|
|
147
|
+
* grows. Install this after seeding, so only the code under test is held to it.
|
|
148
|
+
*
|
|
149
|
+
* @param sqlite - The handle `createTestDatabase` or `createTestApp` returned
|
|
150
|
+
* @returns Nothing; the handle is changed in place
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* const { app, sqlite } = createTestApp();
|
|
154
|
+
* await seed(app);
|
|
155
|
+
* enforceD1BoundParameterLimit(sqlite);
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export function enforceD1BoundParameterLimit(sqlite: Database.Database): void {
|
|
159
|
+
const prepare = sqlite.prepare.bind(sqlite);
|
|
160
|
+
sqlite.prepare = ((source: string) => {
|
|
161
|
+
const count = source.match(/\?/g)?.length ?? 0;
|
|
162
|
+
if (count > D1_MAX_BOUND_PARAMETERS) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`too many SQL variables: ${count} bound parameters, D1 allows ${D1_MAX_BOUND_PARAMETERS}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return prepare(source);
|
|
168
|
+
}) as Database.Database["prepare"];
|
|
169
|
+
}
|
|
@@ -87,10 +87,13 @@ export function makeSiteConfig(over: Partial<SiteConfig> = {}): SiteConfig {
|
|
|
87
87
|
siteUrl: "https://example.com",
|
|
88
88
|
siteDescription: "Export fixture",
|
|
89
89
|
siteLanguage: "en",
|
|
90
|
+
multilingualEnabled: false,
|
|
91
|
+
additionalLanguages: [],
|
|
90
92
|
showJantBrandingOnHome: true,
|
|
91
93
|
publicApiEnabled: true,
|
|
92
94
|
rssFeedsEnabled: true,
|
|
93
95
|
mainRssFeed: "latest",
|
|
96
|
+
archiveDefaultLayout: "list",
|
|
94
97
|
siteFooter: "",
|
|
95
98
|
showHeaderAvatar: false,
|
|
96
99
|
siteAvatarUrl: "",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build an export with the real Hugo binary and read lists back out of it.
|
|
3
|
+
*
|
|
4
|
+
* For the tests that hold an exported page to the list Jant shows: the theme
|
|
5
|
+
* reimplements membership and order in Go templates, and only a real build
|
|
6
|
+
* shows what a reader of the static site gets.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { isStoredExportFile, type ExportFile } from "../../services/export.js";
|
|
14
|
+
|
|
15
|
+
/** Whether `hugo` is on PATH. Tests that need it skip without it. */
|
|
16
|
+
export const hugoAvailable =
|
|
17
|
+
spawnSync("hugo", ["version"], { stdio: "ignore" }).status === 0;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Write an export to a temporary directory and build it, unminified so the
|
|
21
|
+
* markup reads back predictably.
|
|
22
|
+
*
|
|
23
|
+
* @param files - What `generateHugoFiles` returned; stored media is skipped
|
|
24
|
+
* @returns The site directory; the build is in its `public/`
|
|
25
|
+
* @throws {Error} With Hugo's output when the build fails
|
|
26
|
+
* @example
|
|
27
|
+
* const siteDir = await buildHugoSite(await exporter.generateHugoFiles());
|
|
28
|
+
*/
|
|
29
|
+
export async function buildHugoSite(files: ExportFile[]): Promise<string> {
|
|
30
|
+
const siteDir = await mkdtemp(join(tmpdir(), "jant-hugo-site-"));
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
if (isStoredExportFile(file)) continue;
|
|
33
|
+
const target = join(siteDir, file.path);
|
|
34
|
+
await mkdir(dirname(target), { recursive: true });
|
|
35
|
+
await writeFile(target, file.content);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { code, log } = await new Promise<{ code: number; log: string }>(
|
|
39
|
+
(resolve) => {
|
|
40
|
+
const child = spawn("hugo", ["--source", siteDir], {
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
42
|
+
});
|
|
43
|
+
let output = "";
|
|
44
|
+
child.stdout?.on("data", (chunk) => (output += chunk.toString()));
|
|
45
|
+
child.stderr?.on("data", (chunk) => (output += chunk.toString()));
|
|
46
|
+
child.on("close", (exitCode) =>
|
|
47
|
+
resolve({ code: exitCode ?? -1, log: output }),
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
if (code !== 0) throw new Error(`Hugo build failed:\n${log}`);
|
|
52
|
+
return siteDir;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The Thread roots a collection-style page lists, in order, across all of
|
|
57
|
+
* its pages.
|
|
58
|
+
*
|
|
59
|
+
* @param siteDir - From {@link buildHugoSite}
|
|
60
|
+
* @param section - The section's slug
|
|
61
|
+
* @returns Root slugs, first page first
|
|
62
|
+
* @example
|
|
63
|
+
* await readThreadSlugs(siteDir, "ideas"); // ["newest-thread", ...]
|
|
64
|
+
*/
|
|
65
|
+
export async function readThreadSlugs(
|
|
66
|
+
siteDir: string,
|
|
67
|
+
section: string,
|
|
68
|
+
): Promise<string[]> {
|
|
69
|
+
const slugs: string[] = [];
|
|
70
|
+
for (let page = 1; ; page++) {
|
|
71
|
+
const path =
|
|
72
|
+
page === 1
|
|
73
|
+
? join(siteDir, "public", section, "index.html")
|
|
74
|
+
: join(siteDir, "public", section, "page", String(page), "index.html");
|
|
75
|
+
const html = await readFile(path, "utf8").catch(() => null);
|
|
76
|
+
if (html === null) return slugs;
|
|
77
|
+
for (const match of html.matchAll(
|
|
78
|
+
/<div class="thread thread-full[^"]*" data-slug="([^"]+)"/g,
|
|
79
|
+
)) {
|
|
80
|
+
slugs.push(match[1] as string);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The posts a section's Atom feed carries, in order, by slug.
|
|
87
|
+
*
|
|
88
|
+
* @param siteDir - From {@link buildHugoSite}
|
|
89
|
+
* @param section - The section's slug
|
|
90
|
+
* @param siteUrl - The export's site URL, which permalinks start with
|
|
91
|
+
* @returns One slug per entry
|
|
92
|
+
* @example
|
|
93
|
+
* await readFeedSlugs(siteDir, "ideas", "https://example.com");
|
|
94
|
+
*/
|
|
95
|
+
export async function readFeedSlugs(
|
|
96
|
+
siteDir: string,
|
|
97
|
+
section: string,
|
|
98
|
+
siteUrl: string,
|
|
99
|
+
): Promise<string[]> {
|
|
100
|
+
const xml = await readFile(
|
|
101
|
+
join(siteDir, "public", section, "index.xml"),
|
|
102
|
+
"utf8",
|
|
103
|
+
);
|
|
104
|
+
const permalink = new RegExp(
|
|
105
|
+
`href="${siteUrl.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}/([^/"]+)/"`,
|
|
106
|
+
);
|
|
107
|
+
return [...xml.matchAll(/<entry>([\s\S]*?)<\/entry>/g)].map(
|
|
108
|
+
(entry) => entry[1]?.match(permalink)?.[1] ?? "",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The Thread roots a timeline page lists, page by page.
|
|
114
|
+
*
|
|
115
|
+
* A timeline renders each root as a thread preview or a lone post card, and
|
|
116
|
+
* both nest cards that carry their own `data-slug`, so this reads every
|
|
117
|
+
* `data-slug` in order and keeps the first mention of each root.
|
|
118
|
+
*
|
|
119
|
+
* @param siteDir - From {@link buildHugoSite}
|
|
120
|
+
* @param section - The section's slug, or `""` for the home page
|
|
121
|
+
* @param roots - Slugs of every Thread root in the export
|
|
122
|
+
* @returns One list of root slugs per page
|
|
123
|
+
* @example
|
|
124
|
+
* await readRootSlugsByPage(siteDir, "featured", new Set(["hello"]));
|
|
125
|
+
*/
|
|
126
|
+
export async function readRootSlugsByPage(
|
|
127
|
+
siteDir: string,
|
|
128
|
+
section: string,
|
|
129
|
+
roots: ReadonlySet<string>,
|
|
130
|
+
): Promise<string[][]> {
|
|
131
|
+
const pages: string[][] = [];
|
|
132
|
+
for (let page = 1; ; page++) {
|
|
133
|
+
const path =
|
|
134
|
+
page === 1
|
|
135
|
+
? join(siteDir, "public", section, "index.html")
|
|
136
|
+
: join(siteDir, "public", section, "page", String(page), "index.html");
|
|
137
|
+
const html = await readFile(path, "utf8").catch(() => null);
|
|
138
|
+
if (html === null) return pages;
|
|
139
|
+
const slugs: string[] = [];
|
|
140
|
+
for (const match of html.matchAll(/data-slug="([^"]+)"/g)) {
|
|
141
|
+
const slug = match[1] as string;
|
|
142
|
+
if (roots.has(slug) && !slugs.includes(slug)) slugs.push(slug);
|
|
143
|
+
}
|
|
144
|
+
pages.push(slugs);
|
|
145
|
+
}
|
|
146
|
+
}
|