@jant/core 0.6.15 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +67 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +88 -1
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1142 -268
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1691 -12
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +88 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +93 -34
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +1021 -66
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +21 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-display.ts +42 -9
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +11 -2
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +507 -357
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +183 -19
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +139 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +330 -47
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +281 -24
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +401 -301
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +55 -23
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npm run dev
|
|
|
21
21
|
For a traditional server deployment, use the official Docker image:
|
|
22
22
|
|
|
23
23
|
- [`owenyoung/jant`](https://hub.docker.com/r/owenyoung/jant)
|
|
24
|
-
- [Docker deployment guide](https://
|
|
24
|
+
- [Docker deployment guide](https://jant.me/docs/deployment-docker)
|
|
25
25
|
|
|
26
26
|
## Tech Stack
|
|
27
27
|
|
|
@@ -35,16 +35,16 @@ For a traditional server deployment, use the official Docker image:
|
|
|
35
35
|
|
|
36
36
|
## Documentation
|
|
37
37
|
|
|
38
|
-
- [Overview](https://
|
|
39
|
-
- [Getting Started](https://
|
|
40
|
-
- [Writing and Organizing Posts](https://
|
|
41
|
-
- [Deploy on Cloudflare](https://
|
|
42
|
-
- [Deploy with Docker](https://
|
|
43
|
-
- [Configuration](https://
|
|
44
|
-
- [Export and Import](https://
|
|
45
|
-
- [Backups and Recovery](https://
|
|
46
|
-
- [Theming](https://
|
|
47
|
-
- [API Reference](https://
|
|
38
|
+
- [Overview](https://jant.me/docs)
|
|
39
|
+
- [Getting Started](https://jant.me/docs/getting-started)
|
|
40
|
+
- [Writing and Organizing Posts](https://jant.me/docs/writing-and-organizing)
|
|
41
|
+
- [Deploy on Cloudflare](https://jant.me/docs/deployment)
|
|
42
|
+
- [Deploy with Docker](https://jant.me/docs/deployment-docker)
|
|
43
|
+
- [Configuration](https://jant.me/docs/configuration)
|
|
44
|
+
- [Export and Import](https://jant.me/docs/export-and-import)
|
|
45
|
+
- [Backups and Recovery](https://jant.me/docs/backups)
|
|
46
|
+
- [Theming](https://jant.me/docs/theming)
|
|
47
|
+
- [API Reference](https://jant.me/docs/api)
|
|
48
48
|
|
|
49
49
|
## Contributing
|
|
50
50
|
|
|
@@ -6,14 +6,25 @@
|
|
|
6
6
|
* Uses the same S3_* environment variables as media storage. Files with
|
|
7
7
|
* content-hashed names (JS, fonts) are skipped if already present in the
|
|
8
8
|
* bucket — they are immutable. Non-hashed files (e.g. CSS in older builds)
|
|
9
|
-
* are always uploaded.
|
|
9
|
+
* are always uploaded. An object stored without the compression it should
|
|
10
|
+
* have is re-uploaded even though its key exists, so a change to the
|
|
11
|
+
* compression policy reaches assets whose content never changed.
|
|
12
|
+
*
|
|
13
|
+
* Text assets are uploaded brotli-compressed at maximum quality, with
|
|
14
|
+
* `Content-Encoding: br`. The CDN in front of the bucket compresses at a fixed
|
|
15
|
+
* low quality — measured as brotli q4, which is worse than its own gzip — and
|
|
16
|
+
* offers no way to raise it, so the only route to q11 is to compress here and
|
|
17
|
+
* store the result. A client that cannot take brotli is unaffected: the edge
|
|
18
|
+
* decompresses and re-encodes for it (verified against R2 behind a Cloudflare
|
|
19
|
+
* custom domain: `br` is passed through, `gzip` is re-encoded, `identity` is
|
|
20
|
+
* decompressed, all three byte-identical after decoding).
|
|
10
21
|
*
|
|
11
22
|
* Intended CI order: build → upload-assets → deploy container
|
|
12
23
|
*/
|
|
13
24
|
|
|
14
25
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
15
26
|
import { join, relative, extname } from "node:path";
|
|
16
|
-
import {
|
|
27
|
+
import { brotliCompressSync, constants } from "node:zlib";
|
|
17
28
|
import { parseArgs } from "node:util";
|
|
18
29
|
import { fileURLToPath } from "node:url";
|
|
19
30
|
import { dirname, resolve } from "node:path";
|
|
@@ -41,6 +52,59 @@ function getContentType(filePath) {
|
|
|
41
52
|
);
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Extensions worth pre-compressing. Fonts (woff2), images and icons already
|
|
57
|
+
* carry their own compression, and running brotli over them costs build time
|
|
58
|
+
* for nothing — usually for a slightly larger file.
|
|
59
|
+
*/
|
|
60
|
+
const COMPRESSIBLE_EXTENSIONS = new Set([
|
|
61
|
+
".js",
|
|
62
|
+
".css",
|
|
63
|
+
".json",
|
|
64
|
+
".map",
|
|
65
|
+
".svg",
|
|
66
|
+
".txt",
|
|
67
|
+
".html",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Brotli-compress an asset for storage, at a quality only a build can afford.
|
|
72
|
+
*
|
|
73
|
+
* Quality 11 is far too slow to run per request, which is why CDNs compress
|
|
74
|
+
* lower. Here it runs once per file per release, and every visitor gets the
|
|
75
|
+
* result.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} filePath - Source path, used only for its extension.
|
|
78
|
+
* @param {Buffer} body - The file's bytes.
|
|
79
|
+
* @returns {Buffer | null} The compressed body, or `null` when the type is not
|
|
80
|
+
* worth compressing or compression failed to shrink it — upload `body` as-is
|
|
81
|
+
* and omit `Content-Encoding` in that case.
|
|
82
|
+
* @example
|
|
83
|
+
* const packed = compressForUpload("client.css", body);
|
|
84
|
+
* // packed.length < body.length, or null for a .woff2
|
|
85
|
+
*/
|
|
86
|
+
export function compressForUpload(filePath, body) {
|
|
87
|
+
if (!COMPRESSIBLE_EXTENSIONS.has(extname(filePath).toLowerCase())) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const compressed = brotliCompressSync(body, {
|
|
91
|
+
params: {
|
|
92
|
+
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
|
93
|
+
[constants.BROTLI_PARAM_SIZE_HINT]: body.length,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
// Tiny or already-dense files can come back bigger. Storing that would make
|
|
97
|
+
// every request pay for the encoding header and gain nothing.
|
|
98
|
+
return compressed.length < body.length ? compressed : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Human-readable byte count for the upload log. */
|
|
102
|
+
function formatBytes(bytes) {
|
|
103
|
+
return bytes >= 1024 * 1024
|
|
104
|
+
? `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
105
|
+
: `${(bytes / 1024).toFixed(1)} KB`;
|
|
106
|
+
}
|
|
107
|
+
|
|
44
108
|
/**
|
|
45
109
|
* Resolve the default source directory: packages/core/dist/client/_assets
|
|
46
110
|
* relative to this file's location in packages/core/bin/commands/assets/.
|
|
@@ -64,8 +128,12 @@ async function walkDir(dir) {
|
|
|
64
128
|
}
|
|
65
129
|
|
|
66
130
|
async function loadS3(config) {
|
|
67
|
-
const {
|
|
68
|
-
|
|
131
|
+
const {
|
|
132
|
+
S3Client,
|
|
133
|
+
PutObjectCommand,
|
|
134
|
+
ListObjectsV2Command,
|
|
135
|
+
HeadObjectCommand,
|
|
136
|
+
} = await import("@aws-sdk/client-s3");
|
|
69
137
|
const forcePathStyle = !config.endpoint.includes("amazonaws.com");
|
|
70
138
|
const client = new S3Client({
|
|
71
139
|
endpoint: config.endpoint,
|
|
@@ -80,6 +148,7 @@ async function loadS3(config) {
|
|
|
80
148
|
client,
|
|
81
149
|
PutObjectCommand,
|
|
82
150
|
ListObjectsV2Command,
|
|
151
|
+
HeadObjectCommand,
|
|
83
152
|
bucket: config.bucket,
|
|
84
153
|
};
|
|
85
154
|
}
|
|
@@ -107,6 +176,53 @@ async function listExistingKeys(s3, prefix) {
|
|
|
107
176
|
return keys;
|
|
108
177
|
}
|
|
109
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Of the keys that already exist, the ones stored without the compression they
|
|
181
|
+
* should have.
|
|
182
|
+
*
|
|
183
|
+
* Objects are content-addressed, so a key that exists is assumed to hold the
|
|
184
|
+
* right bytes and is skipped. That assumption breaks when the *encoding*
|
|
185
|
+
* changes rather than the content: every asset uploaded before compression was
|
|
186
|
+
* introduced keeps its key, keeps being skipped, and never picks it up. The
|
|
187
|
+
* CDN then compresses those at its own low quality on every request, which is
|
|
188
|
+
* the whole thing compression was meant to avoid.
|
|
189
|
+
*
|
|
190
|
+
* Only worth asking about types we would compress, and only when compression
|
|
191
|
+
* is on. A file that brotli cannot shrink is stored raw on purpose, so its
|
|
192
|
+
* missing `Content-Encoding` is correct and must not read as stale — otherwise
|
|
193
|
+
* it would be re-uploaded on every run, forever.
|
|
194
|
+
*
|
|
195
|
+
* @param {object} s3 - The client bundle from `loadS3`.
|
|
196
|
+
* @param {Array<{filePath: string, key: string}>} candidates - Local files
|
|
197
|
+
* whose key already exists in the bucket.
|
|
198
|
+
* @returns {Promise<Set<string>>} Keys to upload again.
|
|
199
|
+
* @example
|
|
200
|
+
* const stale = await findStaleEncodingKeys(s3, existing);
|
|
201
|
+
* // Set { "_assets/client-cjk-B7Z0snDu.css" }
|
|
202
|
+
*/
|
|
203
|
+
async function findStaleEncodingKeys(s3, candidates) {
|
|
204
|
+
const { client, HeadObjectCommand, bucket } = s3;
|
|
205
|
+
const stale = new Set();
|
|
206
|
+
const CONCURRENCY = 25;
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < candidates.length; i += CONCURRENCY) {
|
|
209
|
+
await Promise.all(
|
|
210
|
+
candidates.slice(i, i + CONCURRENCY).map(async ({ filePath, key }) => {
|
|
211
|
+
const head = await client.send(
|
|
212
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key }),
|
|
213
|
+
);
|
|
214
|
+
if (head.ContentEncoding === "br") return;
|
|
215
|
+
// Read it only now: the answer is only interesting for the few objects
|
|
216
|
+
// that are missing the encoding.
|
|
217
|
+
const body = await readFile(filePath);
|
|
218
|
+
if (compressForUpload(filePath, body) !== null) stale.add(key);
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return stale;
|
|
224
|
+
}
|
|
225
|
+
|
|
110
226
|
export async function run(argv) {
|
|
111
227
|
const { values } = parseArgs({
|
|
112
228
|
args: argv,
|
|
@@ -115,6 +231,8 @@ export async function run(argv) {
|
|
|
115
231
|
prefix: { type: "string", default: "_assets" },
|
|
116
232
|
"source-dir": { type: "string" },
|
|
117
233
|
"dry-run": { type: "boolean", default: false },
|
|
234
|
+
force: { type: "boolean", default: false },
|
|
235
|
+
"no-compress": { type: "boolean", default: false },
|
|
118
236
|
},
|
|
119
237
|
});
|
|
120
238
|
|
|
@@ -135,6 +253,12 @@ export async function run(argv) {
|
|
|
135
253
|
console.log(
|
|
136
254
|
" --dry-run Print what would be uploaded without uploading",
|
|
137
255
|
);
|
|
256
|
+
console.log(
|
|
257
|
+
" --force Re-upload every key, including ones already compressed",
|
|
258
|
+
);
|
|
259
|
+
console.log(
|
|
260
|
+
" --no-compress Upload raw bytes instead of brotli-compressed",
|
|
261
|
+
);
|
|
138
262
|
process.exit(0);
|
|
139
263
|
}
|
|
140
264
|
|
|
@@ -171,6 +295,8 @@ export async function run(argv) {
|
|
|
171
295
|
const sourceDir = values["source-dir"] ?? resolveDefaultSourceDir();
|
|
172
296
|
const prefix = values.prefix.replace(/^\/+|\/+$/g, "");
|
|
173
297
|
const dryRun = values["dry-run"];
|
|
298
|
+
const force = values.force;
|
|
299
|
+
const compress = !values["no-compress"];
|
|
174
300
|
|
|
175
301
|
// Verify source directory exists
|
|
176
302
|
try {
|
|
@@ -181,10 +307,12 @@ export async function run(argv) {
|
|
|
181
307
|
process.exit(1);
|
|
182
308
|
}
|
|
183
309
|
|
|
184
|
-
console.log(`Source:
|
|
185
|
-
console.log(`Bucket:
|
|
186
|
-
console.log(`Prefix:
|
|
187
|
-
|
|
310
|
+
console.log(`Source: ${sourceDir}`);
|
|
311
|
+
console.log(`Bucket: ${bucket}`);
|
|
312
|
+
console.log(`Prefix: ${prefix}`);
|
|
313
|
+
console.log(`Encoding: ${compress ? "brotli (quality 11)" : "raw"}`);
|
|
314
|
+
if (force) console.log("Force: re-uploading keys that already exist");
|
|
315
|
+
if (dryRun) console.log("Dry run: no files will be uploaded");
|
|
188
316
|
console.log("");
|
|
189
317
|
|
|
190
318
|
const s3 = await loadS3({
|
|
@@ -205,19 +333,57 @@ export async function run(argv) {
|
|
|
205
333
|
let skipped = 0;
|
|
206
334
|
|
|
207
335
|
const toUpload = [];
|
|
336
|
+
const alreadyThere = [];
|
|
208
337
|
for (const filePath of files) {
|
|
209
338
|
const relPath = relative(sourceDir, filePath).replace(/\\/g, "/");
|
|
210
339
|
const key = prefix ? `${prefix}/${relPath}` : relPath;
|
|
211
|
-
if (existingKeys.has(key)) {
|
|
212
|
-
|
|
340
|
+
if (existingKeys.has(key) && !force) {
|
|
341
|
+
alreadyThere.push({ filePath, key });
|
|
213
342
|
} else {
|
|
214
343
|
toUpload.push({ filePath, key });
|
|
215
344
|
}
|
|
216
345
|
}
|
|
217
346
|
|
|
347
|
+
// A key that exists is normally left alone, but that skips objects stored
|
|
348
|
+
// before compression was introduced — they would keep their old encoding
|
|
349
|
+
// forever. Re-upload those, and only those.
|
|
350
|
+
let restored = 0;
|
|
351
|
+
if (compress && alreadyThere.length > 0) {
|
|
352
|
+
process.stdout.write("Checking stored encodings... ");
|
|
353
|
+
const stale = await findStaleEncodingKeys(s3, alreadyThere);
|
|
354
|
+
for (const entry of alreadyThere) {
|
|
355
|
+
if (stale.has(entry.key)) toUpload.push(entry);
|
|
356
|
+
else skipped++;
|
|
357
|
+
}
|
|
358
|
+
restored = stale.size;
|
|
359
|
+
console.log(
|
|
360
|
+
stale.size === 0
|
|
361
|
+
? "all compressed"
|
|
362
|
+
: `${stale.size} stored uncompressed, re-uploading`,
|
|
363
|
+
);
|
|
364
|
+
} else {
|
|
365
|
+
skipped += alreadyThere.length;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Bytes as stored, against bytes as built — what the compression bought.
|
|
369
|
+
let rawTotal = 0;
|
|
370
|
+
let storedTotal = 0;
|
|
371
|
+
|
|
372
|
+
/** Read a file and decide how it should be stored. */
|
|
373
|
+
async function prepare(filePath) {
|
|
374
|
+
const body = await readFile(filePath);
|
|
375
|
+
const packed = compress ? compressForUpload(filePath, body) : null;
|
|
376
|
+
rawTotal += body.length;
|
|
377
|
+
storedTotal += (packed ?? body).length;
|
|
378
|
+
return { body: packed ?? body, encoded: packed !== null };
|
|
379
|
+
}
|
|
380
|
+
|
|
218
381
|
if (dryRun) {
|
|
219
|
-
for (const { key } of toUpload) {
|
|
220
|
-
|
|
382
|
+
for (const { filePath, key } of toUpload) {
|
|
383
|
+
const { body, encoded } = await prepare(filePath);
|
|
384
|
+
console.log(
|
|
385
|
+
` [dry-run] upload ${key} (${formatBytes(body.length)}${encoded ? ", br" : ""})`,
|
|
386
|
+
);
|
|
221
387
|
}
|
|
222
388
|
uploaded = toUpload.length;
|
|
223
389
|
} else {
|
|
@@ -226,16 +392,17 @@ export async function run(argv) {
|
|
|
226
392
|
const batch = toUpload.slice(i, i + CONCURRENCY);
|
|
227
393
|
await Promise.all(
|
|
228
394
|
batch.map(async ({ filePath, key }) => {
|
|
229
|
-
const body = await
|
|
395
|
+
const { body, encoded } = await prepare(filePath);
|
|
230
396
|
const command = new s3.PutObjectCommand({
|
|
231
397
|
Bucket: s3.bucket,
|
|
232
398
|
Key: key,
|
|
233
399
|
Body: body,
|
|
234
400
|
ContentType: getContentType(filePath),
|
|
235
401
|
CacheControl: "public, max-age=31536000, immutable",
|
|
402
|
+
...(encoded ? { ContentEncoding: "br" } : {}),
|
|
236
403
|
});
|
|
237
404
|
await s3.client.send(command);
|
|
238
|
-
process.stdout.write(` uploaded ${key}\n`);
|
|
405
|
+
process.stdout.write(` uploaded ${key}${encoded ? " (br)" : ""}\n`);
|
|
239
406
|
uploaded++;
|
|
240
407
|
}),
|
|
241
408
|
);
|
|
@@ -243,5 +410,15 @@ export async function run(argv) {
|
|
|
243
410
|
}
|
|
244
411
|
|
|
245
412
|
console.log("");
|
|
246
|
-
console.log(
|
|
413
|
+
console.log(
|
|
414
|
+
`Done. ${uploaded} uploaded, ${skipped} skipped` +
|
|
415
|
+
`${restored > 0 ? `, ${restored} re-uploaded for compression` : ""}.`,
|
|
416
|
+
);
|
|
417
|
+
if (compress && rawTotal > 0) {
|
|
418
|
+
const saved = rawTotal - storedTotal;
|
|
419
|
+
console.log(
|
|
420
|
+
`Stored ${formatBytes(storedTotal)} of ${formatBytes(rawTotal)} built ` +
|
|
421
|
+
`(${formatBytes(saved)} saved, ${((saved / rawTotal) * 100).toFixed(1)}%).`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
247
424
|
}
|
|
@@ -1290,10 +1290,12 @@ async function apiCall(method, path, apiUrl, token, body) {
|
|
|
1290
1290
|
cause === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" ||
|
|
1291
1291
|
cause?.includes("certificate")
|
|
1292
1292
|
) {
|
|
1293
|
-
console.error(`\
|
|
1294
|
-
console.error(
|
|
1295
|
-
|
|
1296
|
-
|
|
1293
|
+
console.error(`\nCertificate error connecting to ${apiUrl}`);
|
|
1294
|
+
console.error(
|
|
1295
|
+
"The certificate is not signed by a CA that Node trusts. For a local",
|
|
1296
|
+
);
|
|
1297
|
+
console.error("or self-signed target, trust this machine's CA store:");
|
|
1298
|
+
console.error(" NODE_OPTIONS=--use-system-ca jant site import ...");
|
|
1297
1299
|
process.exit(1);
|
|
1298
1300
|
}
|
|
1299
1301
|
throw new Error(
|
|
@@ -1462,6 +1464,15 @@ function createRemoteTarget(apiUrl, token) {
|
|
|
1462
1464
|
toRemotePostPayload(data),
|
|
1463
1465
|
);
|
|
1464
1466
|
},
|
|
1467
|
+
async linkTranslation(postId, otherPostId) {
|
|
1468
|
+
return apiCall(
|
|
1469
|
+
"POST",
|
|
1470
|
+
`/api/posts/${postId}/translations`,
|
|
1471
|
+
apiUrl,
|
|
1472
|
+
token,
|
|
1473
|
+
{ postId: otherPostId },
|
|
1474
|
+
);
|
|
1475
|
+
},
|
|
1465
1476
|
async createAlias(path, targetSlug) {
|
|
1466
1477
|
return apiCall("POST", "/api/custom-urls", apiUrl, token, {
|
|
1467
1478
|
path,
|
|
@@ -1743,6 +1754,13 @@ function getImportedPostStatus(frontMatter) {
|
|
|
1743
1754
|
function shouldImportReplyQuietly(rootFrontMatter, replyFrontMatter) {
|
|
1744
1755
|
if (getImportedPostStatus(replyFrontMatter) !== "published") return false;
|
|
1745
1756
|
|
|
1757
|
+
// Exports since quiet_reply became a stored column say so explicitly.
|
|
1758
|
+
if (typeof replyFrontMatter.quiet_reply === "boolean") {
|
|
1759
|
+
return replyFrontMatter.quiet_reply;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// Older bundles: a reply published after the root's recorded last activity
|
|
1763
|
+
// can only have gotten there by skipping the bump, so it was quiet.
|
|
1746
1764
|
const rootLastActivityAt = getImportedRootLastActivityAt(rootFrontMatter);
|
|
1747
1765
|
const replyPublishedAt = parseImportTimestamp(replyFrontMatter.date);
|
|
1748
1766
|
return (
|
|
@@ -1817,6 +1835,12 @@ function buildPostPayloadFromBundle(bundle, options) {
|
|
|
1817
1835
|
: undefined,
|
|
1818
1836
|
rating:
|
|
1819
1837
|
typeof frontMatter.rating === "number" ? frontMatter.rating : undefined,
|
|
1838
|
+
// Roots only: replies take the root's language from the service, and a
|
|
1839
|
+
// reply bundle has no language of its own in the export.
|
|
1840
|
+
language:
|
|
1841
|
+
!options.replyToId && typeof frontMatter.language === "string"
|
|
1842
|
+
? frontMatter.language
|
|
1843
|
+
: undefined,
|
|
1820
1844
|
quietReply: options.quietReply ? true : undefined,
|
|
1821
1845
|
};
|
|
1822
1846
|
|
|
@@ -1954,6 +1978,8 @@ export async function run(argv) {
|
|
|
1954
1978
|
const apiUrl = url.replace(/\/$/, "");
|
|
1955
1979
|
const skipRemoteMedia = values["skip-remote-media"];
|
|
1956
1980
|
const target = dryRun ? null : createRemoteTarget(apiUrl, token);
|
|
1981
|
+
/** Exported translation-group ID → IDs of the posts created for it. */
|
|
1982
|
+
const translationGroups = new Map();
|
|
1957
1983
|
|
|
1958
1984
|
// 1. Read source — directory or ZIP
|
|
1959
1985
|
const inputPath = resolve(process.cwd(), values.path);
|
|
@@ -2318,6 +2344,18 @@ export async function run(argv) {
|
|
|
2318
2344
|
try {
|
|
2319
2345
|
post = await target.createPost(postData);
|
|
2320
2346
|
postsCreated++;
|
|
2347
|
+
// Translation groups are rebuilt after every post exists: the group ID
|
|
2348
|
+
// in the export is opaque and its members can appear in any order, so
|
|
2349
|
+
// there is nothing to link to until the whole run is done.
|
|
2350
|
+
const groupKey =
|
|
2351
|
+
typeof rootFm.translation_group === "string"
|
|
2352
|
+
? rootFm.translation_group.trim()
|
|
2353
|
+
: "";
|
|
2354
|
+
if (groupKey && post?.id) {
|
|
2355
|
+
const members = translationGroups.get(groupKey) ?? [];
|
|
2356
|
+
members.push(post.id);
|
|
2357
|
+
translationGroups.set(groupKey, members);
|
|
2358
|
+
}
|
|
2321
2359
|
const replyInfo =
|
|
2322
2360
|
rootBundle.children.length > 0
|
|
2323
2361
|
? ` (+${rootBundle.children.length} replies)`
|
|
@@ -2476,6 +2514,28 @@ export async function run(argv) {
|
|
|
2476
2514
|
}
|
|
2477
2515
|
}
|
|
2478
2516
|
|
|
2517
|
+
// Rebuild translation groups. Each member is linked to the first one, so a
|
|
2518
|
+
// group of N takes N-1 calls and never asks the server to merge two groups
|
|
2519
|
+
// — which it refuses, on purpose.
|
|
2520
|
+
let translationsLinked = 0;
|
|
2521
|
+
for (const [groupKey, members] of translationGroups) {
|
|
2522
|
+
if (members.length < 2) continue;
|
|
2523
|
+
const [anchor, ...rest] = members;
|
|
2524
|
+
for (const memberId of rest) {
|
|
2525
|
+
try {
|
|
2526
|
+
await target.linkTranslation(anchor, memberId);
|
|
2527
|
+
translationsLinked++;
|
|
2528
|
+
} catch (err) {
|
|
2529
|
+
// A clash here means the export itself held two posts in the same
|
|
2530
|
+
// language for one group. Say so and keep going: the posts are
|
|
2531
|
+
// imported either way, they are just not linked.
|
|
2532
|
+
console.warn(
|
|
2533
|
+
`Warning: could not link a translation in group "${groupKey}": ${err.message}`,
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2479
2539
|
await target?.close();
|
|
2480
2540
|
|
|
2481
2541
|
// 5. Summary
|
|
@@ -2484,6 +2544,9 @@ export async function run(argv) {
|
|
|
2484
2544
|
console.log(` Posts created: ${postsCreated}`);
|
|
2485
2545
|
console.log(` Replies created: ${repliesCreated}`);
|
|
2486
2546
|
console.log(` Media uploaded: ${mediaUploaded}`);
|
|
2547
|
+
if (translationsLinked > 0) {
|
|
2548
|
+
console.log(` Translations linked: ${translationsLinked}`);
|
|
2549
|
+
}
|
|
2487
2550
|
if (aliasesCreated > 0) {
|
|
2488
2551
|
console.log(` Aliases created: ${aliasesCreated}`);
|
|
2489
2552
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
bootstrapCliRuntime,
|
|
8
8
|
getCliRuntimeLabel,
|
|
9
9
|
} from "../lib/runtime-target.js";
|
|
10
|
-
import { resolveCliSite } from "../lib/site-selection.js";
|
|
10
|
+
import { parseCliSiteSelector, resolveCliSite } from "../lib/site-selection.js";
|
|
11
11
|
|
|
12
12
|
export async function run(argv) {
|
|
13
13
|
const { values } = parseArgs({
|
|
@@ -29,7 +29,9 @@ export async function run(argv) {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
if (values.help) {
|
|
32
|
-
console.log(
|
|
32
|
+
console.log(
|
|
33
|
+
"Usage: jant reset-password [--local | --remote | --node] [--site <key|id> | --host <host> | --url <url>]",
|
|
34
|
+
);
|
|
33
35
|
console.log("");
|
|
34
36
|
console.log("Generate a password reset token (expires in 15 minutes).");
|
|
35
37
|
console.log("");
|
|
@@ -37,7 +39,7 @@ export async function run(argv) {
|
|
|
37
39
|
console.log(" --local Force local D1 instead of DATABASE_URL");
|
|
38
40
|
console.log(" --remote Run against remote D1 database (default: local)");
|
|
39
41
|
console.log(" --node Force Node runtime even if DATABASE_URL is unset");
|
|
40
|
-
console.log(" --site Target site id");
|
|
42
|
+
console.log(" --site Target site key or id");
|
|
41
43
|
console.log(" --host Target site host");
|
|
42
44
|
console.log(" --url Target site URL");
|
|
43
45
|
console.log(" --path-prefix Path prefix used with --host");
|
|
@@ -53,10 +55,20 @@ export async function run(argv) {
|
|
|
53
55
|
"If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
|
|
54
56
|
);
|
|
55
57
|
console.log("this command uses the Node database runtime.");
|
|
58
|
+
console.log("");
|
|
59
|
+
console.log(
|
|
60
|
+
"With SITE_RESOLUTION_MODE=host-based, pass one of --site, --host, or --url.",
|
|
61
|
+
);
|
|
56
62
|
process.exit(0);
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
const { runtime } = bootstrapCliRuntime(values);
|
|
66
|
+
const siteFlags = {
|
|
67
|
+
host: values.host,
|
|
68
|
+
pathPrefix: values["path-prefix"],
|
|
69
|
+
site: values.site,
|
|
70
|
+
url: values.url,
|
|
71
|
+
};
|
|
60
72
|
|
|
61
73
|
const token = randomBytes(32).toString("hex");
|
|
62
74
|
const hash = createHash("sha256").update(token).digest("hex");
|
|
@@ -65,11 +77,15 @@ export async function run(argv) {
|
|
|
65
77
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
66
78
|
|
|
67
79
|
if (runtime === "node") {
|
|
80
|
+
const siteSelector = parseCliSiteSelector(siteFlags);
|
|
68
81
|
const { createNodeCliRuntime } = await loadNodeRuntime();
|
|
69
82
|
const nodeDatabase = await openNodeDatabase(process.env);
|
|
70
83
|
|
|
71
84
|
try {
|
|
72
|
-
const nodeRuntime = await createNodeCliRuntime(
|
|
85
|
+
const nodeRuntime = await createNodeCliRuntime(
|
|
86
|
+
nodeDatabase.bindings,
|
|
87
|
+
siteSelector,
|
|
88
|
+
);
|
|
73
89
|
await nodeRuntime.services.settings.set("PASSWORD_RESET_TOKEN", value);
|
|
74
90
|
} finally {
|
|
75
91
|
await nodeDatabase.close();
|
|
@@ -95,10 +111,7 @@ export async function run(argv) {
|
|
|
95
111
|
},
|
|
96
112
|
{
|
|
97
113
|
env: process.env,
|
|
98
|
-
|
|
99
|
-
pathPrefix: values["path-prefix"],
|
|
100
|
-
site: values.site,
|
|
101
|
-
url: values.url,
|
|
114
|
+
...siteFlags,
|
|
102
115
|
},
|
|
103
116
|
);
|
|
104
117
|
|
|
@@ -215,7 +215,7 @@ export async function run(argv) {
|
|
|
215
215
|
console.log(
|
|
216
216
|
" --output, -o Output directory or .zip file (default: jant-site-snapshot)",
|
|
217
217
|
);
|
|
218
|
-
console.log(" --site Target site id");
|
|
218
|
+
console.log(" --site Target site key or id");
|
|
219
219
|
console.log(" --host Target site host");
|
|
220
220
|
console.log(" --url Target site URL");
|
|
221
221
|
console.log(" --path-prefix Path prefix used with --host");
|
|
@@ -204,7 +204,7 @@ export async function run(argv) {
|
|
|
204
204
|
console.log(
|
|
205
205
|
" --replace Replace the current content scope before importing",
|
|
206
206
|
);
|
|
207
|
-
console.log(" --site Target site id");
|
|
207
|
+
console.log(" --site Target site key or id");
|
|
208
208
|
console.log(" --host Target site host");
|
|
209
209
|
console.log(" --url Target site URL");
|
|
210
210
|
console.log(" --path-prefix Path prefix used with --host");
|
package/bin/lib/d1-query.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runLocalWrangler } from "./wrangler-cli.js";
|
|
2
|
+
import { extractWranglerJson } from "./wrangler-json.js";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_RETRY_ATTEMPTS = 4;
|
|
4
5
|
const DEFAULT_RETRY_DELAY_MS = 500;
|
|
@@ -35,7 +36,7 @@ export function parseWranglerError(output) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
try {
|
|
38
|
-
const parsed = JSON.parse(output.trim());
|
|
39
|
+
const parsed = JSON.parse(extractWranglerJson(output).trim());
|
|
39
40
|
const error = Array.isArray(parsed) ? parsed[0]?.error : parsed?.error;
|
|
40
41
|
if (!error?.text) {
|
|
41
42
|
return undefined;
|
|
@@ -75,18 +76,6 @@ export function isRetryableWranglerD1Failure(output, error) {
|
|
|
75
76
|
].some((fragment) => combined.includes(fragment));
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
/**
|
|
79
|
-
* Strip non-JSON preamble that Wrangler sometimes writes to stdout
|
|
80
|
-
* (e.g. "Proxy environment variables detected…") before the actual JSON payload.
|
|
81
|
-
*/
|
|
82
|
-
function extractJson(raw) {
|
|
83
|
-
const idx = raw.indexOf("[");
|
|
84
|
-
const idx2 = raw.indexOf("{");
|
|
85
|
-
if (idx === -1 && idx2 === -1) return raw;
|
|
86
|
-
const start = idx === -1 ? idx2 : idx2 === -1 ? idx : Math.min(idx, idx2);
|
|
87
|
-
return raw.slice(start);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
79
|
function getWranglerExecutionOptions(options) {
|
|
91
80
|
const executionOptions = { ...options };
|
|
92
81
|
for (const key of [
|
|
@@ -153,7 +142,7 @@ export function executeD1(sql, runtime, options = {}) {
|
|
|
153
142
|
|
|
154
143
|
if (options.quiet) {
|
|
155
144
|
const output = runWrangler([...args, "--json"], options);
|
|
156
|
-
const parsed = JSON.parse(
|
|
145
|
+
const parsed = JSON.parse(extractWranglerJson(output));
|
|
157
146
|
const statements = Array.isArray(parsed) ? parsed : [parsed];
|
|
158
147
|
|
|
159
148
|
for (const statement of statements) {
|
|
@@ -183,7 +172,7 @@ export function queryD1(sql, runtime, options = {}) {
|
|
|
183
172
|
),
|
|
184
173
|
options,
|
|
185
174
|
);
|
|
186
|
-
const parsed = JSON.parse(
|
|
175
|
+
const parsed = JSON.parse(extractWranglerJson(output));
|
|
187
176
|
const statement = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
188
177
|
|
|
189
178
|
if (statement?.error?.text) {
|
|
@@ -208,7 +197,7 @@ export function executeD1File(filePath, runtime, options = {}) {
|
|
|
208
197
|
|
|
209
198
|
if (options.quiet) {
|
|
210
199
|
const output = runWrangler([...args, "--json"], options);
|
|
211
|
-
const parsed = JSON.parse(
|
|
200
|
+
const parsed = JSON.parse(extractWranglerJson(output));
|
|
212
201
|
const statements = Array.isArray(parsed) ? parsed : [parsed];
|
|
213
202
|
|
|
214
203
|
for (const statement of statements) {
|