@jant/core 0.6.16 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +749 -220
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/setup.js +136 -0
- package/bin/commands/site/export.js +71 -34
- package/bin/commands/site/pull-media.js +2 -6
- package/bin/commands/site/snapshot/export.js +19 -61
- package/bin/commands/site/snapshot/import.js +23 -24
- package/bin/lib/d1-query.js +92 -18
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +94 -29
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +37 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
- package/dist/client/_assets/client-DAhoqPdr.css +2 -0
- package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
- package/dist/client/_assets/client-knSEyUJO.js +295 -0
- package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
- package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
- package/dist/github-sync-Gw4orAHk.js +8398 -0
- package/dist/index.js +2 -5
- package/dist/node.js +11 -9
- package/package.json +15 -5
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/dev-scripts.test.ts +203 -0
- package/src/__tests__/export-collection-order.test.ts +276 -0
- package/src/__tests__/export-feed-ids.test.ts +184 -0
- package/src/__tests__/export-feed-order.test.ts +294 -0
- package/src/__tests__/export-hugo-build.test.ts +390 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +842 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/helpers/hugo-site.ts +146 -0
- package/src/__tests__/import-site-command.test.ts +487 -1
- package/src/__tests__/mise-config.test.ts +75 -4
- package/src/__tests__/node-dev-tasks.test.ts +264 -0
- package/src/__tests__/site-export-canonical-import.test.ts +149 -0
- package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
- package/src/__tests__/snapshot-settings.test.ts +97 -0
- package/src/__tests__/snapshot-tables.test.ts +219 -0
- package/src/__tests__/sql-export.test.ts +173 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +86 -22
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +137 -108
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +622 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
- package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +555 -73
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +601 -119
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +614 -132
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +131 -3
- package/src/lib/__tests__/schemas.test.ts +69 -5
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +439 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +62 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +111 -6
- package/src/lib/schemas.ts +251 -15
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +43 -2
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +159 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +53 -25
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
- package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
- package/src/node/__tests__/runtime.test.ts +75 -0
- package/src/node/index.ts +3 -0
- package/src/node/request-handler.ts +3 -1
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +6 -3
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +161 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +44 -7
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +9 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +714 -111
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +487 -379
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +43 -18
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +228 -45
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +22 -5
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +67 -1
- package/src/runtime/readiness.ts +15 -0
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/custom-url.test.ts +34 -0
- package/src/services/__tests__/github-app-installations.test.ts +153 -0
- package/src/services/__tests__/github-sync-push.test.ts +46 -0
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +236 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +120 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +408 -39
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +29 -24
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/alias.html +27 -1
- package/src/services/export-theme/layouts/_default/list.html +2 -63
- package/src/services/export-theme/layouts/_default/rss.xml +103 -38
- package/src/services/export-theme/layouts/collections/list.html +3 -3
- package/src/services/export-theme/layouts/featured/list.html +1 -4
- package/src/services/export-theme/layouts/index.html +40 -26
- package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
- package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
- package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
- package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/head.html +1 -1
- package/src/services/export-theme/layouts/partials/header.html +5 -3
- package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
- package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
- package/src/services/export-theme/layouts/post/list.html +1 -1
- package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
- package/src/services/export-theme/styles/main.css +13 -2
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +880 -72
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +22 -2
- package/src/services/media.ts +22 -7
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +254 -6
- package/src/services/post.ts +1234 -190
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +27 -14
- package/src/services/site.ts +160 -32
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1090 -7216
- package/src/types/app-context.ts +23 -0
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +74 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +277 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/export-Dl5KCiEs.js +0 -5562
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/client/tiptap/exitable-marks.ts +0 -73
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -31,9 +31,9 @@ describe("resolveConfig", () => {
|
|
|
31
31
|
expect(config.publicApiEnabled).toBe(true);
|
|
32
32
|
expect(config.rssFeedsEnabled).toBe(true);
|
|
33
33
|
expect(config.demoMode).toBe(false);
|
|
34
|
-
expect(config.pageSize).toBe(
|
|
35
|
-
expect(config.searchPageSize).toBe(
|
|
36
|
-
expect(config.archivePageSize).toBe(
|
|
34
|
+
expect(config.pageSize).toBe(25);
|
|
35
|
+
expect(config.searchPageSize).toBe(25);
|
|
36
|
+
expect(config.archivePageSize).toBe(25);
|
|
37
37
|
expect(config.rssFeedLimit).toBe(50);
|
|
38
38
|
expect(config.rssPublishDelaySeconds).toBe(300);
|
|
39
39
|
});
|
|
@@ -52,6 +52,73 @@ describe("resolveConfig", () => {
|
|
|
52
52
|
expect(config.siteName).toBe("FromEnv");
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
// A settings row holding an empty string is not always the same thing. The
|
|
56
|
+
// editor decides: a boolean, a number, and an enum with fixed options cannot
|
|
57
|
+
// be saved empty, so an empty one of those came from a path that skipped
|
|
58
|
+
// validation — a snapshot import replaying raw SQL — and the environment
|
|
59
|
+
// variable should still be heard.
|
|
60
|
+
// Text can legitimately be empty, and then the row wins.
|
|
61
|
+
describe("empty settings rows", () => {
|
|
62
|
+
it("lets an environment value through an empty boolean row", () => {
|
|
63
|
+
const config = resolveConfig(makeEnv({ NOINDEX: "true" }), {
|
|
64
|
+
NOINDEX: "",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(config.noindex).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("falls back to the default for an empty boolean row with no env value", () => {
|
|
71
|
+
const config = resolveConfig(makeEnv(), { NOINDEX: "" });
|
|
72
|
+
|
|
73
|
+
expect(config.noindex).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("still lets a saved boolean row override the environment", () => {
|
|
77
|
+
const config = resolveConfig(makeEnv({ NOINDEX: "true" }), {
|
|
78
|
+
NOINDEX: "false",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(config.noindex).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("lets an environment value through an empty numeric row", () => {
|
|
85
|
+
const config = resolveConfig(makeEnv({ PAGE_SIZE: "20" }), {
|
|
86
|
+
PAGE_SIZE: "",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(config.pageSize).toBe(20);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("keeps an empty enum row ahead of the environment when empty is an option", () => {
|
|
93
|
+
// `DASHBOARD_LANGUAGE` lists "" among its options — it is how the
|
|
94
|
+
// dashboard is told to follow the site language.
|
|
95
|
+
const config = resolveConfig(makeEnv({ DASHBOARD_LANGUAGE: "zh-Hans" }), {
|
|
96
|
+
DASHBOARD_LANGUAGE: "",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(config.dashboardLanguage).toBe("");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("keeps an empty text row ahead of the environment", () => {
|
|
103
|
+
const config = resolveConfig(
|
|
104
|
+
makeEnv({ SITE_FOOTER: "From environment" }),
|
|
105
|
+
{ SITE_FOOTER: "" },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(config.siteFooter).toBe("");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("lets an environment value through an empty enum row", () => {
|
|
112
|
+
// `MAIN_RSS_FEED` offers "featured" and "latest" and nothing else, so an
|
|
113
|
+
// empty row is as impossible to save as an empty boolean.
|
|
114
|
+
const config = resolveConfig(makeEnv({ MAIN_RSS_FEED: "latest" }), {
|
|
115
|
+
MAIN_RSS_FEED: "",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(config.mainRssFeed).toBe("latest");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
55
122
|
it("lets an explicit empty DB value override an environment fallback", () => {
|
|
56
123
|
const config = resolveConfig(
|
|
57
124
|
makeEnv({ SITE_DESCRIPTION: "From environment" }),
|
|
@@ -81,6 +148,34 @@ describe("resolveConfig", () => {
|
|
|
81
148
|
expect(config.mainRssFeed).toBe("featured");
|
|
82
149
|
});
|
|
83
150
|
|
|
151
|
+
it("resolves archiveDefaultLayout from DB, env, and defaults", () => {
|
|
152
|
+
const defaultConfig = resolveConfig(makeEnv(), {});
|
|
153
|
+
expect(defaultConfig.archiveDefaultLayout).toBe("list");
|
|
154
|
+
|
|
155
|
+
const envConfig = resolveConfig(
|
|
156
|
+
makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "grid" }),
|
|
157
|
+
{},
|
|
158
|
+
);
|
|
159
|
+
expect(envConfig.archiveDefaultLayout).toBe("grid");
|
|
160
|
+
|
|
161
|
+
const dbConfig = resolveConfig(
|
|
162
|
+
makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "list" }),
|
|
163
|
+
{
|
|
164
|
+
ARCHIVE_DEFAULT_LAYOUT: "grid",
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
expect(dbConfig.archiveDefaultLayout).toBe("grid");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("falls back when the archive layout setting is invalid", () => {
|
|
171
|
+
const config = resolveConfig(
|
|
172
|
+
makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "carousel" }),
|
|
173
|
+
{},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(config.archiveDefaultLayout).toBe("list");
|
|
177
|
+
});
|
|
178
|
+
|
|
84
179
|
it("normalizes legacy time zone values from the database", () => {
|
|
85
180
|
const config = resolveConfig(makeEnv(), { TIME_ZONE: "Beijing" });
|
|
86
181
|
|
|
@@ -172,6 +267,39 @@ describe("resolveConfig", () => {
|
|
|
172
267
|
expect(config.siteAvatarUrl).toBe(
|
|
173
268
|
"https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
|
|
174
269
|
);
|
|
270
|
+
// Without a transform there is nothing to size it down with, so the
|
|
271
|
+
// header falls back to the canonical asset.
|
|
272
|
+
expect(config.siteAvatarThumbUrl).toBe(config.siteAvatarUrl);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("sizes the header avatar down when an image transform is configured", () => {
|
|
276
|
+
const config = resolveConfig(
|
|
277
|
+
makeEnv({
|
|
278
|
+
R2_PUBLIC_URL: "https://r2.example.com",
|
|
279
|
+
IMAGE_TRANSFORM_URL: "https://r2.example.com/cdn-cgi/image",
|
|
280
|
+
STORAGE_DRIVER: "r2",
|
|
281
|
+
}),
|
|
282
|
+
{
|
|
283
|
+
SITE_AVATAR:
|
|
284
|
+
"media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// The canonical URL stays full-resolution: it is what the social card and
|
|
289
|
+
// the settings editor need.
|
|
290
|
+
expect(config.siteAvatarUrl).toBe(
|
|
291
|
+
"https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
|
|
292
|
+
);
|
|
293
|
+
expect(config.siteAvatarThumbUrl).toBe(
|
|
294
|
+
"https://r2.example.com/cdn-cgi/image/width=128,quality=80,format=auto,fit=scale-down/https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("leaves both avatar URLs empty without a stored avatar", () => {
|
|
299
|
+
const config = resolveConfig(makeEnv({}), {});
|
|
300
|
+
|
|
301
|
+
expect(config.siteAvatarUrl).toBe("");
|
|
302
|
+
expect(config.siteAvatarThumbUrl).toBe("");
|
|
175
303
|
});
|
|
176
304
|
|
|
177
305
|
it("returns empty siteAvatarUrl when no avatar set", () => {
|
|
@@ -9,11 +9,12 @@ import {
|
|
|
9
9
|
CreatePostApiSchema,
|
|
10
10
|
UpdatePostSchema,
|
|
11
11
|
UpdatePostApiSchema,
|
|
12
|
-
|
|
12
|
+
SetupAccountSchema,
|
|
13
13
|
SigninSchema,
|
|
14
14
|
UpdateSiteSettingsSchema,
|
|
15
15
|
normalizeEmail,
|
|
16
16
|
parseFormData,
|
|
17
|
+
PostFilterSelectionSchema,
|
|
17
18
|
parseFormDataOptional,
|
|
18
19
|
validateAttachmentCount,
|
|
19
20
|
} from "../schemas.js";
|
|
@@ -60,10 +61,17 @@ describe("RedirectTypeSchema", () => {
|
|
|
60
61
|
expect(RedirectTypeSchema.parse("302")).toBe("302");
|
|
61
62
|
});
|
|
62
63
|
|
|
64
|
+
// The custom URL list answers `redirectType: 301`; sending that back to
|
|
65
|
+
// create one has to work.
|
|
66
|
+
it("accepts 301 and 302 as numbers, as the API answers them", () => {
|
|
67
|
+
expect(RedirectTypeSchema.parse(301)).toBe("301");
|
|
68
|
+
expect(RedirectTypeSchema.parse(302)).toBe("302");
|
|
69
|
+
});
|
|
70
|
+
|
|
63
71
|
it("rejects other values", () => {
|
|
64
72
|
expect(() => RedirectTypeSchema.parse("200")).toThrow();
|
|
65
73
|
expect(() => RedirectTypeSchema.parse("404")).toThrow();
|
|
66
|
-
expect(() => RedirectTypeSchema.parse(
|
|
74
|
+
expect(() => RedirectTypeSchema.parse(308)).toThrow();
|
|
67
75
|
});
|
|
68
76
|
});
|
|
69
77
|
|
|
@@ -75,10 +83,9 @@ describe("normalizeEmail", () => {
|
|
|
75
83
|
});
|
|
76
84
|
});
|
|
77
85
|
|
|
78
|
-
describe("
|
|
86
|
+
describe("SetupAccountSchema", () => {
|
|
79
87
|
it("normalizes email before returning parsed data", () => {
|
|
80
|
-
const result =
|
|
81
|
-
siteName: "Jant",
|
|
88
|
+
const result = SetupAccountSchema.parse({
|
|
82
89
|
email: " Admin@Example.COM ",
|
|
83
90
|
password: "password123",
|
|
84
91
|
});
|
|
@@ -617,6 +624,25 @@ describe("CreatePostApiSchema", () => {
|
|
|
617
624
|
status: "published",
|
|
618
625
|
};
|
|
619
626
|
|
|
627
|
+
it("accepts the creation and edit times a restore passes", () => {
|
|
628
|
+
expect(
|
|
629
|
+
CreatePostApiSchema.parse({
|
|
630
|
+
...validPost,
|
|
631
|
+
createdAt: 1700000000,
|
|
632
|
+
updatedAt: 1700009000,
|
|
633
|
+
}),
|
|
634
|
+
).toMatchObject({ createdAt: 1700000000, updatedAt: 1700009000 });
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("leaves record timestamps out of updates", () => {
|
|
638
|
+
expect(
|
|
639
|
+
UpdatePostApiSchema.safeParse({ createdAt: 1700000000 }).success,
|
|
640
|
+
).toBe(false);
|
|
641
|
+
expect(
|
|
642
|
+
UpdatePostApiSchema.safeParse({ updatedAt: 1700000000 }).success,
|
|
643
|
+
).toBe(false);
|
|
644
|
+
});
|
|
645
|
+
|
|
620
646
|
it("accepts ordered attachment inputs", () => {
|
|
621
647
|
const mediaId = createEntityId("media");
|
|
622
648
|
const result = CreatePostApiSchema.parse({
|
|
@@ -816,3 +842,41 @@ describe("validateAttachmentCount", () => {
|
|
|
816
842
|
);
|
|
817
843
|
});
|
|
818
844
|
});
|
|
845
|
+
|
|
846
|
+
describe("PostFilterSelectionSchema", () => {
|
|
847
|
+
it("accepts a selection a smart collection may store", () => {
|
|
848
|
+
const result = PostFilterSelectionSchema.safeParse({
|
|
849
|
+
format: "note",
|
|
850
|
+
media: "any",
|
|
851
|
+
visibility: "featured",
|
|
852
|
+
collection: ["col_01m0f291t3fzvte3vj2g8d611z"],
|
|
853
|
+
});
|
|
854
|
+
expect(result.success).toBe(true);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it("accepts the empty selection, which means every post", () => {
|
|
858
|
+
expect(PostFilterSelectionSchema.safeParse({}).success).toBe(true);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it("refuses the private visibility a smart collection can never name", () => {
|
|
862
|
+
expect(
|
|
863
|
+
PostFilterSelectionSchema.safeParse({ visibility: "private" }).success,
|
|
864
|
+
).toBe(false);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it("refuses a key that is not a dimension", () => {
|
|
868
|
+
expect(
|
|
869
|
+
PostFilterSelectionSchema.safeParse({ language: "en" }).success,
|
|
870
|
+
).toBe(false);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it("names the condition it rejected", () => {
|
|
874
|
+
const result = PostFilterSelectionSchema.safeParse({ year: 1970 });
|
|
875
|
+
expect(result.success).toBe(false);
|
|
876
|
+
if (result.success) return;
|
|
877
|
+
expect(result.error.issues[0]?.path).toEqual(["year"]);
|
|
878
|
+
expect(result.error.issues[0]?.message).toBe(
|
|
879
|
+
"Invalid value for the year condition",
|
|
880
|
+
);
|
|
881
|
+
});
|
|
882
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderSiteUnavailablePage } from "../site-unavailable-page.js";
|
|
3
|
+
|
|
4
|
+
describe("renderSiteUnavailablePage", () => {
|
|
5
|
+
it("links back to the configured hosting provider", () => {
|
|
6
|
+
const html = renderSiteUnavailablePage({
|
|
7
|
+
HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud-jant.localtest.me",
|
|
8
|
+
HOSTED_CONTROL_PLANE_PROVIDER_NAME: "Jant Cloud",
|
|
9
|
+
SITE_RESOLUTION_MODE: "host-based",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(html).toContain("This site is offline");
|
|
13
|
+
expect(html).toContain(
|
|
14
|
+
'<a href="https://cloud-jant.localtest.me/app">Jant Cloud</a>',
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("stays provider-neutral when no control plane is configured", () => {
|
|
19
|
+
const html = renderSiteUnavailablePage({});
|
|
20
|
+
|
|
21
|
+
expect(html).toContain("There's nothing to read here right now");
|
|
22
|
+
expect(html).toContain("sign in to your hosting provider");
|
|
23
|
+
expect(html).not.toContain("<a ");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -141,4 +141,74 @@ describe("generatePostSlug", () => {
|
|
|
141
141
|
).rejects.toThrow("Could not generate a unique slug");
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
describe("language suffix", () => {
|
|
146
|
+
/** Every slug is taken except the ones listed. */
|
|
147
|
+
const onlyAvailable = (...free: string[]) => {
|
|
148
|
+
const allowed = new Set(free);
|
|
149
|
+
return async (slug: string) => allowed.has(slug);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
it("is not used when the base slug is free", async () => {
|
|
153
|
+
const slug = await generatePostSlug({
|
|
154
|
+
title: "Book Review",
|
|
155
|
+
idLength: 5,
|
|
156
|
+
languageSuffix: "en",
|
|
157
|
+
isAvailable: alwaysAvailable,
|
|
158
|
+
});
|
|
159
|
+
expect(slug).toBe("book-review");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("is tried before a random suffix when the base collides", async () => {
|
|
163
|
+
const slug = await generatePostSlug({
|
|
164
|
+
title: "Book Review",
|
|
165
|
+
idLength: 5,
|
|
166
|
+
languageSuffix: "en",
|
|
167
|
+
isAvailable: onlyAvailable("book-review-en"),
|
|
168
|
+
});
|
|
169
|
+
expect(slug).toBe("book-review-en");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("keeps the multi-part tag intact", async () => {
|
|
173
|
+
const slug = await generatePostSlug({
|
|
174
|
+
title: "Book Review",
|
|
175
|
+
idLength: 5,
|
|
176
|
+
languageSuffix: "zh-hant",
|
|
177
|
+
isAvailable: onlyAvailable("book-review-zh-hant"),
|
|
178
|
+
});
|
|
179
|
+
expect(slug).toBe("book-review-zh-hant");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("falls back to a random suffix when the language slug is taken too", async () => {
|
|
183
|
+
const slug = await generatePostSlug({
|
|
184
|
+
title: "Book Review",
|
|
185
|
+
idLength: 5,
|
|
186
|
+
languageSuffix: "en",
|
|
187
|
+
isAvailable: async (candidate: string) =>
|
|
188
|
+
candidate !== "book-review" && candidate !== "book-review-en",
|
|
189
|
+
});
|
|
190
|
+
expect(slug).toMatch(/^book-review-[a-z0-9]{5}$/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("goes straight to a random suffix without one", async () => {
|
|
194
|
+
const slug = await generatePostSlug({
|
|
195
|
+
title: "Book Review",
|
|
196
|
+
idLength: 5,
|
|
197
|
+
isAvailable: async (candidate: string) => candidate !== "book-review",
|
|
198
|
+
});
|
|
199
|
+
expect(slug).toMatch(/^book-review-[a-z0-9]{5}$/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("skips a language candidate that lands on a reserved path", async () => {
|
|
203
|
+
const slug = await generatePostSlug({
|
|
204
|
+
title: "feed",
|
|
205
|
+
idLength: 5,
|
|
206
|
+
languageSuffix: "en",
|
|
207
|
+
isAvailable: alwaysAvailable,
|
|
208
|
+
});
|
|
209
|
+
// "feed" is reserved, so the base is never offered and the language
|
|
210
|
+
// candidate is never reached — the random fallback owns this case.
|
|
211
|
+
expect(slug).toMatch(/^feed-[a-z0-9]{5}$/);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
144
214
|
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { slugify } from "../slugify.js";
|
|
3
|
+
|
|
4
|
+
describe("slugify", () => {
|
|
5
|
+
it("converts text to lowercase hyphenated slug", () => {
|
|
6
|
+
expect(slugify("Hello World")).toBe("hello-world");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("removes special characters", () => {
|
|
10
|
+
expect(slugify("Hello World! This is a Test.")).toBe(
|
|
11
|
+
"hello-world-this-is-a-test",
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("collapses multiple spaces", () => {
|
|
16
|
+
expect(slugify("Multiple Spaces")).toBe("multiple-spaces");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("trims leading/trailing whitespace and hyphens", () => {
|
|
20
|
+
expect(slugify(" Multiple Spaces ")).toBe("multiple-spaces");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("replaces underscores with hyphens", () => {
|
|
24
|
+
expect(slugify("hello_world")).toBe("hello-world");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("handles already-slugified text", () => {
|
|
28
|
+
expect(slugify("already-a-slug")).toBe("already-a-slug");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("handles empty string", () => {
|
|
32
|
+
expect(slugify("")).toBe("");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("transliterates accented characters", () => {
|
|
36
|
+
expect(slugify("café & résumé")).toBe("cafe-and-resume");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("converts Chinese characters to pinyin", () => {
|
|
40
|
+
expect(slugify("书评")).toBe("shu-ping");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("handles mixed Chinese and English text", () => {
|
|
44
|
+
expect(slugify("我的 Blog")).toBe("wo-de-blog");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("handles CJK characters with spaces", () => {
|
|
48
|
+
expect(slugify("电影 评论")).toBe("dian-ying-ping-lun");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("romanizes Korean titles", () => {
|
|
52
|
+
expect(slugify("안녕하세요 세계")).toBe("annyeonghaseyo-segye");
|
|
53
|
+
expect(slugify("개발 일지 3일차")).toBe("gaebal-ilji-3ilcha");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("romanizes kana-only Japanese titles", () => {
|
|
57
|
+
expect(slugify("カタカナタイトル")).toBe("katakanataitoru");
|
|
58
|
+
expect(slugify("ハンバーガー")).toBe("hanbaga");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("keeps Latin words in mostly-kana Japanese titles", () => {
|
|
62
|
+
expect(slugify("React入門ガイド")).toBe("react-gaido");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns empty for kanji-heavy Japanese titles instead of garbling them", () => {
|
|
66
|
+
// Kanji carries the meaning; without a reading dictionary any output
|
|
67
|
+
// would be gibberish, so callers should fall back to a random ID.
|
|
68
|
+
expect(slugify("日本語のタイトルです")).toBe("");
|
|
69
|
+
expect(slugify("東京タワーに行った")).toBe("");
|
|
70
|
+
expect(slugify("こんにちは世界")).toBe("");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns empty for scripts with no usable transliteration", () => {
|
|
74
|
+
expect(slugify("שלום עולם")).toBe("");
|
|
75
|
+
expect(slugify("สวัสดีชาวโลก")).toBe("");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns empty when most of the title is untransliterable", () => {
|
|
79
|
+
expect(slugify("שלום world")).toBe("");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns empty for emoji-only titles", () => {
|
|
83
|
+
expect(slugify("🎉🎉")).toBe("");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -210,6 +210,38 @@ describe("createS3Driver", () => {
|
|
|
210
210
|
"x-amz-checksum-sha256": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
|
+
|
|
214
|
+
it("omits a request checksum when the caller did not supply one", async () => {
|
|
215
|
+
const driver = createS3Driver({
|
|
216
|
+
endpoint: "https://s3.example.com",
|
|
217
|
+
bucket: "jant-media",
|
|
218
|
+
accessKeyId: "access-key",
|
|
219
|
+
secretAccessKey: "secret-key",
|
|
220
|
+
region: "auto",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const presigned = await driver.presignPut?.("media/test.webp", {
|
|
224
|
+
contentType: "image/webp",
|
|
225
|
+
contentDisposition: "inline",
|
|
226
|
+
cacheControl: "public, max-age=31536000, immutable",
|
|
227
|
+
expiresInSeconds: 900,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (!presigned) {
|
|
231
|
+
throw new Error("Expected presigned PUT target");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Presigning runs with an empty body, so a default request checksum would
|
|
235
|
+
// pin CRC32-of-nothing to the URL and every real upload would 400.
|
|
236
|
+
const params = new URL(presigned.url).searchParams;
|
|
237
|
+
expect(params.has("x-amz-checksum-crc32")).toBe(false);
|
|
238
|
+
expect(params.has("x-amz-sdk-checksum-algorithm")).toBe(false);
|
|
239
|
+
expect(presigned.headers).toEqual({
|
|
240
|
+
"Content-Type": "image/webp",
|
|
241
|
+
"Content-Disposition": "inline",
|
|
242
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
243
|
+
});
|
|
244
|
+
});
|
|
213
245
|
});
|
|
214
246
|
|
|
215
247
|
describe("createR2Driver", () => {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
foldThreadReplies,
|
|
4
|
+
getThreadHiddenCount,
|
|
5
|
+
THREAD_LEADING_REPLIES,
|
|
6
|
+
THREAD_TRAILING_REPLIES,
|
|
7
|
+
} from "../thread-fold.js";
|
|
8
|
+
|
|
9
|
+
const reply = (n: number) => ({ id: `reply-${n}` });
|
|
10
|
+
const replies = (count: number) =>
|
|
11
|
+
Array.from({ length: count }, (_unused, index) => reply(index + 1));
|
|
12
|
+
|
|
13
|
+
const ids = (list: { id: string }[]) => list.map((item) => item.id);
|
|
14
|
+
|
|
15
|
+
describe("foldThreadReplies", () => {
|
|
16
|
+
it("has no fold for a thread with no replies", () => {
|
|
17
|
+
expect(foldThreadReplies([])).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// The window is the first two plus the last three, so anything up to six
|
|
21
|
+
// posts arrives whole and there is nothing for a gap link to stand for.
|
|
22
|
+
it.each([1, 2, 3, 4, 5])("hides nothing with %i replies", (count) => {
|
|
23
|
+
const fold = foldThreadReplies(replies(count));
|
|
24
|
+
|
|
25
|
+
expect(fold?.hiddenCount).toBe(0);
|
|
26
|
+
expect(fold?.firstHiddenReply).toBeNull();
|
|
27
|
+
// Every reply is shown exactly once, the hero included.
|
|
28
|
+
const shown = [
|
|
29
|
+
...ids(fold?.leadingReplies ?? []),
|
|
30
|
+
...ids(fold?.trailingReplies ?? []),
|
|
31
|
+
fold?.latestReply.id,
|
|
32
|
+
];
|
|
33
|
+
expect(new Set(shown).size).toBe(shown.length);
|
|
34
|
+
expect(shown.length).toBe(count);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// The shortest thread puts one reply in every role at once. It must still
|
|
38
|
+
// be rendered once, not three times.
|
|
39
|
+
it("shows a lone reply once, as the hero", () => {
|
|
40
|
+
const fold = foldThreadReplies(replies(1));
|
|
41
|
+
|
|
42
|
+
expect(fold?.latestReply.id).toBe("reply-1");
|
|
43
|
+
expect(fold?.leadingReplies).toEqual([]);
|
|
44
|
+
expect(fold?.trailingReplies).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("folds the middle out of a long thread", () => {
|
|
48
|
+
const fold = foldThreadReplies(replies(7));
|
|
49
|
+
|
|
50
|
+
expect(ids(fold?.leadingReplies ?? [])).toEqual(["reply-1", "reply-2"]);
|
|
51
|
+
expect(ids(fold?.trailingReplies ?? [])).toEqual(["reply-5", "reply-6"]);
|
|
52
|
+
expect(fold?.latestReply.id).toBe("reply-7");
|
|
53
|
+
expect(fold?.hiddenCount).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// The gap stands for a run of posts and opens the first of them, so a reader
|
|
57
|
+
// following it lands where the missing stretch starts.
|
|
58
|
+
it("points the gap at the earliest post it hides", () => {
|
|
59
|
+
expect(foldThreadReplies(replies(20))?.firstHiddenReply?.id).toBe(
|
|
60
|
+
"reply-3",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("keeps the window fixed however long the thread runs", () => {
|
|
65
|
+
const fold = foldThreadReplies(replies(50));
|
|
66
|
+
const shown =
|
|
67
|
+
(fold?.leadingReplies.length ?? 0) +
|
|
68
|
+
(fold?.trailingReplies.length ?? 0) +
|
|
69
|
+
1;
|
|
70
|
+
|
|
71
|
+
expect(shown).toBe(THREAD_LEADING_REPLIES + THREAD_TRAILING_REPLIES);
|
|
72
|
+
expect(fold?.hiddenCount).toBe(50 - shown);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("getThreadHiddenCount", () => {
|
|
77
|
+
// The site selects its buckets in SQL and they overlap on short threads, so
|
|
78
|
+
// the count has to dedupe rather than subtract two lengths.
|
|
79
|
+
it("counts a reply in two buckets once", () => {
|
|
80
|
+
expect(
|
|
81
|
+
getThreadHiddenCount({
|
|
82
|
+
leadingReplies: [reply(1), reply(2)],
|
|
83
|
+
trailingReplies: [reply(1)],
|
|
84
|
+
latestReply: reply(2),
|
|
85
|
+
totalReplyCount: 2,
|
|
86
|
+
}),
|
|
87
|
+
).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("counts what a long thread leaves out", () => {
|
|
91
|
+
expect(
|
|
92
|
+
getThreadHiddenCount({
|
|
93
|
+
leadingReplies: [reply(1), reply(2)],
|
|
94
|
+
trailingReplies: [reply(6), reply(7)],
|
|
95
|
+
latestReply: reply(8),
|
|
96
|
+
totalReplyCount: 8,
|
|
97
|
+
}),
|
|
98
|
+
).toBe(3);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// A reply published between the count and the selection would otherwise
|
|
102
|
+
// report a negative number of hidden posts.
|
|
103
|
+
it("never goes below zero", () => {
|
|
104
|
+
expect(
|
|
105
|
+
getThreadHiddenCount({
|
|
106
|
+
leadingReplies: [reply(1), reply(2)],
|
|
107
|
+
trailingReplies: [],
|
|
108
|
+
latestReply: reply(3),
|
|
109
|
+
totalReplyCount: 1,
|
|
110
|
+
}),
|
|
111
|
+
).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
});
|