@jant/core 0.6.16 → 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 +60 -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-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- 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-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- 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__/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 +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +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-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.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 +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/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-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +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 +1526 -15
- 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__/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 +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +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 +926 -73
- 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 +16 -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/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-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 +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -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__/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 +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -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 +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/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/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- 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 +178 -17
- 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 +108 -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.test.ts +46 -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 +331 -46
- 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 +278 -19
- 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 +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -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 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- 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 +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +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/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|