@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,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole self-hosted setup, walked once with nothing mocked.
|
|
3
|
+
*
|
|
4
|
+
* Real services, a real better-auth, a real database: the other setup tests
|
|
5
|
+
* each hold one seam still, and this one is here to catch what only shows up
|
|
6
|
+
* when they are all moving — the session the first screen mints being the one
|
|
7
|
+
* the second screen accepts, the status the first screen writes being the one
|
|
8
|
+
* the second screen reads, and the site ending up named after neither of them
|
|
9
|
+
* guessed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
14
|
+
import { createAuth } from "../../../auth.js";
|
|
15
|
+
import { attachSession } from "../../../middleware/session.js";
|
|
16
|
+
import {
|
|
17
|
+
settings as settingsTable,
|
|
18
|
+
siteMembers,
|
|
19
|
+
user,
|
|
20
|
+
} from "../../../db/schema.js";
|
|
21
|
+
import { setupRoutes } from "../setup.js";
|
|
22
|
+
|
|
23
|
+
const PASSWORD = "correct-horse-battery";
|
|
24
|
+
|
|
25
|
+
function createSetupApp(options: Parameters<typeof createTestApp>[0] = {}) {
|
|
26
|
+
const { app, services, db } = createTestApp(options);
|
|
27
|
+
|
|
28
|
+
// The helper installs a stub that answers every session question the same
|
|
29
|
+
// way. Setup needs the real thing: it mints a session on the first screen and
|
|
30
|
+
// has to be believed on the second.
|
|
31
|
+
const auth = createAuth(db, {
|
|
32
|
+
secret: "test-secret-with-enough-entropy-for-session-cookies",
|
|
33
|
+
baseURL: "http://localhost",
|
|
34
|
+
useSecureCookies: false,
|
|
35
|
+
});
|
|
36
|
+
app.use("*", async (c, next) => {
|
|
37
|
+
c.set("auth", auth);
|
|
38
|
+
await next();
|
|
39
|
+
});
|
|
40
|
+
app.use("*", attachSession());
|
|
41
|
+
app.route("/", setupRoutes);
|
|
42
|
+
|
|
43
|
+
return { app, services, db, auth };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function post(
|
|
47
|
+
app: ReturnType<typeof createSetupApp>["app"],
|
|
48
|
+
body: Record<string, unknown>,
|
|
49
|
+
cookie?: string,
|
|
50
|
+
) {
|
|
51
|
+
return app.request("/setup", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
...(cookie ? { Cookie: cookie } : {}),
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify(body),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The session cookie a response minted, in the form a request sends back. */
|
|
62
|
+
function sessionCookie(res: Response): string {
|
|
63
|
+
const cookie = res.headers
|
|
64
|
+
.getSetCookie()
|
|
65
|
+
.find((value) => value.includes("session_token="));
|
|
66
|
+
expect(cookie).toBeDefined();
|
|
67
|
+
return cookie!.split(";")[0]!;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readSettings(db: ReturnType<typeof createSetupApp>["db"]) {
|
|
71
|
+
const rows = await db.select().from(settingsTable);
|
|
72
|
+
return Object.fromEntries(rows.map((row) => [row.key, row.value]));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("self-hosted setup, both screens", () => {
|
|
76
|
+
it("walks an empty install to a finished site", async () => {
|
|
77
|
+
const { app, db } = createSetupApp();
|
|
78
|
+
|
|
79
|
+
const first = await app.request("/setup");
|
|
80
|
+
await expect(first.text()).resolves.toContain("Setup · Step 1 of 2");
|
|
81
|
+
|
|
82
|
+
const account = await post(app, {
|
|
83
|
+
email: "owner@example.com",
|
|
84
|
+
password: PASSWORD,
|
|
85
|
+
});
|
|
86
|
+
const cookie = sessionCookie(account);
|
|
87
|
+
|
|
88
|
+
// Between the screens: an owner, a navigation profile, and no answers.
|
|
89
|
+
const midway = await readSettings(db);
|
|
90
|
+
expect(midway["ONBOARDING_STATUS"]).toBe("provisioned");
|
|
91
|
+
expect(midway["SITE_NAME"]).toBeUndefined();
|
|
92
|
+
|
|
93
|
+
// The session the first screen minted is the one the second screen trusts.
|
|
94
|
+
const second = await app.request("/setup", { headers: { Cookie: cookie } });
|
|
95
|
+
const secondHtml = await second.text();
|
|
96
|
+
expect(secondHtml).toContain("Setup · Step 2 of 2");
|
|
97
|
+
expect(secondHtml).toContain("setup-site-name");
|
|
98
|
+
|
|
99
|
+
await post(
|
|
100
|
+
app,
|
|
101
|
+
{
|
|
102
|
+
siteName: "My Blog",
|
|
103
|
+
contentLanguage: "zh-Hans",
|
|
104
|
+
language: "en-GB",
|
|
105
|
+
timezone: "Asia/Shanghai",
|
|
106
|
+
discover: true,
|
|
107
|
+
},
|
|
108
|
+
cookie,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const finished = await readSettings(db);
|
|
112
|
+
expect(finished["ONBOARDING_STATUS"]).toBe("completed");
|
|
113
|
+
expect(finished["SITE_NAME"]).toBe("My Blog");
|
|
114
|
+
expect(finished["SITE_LANGUAGE"]).toBe("zh-Hans");
|
|
115
|
+
expect(finished["TIME_ZONE"]).toBe("Asia/Shanghai");
|
|
116
|
+
expect(finished["DISCOVER"]).toBe("latest");
|
|
117
|
+
// Left following the content language. An English browser is what every
|
|
118
|
+
// unconfigured machine reports, so it is no evidence against the language
|
|
119
|
+
// the author just chose by hand one field above.
|
|
120
|
+
expect(finished["DASHBOARD_LANGUAGE"]).toBeUndefined();
|
|
121
|
+
|
|
122
|
+
// The account opened under the address, and the second screen renamed it.
|
|
123
|
+
const [owner] = await db.select().from(user);
|
|
124
|
+
expect(owner?.name).toBe("My Blog");
|
|
125
|
+
expect(owner?.email).toBe("owner@example.com");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// A finished site must never offer the screen that opens the first account
|
|
129
|
+
// again, to anyone, signed in or not.
|
|
130
|
+
it("closes setup once the site is finished", async () => {
|
|
131
|
+
const { app } = createSetupApp();
|
|
132
|
+
|
|
133
|
+
const cookie = sessionCookie(
|
|
134
|
+
await post(app, { email: "owner@example.com", password: PASSWORD }),
|
|
135
|
+
);
|
|
136
|
+
await post(app, { siteName: "My Blog", contentLanguage: "en" }, cookie);
|
|
137
|
+
|
|
138
|
+
const page = await app.request("/setup", {
|
|
139
|
+
headers: { Cookie: cookie },
|
|
140
|
+
redirect: "manual",
|
|
141
|
+
});
|
|
142
|
+
expect(page.status).toBe(302);
|
|
143
|
+
expect(page.headers.get("Location")).toBe("/");
|
|
144
|
+
|
|
145
|
+
const submitted = await post(
|
|
146
|
+
app,
|
|
147
|
+
{ email: "someone@example.com", password: PASSWORD },
|
|
148
|
+
cookie,
|
|
149
|
+
);
|
|
150
|
+
expect(submitted.status).toBe(302);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Losing the session between the screens is the failure the split invented.
|
|
154
|
+
// The site is provisioned, so the setup screen refuses an unauthenticated
|
|
155
|
+
// visitor — and `/signin` stays open for them, which is how they get back.
|
|
156
|
+
it("hides the second screen from a lost session", async () => {
|
|
157
|
+
const { app } = createSetupApp();
|
|
158
|
+
|
|
159
|
+
await post(app, { email: "owner@example.com", password: PASSWORD });
|
|
160
|
+
|
|
161
|
+
const res = await app.request("/setup", { redirect: "manual" });
|
|
162
|
+
|
|
163
|
+
expect(res.status).toBe(302);
|
|
164
|
+
expect(res.headers.get("Location")).toBe("/");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// The failure that used to brick an install: the account is opened, standing
|
|
168
|
+
// the site up then fails, and the site is left `pending` with a user row in
|
|
169
|
+
// it. From there `createAuth` refuses every signup and the onboarding
|
|
170
|
+
// middleware sends `/signin` and `/reset` back to this screen — so retrying
|
|
171
|
+
// the screen has to be the way out.
|
|
172
|
+
it("resumes a first screen that failed after opening the account", async () => {
|
|
173
|
+
const { app, services, db } = createSetupApp();
|
|
174
|
+
const provision = vi.spyOn(services.bootstrap, "provisionOwnerAccount");
|
|
175
|
+
provision.mockRejectedValueOnce(new Error("D1 unavailable"));
|
|
176
|
+
|
|
177
|
+
const failed = await post(app, {
|
|
178
|
+
email: "owner@example.com",
|
|
179
|
+
password: PASSWORD,
|
|
180
|
+
});
|
|
181
|
+
await expect(failed.text()).resolves.toContain("Couldn't create your");
|
|
182
|
+
|
|
183
|
+
// The account outlived the failure, and the site did not advance.
|
|
184
|
+
expect((await db.select().from(user)).length).toBe(1);
|
|
185
|
+
expect(await services.settings.getOnboardingStatus()).toBe("pending");
|
|
186
|
+
|
|
187
|
+
// Registration is closed to everyone now, this address included — which is
|
|
188
|
+
// why the retry has to resume rather than create.
|
|
189
|
+
const resumed = await post(app, {
|
|
190
|
+
email: "owner@example.com",
|
|
191
|
+
password: PASSWORD,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(await services.settings.getOnboardingStatus()).toBe("provisioned");
|
|
195
|
+
const second = await app.request("/setup", {
|
|
196
|
+
headers: { Cookie: sessionCookie(resumed) },
|
|
197
|
+
});
|
|
198
|
+
await expect(second.text()).resolves.toContain("Setup · Step 2 of 2");
|
|
199
|
+
// Still one account: resuming picked the existing one up.
|
|
200
|
+
expect((await db.select().from(user)).length).toBe(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("refuses to resume on the wrong password", async () => {
|
|
204
|
+
const { app, services, db } = createSetupApp();
|
|
205
|
+
vi.spyOn(services.bootstrap, "provisionOwnerAccount").mockRejectedValueOnce(
|
|
206
|
+
new Error("D1 unavailable"),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
await post(app, { email: "owner@example.com", password: PASSWORD });
|
|
210
|
+
const res = await post(app, {
|
|
211
|
+
email: "owner@example.com",
|
|
212
|
+
password: "not-the-password",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await expect(res.text()).resolves.toContain("Couldn't create your");
|
|
216
|
+
expect(res.headers.getSetCookie()).toHaveLength(0);
|
|
217
|
+
expect(await services.settings.getOnboardingStatus()).toBe("pending");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// The reason a different address is not an escape hatch, and so the reason
|
|
221
|
+
// resuming had to be built: the registration hook counts rows, not emails.
|
|
222
|
+
it("will not open a second account to get around it", async () => {
|
|
223
|
+
const { app, services, db } = createSetupApp();
|
|
224
|
+
vi.spyOn(services.bootstrap, "provisionOwnerAccount").mockRejectedValueOnce(
|
|
225
|
+
new Error("D1 unavailable"),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await post(app, { email: "owner@example.com", password: PASSWORD });
|
|
229
|
+
const res = await post(app, {
|
|
230
|
+
email: "someone-else@example.com",
|
|
231
|
+
password: PASSWORD,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await expect(res.text()).resolves.toContain("Couldn't create your");
|
|
235
|
+
expect((await db.select().from(user)).length).toBe(1);
|
|
236
|
+
expect(await services.settings.getOnboardingStatus()).toBe("pending");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// A hosted site's owner arrives through the control plane's handoff, never
|
|
241
|
+
// through this screen. The control plane creates each hosted site already
|
|
242
|
+
// provisioned, so a `pending` one is a site that lost its status somehow — and
|
|
243
|
+
// the database behind it holds every tenant's accounts. Signing in here with
|
|
244
|
+
// any of them must not come away owning the site.
|
|
245
|
+
describe("setup on a host-based install", () => {
|
|
246
|
+
it("never makes an existing account the owner of a hosted site", async () => {
|
|
247
|
+
const { app, services, db, auth } = createSetupApp({
|
|
248
|
+
siteResolutionMode: "host-based",
|
|
249
|
+
});
|
|
250
|
+
// Another tenant's owner. Being the first account, it also closes
|
|
251
|
+
// registration, as it is closed on any hosted database.
|
|
252
|
+
await auth.api.signUpEmail({
|
|
253
|
+
body: {
|
|
254
|
+
name: "Other Tenant",
|
|
255
|
+
email: "other@example.com",
|
|
256
|
+
password: PASSWORD,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const res = await post(app, {
|
|
261
|
+
email: "other@example.com",
|
|
262
|
+
password: PASSWORD,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await expect(res.text()).resolves.toContain("Couldn't create your");
|
|
266
|
+
expect(res.headers.getSetCookie()).toHaveLength(0);
|
|
267
|
+
expect(await db.select().from(siteMembers)).toHaveLength(0);
|
|
268
|
+
expect(await services.settings.getOnboardingStatus()).toBe("pending");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for what the first-run screens say.
|
|
3
|
+
*
|
|
4
|
+
* These render the component rather than the route: the point under test is
|
|
5
|
+
* that an author is told which step this is — a hosted one landing on an
|
|
6
|
+
* unfamiliar domain, whose site it belongs to; a self-hosted one, how much of
|
|
7
|
+
* setup is left. Neither is something the POST handler or the onboarding
|
|
8
|
+
* middleware can vouch for.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Context } from "hono";
|
|
12
|
+
import { renderToString } from "hono/jsx/dom/server";
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
import { I18nProvider } from "../../../i18n/context.js";
|
|
15
|
+
import { createI18n } from "../../../i18n/i18n.js";
|
|
16
|
+
import type { DiscoverPageUrls } from "../../../lib/discover.js";
|
|
17
|
+
import { getDiscoverCopy } from "../../../ui/dash/settings/discover-copy.js";
|
|
18
|
+
import { SetupContent } from "../setup.js";
|
|
19
|
+
|
|
20
|
+
type SetupProps = Parameters<typeof SetupContent>[0];
|
|
21
|
+
|
|
22
|
+
const PAGES: DiscoverPageUrls = {
|
|
23
|
+
home: "https://jant.me/discover",
|
|
24
|
+
links: "https://jant.me/links",
|
|
25
|
+
quotes: "https://jant.me/quotes",
|
|
26
|
+
rules: "https://jant.me/discover/about",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function render(props: SetupProps): string {
|
|
30
|
+
const i18n = createI18n("en");
|
|
31
|
+
const c = {
|
|
32
|
+
get: (key: string) => (key === "i18n" ? i18n : undefined),
|
|
33
|
+
} as unknown as Context;
|
|
34
|
+
|
|
35
|
+
I18nProvider({ c, children: "" });
|
|
36
|
+
return renderToString(SetupContent(props));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("SetupContent — hosted site", () => {
|
|
40
|
+
const provisioned: SetupProps = {
|
|
41
|
+
mode: "site",
|
|
42
|
+
askSiteName: false,
|
|
43
|
+
contentLanguage: "en",
|
|
44
|
+
siteName: "My Blog",
|
|
45
|
+
discoverAvailable: true,
|
|
46
|
+
discoverDefault: true,
|
|
47
|
+
discoverPages: PAGES,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
it("names the step and the site in one line", () => {
|
|
51
|
+
const html = render(provisioned);
|
|
52
|
+
|
|
53
|
+
expect(html).toContain("Setup · My Blog");
|
|
54
|
+
expect(html).toContain("What language do you write in?");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("keeps the line when the site has no name", () => {
|
|
58
|
+
const { siteName: _omitted, ...unnamed } = provisioned;
|
|
59
|
+
const html = render(unnamed);
|
|
60
|
+
|
|
61
|
+
expect(html).toContain(">Setup</p>");
|
|
62
|
+
expect(html).not.toContain("·");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("drops a blank name rather than trailing a separator", () => {
|
|
66
|
+
const html = render({ ...provisioned, siteName: " " });
|
|
67
|
+
|
|
68
|
+
expect(html).not.toContain("·");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("asks nothing but the language and Discover", () => {
|
|
72
|
+
const html = render(provisioned);
|
|
73
|
+
|
|
74
|
+
expect(html).toContain("setup-content-language");
|
|
75
|
+
expect(html).not.toContain("setup-email");
|
|
76
|
+
expect(html).not.toContain("setup-password");
|
|
77
|
+
expect(html).not.toContain("setup-site-name");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// A hosted author never saw a first screen — they created their account in
|
|
81
|
+
// the control plane — so counting steps at them would name one they cannot
|
|
82
|
+
// account for.
|
|
83
|
+
it("counts no steps on the only screen it shows", () => {
|
|
84
|
+
const html = render(provisioned);
|
|
85
|
+
|
|
86
|
+
expect(html).not.toContain("Step");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// The control plane already set the clock; this screen can be opened from
|
|
90
|
+
// anywhere, and the browser reporting another zone is not a decision.
|
|
91
|
+
it("reports no time zone", () => {
|
|
92
|
+
const html = render(provisioned);
|
|
93
|
+
|
|
94
|
+
expect(html).not.toContain("timezone");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The box's state is carried by the Datastar signal, not by a `checked`
|
|
100
|
+
* attribute: `data-bind` initialises the control from the signal on upgrade, so
|
|
101
|
+
* the signal is what the server actually decides.
|
|
102
|
+
*/
|
|
103
|
+
describe("SetupContent — the Discover question", () => {
|
|
104
|
+
const base: SetupProps = {
|
|
105
|
+
mode: "site",
|
|
106
|
+
askSiteName: true,
|
|
107
|
+
contentLanguage: "en",
|
|
108
|
+
discoverAvailable: true,
|
|
109
|
+
discoverDefault: false,
|
|
110
|
+
discoverPages: PAGES,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
it("starts clear where the deployment lists nothing by default", () => {
|
|
114
|
+
const html = render(base);
|
|
115
|
+
|
|
116
|
+
expect(html).toContain("setup-discover");
|
|
117
|
+
expect(html).toContain("discover: false");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Hosted Jant sets `DISCOVER=latest`, and the box has to say so rather than
|
|
121
|
+
// showing a refusal the site would not honour.
|
|
122
|
+
it("starts ticked where the deployment lists its blogs", () => {
|
|
123
|
+
const html = render({ ...base, discoverDefault: true });
|
|
124
|
+
|
|
125
|
+
expect(html).toContain("discover: true");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("is asked on the hosted screen too", () => {
|
|
129
|
+
const html = render({
|
|
130
|
+
...base,
|
|
131
|
+
askSiteName: false,
|
|
132
|
+
siteName: "My Blog",
|
|
133
|
+
discoverDefault: true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(html).toContain("setup-discover");
|
|
137
|
+
expect(html).toContain("discover: true");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Demo mode and feeds-off both outlive setup, so the question would be a
|
|
141
|
+
// promise the next screen breaks.
|
|
142
|
+
it("is not asked where the answer could not be honoured", () => {
|
|
143
|
+
const html = render({
|
|
144
|
+
...base,
|
|
145
|
+
discoverAvailable: false,
|
|
146
|
+
discoverDefault: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(html).not.toContain("setup-discover");
|
|
150
|
+
expect(html).toContain("discover: false");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// One control appearing twice should not describe itself two ways: the
|
|
154
|
+
// label is the settings page's, and the help line is the settings page's
|
|
155
|
+
// opening sentence rather than a wording of setup's own. The delay, the
|
|
156
|
+
// lists and the rules wait for Settings, where an author has posts to show.
|
|
157
|
+
it("opens the way the settings page does, and stops there", () => {
|
|
158
|
+
const copy = getDiscoverCopy(createI18n("en"));
|
|
159
|
+
const html = render({ ...base, discoverPages: null });
|
|
160
|
+
|
|
161
|
+
expect(copy.intro.startsWith(copy.about)).toBe(true);
|
|
162
|
+
expect(html).toContain(`<span>${copy.label}</span>`);
|
|
163
|
+
expect(html).toContain(`${copy.about}</p>`);
|
|
164
|
+
expect(html).not.toContain("Featured");
|
|
165
|
+
expect(html).not.toContain(copy.rules);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// What Discover is, is best answered by the list itself, so its name in the
|
|
169
|
+
// help line is the way there — the same link the settings page puts on it.
|
|
170
|
+
it("links the directory's name in the help line to the directory", () => {
|
|
171
|
+
const html = render(base);
|
|
172
|
+
|
|
173
|
+
expect(html).toContain(
|
|
174
|
+
`<a href="${PAGES.home}" target="_blank" rel="noopener noreferrer" class="underline hover:text-foreground transition-colors">Jant Discover</a> is a directory of Jant blogs`,
|
|
175
|
+
);
|
|
176
|
+
expect(html.match(/<a href/g)).toHaveLength(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// A self-hosted site that announces to no directory has no address to link,
|
|
180
|
+
// and a sentence with a dead link in it is worse than a plain one.
|
|
181
|
+
it("leaves the help line plain when no directory is configured", () => {
|
|
182
|
+
const html = render({ ...base, discoverPages: null });
|
|
183
|
+
|
|
184
|
+
expect(html).toContain("Jant Discover is a directory of Jant blogs");
|
|
185
|
+
expect(html).not.toContain("<a href");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// The screen's footnote already says every answer here can change later.
|
|
189
|
+
// Saying it again under one checkbox was saying it twice.
|
|
190
|
+
it("does not repeat the way back to Settings", () => {
|
|
191
|
+
const copy = getDiscoverCopy(createI18n("en"));
|
|
192
|
+
const html = render({ ...base, discoverPages: null });
|
|
193
|
+
|
|
194
|
+
expect(html).not.toContain("→");
|
|
195
|
+
expect(html).toContain(`${copy.about}</p>`);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Where "you can change this later" sits. Under the heading, before the
|
|
201
|
+
* questions, it reads as permission to leave them blank; after the button it is
|
|
202
|
+
* only what it is.
|
|
203
|
+
*/
|
|
204
|
+
describe("SetupContent — the note that nothing is final", () => {
|
|
205
|
+
const site: SetupProps = {
|
|
206
|
+
mode: "site",
|
|
207
|
+
askSiteName: true,
|
|
208
|
+
contentLanguage: "en",
|
|
209
|
+
discoverAvailable: true,
|
|
210
|
+
discoverDefault: false,
|
|
211
|
+
discoverPages: null,
|
|
212
|
+
};
|
|
213
|
+
const note = "You can change all of this later in Settings.";
|
|
214
|
+
|
|
215
|
+
it("sits in the card's footer, after the button", () => {
|
|
216
|
+
const html = render(site);
|
|
217
|
+
|
|
218
|
+
expect(html).toContain(
|
|
219
|
+
`<footer><p class="text-sm text-muted-foreground">${note}</p></footer>`,
|
|
220
|
+
);
|
|
221
|
+
expect(html.indexOf(note)).toBeGreaterThan(html.indexOf("</button>"));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("leaves nothing under the heading", () => {
|
|
225
|
+
const html = render(site);
|
|
226
|
+
const header = html.slice(
|
|
227
|
+
html.indexOf("<header>"),
|
|
228
|
+
html.indexOf("</header>"),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect(header).toContain("<h2>");
|
|
232
|
+
expect(header).not.toContain(note);
|
|
233
|
+
expect(header.match(/<p/g)).toHaveLength(1);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Same component, same reason: the hosted variant asks a question in its
|
|
237
|
+
// heading, and a reassurance sitting right under it dilutes the question.
|
|
238
|
+
it("sits in the same place on the hosted screen", () => {
|
|
239
|
+
const html = render({ ...site, askSiteName: false, siteName: "My Blog" });
|
|
240
|
+
const header = html.slice(
|
|
241
|
+
html.indexOf("<header>"),
|
|
242
|
+
html.indexOf("</header>"),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(header).not.toContain(note);
|
|
246
|
+
expect(html.indexOf(note)).toBeGreaterThan(html.indexOf("</button>"));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// The account screen has no answers to reassure anyone about.
|
|
250
|
+
it("is absent from the account screen", () => {
|
|
251
|
+
const html = render({ mode: "account" });
|
|
252
|
+
|
|
253
|
+
expect(html).not.toContain("<footer>");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("SetupContent — self-hosted, first screen", () => {
|
|
258
|
+
const account: SetupProps = { mode: "account" };
|
|
259
|
+
|
|
260
|
+
it("wears the same one-line shell as the hosted screen, plus the count", () => {
|
|
261
|
+
const html = render(account);
|
|
262
|
+
|
|
263
|
+
expect(html).toContain("Setup · Step 1 of 2");
|
|
264
|
+
expect(html).toContain("Welcome to Jant");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// The whole reason credentials get a screen of their own: an email and a
|
|
268
|
+
// password with nothing between them is the shape password managers look for.
|
|
269
|
+
it("asks for credentials and nothing else", () => {
|
|
270
|
+
const html = render(account);
|
|
271
|
+
|
|
272
|
+
expect(html).toContain("setup-email");
|
|
273
|
+
expect(html).toContain("setup-password");
|
|
274
|
+
expect(html).not.toContain("setup-site-name");
|
|
275
|
+
expect(html).not.toContain("setup-content-language");
|
|
276
|
+
expect(html).not.toContain("setup-discover");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// "Complete Setup" here would make the next screen read as a rejection.
|
|
280
|
+
it("says it continues rather than finishes", () => {
|
|
281
|
+
const html = render(account);
|
|
282
|
+
|
|
283
|
+
expect(html).toContain(">Continue</button>");
|
|
284
|
+
expect(html).not.toContain("Complete Setup");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("SetupContent — self-hosted, second screen", () => {
|
|
289
|
+
const site: SetupProps = {
|
|
290
|
+
mode: "site",
|
|
291
|
+
askSiteName: true,
|
|
292
|
+
contentLanguage: "en",
|
|
293
|
+
discoverAvailable: true,
|
|
294
|
+
discoverDefault: false,
|
|
295
|
+
discoverPages: PAGES,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
it("counts itself as the last step and names no site yet", () => {
|
|
299
|
+
const html = render(site);
|
|
300
|
+
|
|
301
|
+
expect(html).toContain("Setup · Step 2 of 2");
|
|
302
|
+
expect(html).toContain("Set up your site");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("asks for the site and not the account", () => {
|
|
306
|
+
const html = render(site);
|
|
307
|
+
|
|
308
|
+
expect(html).toContain("setup-site-name");
|
|
309
|
+
expect(html).toContain("setup-content-language");
|
|
310
|
+
expect(html).toContain("setup-discover");
|
|
311
|
+
expect(html).not.toContain("setup-email");
|
|
312
|
+
expect(html).not.toContain("setup-password");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// The install that is still choosing its own clock is the one that reports
|
|
316
|
+
// it, so the site's time zone is settled by the screen that names the site.
|
|
317
|
+
it("reports the browser's time zone", () => {
|
|
318
|
+
const html = render(site);
|
|
319
|
+
|
|
320
|
+
expect(html).toContain("timezone");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("ends the flow rather than continuing it", () => {
|
|
324
|
+
const html = render(site);
|
|
325
|
+
|
|
326
|
+
expect(html).toContain("Start writing");
|
|
327
|
+
expect(html).not.toContain(">Continue</button>");
|
|
328
|
+
});
|
|
329
|
+
});
|