@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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-trip budget for the post page.
|
|
3
|
+
*
|
|
4
|
+
* On Workers a public page's cost is almost entirely the number of sequential
|
|
5
|
+
* D1 reads it makes — the same render against local SQLite is about a
|
|
6
|
+
* millisecond. Serving `/{slug}` used to read `path_registry` twice for one
|
|
7
|
+
* address and the `post` table four times for one post, and nothing failed
|
|
8
|
+
* when it did. These tests are that missing failure.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
13
|
+
import { withConfig } from "../../../middleware/config.js";
|
|
14
|
+
import { i18nMiddleware } from "../../../i18n/index.js";
|
|
15
|
+
import { normalizePath, toPublicHref } from "../../../lib/url.js";
|
|
16
|
+
import { pageRoutes } from "../page.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Mount the catch-all behind the middleware `createApp()` puts in front of it.
|
|
20
|
+
*
|
|
21
|
+
* The stored-redirect middleware is reproduced rather than imported because it
|
|
22
|
+
* is defined inline in `app.tsx`; what is under test is that it hands its
|
|
23
|
+
* lookup on and the route takes it, so the stash is the part that must match.
|
|
24
|
+
*/
|
|
25
|
+
function createPostPageApp() {
|
|
26
|
+
const testApp = createTestApp();
|
|
27
|
+
const { app } = testApp;
|
|
28
|
+
|
|
29
|
+
app.use("*", async (c, next) => {
|
|
30
|
+
c.set("publicPath", c.req.path);
|
|
31
|
+
c.set("publicRequestUrl", c.req.url);
|
|
32
|
+
await next();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
app.use("*", async (c, next) => {
|
|
36
|
+
const storedPath = normalizePath(new URL(c.req.url).pathname);
|
|
37
|
+
const record = await c.var.services.paths.resolve(storedPath);
|
|
38
|
+
c.set("pathLookup", { path: storedPath, record });
|
|
39
|
+
if (record?.kind === "redirect" && record.redirectToPath) {
|
|
40
|
+
return c.redirect(
|
|
41
|
+
toPublicHref(`/${record.redirectToPath}`),
|
|
42
|
+
record.redirectType ?? 301,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
await next();
|
|
46
|
+
});
|
|
47
|
+
app.use("*", withConfig());
|
|
48
|
+
app.use("*", i18nMiddleware());
|
|
49
|
+
app.route("/", pageRoutes);
|
|
50
|
+
|
|
51
|
+
return testApp;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Every SQL statement prepared while `run` executes. */
|
|
55
|
+
async function captureStatements<T>(
|
|
56
|
+
sqlite: ReturnType<typeof createTestApp>["sqlite"],
|
|
57
|
+
run: () => Promise<T>,
|
|
58
|
+
): Promise<[T, string[]]> {
|
|
59
|
+
const original = sqlite.prepare.bind(sqlite);
|
|
60
|
+
const statements: string[] = [];
|
|
61
|
+
const spy = vi.spyOn(sqlite, "prepare").mockImplementation(((
|
|
62
|
+
source: string,
|
|
63
|
+
) => {
|
|
64
|
+
statements.push(source.replace(/\s+/g, " "));
|
|
65
|
+
return original(source);
|
|
66
|
+
}) as typeof sqlite.prepare);
|
|
67
|
+
try {
|
|
68
|
+
return [await run(), statements];
|
|
69
|
+
} finally {
|
|
70
|
+
spy.mockRestore();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Reads that resolve one address, i.e. filter `path_registry` by `path`. */
|
|
75
|
+
function countAddressLookups(statements: string[]): number {
|
|
76
|
+
return statements.filter(
|
|
77
|
+
(sql) =>
|
|
78
|
+
/from "path_registry"/i.test(sql) &&
|
|
79
|
+
/"path_registry"\."path" = \?/i.test(sql),
|
|
80
|
+
).length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Reads that scan the `post` table. */
|
|
84
|
+
function countPostReads(statements: string[]): number {
|
|
85
|
+
return statements.filter((sql) => /from "post"/i.test(sql)).length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe("post page round trips", () => {
|
|
89
|
+
it("resolves the requested address once, and keeps its post reads to three", async () => {
|
|
90
|
+
const { app, services, sqlite } = createPostPageApp();
|
|
91
|
+
const post = await services.posts.create({
|
|
92
|
+
format: "note",
|
|
93
|
+
title: "Budget",
|
|
94
|
+
bodyMarkdown: "Body",
|
|
95
|
+
status: "published",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const [res, statements] = await captureStatements(sqlite, () =>
|
|
99
|
+
app.request(`/${post.slug}`),
|
|
100
|
+
);
|
|
101
|
+
expect(res.status).toBe(200);
|
|
102
|
+
|
|
103
|
+
// The middleware's lookup is the route's lookup.
|
|
104
|
+
expect(countAddressLookups(statements)).toBe(1);
|
|
105
|
+
|
|
106
|
+
// Three reads, and each is for something different: the post with its
|
|
107
|
+
// canonical alias, its thread, and the thread ids that collection
|
|
108
|
+
// membership is keyed by. A root post's visibility rides along in its own
|
|
109
|
+
// row, so nothing goes back for that.
|
|
110
|
+
expect(countPostReads(statements)).toBe(3);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("redirects a slug to its custom URL without a separate alias lookup", async () => {
|
|
114
|
+
const { app, services, sqlite } = createPostPageApp();
|
|
115
|
+
const post = await services.posts.create({
|
|
116
|
+
format: "note",
|
|
117
|
+
title: "Renamed",
|
|
118
|
+
bodyMarkdown: "Body",
|
|
119
|
+
status: "published",
|
|
120
|
+
});
|
|
121
|
+
await services.customUrls.create({
|
|
122
|
+
path: "renamed-url",
|
|
123
|
+
targetType: "post",
|
|
124
|
+
targetId: post.id,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const [res, statements] = await captureStatements(sqlite, () =>
|
|
128
|
+
app.request(`/${post.slug}`),
|
|
129
|
+
);
|
|
130
|
+
expect(res.status).toBe(301);
|
|
131
|
+
expect(res.headers.get("location")).toBe("/renamed-url");
|
|
132
|
+
|
|
133
|
+
// The address lookup, and nothing more: the alias came back with the post.
|
|
134
|
+
expect(countAddressLookups(statements)).toBe(1);
|
|
135
|
+
expect(countPostReads(statements)).toBe(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("takes the newest alias as the canonical address", async () => {
|
|
139
|
+
const { app, services } = createPostPageApp();
|
|
140
|
+
const post = await services.posts.create({
|
|
141
|
+
format: "note",
|
|
142
|
+
title: "Renamed twice",
|
|
143
|
+
bodyMarkdown: "Body",
|
|
144
|
+
status: "published",
|
|
145
|
+
});
|
|
146
|
+
await services.customUrls.create({
|
|
147
|
+
path: "first-name",
|
|
148
|
+
targetType: "post",
|
|
149
|
+
targetId: post.id,
|
|
150
|
+
});
|
|
151
|
+
await services.customUrls.create({
|
|
152
|
+
path: "second-name",
|
|
153
|
+
targetType: "post",
|
|
154
|
+
targetId: post.id,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const res = await app.request(`/${post.slug}`);
|
|
158
|
+
expect(res.status).toBe(301);
|
|
159
|
+
expect(res.headers.get("location")).toBe("/second-name");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("post list round trips", () => {
|
|
164
|
+
it("reads thread-root visibility from the rows it already loaded", async () => {
|
|
165
|
+
const { services, sqlite } = createTestApp();
|
|
166
|
+
for (let i = 0; i < 3; i++) {
|
|
167
|
+
await services.posts.create({
|
|
168
|
+
format: "note",
|
|
169
|
+
title: `Post ${i}`,
|
|
170
|
+
bodyMarkdown: "Body",
|
|
171
|
+
status: "published",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const [listed, statements] = await captureStatements(sqlite, () =>
|
|
176
|
+
services.posts.list({ status: "published", excludeReplies: true }),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(listed).toHaveLength(3);
|
|
180
|
+
// A list of thread roots carries its own visibility, so the only `post`
|
|
181
|
+
// read is the list itself.
|
|
182
|
+
expect(countPostReads(statements)).toBe(1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("still looks up the root for a reply loaded on its own", async () => {
|
|
186
|
+
const { services, sqlite } = createTestApp();
|
|
187
|
+
const root = await services.posts.create({
|
|
188
|
+
format: "note",
|
|
189
|
+
title: "Root",
|
|
190
|
+
bodyMarkdown: "Body",
|
|
191
|
+
status: "published",
|
|
192
|
+
visibility: "private",
|
|
193
|
+
});
|
|
194
|
+
const reply = await services.posts.create({
|
|
195
|
+
format: "note",
|
|
196
|
+
bodyMarkdown: "Reply",
|
|
197
|
+
replyToId: root.id,
|
|
198
|
+
status: "published",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const [loaded, statements] = await captureStatements(sqlite, () =>
|
|
202
|
+
services.posts.getById(reply.id),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// A reply's own visibility column is null — it inherits the root's — and
|
|
206
|
+
// the root is not among the rows loaded here, so the second read is what
|
|
207
|
+
// makes the reply private too.
|
|
208
|
+
expect(countPostReads(statements)).toBe(2);
|
|
209
|
+
expect(loaded?.visibility).toBe("private");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
|
+
import { partialPageRoutes } from "../partials.js";
|
|
4
|
+
|
|
5
|
+
function createPartialTestApp(options: { authenticated?: boolean } = {}) {
|
|
6
|
+
const testApp = createTestApp(options);
|
|
7
|
+
testApp.app.route("/", partialPageRoutes);
|
|
8
|
+
return testApp;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("/_/post-preview/:postId", () => {
|
|
12
|
+
it("renders the post the way its own page does", async () => {
|
|
13
|
+
// The composer shows this while writing a translation, and the original's
|
|
14
|
+
// structure is part of what gets translated — so it has to arrive as the
|
|
15
|
+
// post, not as a body-only excerpt.
|
|
16
|
+
const { app, services } = createPartialTestApp({ authenticated: true });
|
|
17
|
+
const post = await services.posts.create({
|
|
18
|
+
format: "note",
|
|
19
|
+
title: "Yirgacheffe",
|
|
20
|
+
bodyMarkdown: "## Brewing\n\nNinety-two degrees was too hot.",
|
|
21
|
+
status: "published",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const res = await app.request(`/_/post-preview/${post.id}`);
|
|
25
|
+
|
|
26
|
+
expect(res.status).toBe(200);
|
|
27
|
+
const html = await res.text();
|
|
28
|
+
expect(html).toContain("Yirgacheffe");
|
|
29
|
+
expect(html).toContain("Brewing");
|
|
30
|
+
expect(html).toContain("Ninety-two degrees was too hot.");
|
|
31
|
+
// Detail styling, not the feed card's summary.
|
|
32
|
+
expect(html).toContain("post-detail-title");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("carries a quote's attribution, which a body-only preview would drop", async () => {
|
|
36
|
+
const { app, services } = createPartialTestApp({ authenticated: true });
|
|
37
|
+
// A Quote stores its attribution in `title` / `url`; the API surfaces the
|
|
38
|
+
// same two as `sourceName` / `sourceUrl`.
|
|
39
|
+
const post = await services.posts.create({
|
|
40
|
+
format: "quote",
|
|
41
|
+
quoteText: "Focus is about saying no.",
|
|
42
|
+
title: "Some Author",
|
|
43
|
+
url: "https://example.com/focus",
|
|
44
|
+
status: "published",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const html = await (await app.request(`/_/post-preview/${post.id}`)).text();
|
|
48
|
+
|
|
49
|
+
expect(html).toContain("Focus is about saying no.");
|
|
50
|
+
expect(html).toContain("Some Author");
|
|
51
|
+
expect(html).toContain("https://example.com/focus");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("offers no control that would act on the post being previewed", async () => {
|
|
55
|
+
// Every action in the composer belongs to the post being written. A menu
|
|
56
|
+
// or reply trigger here would aim at the wrong one.
|
|
57
|
+
const { app, services } = createPartialTestApp({ authenticated: true });
|
|
58
|
+
const post = await services.posts.create({
|
|
59
|
+
format: "note",
|
|
60
|
+
title: "Yirgacheffe",
|
|
61
|
+
bodyMarkdown: "Body",
|
|
62
|
+
status: "published",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const html = await (await app.request(`/_/post-preview/${post.id}`)).text();
|
|
66
|
+
|
|
67
|
+
expect(html).not.toContain("data-post-menu-trigger");
|
|
68
|
+
expect(html).not.toContain("data-reply-trigger");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("prints a titled post's date once, not in both header and footer", async () => {
|
|
72
|
+
// The card drops the footer's copy for a titled detail post, but only when
|
|
73
|
+
// `hideTimestamp` is left undefined — stating it either way brings it back.
|
|
74
|
+
const { app, services } = createPartialTestApp({ authenticated: true });
|
|
75
|
+
const post = await services.posts.create({
|
|
76
|
+
format: "note",
|
|
77
|
+
title: "Yirgacheffe",
|
|
78
|
+
bodyMarkdown: "Body",
|
|
79
|
+
status: "published",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const html = await (await app.request(`/_/post-preview/${post.id}`)).text();
|
|
83
|
+
|
|
84
|
+
expect(html.match(/<time/g) ?? []).toHaveLength(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("404s on a post that is not there", async () => {
|
|
88
|
+
const { app } = createPartialTestApp({ authenticated: true });
|
|
89
|
+
|
|
90
|
+
const res = await app.request(
|
|
91
|
+
"/_/post-preview/pst_01000000000000000000000000",
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(res.status).toBe(404);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
|
+
import { pageRoutes } from "../page.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The smart collection page, end to end through the path registry.
|
|
7
|
+
*
|
|
8
|
+
* The property these guard is the one the design turns on: a smart collection
|
|
9
|
+
* is always public, and the posts inside it are exactly as visible as they are
|
|
10
|
+
* anywhere else. Those are different sentences, and confusing them is how a
|
|
11
|
+
* page ends up either 404ing for readers or leaking a draft.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function setup(authenticated = false) {
|
|
15
|
+
const { app, services } = createTestApp({ authenticated });
|
|
16
|
+
app.route("/", pageRoutes);
|
|
17
|
+
return { app, services };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function seedQuotes(services: {
|
|
21
|
+
posts: {
|
|
22
|
+
create: (input: Record<string, unknown>) => Promise<{ id: string }>;
|
|
23
|
+
};
|
|
24
|
+
}) {
|
|
25
|
+
await services.posts.create({
|
|
26
|
+
format: "quote",
|
|
27
|
+
quoteText: "A public quote",
|
|
28
|
+
bodyMarkdown: "public quote body",
|
|
29
|
+
status: "published",
|
|
30
|
+
});
|
|
31
|
+
await services.posts.create({
|
|
32
|
+
format: "quote",
|
|
33
|
+
quoteText: "A private quote",
|
|
34
|
+
bodyMarkdown: "private quote body",
|
|
35
|
+
status: "published",
|
|
36
|
+
visibility: "private",
|
|
37
|
+
});
|
|
38
|
+
await services.posts.create({
|
|
39
|
+
format: "note",
|
|
40
|
+
bodyMarkdown: "a plain note body",
|
|
41
|
+
status: "published",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("smart collection page", () => {
|
|
46
|
+
it("renders for a signed-out reader, with title, count, and description", async () => {
|
|
47
|
+
const { app, services } = setup();
|
|
48
|
+
await seedQuotes(services);
|
|
49
|
+
await services.smartCollections.create({
|
|
50
|
+
slug: "quotes",
|
|
51
|
+
title: "Quotes",
|
|
52
|
+
description: "Things worth keeping.",
|
|
53
|
+
selection: { format: "quote" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const res = await app.request("/quotes");
|
|
57
|
+
expect(res.status).toBe(200);
|
|
58
|
+
const html = await res.text();
|
|
59
|
+
|
|
60
|
+
expect(html).toContain("Quotes");
|
|
61
|
+
expect(html).toContain("Things worth keeping.");
|
|
62
|
+
// A written description takes the slot; the conditions — and the way to
|
|
63
|
+
// the archive — stay reachable through the marker beside the title.
|
|
64
|
+
expect(html).not.toContain(">Automatically collects: Quotes<");
|
|
65
|
+
expect(html).toContain('title="Automatically collects: Quotes"');
|
|
66
|
+
expect(html).toContain(
|
|
67
|
+
'<a href="/archive?format=quote" class="collection-page-smart-icon"',
|
|
68
|
+
);
|
|
69
|
+
expect(html).toContain("public quote body");
|
|
70
|
+
expect(html).not.toContain("a plain note body");
|
|
71
|
+
// The archive has a chip bar; this page does not.
|
|
72
|
+
expect(html).not.toContain("archive-filters-chips");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("describes itself by its conditions until a description is written", async () => {
|
|
76
|
+
const { app, services } = setup();
|
|
77
|
+
await seedQuotes(services);
|
|
78
|
+
await services.smartCollections.create({
|
|
79
|
+
slug: "quotes",
|
|
80
|
+
title: "Quotes",
|
|
81
|
+
selection: { format: "quote" },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const html = await (await app.request("/quotes")).text();
|
|
85
|
+
expect(html).toContain(">Automatically collects: Quotes<");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("links its conditions to the archive showing the same posts", async () => {
|
|
89
|
+
const { app, services } = setup();
|
|
90
|
+
await seedQuotes(services);
|
|
91
|
+
await services.smartCollections.create({
|
|
92
|
+
slug: "quotes",
|
|
93
|
+
title: "Quotes",
|
|
94
|
+
selection: { format: "quote", media: "none" },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const html = await (await app.request("/quotes")).text();
|
|
98
|
+
expect(html).toContain("/archive?format=quote&media=none");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("says so when nothing matches, rather than rendering blank", async () => {
|
|
102
|
+
const { app, services } = setup();
|
|
103
|
+
await seedQuotes(services);
|
|
104
|
+
await services.smartCollections.create({
|
|
105
|
+
slug: "links",
|
|
106
|
+
title: "Links",
|
|
107
|
+
selection: { format: "link" },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const html = await (await app.request("/links")).text();
|
|
111
|
+
expect(html).toContain("Nothing matches these conditions yet.");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("collects every post when no condition narrows it", async () => {
|
|
115
|
+
const { app, services } = setup();
|
|
116
|
+
await seedQuotes(services);
|
|
117
|
+
await services.smartCollections.create({
|
|
118
|
+
slug: "everything",
|
|
119
|
+
title: "Everything",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const html = await (await app.request("/everything")).text();
|
|
123
|
+
expect(html).toContain("Automatically collects every post.");
|
|
124
|
+
expect(html).toContain("public quote body");
|
|
125
|
+
expect(html).toContain("a plain note body");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// The whole point of forbidding private conditions: there is no smart
|
|
129
|
+
// collection page that answers 404 to a reader.
|
|
130
|
+
it("shows the private post to its author and to nobody else", async () => {
|
|
131
|
+
const reader = setup(false);
|
|
132
|
+
await seedQuotes(reader.services);
|
|
133
|
+
await reader.services.smartCollections.create({
|
|
134
|
+
slug: "quotes",
|
|
135
|
+
title: "Quotes",
|
|
136
|
+
selection: { format: "quote" },
|
|
137
|
+
});
|
|
138
|
+
const readerHtml = await (await reader.app.request("/quotes")).text();
|
|
139
|
+
expect(readerHtml).toContain("public quote body");
|
|
140
|
+
expect(readerHtml).not.toContain("private quote body");
|
|
141
|
+
expect(readerHtml).toContain("1 thread");
|
|
142
|
+
|
|
143
|
+
const authorApp = setup(true);
|
|
144
|
+
await seedQuotes(authorApp.services);
|
|
145
|
+
await authorApp.services.smartCollections.create({
|
|
146
|
+
slug: "quotes",
|
|
147
|
+
title: "Quotes",
|
|
148
|
+
selection: { format: "quote" },
|
|
149
|
+
});
|
|
150
|
+
const authorHtml = await (await authorApp.app.request("/quotes")).text();
|
|
151
|
+
expect(authorHtml).toContain("private quote body");
|
|
152
|
+
expect(authorHtml).toContain("2 threads");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("ignores condition parameters in the URL", async () => {
|
|
156
|
+
const { app, services } = setup();
|
|
157
|
+
await seedQuotes(services);
|
|
158
|
+
await services.smartCollections.create({
|
|
159
|
+
slug: "quotes",
|
|
160
|
+
title: "Quotes",
|
|
161
|
+
selection: { format: "quote" },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Membership is edited in the dialog. A reader appending `?format=note`
|
|
165
|
+
// must not be able to redefine what the page collects.
|
|
166
|
+
const html = await (await app.request("/quotes?format=note")).text();
|
|
167
|
+
expect(html).toContain("public quote body");
|
|
168
|
+
expect(html).not.toContain("a plain note body");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("honors ?sort= over the stored default", async () => {
|
|
172
|
+
const { app, services } = setup();
|
|
173
|
+
await services.posts.create({
|
|
174
|
+
format: "quote",
|
|
175
|
+
quoteText: "Older",
|
|
176
|
+
bodyMarkdown: "older body",
|
|
177
|
+
status: "published",
|
|
178
|
+
publishedAt: Date.UTC(2024, 0, 1) / 1000,
|
|
179
|
+
});
|
|
180
|
+
await services.posts.create({
|
|
181
|
+
format: "quote",
|
|
182
|
+
quoteText: "Newer",
|
|
183
|
+
bodyMarkdown: "newer body",
|
|
184
|
+
status: "published",
|
|
185
|
+
publishedAt: Date.UTC(2026, 0, 1) / 1000,
|
|
186
|
+
});
|
|
187
|
+
await services.smartCollections.create({
|
|
188
|
+
slug: "quotes",
|
|
189
|
+
title: "Quotes",
|
|
190
|
+
selection: { format: "quote" },
|
|
191
|
+
sort: "newest",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const stored = await (await app.request("/quotes")).text();
|
|
195
|
+
expect(stored.indexOf("newer body")).toBeLessThan(
|
|
196
|
+
stored.indexOf("older body"),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const flipped = await (await app.request("/quotes?sort=oldest")).text();
|
|
200
|
+
expect(flipped.indexOf("older body")).toBeLessThan(
|
|
201
|
+
flipped.indexOf("newer body"),
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("orders newest by date, not by rating", async () => {
|
|
206
|
+
const { app, services } = setup();
|
|
207
|
+
await services.posts.create({
|
|
208
|
+
format: "quote",
|
|
209
|
+
quoteText: "Older",
|
|
210
|
+
bodyMarkdown: "older body",
|
|
211
|
+
status: "published",
|
|
212
|
+
publishedAt: Date.UTC(2024, 0, 1) / 1000,
|
|
213
|
+
rating: 5,
|
|
214
|
+
});
|
|
215
|
+
await services.posts.create({
|
|
216
|
+
format: "quote",
|
|
217
|
+
quoteText: "Newer",
|
|
218
|
+
bodyMarkdown: "newer body",
|
|
219
|
+
status: "published",
|
|
220
|
+
publishedAt: Date.UTC(2026, 0, 1) / 1000,
|
|
221
|
+
rating: 1,
|
|
222
|
+
});
|
|
223
|
+
await services.smartCollections.create({
|
|
224
|
+
slug: "quotes",
|
|
225
|
+
title: "Quotes",
|
|
226
|
+
selection: { format: "quote" },
|
|
227
|
+
sort: "newest",
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// The page once handed `newest` to a query that read it as the ascending
|
|
231
|
+
// rating order, which put the worst-rated post on top of a dated page.
|
|
232
|
+
const page = await (await app.request("/quotes")).text();
|
|
233
|
+
expect(page.indexOf("newer body")).toBeLessThan(page.indexOf("older body"));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("still answers a link to the retired ?sort=updated", async () => {
|
|
237
|
+
const { app, services } = setup();
|
|
238
|
+
await services.posts.create({
|
|
239
|
+
format: "quote",
|
|
240
|
+
quoteText: "Older",
|
|
241
|
+
bodyMarkdown: "older body",
|
|
242
|
+
status: "published",
|
|
243
|
+
publishedAt: Date.UTC(2024, 0, 1) / 1000,
|
|
244
|
+
});
|
|
245
|
+
await services.posts.create({
|
|
246
|
+
format: "quote",
|
|
247
|
+
quoteText: "Newer",
|
|
248
|
+
bodyMarkdown: "newer body",
|
|
249
|
+
status: "published",
|
|
250
|
+
publishedAt: Date.UTC(2026, 0, 1) / 1000,
|
|
251
|
+
});
|
|
252
|
+
await services.smartCollections.create({
|
|
253
|
+
slug: "quotes",
|
|
254
|
+
title: "Quotes",
|
|
255
|
+
selection: { format: "quote" },
|
|
256
|
+
// A stored order the legacy value must not fall back to, or the test
|
|
257
|
+
// would pass on the fallback rather than on the mapping.
|
|
258
|
+
sort: "oldest",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const legacy = await (await app.request("/quotes?sort=updated")).text();
|
|
262
|
+
expect(legacy.indexOf("newer body")).toBeLessThan(
|
|
263
|
+
legacy.indexOf("older body"),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("404s an address no smart collection holds", async () => {
|
|
268
|
+
const { app } = setup();
|
|
269
|
+
expect((await app.request("/nothing-here")).status).toBe(404);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("smart collection feed", () => {
|
|
274
|
+
it("serves an anonymous feed with no guard to trip over", async () => {
|
|
275
|
+
const { app, services } = setup();
|
|
276
|
+
await seedQuotes(services);
|
|
277
|
+
await services.smartCollections.create({
|
|
278
|
+
slug: "quotes",
|
|
279
|
+
title: "Quotes",
|
|
280
|
+
selection: { format: "quote" },
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const res = await app.request("/quotes/feed");
|
|
284
|
+
expect(res.status).toBe(200);
|
|
285
|
+
const xml = await res.text();
|
|
286
|
+
|
|
287
|
+
expect(xml).toContain("A public quote");
|
|
288
|
+
// A feed carries no session, so the private quote can never appear in one.
|
|
289
|
+
expect(xml).not.toContain("A private quote");
|
|
290
|
+
expect(xml).toContain('rel="self"');
|
|
291
|
+
expect(xml).toContain("/quotes/feed");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("404s a feed for an address no smart collection holds", async () => {
|
|
295
|
+
const { app } = setup();
|
|
296
|
+
expect((await app.request("/nothing-here/feed")).status).toBe(404);
|
|
297
|
+
});
|
|
298
|
+
});
|