@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
|
@@ -486,6 +486,133 @@ describe("PostService - Timeline features", () => {
|
|
|
486
486
|
});
|
|
487
487
|
});
|
|
488
488
|
|
|
489
|
+
// A reply can be older than its root: a post moved into a Thread keeps its
|
|
490
|
+
// own creation time, and an export without `created` dates every post by
|
|
491
|
+
// publication. Thread order still opens on the root.
|
|
492
|
+
describe("a reply created before its root", () => {
|
|
493
|
+
async function createThreadWithOlderReplies() {
|
|
494
|
+
const root = await postService.create({
|
|
495
|
+
format: "note",
|
|
496
|
+
bodyMarkdown: "Root",
|
|
497
|
+
featured: true,
|
|
498
|
+
publishedAt: 3000,
|
|
499
|
+
});
|
|
500
|
+
const first = await postService.create({
|
|
501
|
+
format: "note",
|
|
502
|
+
bodyMarkdown: "First reply",
|
|
503
|
+
replyToId: root.id,
|
|
504
|
+
publishedAt: 1000,
|
|
505
|
+
});
|
|
506
|
+
const second = await postService.create({
|
|
507
|
+
format: "note",
|
|
508
|
+
bodyMarkdown: "Second reply",
|
|
509
|
+
replyToId: first.id,
|
|
510
|
+
publishedAt: 2000,
|
|
511
|
+
});
|
|
512
|
+
// Backdated after the fact, the way a restore lands them, so each test
|
|
513
|
+
// reads the shape without going through the reply guard.
|
|
514
|
+
for (const [id, createdAt] of [
|
|
515
|
+
[root.id, 3000],
|
|
516
|
+
[first.id, 1000],
|
|
517
|
+
[second.id, 2000],
|
|
518
|
+
] as const) {
|
|
519
|
+
await db.update(posts).set({ createdAt }).where(eq(posts.id, id));
|
|
520
|
+
}
|
|
521
|
+
return { root, first, second };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
it("opens getThread on the root", async () => {
|
|
525
|
+
const { root, first, second } = await createThreadWithOlderReplies();
|
|
526
|
+
|
|
527
|
+
const thread = await postService.getThread(root.id);
|
|
528
|
+
|
|
529
|
+
expect(thread.map((post) => post.id)).toEqual([
|
|
530
|
+
root.id,
|
|
531
|
+
first.id,
|
|
532
|
+
second.id,
|
|
533
|
+
]);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("opens getPublishedThreads on the root", async () => {
|
|
537
|
+
const { root, first, second } = await createThreadWithOlderReplies();
|
|
538
|
+
|
|
539
|
+
const threads = await postService.getPublishedThreads([root.id]);
|
|
540
|
+
|
|
541
|
+
expect(threads.get(root.id)?.map((post) => post.id)).toEqual([
|
|
542
|
+
root.id,
|
|
543
|
+
first.id,
|
|
544
|
+
second.id,
|
|
545
|
+
]);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("puts the root at position 0 of the Featured projection", async () => {
|
|
549
|
+
const { root, second } = await createThreadWithOlderReplies();
|
|
550
|
+
|
|
551
|
+
const result = await postService.getFeaturedThreadTimelineData([root.id]);
|
|
552
|
+
|
|
553
|
+
expect(
|
|
554
|
+
result
|
|
555
|
+
.get(root.id)
|
|
556
|
+
?.posts.map(({ post, position }) => [post.id, position]),
|
|
557
|
+
).toEqual([
|
|
558
|
+
[root.id, 0],
|
|
559
|
+
[second.id, 2],
|
|
560
|
+
]);
|
|
561
|
+
expect(result.get(root.id)?.featuredPostIds).toEqual([root.id]);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("ends the Thread on the newest reply, not the root", async () => {
|
|
565
|
+
const { root, second } = await createThreadWithOlderReplies();
|
|
566
|
+
|
|
567
|
+
const published = await postService.getThreadTailIds([root.id]);
|
|
568
|
+
const withDrafts = await postService.getThreadTailIds([root.id], {
|
|
569
|
+
includeDrafts: true,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(published.get(root.id)).toBe(second.id);
|
|
573
|
+
expect(withDrafts.get(root.id)).toBe(second.id);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("refuses a reply to the root once a reply exists", async () => {
|
|
577
|
+
const { root, second } = await createThreadWithOlderReplies();
|
|
578
|
+
|
|
579
|
+
await expect(
|
|
580
|
+
postService.create({
|
|
581
|
+
format: "note",
|
|
582
|
+
bodyMarkdown: "Fork",
|
|
583
|
+
replyToId: root.id,
|
|
584
|
+
}),
|
|
585
|
+
).rejects.toThrow("This post is no longer the end of the thread.");
|
|
586
|
+
|
|
587
|
+
const next = await postService.create({
|
|
588
|
+
format: "note",
|
|
589
|
+
bodyMarkdown: "Third reply",
|
|
590
|
+
replyToId: second.id,
|
|
591
|
+
});
|
|
592
|
+
expect(next.threadId).toBe(root.id);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("folds the replies for the timeline in Thread order", async () => {
|
|
596
|
+
const { root, first, second } = await createThreadWithOlderReplies();
|
|
597
|
+
|
|
598
|
+
const context = (
|
|
599
|
+
await postService.getThreadTimelineContext([root.id])
|
|
600
|
+
).get(root.id);
|
|
601
|
+
const previews = await postService.getThreadPreviews([root.id]);
|
|
602
|
+
|
|
603
|
+
expect(context?.leadingReplies.map((post) => post.id)).toEqual([
|
|
604
|
+
first.id,
|
|
605
|
+
second.id,
|
|
606
|
+
]);
|
|
607
|
+
expect(context?.latestReply.id).toBe(second.id);
|
|
608
|
+
expect(context?.totalReplyCount).toBe(2);
|
|
609
|
+
expect(previews.get(root.id)?.map((post) => post.id)).toEqual([
|
|
610
|
+
first.id,
|
|
611
|
+
second.id,
|
|
612
|
+
]);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
489
616
|
describe("timeline assembly", () => {
|
|
490
617
|
it("fetches published non-reply posts for the timeline", async () => {
|
|
491
618
|
const root = await postService.create({
|
|
@@ -13,6 +13,7 @@ import type { Database } from "../../db/index.js";
|
|
|
13
13
|
import { createPathService } from "../path.js";
|
|
14
14
|
import type { MediaService } from "../media.js";
|
|
15
15
|
import { POST_BODY_HTML_VERSION } from "../../lib/post-body-html.js";
|
|
16
|
+
import { ValidationError } from "../../lib/errors.js";
|
|
16
17
|
import type BetterSqlite3 from "better-sqlite3";
|
|
17
18
|
|
|
18
19
|
function createMockStorage() {
|
|
@@ -90,6 +91,79 @@ describe("PostService", () => {
|
|
|
90
91
|
expect(post.threadId).toBe(post.id);
|
|
91
92
|
});
|
|
92
93
|
|
|
94
|
+
it("stores an empty list item with the paragraph the schema requires", async () => {
|
|
95
|
+
const post = await postService.create({
|
|
96
|
+
format: "note",
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
type: "doc",
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: "orderedList",
|
|
102
|
+
content: [
|
|
103
|
+
{ type: "listItem", content: [] },
|
|
104
|
+
{
|
|
105
|
+
type: "listItem",
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: "paragraph",
|
|
109
|
+
content: [{ type: "text", text: "Install" }],
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const stored = JSON.parse(post.body ?? "");
|
|
120
|
+
expect(stored.content[0].content[0]).toEqual({
|
|
121
|
+
type: "listItem",
|
|
122
|
+
content: [{ type: "paragraph" }],
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("restores the creation and edit times an import passes", async () => {
|
|
127
|
+
const published = await postService.create({
|
|
128
|
+
format: "note",
|
|
129
|
+
bodyMarkdown: "Moved from another site",
|
|
130
|
+
publishedAt: 1700000500,
|
|
131
|
+
createdAt: 1700000000,
|
|
132
|
+
updatedAt: 1700009000,
|
|
133
|
+
});
|
|
134
|
+
expect(published).toMatchObject({
|
|
135
|
+
createdAt: 1700000000,
|
|
136
|
+
updatedAt: 1700009000,
|
|
137
|
+
lastActivityAt: 1700000500,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const draft = await postService.create({
|
|
141
|
+
format: "note",
|
|
142
|
+
status: "draft",
|
|
143
|
+
bodyMarkdown: "Unfinished",
|
|
144
|
+
createdAt: 1700000000,
|
|
145
|
+
updatedAt: 1700004000,
|
|
146
|
+
});
|
|
147
|
+
expect(draft).toMatchObject({
|
|
148
|
+
createdAt: 1700000000,
|
|
149
|
+
updatedAt: 1700004000,
|
|
150
|
+
lastActivityAt: 1700004000,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const backdated = await postService.create({
|
|
154
|
+
format: "note",
|
|
155
|
+
bodyMarkdown: "Only a creation time",
|
|
156
|
+
createdAt: 1700000000,
|
|
157
|
+
});
|
|
158
|
+
expect(backdated.updatedAt).toBe(1700000000);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("rejects a body that isn't TipTap JSON", async () => {
|
|
162
|
+
await expect(
|
|
163
|
+
postService.create({ format: "note", body: "not json" }),
|
|
164
|
+
).rejects.toThrow(ValidationError);
|
|
165
|
+
});
|
|
166
|
+
|
|
93
167
|
it("creates a link post with commentary", async () => {
|
|
94
168
|
const body = JSON.stringify({
|
|
95
169
|
type: "doc",
|
|
@@ -570,20 +644,53 @@ describe("PostService", () => {
|
|
|
570
644
|
const descending = await postService.list({
|
|
571
645
|
sortOrder: "rating_desc",
|
|
572
646
|
});
|
|
573
|
-
const ascending = await postService.list({
|
|
574
|
-
sortOrder: "rating_asc",
|
|
575
|
-
});
|
|
576
647
|
|
|
577
648
|
expect(descending.map((post) => post.bodyText)).toEqual([
|
|
578
649
|
"five stars",
|
|
579
650
|
"three stars",
|
|
580
651
|
"unrated newest",
|
|
581
652
|
]);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("orders by time for an explicit newest, ratings and all", async () => {
|
|
656
|
+
// `newest` used to have no branch of its own in the ORDER BY and fell
|
|
657
|
+
// into the one written for the ascending rating order, so anything that
|
|
658
|
+
// named it — a smart collection page, most visibly — got its posts
|
|
659
|
+
// grouped by rating instead of dated. Rated rows here on purpose.
|
|
660
|
+
await postService.create({
|
|
661
|
+
format: "note",
|
|
662
|
+
bodyMarkdown: "oldest, best",
|
|
663
|
+
publishedAt: 1000,
|
|
664
|
+
rating: 5,
|
|
665
|
+
});
|
|
666
|
+
await postService.create({
|
|
667
|
+
format: "note",
|
|
668
|
+
bodyMarkdown: "middle, worst",
|
|
669
|
+
publishedAt: 2000,
|
|
670
|
+
rating: 1,
|
|
671
|
+
});
|
|
672
|
+
await postService.create({
|
|
673
|
+
format: "note",
|
|
674
|
+
bodyMarkdown: "newest, unrated",
|
|
675
|
+
publishedAt: 3000,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const ordered = await postService.list({ sortOrder: "newest" });
|
|
679
|
+
expect(ordered.map((post) => post.bodyText)).toEqual([
|
|
680
|
+
"newest, unrated",
|
|
681
|
+
"middle, worst",
|
|
682
|
+
"oldest, best",
|
|
586
683
|
]);
|
|
684
|
+
|
|
685
|
+
// The keyset cursor reads the same keys or pagination skips rows.
|
|
686
|
+
expect(
|
|
687
|
+
(
|
|
688
|
+
await postService.list({
|
|
689
|
+
sortOrder: "newest",
|
|
690
|
+
cursor: ordered[0]?.id,
|
|
691
|
+
})
|
|
692
|
+
).map((post) => post.bodyText),
|
|
693
|
+
).toEqual(["middle, worst", "oldest, best"]);
|
|
587
694
|
});
|
|
588
695
|
|
|
589
696
|
it("orders drafts by updatedAt descending", async () => {
|
|
@@ -1570,7 +1677,7 @@ describe("PostService", () => {
|
|
|
1570
1677
|
expect(updated?.pinnedAt).toBeTypeOf("number");
|
|
1571
1678
|
});
|
|
1572
1679
|
|
|
1573
|
-
it("updates slug and its page navigation
|
|
1680
|
+
it("updates slug and title on its page navigation entry", async () => {
|
|
1574
1681
|
const post = await postService.create({
|
|
1575
1682
|
format: "note",
|
|
1576
1683
|
title: "Test page",
|
|
@@ -1585,11 +1692,15 @@ describe("PostService", () => {
|
|
|
1585
1692
|
|
|
1586
1693
|
const updated = await postService.update(post.id, {
|
|
1587
1694
|
slug: "new-slug",
|
|
1695
|
+
title: "Renamed page",
|
|
1588
1696
|
});
|
|
1589
1697
|
|
|
1590
1698
|
expect(updated?.slug).toBe("new-slug");
|
|
1699
|
+
// The URL is synced on write; the title is read through, so renaming the
|
|
1700
|
+
// page carries its navigation entry along with no sync step at all.
|
|
1591
1701
|
expect(await navItemService.getById(navItem.id)).toMatchObject({
|
|
1592
|
-
label: "
|
|
1702
|
+
label: "",
|
|
1703
|
+
targetTitle: "Renamed page",
|
|
1593
1704
|
url: "/new-slug",
|
|
1594
1705
|
});
|
|
1595
1706
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
createTestDatabase,
|
|
4
4
|
DEFAULT_TEST_SITE_ID,
|
|
@@ -170,7 +170,6 @@ describe("SettingsService", () => {
|
|
|
170
170
|
siteDescription: "",
|
|
171
171
|
siteFooter: "",
|
|
172
172
|
siteLanguage: "en",
|
|
173
|
-
cjkSerifFont: "off",
|
|
174
173
|
showJantBrandingOnHome: false,
|
|
175
174
|
mainRssFeed: "featured",
|
|
176
175
|
timeZone: "UTC",
|
|
@@ -361,6 +360,57 @@ describe("SettingsService", () => {
|
|
|
361
360
|
const result = await settingsService.isOnboardingComplete();
|
|
362
361
|
expect(result).toBe(true);
|
|
363
362
|
});
|
|
363
|
+
|
|
364
|
+
it("reports the three first-run states", async () => {
|
|
365
|
+
expect(await settingsService.getOnboardingStatus()).toBe("pending");
|
|
366
|
+
|
|
367
|
+
await settingsService.markSiteProvisioned();
|
|
368
|
+
expect(await settingsService.getOnboardingStatus()).toBe("provisioned");
|
|
369
|
+
// A provisioned site is real, but setup is not finished with it.
|
|
370
|
+
expect(await settingsService.isOnboardingComplete()).toBe(false);
|
|
371
|
+
|
|
372
|
+
await settingsService.completeOnboarding();
|
|
373
|
+
expect(await settingsService.getOnboardingStatus()).toBe("completed");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("finishes setup when the author confirms the language they write in", async () => {
|
|
377
|
+
await settingsService.markSiteProvisioned();
|
|
378
|
+
// The control plane's guess, taken from the locale the owner browsed it
|
|
379
|
+
// in, is what the site starts with.
|
|
380
|
+
await settingsService.set("SITE_LANGUAGE", "zh-Hans");
|
|
381
|
+
|
|
382
|
+
await settingsService.confirmFirstRunLanguage(
|
|
383
|
+
{ siteLanguage: "en", browserLanguage: "zh-Hans" },
|
|
384
|
+
{ oldLanguage: "zh-Hans" },
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
expect(await settingsService.get("SITE_LANGUAGE")).toBe("en");
|
|
388
|
+
// English site, Chinese dashboard — the split the question exists for.
|
|
389
|
+
expect(await settingsService.get("DASHBOARD_LANGUAGE")).toBe("zh-Hans");
|
|
390
|
+
expect(await settingsService.isOnboardingComplete()).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("leaves the dashboard following when the browser sent none", async () => {
|
|
394
|
+
await settingsService.confirmFirstRunLanguage(
|
|
395
|
+
{ siteLanguage: "zh-TW", browserLanguage: "" },
|
|
396
|
+
{ oldLanguage: "en" },
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
expect(await settingsService.get("SITE_LANGUAGE")).toBe("zh-TW");
|
|
400
|
+
expect(await settingsService.get("DASHBOARD_LANGUAGE")).toBeNull();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("leaves the dashboard following the language the author confirmed", async () => {
|
|
404
|
+
// An English browser says nothing about someone who just answered
|
|
405
|
+
// "Simplified Chinese" — the dashboard follows that answer.
|
|
406
|
+
await settingsService.confirmFirstRunLanguage(
|
|
407
|
+
{ siteLanguage: "zh-Hans", browserLanguage: "en-US" },
|
|
408
|
+
{ oldLanguage: "en" },
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
expect(await settingsService.get("SITE_LANGUAGE")).toBe("zh-Hans");
|
|
412
|
+
expect(await settingsService.get("DASHBOARD_LANGUAGE")).toBeNull();
|
|
413
|
+
});
|
|
364
414
|
});
|
|
365
415
|
|
|
366
416
|
describe("grouped settings updates", () => {
|
|
@@ -390,7 +440,6 @@ describe("SettingsService", () => {
|
|
|
390
440
|
const result = await settingsService.updateLocaleSettings(
|
|
391
441
|
{
|
|
392
442
|
siteLanguage: "zh-Hans",
|
|
393
|
-
cjkSerifFont: "zh-Hans",
|
|
394
443
|
timeZone: "America/New_York",
|
|
395
444
|
},
|
|
396
445
|
{
|
|
@@ -400,29 +449,32 @@ describe("SettingsService", () => {
|
|
|
400
449
|
|
|
401
450
|
expect(result.languageChanged).toBe(true);
|
|
402
451
|
expect(await settingsService.get("SITE_LANGUAGE")).toBe("zh-Hans");
|
|
403
|
-
expect(await settingsService.get("CJK_SERIF_FONT")).toBe("zh-Hans");
|
|
404
452
|
expect(await settingsService.get("TIME_ZONE")).toBe("America/New_York");
|
|
405
453
|
});
|
|
406
454
|
|
|
407
|
-
|
|
408
|
-
|
|
455
|
+
// The Language and General pages each own part of this group, so an absent
|
|
456
|
+
// field has to mean "leave it alone" rather than "clear it".
|
|
457
|
+
it("leaves out-of-scope fields untouched", async () => {
|
|
458
|
+
await settingsService.set("SITE_LANGUAGE", "ja");
|
|
459
|
+
await settingsService.set("TIME_ZONE", "Asia/Tokyo");
|
|
460
|
+
|
|
409
461
|
await settingsService.updateLocaleSettings(
|
|
410
|
-
{
|
|
411
|
-
|
|
412
|
-
cjkSerifFont: "off",
|
|
413
|
-
timeZone: "UTC",
|
|
414
|
-
},
|
|
415
|
-
{ oldLanguage: "en" },
|
|
462
|
+
{ timeZone: "America/New_York" },
|
|
463
|
+
{ oldLanguage: "ja" },
|
|
416
464
|
);
|
|
465
|
+
expect(await settingsService.get("SITE_LANGUAGE")).toBe("ja");
|
|
417
466
|
|
|
418
|
-
|
|
467
|
+
await settingsService.updateLocaleSettings(
|
|
468
|
+
{ siteLanguage: "en" },
|
|
469
|
+
{ oldLanguage: "ja" },
|
|
470
|
+
);
|
|
471
|
+
expect(await settingsService.get("TIME_ZONE")).toBe("America/New_York");
|
|
419
472
|
});
|
|
420
473
|
|
|
421
474
|
it("stores the base locale when language input is blank", async () => {
|
|
422
475
|
const result = await settingsService.updateLocaleSettings(
|
|
423
476
|
{
|
|
424
477
|
siteLanguage: "",
|
|
425
|
-
cjkSerifFont: "off",
|
|
426
478
|
timeZone: "UTC",
|
|
427
479
|
},
|
|
428
480
|
{
|
|
@@ -439,7 +491,6 @@ describe("SettingsService", () => {
|
|
|
439
491
|
{
|
|
440
492
|
siteLanguage: "fr",
|
|
441
493
|
dashboardLanguage: "zh-Hant",
|
|
442
|
-
cjkSerifFont: "off",
|
|
443
494
|
timeZone: "UTC",
|
|
444
495
|
},
|
|
445
496
|
{ oldLanguage: "fr", oldDashboardLanguage: "" },
|
|
@@ -456,7 +507,6 @@ describe("SettingsService", () => {
|
|
|
456
507
|
{
|
|
457
508
|
siteLanguage: "en",
|
|
458
509
|
dashboardLanguage: "",
|
|
459
|
-
cjkSerifFont: "off",
|
|
460
510
|
timeZone: "UTC",
|
|
461
511
|
},
|
|
462
512
|
{ oldLanguage: "en", oldDashboardLanguage: "zh-Hans" },
|
|
@@ -481,7 +531,6 @@ describe("SettingsService", () => {
|
|
|
481
531
|
{
|
|
482
532
|
siteLanguage: "en",
|
|
483
533
|
dashboardLanguage: "fr",
|
|
484
|
-
cjkSerifFont: "off",
|
|
485
534
|
timeZone: "UTC",
|
|
486
535
|
},
|
|
487
536
|
{ oldLanguage: "en" },
|
|
@@ -509,4 +558,166 @@ describe("SettingsService", () => {
|
|
|
509
558
|
).toBeNull();
|
|
510
559
|
});
|
|
511
560
|
});
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* The ping exists so a self-hosted site can be found at all, and it says
|
|
564
|
+
* "read me now" rather than "list me". What it fires on is therefore the
|
|
565
|
+
* interesting part: the two moments the answer changes, and neither of the
|
|
566
|
+
* moments it does not.
|
|
567
|
+
*/
|
|
568
|
+
describe("updateDiscoverSetting", () => {
|
|
569
|
+
it("stores the choice explicitly, including off", async () => {
|
|
570
|
+
await settingsService.updateDiscoverSetting("off", { demoMode: false });
|
|
571
|
+
expect(await settingsService.get("DISCOVER")).toBe("off");
|
|
572
|
+
|
|
573
|
+
await settingsService.updateDiscoverSetting("featured", {
|
|
574
|
+
demoMode: false,
|
|
575
|
+
});
|
|
576
|
+
expect(await settingsService.get("DISCOVER")).toBe("featured");
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// A site that has never used the control has never told anyone it exists,
|
|
580
|
+
// so confirming the default is exactly as much of an opt-in as ticking a
|
|
581
|
+
// box that was off.
|
|
582
|
+
it("announces when an untouched site opts in", async () => {
|
|
583
|
+
const result = await settingsService.updateDiscoverSetting("latest", {
|
|
584
|
+
demoMode: false,
|
|
585
|
+
});
|
|
586
|
+
expect(result.shouldAnnounce).toBe(true);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("announces when a site that had opted out comes back", async () => {
|
|
590
|
+
await settingsService.updateDiscoverSetting("off", { demoMode: false });
|
|
591
|
+
const result = await settingsService.updateDiscoverSetting("latest", {
|
|
592
|
+
demoMode: false,
|
|
593
|
+
});
|
|
594
|
+
expect(result.shouldAnnounce).toBe(true);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// The directory already knows about a listed site; which of its posts it
|
|
598
|
+
// draws from is not news.
|
|
599
|
+
it("says nothing when an enrolled site changes which posts it offers", async () => {
|
|
600
|
+
await settingsService.updateDiscoverSetting("latest", {
|
|
601
|
+
demoMode: false,
|
|
602
|
+
});
|
|
603
|
+
const result = await settingsService.updateDiscoverSetting("featured", {
|
|
604
|
+
demoMode: false,
|
|
605
|
+
});
|
|
606
|
+
expect(result.shouldAnnounce).toBe(false);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// The half that used to be missing. A directory that is not told keeps the
|
|
610
|
+
// blog until its next scheduled read of the feed, and the owner watches
|
|
611
|
+
// something they just removed sit there for another hour.
|
|
612
|
+
it("announces when a listed site opts out", async () => {
|
|
613
|
+
await settingsService.updateDiscoverSetting("latest", {
|
|
614
|
+
demoMode: false,
|
|
615
|
+
});
|
|
616
|
+
const result = await settingsService.updateDiscoverSetting("off", {
|
|
617
|
+
demoMode: false,
|
|
618
|
+
});
|
|
619
|
+
expect(result.shouldAnnounce).toBe(true);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Nothing changed, so there is nothing to say: this site was not in any
|
|
623
|
+
// directory a moment ago either.
|
|
624
|
+
it("says nothing when a site that was never listed sets off", async () => {
|
|
625
|
+
const result = await settingsService.updateDiscoverSetting("off", {
|
|
626
|
+
demoMode: false,
|
|
627
|
+
});
|
|
628
|
+
expect(result.shouldAnnounce).toBe(false);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("never announces a demo site", async () => {
|
|
632
|
+
const result = await settingsService.updateDiscoverSetting("latest", {
|
|
633
|
+
demoMode: true,
|
|
634
|
+
});
|
|
635
|
+
expect(result.shouldAnnounce).toBe(false);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* The stored outcome is the owner's record of the announcement, and it does
|
|
641
|
+
* not always reach them — a hosted site's settings page hides the status
|
|
642
|
+
* block entirely. The log is how a deployment answers "did that get through"
|
|
643
|
+
* when the dashboard cannot.
|
|
644
|
+
*/
|
|
645
|
+
describe("announceToDiscover", () => {
|
|
646
|
+
afterEach(() => {
|
|
647
|
+
vi.restoreAllMocks();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
function stubDirectory(response: Response | Error) {
|
|
651
|
+
return vi
|
|
652
|
+
.spyOn(globalThis, "fetch")
|
|
653
|
+
.mockImplementation(() =>
|
|
654
|
+
response instanceof Error
|
|
655
|
+
? Promise.reject(response)
|
|
656
|
+
: Promise.resolve(response.clone()),
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
it("records and logs an announcement that got through", async () => {
|
|
661
|
+
stubDirectory(new Response(null, { status: 202 }));
|
|
662
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
663
|
+
|
|
664
|
+
const outcome = await settingsService.announceToDiscover({
|
|
665
|
+
endpoint: "https://cloud.example/api/discover/ping",
|
|
666
|
+
feedUrl: "https://blog.example/latest/feed",
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(outcome.ok).toBe(true);
|
|
670
|
+
expect(
|
|
671
|
+
JSON.parse(
|
|
672
|
+
(await settingsService.get("DISCOVER_ANNOUNCE_STATE")) ?? "",
|
|
673
|
+
),
|
|
674
|
+
).toMatchObject({
|
|
675
|
+
ok: true,
|
|
676
|
+
feedUrl: "https://blog.example/latest/feed",
|
|
677
|
+
});
|
|
678
|
+
expect(log).toHaveBeenCalledWith(
|
|
679
|
+
expect.stringContaining("https://cloud.example/api/discover/ping"),
|
|
680
|
+
);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// The failure this exists for: a directory that answers, but not about
|
|
684
|
+
// this site. Silently stored, it reads from the dashboard like a success.
|
|
685
|
+
it("logs the reason an announcement failed", async () => {
|
|
686
|
+
stubDirectory(new Response(null, { status: 404 }));
|
|
687
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
688
|
+
|
|
689
|
+
const outcome = await settingsService.announceToDiscover({
|
|
690
|
+
endpoint: "https://jant.me/api/discover/ping",
|
|
691
|
+
feedUrl: "https://blog.example/latest/feed",
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
expect(outcome).toMatchObject({ ok: false, status: 404 });
|
|
695
|
+
expect(error).toHaveBeenCalledWith(
|
|
696
|
+
expect.stringContaining("The directory answered 404."),
|
|
697
|
+
);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// The caller orphans this promise behind `waitUntil`, so a rejection has
|
|
701
|
+
// nobody to reject to — on a Node runtime an unhandled one ends the
|
|
702
|
+
// process. A settings row that could not be written is worth a log, not
|
|
703
|
+
// the site.
|
|
704
|
+
it("resolves even when the outcome cannot be recorded", async () => {
|
|
705
|
+
stubDirectory(new Response(null, { status: 202 }));
|
|
706
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
707
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
708
|
+
vi.spyOn(settingsService, "set").mockRejectedValue(
|
|
709
|
+
new Error("D1_ERROR: database is locked"),
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
const outcome = await settingsService.announceToDiscover({
|
|
713
|
+
endpoint: "https://jant.me/api/discover/ping",
|
|
714
|
+
feedUrl: "https://blog.example/latest/feed",
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
expect(outcome.ok).toBe(true);
|
|
718
|
+
expect(error).toHaveBeenCalledWith(
|
|
719
|
+
expect.stringContaining("database is locked"),
|
|
720
|
+
);
|
|
721
|
+
});
|
|
722
|
+
});
|
|
512
723
|
});
|
|
@@ -53,9 +53,9 @@ describe("SiteAdminService", () => {
|
|
|
53
53
|
|
|
54
54
|
expect(navigationRows).toEqual([
|
|
55
55
|
{ systemKey: "featured", placement: "header" },
|
|
56
|
-
{ systemKey: "collections", placement: "header" },
|
|
57
56
|
{ systemKey: "archive", placement: "header" },
|
|
58
|
-
{ systemKey: "
|
|
57
|
+
{ systemKey: "collections", placement: "more" },
|
|
58
|
+
{ systemKey: "subscribe", placement: "more" },
|
|
59
59
|
{ systemKey: "settings", placement: "more" },
|
|
60
60
|
]);
|
|
61
61
|
});
|
|
@@ -182,12 +182,12 @@ describe("SiteAdminService", () => {
|
|
|
182
182
|
expect(recovered.site.id).toBe(first.site.id);
|
|
183
183
|
expect(navigationRows.map((row) => row.systemKey)).toEqual([
|
|
184
184
|
"featured",
|
|
185
|
-
"collections",
|
|
186
185
|
"archive",
|
|
187
|
-
"
|
|
186
|
+
"collections",
|
|
187
|
+
"subscribe",
|
|
188
188
|
"settings",
|
|
189
189
|
]);
|
|
190
|
-
expect(onboarding.value).toBe("
|
|
190
|
+
expect(onboarding.value).toBe("provisioned");
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
it("rejects reuse of an idempotency key with different key or primary host", async () => {
|