@jant/core 0.6.15 → 0.7.0
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/import-site.js +67 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- 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-CD3FhrOB.js +257 -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-CgaL2jCs.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-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- 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__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -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__/mise-config.test.ts +22 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- 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 +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- 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 +88 -1
- 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__/compose-format-convert.test.ts +0 -19
- 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 +79 -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 +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- 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 +541 -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-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- 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 +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- 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 +1142 -268
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -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 +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- 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/post-refresh.ts +135 -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__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- 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__/backfill-thread-activity.test.ts +234 -0
- 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/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -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/0031_snapshot.json +2562 -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 +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -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/0029_snapshot.json +3289 -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 +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- 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 +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- 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 +1691 -12
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -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__/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.test.ts +16 -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 +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +88 -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__/translit.test.ts +87 -0
- 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 +93 -34
- 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 +437 -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 +1021 -66
- package/src/lib/filter-dimensions.ts +1349 -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 +21 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- 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-display.ts +42 -9
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- 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 +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -23
- 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-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- 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__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/post-translations.test.ts +829 -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/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 +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- 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 +11 -2
- 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__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- 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 +657 -1
- 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__/featured.test.ts +6 -2
- 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-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- 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/archive.tsx +507 -357
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- 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 +183 -19
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- 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/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +139 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -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 +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +330 -47
- package/src/services/custom-url.ts +27 -22
- 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/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- 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/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +281 -24
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- 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 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- 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 +275 -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/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- 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 +401 -301
- 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 +122 -3
- 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/DraftPreviewBar.tsx +25 -9
- 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 +55 -23
- 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/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.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-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- 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/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Collections API Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import type { Bindings } from "../../types.js";
|
|
8
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
9
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
10
|
+
import {
|
|
11
|
+
ContentLanguageSchema,
|
|
12
|
+
CreateSmartCollectionSchema,
|
|
13
|
+
SmartCollectionIdSchema,
|
|
14
|
+
SmartCollectionPreviewSchema,
|
|
15
|
+
UpdateSmartCollectionSchema,
|
|
16
|
+
parseValidated,
|
|
17
|
+
} from "../../lib/schemas.js";
|
|
18
|
+
import { assertFound, parseIdParam } from "../../lib/errors.js";
|
|
19
|
+
import { ID_PREFIX } from "../../lib/ids.js";
|
|
20
|
+
|
|
21
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
22
|
+
|
|
23
|
+
export const smartCollectionsApiRoutes = new Hono<Env>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Address availability, in the shape posts already use.
|
|
27
|
+
*
|
|
28
|
+
* `suggest` derives one from a title; `check` answers whether a typed one is
|
|
29
|
+
* free. Collections answer the same question at `/api/collections/slug`.
|
|
30
|
+
*/
|
|
31
|
+
const SlugQuerySchema = z.discriminatedUnion("mode", [
|
|
32
|
+
z.object({
|
|
33
|
+
mode: z.literal("suggest"),
|
|
34
|
+
title: z.string().trim().max(300).optional(),
|
|
35
|
+
smartCollectionId: SmartCollectionIdSchema.optional(),
|
|
36
|
+
}),
|
|
37
|
+
z.object({
|
|
38
|
+
mode: z.literal("check"),
|
|
39
|
+
slug: z.string().trim().toLowerCase().min(1).max(200),
|
|
40
|
+
smartCollectionId: SmartCollectionIdSchema.optional(),
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const PreviewQuerySchema = z.object({
|
|
45
|
+
lang: ContentLanguageSchema.optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// --- Address ---------------------------------------------------------------
|
|
49
|
+
// Declared before `/:id` so the literal path wins.
|
|
50
|
+
|
|
51
|
+
smartCollectionsApiRoutes.get("/slug", requireAuthApi(), async (c) => {
|
|
52
|
+
const query = parseValidated(SlugQuerySchema, c.req.query());
|
|
53
|
+
|
|
54
|
+
if (query.mode === "suggest") {
|
|
55
|
+
const slug = await c.var.services.smartCollections.suggestSlug({
|
|
56
|
+
title: query.title,
|
|
57
|
+
excludeId: query.smartCollectionId,
|
|
58
|
+
});
|
|
59
|
+
return c.json({ slug });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const available = await c.var.services.smartCollections.checkSlugAvailability(
|
|
63
|
+
query.slug,
|
|
64
|
+
query.smartCollectionId,
|
|
65
|
+
);
|
|
66
|
+
return c.json({ slug: query.slug, available });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// --- Preview ---------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* How many threads a set of conditions would gather, against the site total.
|
|
73
|
+
*
|
|
74
|
+
* POST rather than GET: the conditions are a typed body in the same shape the
|
|
75
|
+
* create endpoint validates, and putting them in a URL would mean inventing a
|
|
76
|
+
* second spelling for them.
|
|
77
|
+
*/
|
|
78
|
+
smartCollectionsApiRoutes.post("/preview", requireAuthApi(), async (c) => {
|
|
79
|
+
const body = parseValidated(SmartCollectionPreviewSchema, await c.req.json());
|
|
80
|
+
const query = parseValidated(PreviewQuerySchema, c.req.query());
|
|
81
|
+
|
|
82
|
+
const { count, baseline } = await c.var.services.smartCollections.preview(
|
|
83
|
+
body.selection ?? {},
|
|
84
|
+
{ isAuthenticated: c.var.isAuthenticated, lang: query.lang },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
c.header("Cache-Control", "no-store");
|
|
88
|
+
return c.json({ count, baseline });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// --- CRUD ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
smartCollectionsApiRoutes.get("/", requireAuthApi(), async (c) => {
|
|
94
|
+
const query = parseValidated(PreviewQuerySchema, c.req.query());
|
|
95
|
+
const smartCollections =
|
|
96
|
+
await c.var.services.smartCollections.listDirectoryEntries({
|
|
97
|
+
isAuthenticated: c.var.isAuthenticated,
|
|
98
|
+
lang: query.lang,
|
|
99
|
+
});
|
|
100
|
+
return c.json({ smartCollections });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
smartCollectionsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
104
|
+
const body = parseValidated(CreateSmartCollectionSchema, await c.req.json());
|
|
105
|
+
const smartCollection = await c.var.services.smartCollections.create(body);
|
|
106
|
+
return c.json({ smartCollection }, 201);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
smartCollectionsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
|
|
110
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.smartCollection);
|
|
111
|
+
const smartCollection = await c.var.services.smartCollections.getById(id);
|
|
112
|
+
assertFound(smartCollection, "Smart collection");
|
|
113
|
+
return c.json({ smartCollection });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
smartCollectionsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
117
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.smartCollection);
|
|
118
|
+
const body = parseValidated(UpdateSmartCollectionSchema, await c.req.json());
|
|
119
|
+
const smartCollection = await c.var.services.smartCollections.update(
|
|
120
|
+
id,
|
|
121
|
+
body,
|
|
122
|
+
);
|
|
123
|
+
assertFound(smartCollection, "Smart collection");
|
|
124
|
+
return c.json({ smartCollection });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
smartCollectionsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
128
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.smartCollection);
|
|
129
|
+
const deleted = await c.var.services.smartCollections.delete(id);
|
|
130
|
+
assertFound(deleted || null, "Smart collection");
|
|
131
|
+
return c.json({ success: true });
|
|
132
|
+
});
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
getTelegramWebhookSecret,
|
|
26
26
|
} from "../../lib/env.js";
|
|
27
27
|
import { timingSafeEqualText } from "../../lib/crypto.js";
|
|
28
|
+
import { runDeferred } from "../../lib/deferred.js";
|
|
28
29
|
import {
|
|
29
30
|
answerCallbackQuery,
|
|
30
31
|
buildDeepLink,
|
|
@@ -316,7 +317,7 @@ async function processUpdate(
|
|
|
316
317
|
posterFileId: media.posterFileId ?? null,
|
|
317
318
|
});
|
|
318
319
|
|
|
319
|
-
runDeferred(c, async () => {
|
|
320
|
+
runDeferred(c, "Telegram", async () => {
|
|
320
321
|
await sleep(ALBUM_BUFFER_DELAY_MS);
|
|
321
322
|
const claimed = await telegram.claimAlbumGroup(botId, mediaGroupId);
|
|
322
323
|
if (claimed.length === 0) return;
|
|
@@ -371,45 +372,6 @@ function sleep(ms: number): Promise<void> {
|
|
|
371
372
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
372
373
|
}
|
|
373
374
|
|
|
374
|
-
/**
|
|
375
|
-
* Schedules background work that must outlive the current HTTP response so
|
|
376
|
-
* Telegram can deliver the next webhook in this chat without waiting.
|
|
377
|
-
*
|
|
378
|
-
* On Cloudflare Workers `c.executionCtx.waitUntil` keeps the worker alive
|
|
379
|
-
* until the promise settles. In Node / tests there's no such API, so we just
|
|
380
|
-
* let the promise float — Node's event loop keeps running it. Either way the
|
|
381
|
-
* promise has its own try/catch so unhandled rejections never bubble up.
|
|
382
|
-
*
|
|
383
|
-
* Background work also tracks the pending-promises array we register on the
|
|
384
|
-
* env binding for tests so a vitest can `await` them after advancing timers.
|
|
385
|
-
*/
|
|
386
|
-
function runDeferred(
|
|
387
|
-
c: {
|
|
388
|
-
env: Bindings;
|
|
389
|
-
executionCtx?: { waitUntil: (promise: Promise<unknown>) => void };
|
|
390
|
-
},
|
|
391
|
-
work: () => Promise<void>,
|
|
392
|
-
): void {
|
|
393
|
-
const promise = work().catch((err) => {
|
|
394
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
395
|
-
// eslint-disable-next-line no-console -- Background failures must be visible.
|
|
396
|
-
console.error(`[Jant] Telegram background error: ${message}`);
|
|
397
|
-
});
|
|
398
|
-
try {
|
|
399
|
-
c.executionCtx?.waitUntil(promise);
|
|
400
|
-
} catch {
|
|
401
|
-
// executionCtx not available (e.g. Node, tests) — promise still runs.
|
|
402
|
-
}
|
|
403
|
-
const pending = (
|
|
404
|
-
c.env as Bindings & {
|
|
405
|
-
__telegramPending?: Promise<unknown>[];
|
|
406
|
-
}
|
|
407
|
-
).__telegramPending;
|
|
408
|
-
if (Array.isArray(pending)) {
|
|
409
|
-
pending.push(promise);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
375
|
/**
|
|
414
376
|
* Convert a message's caption + caption_entities into markdown, returning
|
|
415
377
|
* `null` when there's nothing to record. Centralized so single-media and album
|
|
@@ -116,8 +116,7 @@ function scheduleExpiredUploadCleanup(
|
|
|
116
116
|
.catch(() => {});
|
|
117
117
|
|
|
118
118
|
let executionCtx:
|
|
119
|
-
|
|
120
|
-
| undefined;
|
|
119
|
+
{ waitUntil: (promise: Promise<unknown>) => void } | undefined;
|
|
121
120
|
try {
|
|
122
121
|
executionCtx = c.executionCtx;
|
|
123
122
|
} catch {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for who may answer the last setup question on a provisioned site.
|
|
3
|
+
*
|
|
4
|
+
* A provisioned site already has an owner, so `POST /setup` writes the site's
|
|
5
|
+
* name and language on behalf of that owner. Carrying a session is not enough
|
|
6
|
+
* to be that owner — the session may belong to someone this site has never
|
|
7
|
+
* heard of.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { Hono } from "hono";
|
|
12
|
+
import { createI18n } from "../../../i18n/index.js";
|
|
13
|
+
import { attachSession } from "../../../middleware/session.js";
|
|
14
|
+
import { ONBOARDING_STATUS } from "../../../lib/constants.js";
|
|
15
|
+
import { setupRoutes } from "../setup.js";
|
|
16
|
+
import type { Bindings } from "../../../types.js";
|
|
17
|
+
import type { AppVariables } from "../../../types/app-context.js";
|
|
18
|
+
|
|
19
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
20
|
+
|
|
21
|
+
function createMockAuth(signedIn: boolean) {
|
|
22
|
+
return {
|
|
23
|
+
api: {
|
|
24
|
+
getSession: async () => ({
|
|
25
|
+
headers: new Headers(),
|
|
26
|
+
response: signedIn
|
|
27
|
+
? {
|
|
28
|
+
user: {
|
|
29
|
+
id: "usr_outsider",
|
|
30
|
+
email: "someone@test.com",
|
|
31
|
+
name: "S",
|
|
32
|
+
},
|
|
33
|
+
session: { id: "ses_outsider" },
|
|
34
|
+
}
|
|
35
|
+
: null,
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
} as unknown as AppVariables["auth"];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createProvisionedSetupApp(options: {
|
|
42
|
+
signedIn: boolean;
|
|
43
|
+
isMember: boolean;
|
|
44
|
+
}) {
|
|
45
|
+
const completeSiteSetup = vi.fn(async () => {});
|
|
46
|
+
|
|
47
|
+
const app = new Hono<Env>();
|
|
48
|
+
app.use("*", async (c, next) => {
|
|
49
|
+
c.env = {} as Bindings;
|
|
50
|
+
c.set("auth", createMockAuth(options.signedIn));
|
|
51
|
+
c.set("services", {
|
|
52
|
+
settings: {
|
|
53
|
+
getOnboardingStatus: async () => ONBOARDING_STATUS.PROVISIONED,
|
|
54
|
+
get: async () => "My Blog",
|
|
55
|
+
},
|
|
56
|
+
bootstrap: { completeSiteSetup },
|
|
57
|
+
siteMembers: {
|
|
58
|
+
get: async () =>
|
|
59
|
+
options.isMember
|
|
60
|
+
? {
|
|
61
|
+
createdAt: 0,
|
|
62
|
+
role: "owner",
|
|
63
|
+
siteId: "sit_test",
|
|
64
|
+
updatedAt: 0,
|
|
65
|
+
userId: "usr_outsider",
|
|
66
|
+
}
|
|
67
|
+
: null,
|
|
68
|
+
},
|
|
69
|
+
} as unknown as AppVariables["services"]);
|
|
70
|
+
c.set("currentSite", {
|
|
71
|
+
createdAt: 0,
|
|
72
|
+
id: "sit_test",
|
|
73
|
+
key: "default",
|
|
74
|
+
status: "active",
|
|
75
|
+
updatedAt: 0,
|
|
76
|
+
});
|
|
77
|
+
c.set("appConfig", {
|
|
78
|
+
siteName: "Jant",
|
|
79
|
+
sitePathPrefix: "",
|
|
80
|
+
siteLanguage: "en",
|
|
81
|
+
} as AppVariables["appConfig"]);
|
|
82
|
+
c.set("lang", "en");
|
|
83
|
+
c.set("i18n", createI18n("en"));
|
|
84
|
+
await next();
|
|
85
|
+
});
|
|
86
|
+
app.use("*", attachSession());
|
|
87
|
+
app.route("/", setupRoutes);
|
|
88
|
+
|
|
89
|
+
return { app, completeSiteSetup };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function postLanguage(app: Hono<Env>) {
|
|
93
|
+
return app.request("/setup", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify({ contentLanguage: "zh-Hans" }),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe("POST /setup on a provisioned site", () => {
|
|
101
|
+
it("refuses a session that is not a member of this site", async () => {
|
|
102
|
+
const { app, completeSiteSetup } = createProvisionedSetupApp({
|
|
103
|
+
signedIn: true,
|
|
104
|
+
isMember: false,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const res = await postLanguage(app);
|
|
108
|
+
|
|
109
|
+
await expect(res.text()).resolves.toContain("/signin");
|
|
110
|
+
expect(completeSiteSetup).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("lets the site's own owner answer", async () => {
|
|
114
|
+
const { app, completeSiteSetup } = createProvisionedSetupApp({
|
|
115
|
+
signedIn: true,
|
|
116
|
+
isMember: true,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await postLanguage(app);
|
|
120
|
+
|
|
121
|
+
expect(completeSiteSetup).toHaveBeenCalledOnce();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("GET /setup on a provisioned site", () => {
|
|
126
|
+
it("sends a non-member to the site instead of showing the form", async () => {
|
|
127
|
+
const { app } = createProvisionedSetupApp({
|
|
128
|
+
signedIn: true,
|
|
129
|
+
isMember: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const res = await app.request("/setup", { redirect: "manual" });
|
|
133
|
+
|
|
134
|
+
expect(res.status).toBe(302);
|
|
135
|
+
expect(res.headers.get("Location")).toBe("/");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Discover answer the setup screen collects.
|
|
3
|
+
*
|
|
4
|
+
* The point under test is that setup records an answer rather than inferring
|
|
5
|
+
* one — a ticked box and a cleared box both write, and a form that carried no
|
|
6
|
+
* field at all writes nothing, since that last case is an older client or a
|
|
7
|
+
* scripted setup and reading it as a refusal would opt sites out that never
|
|
8
|
+
* said so — and then acts on it, on the same terms as the settings page's own
|
|
9
|
+
* switch.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { Hono } from "hono";
|
|
14
|
+
import { createI18n } from "../../../i18n/index.js";
|
|
15
|
+
import { attachSession } from "../../../middleware/session.js";
|
|
16
|
+
import { ONBOARDING_STATUS } from "../../../lib/constants.js";
|
|
17
|
+
import { setupRoutes } from "../setup.js";
|
|
18
|
+
import type { Bindings } from "../../../types.js";
|
|
19
|
+
import type { AppVariables } from "../../../types/app-context.js";
|
|
20
|
+
|
|
21
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
22
|
+
|
|
23
|
+
function createMockAuth() {
|
|
24
|
+
return {
|
|
25
|
+
api: {
|
|
26
|
+
getSession: async () => ({
|
|
27
|
+
headers: new Headers(),
|
|
28
|
+
response: {
|
|
29
|
+
user: { id: "usr_owner", email: "owner@test.com", name: "O" },
|
|
30
|
+
session: { id: "ses_owner" },
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
} as unknown as AppVariables["auth"];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A provisioned site whose owner is answering. Both install kinds reach the
|
|
39
|
+
* Discover write through this one branch — a self-hosted site arrives here from
|
|
40
|
+
* its second screen, a hosted one from its only screen.
|
|
41
|
+
*/
|
|
42
|
+
function createSetupApp(
|
|
43
|
+
options: { demoMode?: boolean; shouldAnnounce?: boolean } = {},
|
|
44
|
+
) {
|
|
45
|
+
const completeSiteSetup = vi.fn(async () => {});
|
|
46
|
+
const updateDiscoverSetting = vi.fn(async () => ({
|
|
47
|
+
shouldAnnounce: options.shouldAnnounce ?? true,
|
|
48
|
+
}));
|
|
49
|
+
const announceToDiscover = vi.fn(async () => {});
|
|
50
|
+
// The hook `runDeferred` offers tests: every backgrounded promise lands
|
|
51
|
+
// here, so the announcement can be awaited instead of raced.
|
|
52
|
+
const pending: Promise<unknown>[] = [];
|
|
53
|
+
|
|
54
|
+
const app = new Hono<Env>();
|
|
55
|
+
app.use("*", async (c, next) => {
|
|
56
|
+
c.env = {
|
|
57
|
+
DISCOVER_PING_URL: "https://directory.test/api/discover/ping",
|
|
58
|
+
__pendingDeferred: pending,
|
|
59
|
+
} as unknown as Bindings;
|
|
60
|
+
c.set("auth", createMockAuth());
|
|
61
|
+
c.set("services", {
|
|
62
|
+
settings: {
|
|
63
|
+
getOnboardingStatus: async () => ONBOARDING_STATUS.PROVISIONED,
|
|
64
|
+
// Already named, so this is the hosted screen: language and Discover
|
|
65
|
+
// and nothing else.
|
|
66
|
+
get: async () => "My Blog",
|
|
67
|
+
updateDiscoverSetting,
|
|
68
|
+
announceToDiscover,
|
|
69
|
+
},
|
|
70
|
+
bootstrap: { completeSiteSetup },
|
|
71
|
+
siteMembers: {
|
|
72
|
+
get: async () => ({
|
|
73
|
+
createdAt: 0,
|
|
74
|
+
role: "owner",
|
|
75
|
+
siteId: "sit_test",
|
|
76
|
+
updatedAt: 0,
|
|
77
|
+
userId: "usr_owner",
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
} as unknown as AppVariables["services"]);
|
|
81
|
+
c.set("currentSite", {
|
|
82
|
+
createdAt: 0,
|
|
83
|
+
id: "sit_test",
|
|
84
|
+
key: "default",
|
|
85
|
+
status: "active",
|
|
86
|
+
updatedAt: 0,
|
|
87
|
+
});
|
|
88
|
+
c.set("appConfig", {
|
|
89
|
+
siteName: "Jant",
|
|
90
|
+
sitePathPrefix: "",
|
|
91
|
+
siteLanguage: "en",
|
|
92
|
+
siteUrl: "https://example.com",
|
|
93
|
+
demoMode: options.demoMode ?? false,
|
|
94
|
+
noindex: false,
|
|
95
|
+
rssFeedsEnabled: true,
|
|
96
|
+
} as AppVariables["appConfig"]);
|
|
97
|
+
c.set("lang", "en");
|
|
98
|
+
c.set("i18n", createI18n("en"));
|
|
99
|
+
await next();
|
|
100
|
+
});
|
|
101
|
+
app.use("*", attachSession());
|
|
102
|
+
app.route("/", setupRoutes);
|
|
103
|
+
|
|
104
|
+
return { app, updateDiscoverSetting, announceToDiscover, pending };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function post(app: Hono<Env>, body: Record<string, unknown>) {
|
|
108
|
+
return app.request("/setup", {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
body: JSON.stringify({ contentLanguage: "zh-Hans", ...body }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe("POST /setup, the Discover answer", () => {
|
|
116
|
+
it("stores `latest` when the box was ticked", async () => {
|
|
117
|
+
const { app, updateDiscoverSetting } = createSetupApp();
|
|
118
|
+
|
|
119
|
+
await post(app, { discover: true });
|
|
120
|
+
|
|
121
|
+
expect(updateDiscoverSetting).toHaveBeenCalledWith("latest", {
|
|
122
|
+
demoMode: false,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// The half that matters on a deployment which lists by default: a cleared box
|
|
127
|
+
// has to be written, or `DISCOVER=latest` keeps deciding for someone who just
|
|
128
|
+
// said no.
|
|
129
|
+
it("stores `off` when the box was cleared", async () => {
|
|
130
|
+
const { app, updateDiscoverSetting } = createSetupApp();
|
|
131
|
+
|
|
132
|
+
await post(app, { discover: false });
|
|
133
|
+
|
|
134
|
+
expect(updateDiscoverSetting).toHaveBeenCalledWith("off", {
|
|
135
|
+
demoMode: false,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("writes nothing when the form carried no answer", async () => {
|
|
140
|
+
const { app, updateDiscoverSetting } = createSetupApp();
|
|
141
|
+
|
|
142
|
+
await post(app, {});
|
|
143
|
+
|
|
144
|
+
expect(updateDiscoverSetting).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// The answer takes effect now, not when the author next happens to open a
|
|
148
|
+
// settings page they have no reason to open. A directory decides eligibility
|
|
149
|
+
// for itself and re-reads the feed on its own schedule, so a site with
|
|
150
|
+
// nothing published yet loses nothing by saying hello early.
|
|
151
|
+
it("announces the site when the box was ticked", async () => {
|
|
152
|
+
const { app, announceToDiscover, pending } = createSetupApp();
|
|
153
|
+
|
|
154
|
+
await post(app, { discover: true });
|
|
155
|
+
await Promise.all(pending);
|
|
156
|
+
|
|
157
|
+
expect(announceToDiscover).toHaveBeenCalledWith({
|
|
158
|
+
endpoint: "https://directory.test/api/discover/ping",
|
|
159
|
+
feedUrl: "https://example.com/latest/feed",
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Not awaited: a directory takes up to twelve seconds to give up on, and the
|
|
164
|
+
// last screen of setup is not a place to spend them. The outcome is recorded
|
|
165
|
+
// for the settings page's status block to read back.
|
|
166
|
+
it("does not hold the response while the directory answers", async () => {
|
|
167
|
+
const { app, announceToDiscover, pending } = createSetupApp();
|
|
168
|
+
let settled = false;
|
|
169
|
+
announceToDiscover.mockImplementation(async () => {
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
171
|
+
settled = true;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await post(app, { discover: true });
|
|
175
|
+
|
|
176
|
+
expect(settled).toBe(false);
|
|
177
|
+
await Promise.all(pending);
|
|
178
|
+
expect(settled).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Nothing to tell a directory that was never told anything: going from unset
|
|
182
|
+
// to `off` leaves the site exactly as unlisted as it already was.
|
|
183
|
+
it("announces nothing when the setting service reports no change", async () => {
|
|
184
|
+
const { app, announceToDiscover, pending } = createSetupApp({
|
|
185
|
+
shouldAnnounce: false,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await post(app, { discover: false });
|
|
189
|
+
await Promise.all(pending);
|
|
190
|
+
|
|
191
|
+
expect(announceToDiscover).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("announces nothing from a demo site", async () => {
|
|
195
|
+
const { app, announceToDiscover, pending } = createSetupApp({
|
|
196
|
+
demoMode: true,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await post(app, { discover: true });
|
|
200
|
+
await Promise.all(pending);
|
|
201
|
+
|
|
202
|
+
expect(announceToDiscover).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
});
|