@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,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for which of the two self-hosted setup screens a request gets.
|
|
3
|
+
*
|
|
4
|
+
* The step is never in the URL. It is read from the onboarding status and, on
|
|
5
|
+
* the second screen, from whether the site has a name yet — the only records
|
|
6
|
+
* that survive a closed tab, a reload, or a back button. These cover the route
|
|
7
|
+
* making that choice; `setup-page.test.tsx` covers what each screen then says.
|
|
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
|
+
const SESSION_COOKIE = "jant.session_token=abc; Path=/; HttpOnly";
|
|
22
|
+
|
|
23
|
+
function createMockAuth(signedIn: boolean) {
|
|
24
|
+
const signUpEmail = vi.fn(async () => ({
|
|
25
|
+
headers: new Headers({ "Set-Cookie": SESSION_COOKIE }),
|
|
26
|
+
response: { user: { id: "usr_new-owner" } },
|
|
27
|
+
}));
|
|
28
|
+
const updateUser = vi.fn(async () => ({}));
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
signUpEmail,
|
|
32
|
+
updateUser,
|
|
33
|
+
auth: {
|
|
34
|
+
api: {
|
|
35
|
+
signUpEmail,
|
|
36
|
+
updateUser,
|
|
37
|
+
getSession: async () => ({
|
|
38
|
+
headers: new Headers(),
|
|
39
|
+
response: signedIn
|
|
40
|
+
? {
|
|
41
|
+
user: { id: "usr_owner", email: "owner@test.com", name: "O" },
|
|
42
|
+
session: { id: "ses_owner" },
|
|
43
|
+
}
|
|
44
|
+
: null,
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
} as unknown as AppVariables["auth"],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createSetupApp(options: {
|
|
52
|
+
status: (typeof ONBOARDING_STATUS)[keyof typeof ONBOARDING_STATUS];
|
|
53
|
+
/** What `SITE_NAME` holds — null on a site nobody has named yet. */
|
|
54
|
+
storedSiteName?: string | null;
|
|
55
|
+
signedIn?: boolean;
|
|
56
|
+
isMember?: boolean;
|
|
57
|
+
}) {
|
|
58
|
+
const provisionOwnerAccount = vi.fn(async () => {});
|
|
59
|
+
const completeSiteSetup = vi.fn(async () => {});
|
|
60
|
+
const updateDiscoverSetting = vi.fn(async () => ({ shouldAnnounce: false }));
|
|
61
|
+
const { auth, signUpEmail, updateUser } = createMockAuth(
|
|
62
|
+
options.signedIn ?? false,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const app = new Hono<Env>();
|
|
66
|
+
app.use("*", async (c, next) => {
|
|
67
|
+
c.env = {} as Bindings;
|
|
68
|
+
c.set("auth", auth);
|
|
69
|
+
c.set("services", {
|
|
70
|
+
settings: {
|
|
71
|
+
getOnboardingStatus: async () => options.status,
|
|
72
|
+
get: async (key: string) =>
|
|
73
|
+
key === "SITE_NAME" ? (options.storedSiteName ?? null) : null,
|
|
74
|
+
updateDiscoverSetting,
|
|
75
|
+
},
|
|
76
|
+
bootstrap: { provisionOwnerAccount, completeSiteSetup },
|
|
77
|
+
siteMembers: {
|
|
78
|
+
get: async () =>
|
|
79
|
+
options.isMember
|
|
80
|
+
? {
|
|
81
|
+
createdAt: 0,
|
|
82
|
+
role: "owner",
|
|
83
|
+
siteId: "sit_test",
|
|
84
|
+
updatedAt: 0,
|
|
85
|
+
userId: "usr_owner",
|
|
86
|
+
}
|
|
87
|
+
: null,
|
|
88
|
+
},
|
|
89
|
+
} as unknown as AppVariables["services"]);
|
|
90
|
+
c.set("currentSite", {
|
|
91
|
+
createdAt: 0,
|
|
92
|
+
id: "sit_test",
|
|
93
|
+
key: "default",
|
|
94
|
+
status: "active",
|
|
95
|
+
updatedAt: 0,
|
|
96
|
+
});
|
|
97
|
+
c.set("appConfig", {
|
|
98
|
+
siteName: "Jant",
|
|
99
|
+
sitePathPrefix: "",
|
|
100
|
+
siteLanguage: "en",
|
|
101
|
+
demoMode: false,
|
|
102
|
+
noindex: false,
|
|
103
|
+
rssFeedsEnabled: true,
|
|
104
|
+
} as AppVariables["appConfig"]);
|
|
105
|
+
c.set("lang", "en");
|
|
106
|
+
c.set("i18n", createI18n("en"));
|
|
107
|
+
await next();
|
|
108
|
+
});
|
|
109
|
+
app.use("*", attachSession());
|
|
110
|
+
app.route("/", setupRoutes);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
app,
|
|
114
|
+
provisionOwnerAccount,
|
|
115
|
+
completeSiteSetup,
|
|
116
|
+
signUpEmail,
|
|
117
|
+
updateUser,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function post(app: Hono<Env>, body: Record<string, unknown>) {
|
|
122
|
+
return app.request("/setup", {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: { "Content-Type": "application/json" },
|
|
125
|
+
body: JSON.stringify(body),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
describe("GET /setup picks the step", () => {
|
|
130
|
+
it("asks an empty site for credentials, with no session needed", async () => {
|
|
131
|
+
const { app } = createSetupApp({ status: ONBOARDING_STATUS.PENDING });
|
|
132
|
+
|
|
133
|
+
const html = await (await app.request("/setup")).text();
|
|
134
|
+
|
|
135
|
+
expect(html).toContain("Setup · Step 1 of 2");
|
|
136
|
+
expect(html).toContain("setup-password");
|
|
137
|
+
expect(html).not.toContain("setup-site-name");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// The state the site rests in between the screens: an owner, and no answers.
|
|
141
|
+
it("asks a provisioned but unnamed site for its name", async () => {
|
|
142
|
+
const { app } = createSetupApp({
|
|
143
|
+
status: ONBOARDING_STATUS.PROVISIONED,
|
|
144
|
+
storedSiteName: null,
|
|
145
|
+
signedIn: true,
|
|
146
|
+
isMember: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const html = await (await app.request("/setup")).text();
|
|
150
|
+
|
|
151
|
+
expect(html).toContain("Setup · Step 2 of 2");
|
|
152
|
+
expect(html).toContain("setup-site-name");
|
|
153
|
+
expect(html).not.toContain("setup-password");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// The hosted arrival at the same screen. A name it already has is a question
|
|
157
|
+
// it must not be asked again.
|
|
158
|
+
it("asks a named site only what it is missing", async () => {
|
|
159
|
+
const { app } = createSetupApp({
|
|
160
|
+
status: ONBOARDING_STATUS.PROVISIONED,
|
|
161
|
+
storedSiteName: "My Blog",
|
|
162
|
+
signedIn: true,
|
|
163
|
+
isMember: true,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const html = await (await app.request("/setup")).text();
|
|
167
|
+
|
|
168
|
+
expect(html).not.toContain("setup-site-name");
|
|
169
|
+
expect(html).not.toContain("Step");
|
|
170
|
+
expect(html).toContain("setup-content-language");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("sends a finished site home", async () => {
|
|
174
|
+
const { app } = createSetupApp({ status: ONBOARDING_STATUS.COMPLETED });
|
|
175
|
+
|
|
176
|
+
const res = await app.request("/setup", { redirect: "manual" });
|
|
177
|
+
|
|
178
|
+
expect(res.status).toBe(302);
|
|
179
|
+
expect(res.headers.get("Location")).toBe("/");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("POST /setup, the account step", () => {
|
|
184
|
+
it("opens the account, stands the site up, and comes back for step two", async () => {
|
|
185
|
+
const { app, provisionOwnerAccount, signUpEmail } = createSetupApp({
|
|
186
|
+
status: ONBOARDING_STATUS.PENDING,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const res = await post(app, {
|
|
190
|
+
email: " Owner@Example.COM ",
|
|
191
|
+
password: "correct-horse-battery",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Named after the address until the next screen has a site name to put
|
|
195
|
+
// there, and normalized before it is used for either.
|
|
196
|
+
expect(signUpEmail).toHaveBeenCalledWith(
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
body: {
|
|
199
|
+
name: "owner",
|
|
200
|
+
email: "owner@example.com",
|
|
201
|
+
password: "correct-horse-battery",
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
expect(provisionOwnerAccount).toHaveBeenCalledWith({
|
|
206
|
+
ownerUserId: "usr_new-owner",
|
|
207
|
+
});
|
|
208
|
+
await expect(res.text()).resolves.toContain("/setup");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// The whole reason there is no `/signin` hop between the screens: the second
|
|
212
|
+
// one can only be answered by a member, so the first has to leave a session
|
|
213
|
+
// behind.
|
|
214
|
+
it("signs the new owner in on the way out", async () => {
|
|
215
|
+
const { app } = createSetupApp({ status: ONBOARDING_STATUS.PENDING });
|
|
216
|
+
|
|
217
|
+
const res = await post(app, {
|
|
218
|
+
email: "owner@example.com",
|
|
219
|
+
password: "correct-horse-battery",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(res.headers.get("Set-Cookie")).toBe(SESSION_COOKIE);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("rejects a password too short to be one", async () => {
|
|
226
|
+
const { app, provisionOwnerAccount } = createSetupApp({
|
|
227
|
+
status: ONBOARDING_STATUS.PENDING,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const res = await post(app, { email: "owner@example.com", password: "sh" });
|
|
231
|
+
|
|
232
|
+
await expect(res.text()).resolves.toContain("8 characters");
|
|
233
|
+
expect(provisionOwnerAccount).not.toHaveBeenCalled();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("POST /setup, the site step", () => {
|
|
238
|
+
const owner = {
|
|
239
|
+
status: ONBOARDING_STATUS.PROVISIONED,
|
|
240
|
+
storedSiteName: null,
|
|
241
|
+
signedIn: true,
|
|
242
|
+
isMember: true,
|
|
243
|
+
} as const;
|
|
244
|
+
|
|
245
|
+
it("carries the name, the language, and the browser's clock", async () => {
|
|
246
|
+
const { app, completeSiteSetup } = createSetupApp(owner);
|
|
247
|
+
|
|
248
|
+
await post(app, {
|
|
249
|
+
siteName: "My Blog",
|
|
250
|
+
contentLanguage: "zh-Hans",
|
|
251
|
+
language: "en-GB",
|
|
252
|
+
timezone: "Asia/Shanghai",
|
|
253
|
+
discover: true,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(completeSiteSetup).toHaveBeenCalledWith(
|
|
257
|
+
{
|
|
258
|
+
siteName: "My Blog",
|
|
259
|
+
siteLanguage: "zh-Hans",
|
|
260
|
+
browserLanguage: "en-GB",
|
|
261
|
+
timeZone: "Asia/Shanghai",
|
|
262
|
+
},
|
|
263
|
+
{ oldLanguage: "en" },
|
|
264
|
+
expect.anything(),
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// The rename better-auth needs, wired through the service rather than run in
|
|
269
|
+
// the route, and reaching better-auth with the request's own headers.
|
|
270
|
+
it("hands the service a way to rename the operator's account", async () => {
|
|
271
|
+
const { app, completeSiteSetup, updateUser } = createSetupApp(owner);
|
|
272
|
+
|
|
273
|
+
await post(app, { siteName: "My Blog", contentLanguage: "en" });
|
|
274
|
+
|
|
275
|
+
const deps = completeSiteSetup.mock.calls[0]?.[2] as {
|
|
276
|
+
updateCurrentUserName: (name: string) => Promise<void>;
|
|
277
|
+
};
|
|
278
|
+
await deps.updateCurrentUserName("My Blog");
|
|
279
|
+
|
|
280
|
+
expect(updateUser).toHaveBeenCalledWith(
|
|
281
|
+
expect.objectContaining({ body: { name: "My Blog" } }),
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("refuses to finish an unnamed site with no name", async () => {
|
|
286
|
+
const { app, completeSiteSetup } = createSetupApp(owner);
|
|
287
|
+
|
|
288
|
+
const res = await post(app, { contentLanguage: "en" });
|
|
289
|
+
|
|
290
|
+
await expect(res.text()).resolves.toContain("Site name");
|
|
291
|
+
expect(completeSiteSetup).not.toHaveBeenCalled();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// A named site is never asked for one, so a name arriving anyway is not the
|
|
295
|
+
// author renaming their site from the setup screen.
|
|
296
|
+
it("leaves a named site's name alone", async () => {
|
|
297
|
+
const { app, completeSiteSetup } = createSetupApp({
|
|
298
|
+
...owner,
|
|
299
|
+
storedSiteName: "My Blog",
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await post(app, { siteName: "Something Else", contentLanguage: "en" });
|
|
303
|
+
|
|
304
|
+
expect(completeSiteSetup).toHaveBeenCalledWith(
|
|
305
|
+
expect.objectContaining({ siteName: undefined, timeZone: undefined }),
|
|
306
|
+
expect.anything(),
|
|
307
|
+
expect.anything(),
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -1,26 +1,72 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
|
+
import {
|
|
4
|
+
createTestDatabase,
|
|
5
|
+
DEFAULT_TEST_SITE_ID,
|
|
6
|
+
} from "../../../__tests__/helpers/db.js";
|
|
3
7
|
import { sql } from "drizzle-orm";
|
|
4
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
navItems,
|
|
10
|
+
settings,
|
|
11
|
+
siteDomains,
|
|
12
|
+
siteMembers,
|
|
13
|
+
sites,
|
|
14
|
+
} from "../../../db/schema.js";
|
|
5
15
|
import { createBootstrapService } from "../../../services/bootstrap.js";
|
|
16
|
+
import {
|
|
17
|
+
createSiteService,
|
|
18
|
+
TRANSIENT_SINGLE_SITE_ID,
|
|
19
|
+
} from "../../../services/site.js";
|
|
6
20
|
import type { Database } from "../../../db/index.js";
|
|
7
21
|
import type { BootstrapService } from "../../../services/bootstrap.js";
|
|
8
22
|
|
|
9
23
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
24
|
+
* The bootstrap service one self-hosted setup request gets: bound to the site
|
|
25
|
+
* that request resolved, the way the runtime binds it. Resolved afresh for
|
|
26
|
+
* each screen, because the account step is what creates the site the second
|
|
27
|
+
* screen finds.
|
|
28
|
+
*/
|
|
29
|
+
async function bootstrapForRequest(db: Database): Promise<BootstrapService> {
|
|
30
|
+
const { site } = await createSiteService(db).resolveSingleSite();
|
|
31
|
+
return createBootstrapService(db, site.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reproduces both halves of POST /setup back to back — the shape a self-hosted
|
|
36
|
+
* install ends up in — to verify setup stays idempotent even when managed shell
|
|
37
|
+
* data already exists.
|
|
12
38
|
*/
|
|
13
39
|
async function runSetupBootstrap(
|
|
14
|
-
services: {
|
|
15
|
-
overrides: Partial<
|
|
16
|
-
Parameters<BootstrapService["completeInitialSetup"]>[0]
|
|
17
|
-
> = {},
|
|
40
|
+
services: { db: Database },
|
|
41
|
+
overrides: Partial<Parameters<BootstrapService["completeSiteSetup"]>[0]> = {},
|
|
18
42
|
) {
|
|
19
|
-
await
|
|
43
|
+
await (
|
|
44
|
+
await bootstrapForRequest(services.db)
|
|
45
|
+
).provisionOwnerAccount({
|
|
20
46
|
ownerUserId: "usr_test-owner",
|
|
21
|
-
siteName: "Jant Demo",
|
|
22
|
-
...overrides,
|
|
23
47
|
});
|
|
48
|
+
await (
|
|
49
|
+
await bootstrapForRequest(services.db)
|
|
50
|
+
).completeSiteSetup(
|
|
51
|
+
{ siteName: "Jant Demo", ...overrides },
|
|
52
|
+
{ oldLanguage: "" },
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Adds a second tenant, which is what a hosted database always has. */
|
|
57
|
+
async function insertOtherSite(
|
|
58
|
+
db: Database,
|
|
59
|
+
id = "sit_other00000000000000000000",
|
|
60
|
+
) {
|
|
61
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
62
|
+
await db.insert(sites).values({
|
|
63
|
+
id,
|
|
64
|
+
key: "other",
|
|
65
|
+
status: "active",
|
|
66
|
+
createdAt: timestamp - 60,
|
|
67
|
+
updatedAt: timestamp - 60,
|
|
68
|
+
});
|
|
69
|
+
return id;
|
|
24
70
|
}
|
|
25
71
|
|
|
26
72
|
describe("Setup bootstrap logic", () => {
|
|
@@ -34,7 +80,8 @@ describe("Setup bootstrap logic", () => {
|
|
|
34
80
|
const db = testDb.db as unknown as Database;
|
|
35
81
|
services = {
|
|
36
82
|
db,
|
|
37
|
-
|
|
83
|
+
// The site both screens resolve once its row exists.
|
|
84
|
+
bootstrap: createBootstrapService(db, DEFAULT_TEST_SITE_ID),
|
|
38
85
|
};
|
|
39
86
|
});
|
|
40
87
|
|
|
@@ -46,9 +93,9 @@ describe("Setup bootstrap logic", () => {
|
|
|
46
93
|
|
|
47
94
|
expect(navItemsList.map((item) => item.systemKey)).toEqual([
|
|
48
95
|
"featured",
|
|
49
|
-
"collections",
|
|
50
96
|
"archive",
|
|
51
|
-
"
|
|
97
|
+
"collections",
|
|
98
|
+
"subscribe",
|
|
52
99
|
"settings",
|
|
53
100
|
]);
|
|
54
101
|
});
|
|
@@ -61,35 +108,150 @@ describe("Setup bootstrap logic", () => {
|
|
|
61
108
|
expect(onboardingRow?.value).toBe("completed");
|
|
62
109
|
});
|
|
63
110
|
|
|
64
|
-
|
|
111
|
+
// The state the site rests in between the two setup screens. It has to be
|
|
112
|
+
// `provisioned` and not `pending`: that is what lets an author who lost the
|
|
113
|
+
// session in between sign back in and finish, rather than being bounced back
|
|
114
|
+
// to a form that would try to create their account a second time.
|
|
115
|
+
it("leaves the site provisioned and unnamed after the account step", async () => {
|
|
116
|
+
await services.bootstrap.provisionOwnerAccount({
|
|
117
|
+
ownerUserId: "usr_test-owner",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const rows = await services.db.select().from(settings);
|
|
121
|
+
expect(rows.find((row) => row.key === "ONBOARDING_STATUS")?.value).toBe(
|
|
122
|
+
"provisioned",
|
|
123
|
+
);
|
|
124
|
+
expect(rows.find((row) => row.key === "SITE_NAME")).toBeUndefined();
|
|
125
|
+
|
|
126
|
+
const navItemsList = await services.db.select().from(navItems);
|
|
127
|
+
expect(navItemsList).toHaveLength(5);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// The hosted path through the same method: the control plane named the site
|
|
131
|
+
// at creation, and the author answering the last screen must not blank it.
|
|
132
|
+
it("leaves an existing name alone when the last screen asked for none", async () => {
|
|
133
|
+
await runSetupBootstrap(services);
|
|
134
|
+
await services.bootstrap.completeSiteSetup(
|
|
135
|
+
{ siteLanguage: "en" },
|
|
136
|
+
{ oldLanguage: "en" },
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const rows = await services.db.select().from(settings);
|
|
140
|
+
expect(rows.find((row) => row.key === "SITE_NAME")?.value).toBe(
|
|
141
|
+
"Jant Demo",
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("renames the operator's account to match the site", async () => {
|
|
146
|
+
const updateCurrentUserName = vi.fn(async () => undefined);
|
|
147
|
+
|
|
148
|
+
await services.bootstrap.provisionOwnerAccount({
|
|
149
|
+
ownerUserId: "usr_test-owner",
|
|
150
|
+
});
|
|
151
|
+
await services.bootstrap.completeSiteSetup(
|
|
152
|
+
{ siteName: " Jant Demo " },
|
|
153
|
+
{ oldLanguage: "" },
|
|
154
|
+
{ updateCurrentUserName },
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(updateCurrentUserName).toHaveBeenCalledWith("Jant Demo");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// The rename runs before onboarding closes, so a better-auth failure leaves
|
|
161
|
+
// the author on the screen they were already on instead of inside a finished
|
|
162
|
+
// site under a stale account name.
|
|
163
|
+
it("leaves setup open when the account rename fails", async () => {
|
|
164
|
+
await services.bootstrap.provisionOwnerAccount({
|
|
165
|
+
ownerUserId: "usr_test-owner",
|
|
166
|
+
});
|
|
167
|
+
await expect(
|
|
168
|
+
services.bootstrap.completeSiteSetup(
|
|
169
|
+
{ siteName: "Jant Demo" },
|
|
170
|
+
{ oldLanguage: "" },
|
|
171
|
+
{
|
|
172
|
+
updateCurrentUserName: async () => {
|
|
173
|
+
throw new Error("better-auth unavailable");
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
),
|
|
177
|
+
).rejects.toThrow("better-auth unavailable");
|
|
178
|
+
|
|
179
|
+
const rows = await services.db.select().from(settings);
|
|
180
|
+
expect(rows.find((row) => row.key === "ONBOARDING_STATUS")?.value).toBe(
|
|
181
|
+
"provisioned",
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("stores the chosen content language and timezone during setup", async () => {
|
|
65
186
|
await runSetupBootstrap(services, {
|
|
66
187
|
siteLanguage: "en",
|
|
67
|
-
cjkSerifFont: "zh-Hans",
|
|
68
188
|
timeZone: "Asia/Shanghai",
|
|
69
189
|
});
|
|
70
190
|
|
|
71
191
|
const rows = await services.db.select().from(settings);
|
|
72
192
|
expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe("en");
|
|
73
|
-
expect(rows.find((row) => row.key === "CJK_SERIF_FONT")?.value).toBe(
|
|
74
|
-
"zh-Hans",
|
|
75
|
-
);
|
|
76
193
|
expect(rows.find((row) => row.key === "TIME_ZONE")?.value).toBe(
|
|
77
194
|
"Asia/Shanghai",
|
|
78
195
|
);
|
|
79
196
|
});
|
|
80
197
|
|
|
81
|
-
it("pins the dashboard language
|
|
198
|
+
it("pins the dashboard language when only the browser knows it", async () => {
|
|
199
|
+
// Someone running an English blog from a Chinese browser: English site,
|
|
200
|
+
// Chinese dashboard. Following the content language would miss this.
|
|
201
|
+
await runSetupBootstrap(services, {
|
|
202
|
+
siteLanguage: "en",
|
|
203
|
+
browserLanguage: "zh-TW",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const rows = await services.db.select().from(settings);
|
|
207
|
+
expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe("en");
|
|
208
|
+
expect(rows.find((row) => row.key === "DASHBOARD_LANGUAGE")?.value).toBe(
|
|
209
|
+
"zh-Hant",
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("leaves the dashboard following a content language the author chose", async () => {
|
|
214
|
+
// The reverse mismatch, and the common one: an English browser is what
|
|
215
|
+
// every unconfigured machine reports, so it is no evidence against the
|
|
216
|
+
// language just chosen by hand. The dashboard must not freeze to English.
|
|
217
|
+
await runSetupBootstrap(services, {
|
|
218
|
+
siteLanguage: "zh-Hans",
|
|
219
|
+
browserLanguage: "en",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const rows = await services.db.select().from(settings);
|
|
223
|
+
expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe(
|
|
224
|
+
"zh-Hans",
|
|
225
|
+
);
|
|
226
|
+
expect(
|
|
227
|
+
rows.find((row) => row.key === "DASHBOARD_LANGUAGE"),
|
|
228
|
+
).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("leaves the dashboard following when the browser sent none", async () => {
|
|
82
232
|
await runSetupBootstrap(services, { siteLanguage: "zh-TW" });
|
|
83
233
|
|
|
84
234
|
const rows = await services.db.select().from(settings);
|
|
85
|
-
// Content language stays verbatim
|
|
86
|
-
//
|
|
235
|
+
// Content language stays verbatim, and the dashboard derives zh-Hant from
|
|
236
|
+
// it at render time rather than being pinned here.
|
|
87
237
|
expect(rows.find((row) => row.key === "SITE_LANGUAGE")?.value).toBe(
|
|
88
238
|
"zh-TW",
|
|
89
239
|
);
|
|
90
|
-
expect(
|
|
91
|
-
"
|
|
92
|
-
);
|
|
240
|
+
expect(
|
|
241
|
+
rows.find((row) => row.key === "DASHBOARD_LANGUAGE"),
|
|
242
|
+
).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("leaves the dashboard following when browser and content agree", async () => {
|
|
246
|
+
await runSetupBootstrap(services, {
|
|
247
|
+
siteLanguage: "zh-Hans",
|
|
248
|
+
browserLanguage: "zh-CN",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const rows = await services.db.select().from(settings);
|
|
252
|
+
expect(
|
|
253
|
+
rows.find((row) => row.key === "DASHBOARD_LANGUAGE"),
|
|
254
|
+
).toBeUndefined();
|
|
93
255
|
});
|
|
94
256
|
|
|
95
257
|
it("is idempotent when default navigation already exists", async () => {
|
|
@@ -141,5 +303,95 @@ describe("Setup bootstrap logic", () => {
|
|
|
141
303
|
const domainRows = await services.db.select().from(siteDomains);
|
|
142
304
|
expect(siteRows).toHaveLength(1);
|
|
143
305
|
expect(domainRows).toHaveLength(0);
|
|
306
|
+
|
|
307
|
+
const rows = await services.db.select().from(settings);
|
|
308
|
+
expect(rows.find((row) => row.key === "SITE_NAME")?.siteId).toBe(
|
|
309
|
+
siteRows[0]?.id,
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Settings written under the placeholder would belong to no site at all.
|
|
314
|
+
it("refuses to finish a site the account step has not created", async () => {
|
|
315
|
+
await services.db.run(sql`DELETE FROM "site"`);
|
|
316
|
+
|
|
317
|
+
await expect(
|
|
318
|
+
createBootstrapService(
|
|
319
|
+
services.db,
|
|
320
|
+
TRANSIENT_SINGLE_SITE_ID,
|
|
321
|
+
).completeSiteSetup({ siteName: "Jant Demo" }, { oldLanguage: "" }),
|
|
322
|
+
).rejects.toThrow("completeSiteSetup needs an existing site");
|
|
323
|
+
|
|
324
|
+
expect(await services.db.select().from(settings)).toHaveLength(0);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// A hosted database holds every tenant, so "the only site" has no answer there.
|
|
329
|
+
// Setup's last screen must write to the site the request's host resolved — the
|
|
330
|
+
// one the route's membership check just read — and leave every other tenant
|
|
331
|
+
// alone. The account screen has no hosted use at all.
|
|
332
|
+
describe("Setup bootstrap on a host-based install", () => {
|
|
333
|
+
function createHostedServices() {
|
|
334
|
+
const { services, db } = createTestApp({
|
|
335
|
+
siteResolutionMode: "host-based",
|
|
336
|
+
});
|
|
337
|
+
return { services, db };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
it("finishes the request's site while other sites share the database", async () => {
|
|
341
|
+
const { services, db } = createHostedServices();
|
|
342
|
+
const otherSiteId = await insertOtherSite(db);
|
|
343
|
+
|
|
344
|
+
await services.bootstrap.completeSiteSetup(
|
|
345
|
+
{ siteLanguage: "en" },
|
|
346
|
+
{ oldLanguage: "" },
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const rows = await db.select().from(settings);
|
|
350
|
+
expect(
|
|
351
|
+
rows.find(
|
|
352
|
+
(row) =>
|
|
353
|
+
row.siteId === DEFAULT_TEST_SITE_ID &&
|
|
354
|
+
row.key === "ONBOARDING_STATUS",
|
|
355
|
+
)?.value,
|
|
356
|
+
).toBe("completed");
|
|
357
|
+
expect(rows.filter((row) => row.siteId === otherSiteId)).toHaveLength(0);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// A hosted site's owner arrives through the control plane's handoff. The
|
|
361
|
+
// account step would make whoever signed in the owner of the site the host
|
|
362
|
+
// resolved, so it refuses before writing anything to any tenant.
|
|
363
|
+
it("refuses the account step and writes nothing", async () => {
|
|
364
|
+
const { services, db } = createHostedServices();
|
|
365
|
+
await insertOtherSite(db);
|
|
366
|
+
|
|
367
|
+
await expect(
|
|
368
|
+
services.bootstrap.provisionOwnerAccount({
|
|
369
|
+
ownerUserId: "usr_test-owner",
|
|
370
|
+
}),
|
|
371
|
+
).rejects.toThrow(
|
|
372
|
+
"provisionOwnerAccount runs on self-hosted installs only",
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
expect(await db.select().from(sites)).toHaveLength(2);
|
|
376
|
+
expect(await db.select().from(siteMembers)).toHaveLength(0);
|
|
377
|
+
expect(await db.select().from(navItems)).toHaveLength(0);
|
|
378
|
+
expect(await db.select().from(settings)).toHaveLength(0);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// The control plane creates hosted sites. Materializing one here would add a
|
|
382
|
+
// stray tenant to a database that holds real ones.
|
|
383
|
+
it("never creates a site when the request resolved none", async () => {
|
|
384
|
+
const db = createTestDatabase().db as unknown as Database;
|
|
385
|
+
await db.run(sql`DELETE FROM "site"`);
|
|
386
|
+
|
|
387
|
+
await expect(
|
|
388
|
+
createBootstrapService(db, TRANSIENT_SINGLE_SITE_ID, {
|
|
389
|
+
siteResolutionMode: "host-based",
|
|
390
|
+
}).provisionOwnerAccount({ ownerUserId: "usr_test-owner" }),
|
|
391
|
+
).rejects.toThrow(
|
|
392
|
+
"provisionOwnerAccount runs on self-hosted installs only",
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
expect(await db.select().from(sites)).toHaveLength(0);
|
|
144
396
|
});
|
|
145
397
|
});
|