@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
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
mkdtemp,
|
|
4
|
-
readFile,
|
|
5
|
-
readdir,
|
|
6
|
-
rm,
|
|
7
|
-
stat,
|
|
8
|
-
writeFile,
|
|
9
|
-
} from "node:fs/promises";
|
|
1
|
+
import { openAsBlob } from "node:fs";
|
|
2
|
+
import { mkdtemp, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
10
3
|
import { resolve, join, extname, dirname, basename } from "node:path";
|
|
11
4
|
import { tmpdir } from "node:os";
|
|
12
5
|
import { parseArgs } from "node:util";
|
|
13
|
-
import { typeidUnboxed } from "typeid-js";
|
|
14
6
|
import { CLI_API_TOKEN_ENV_VAR, getCliApiToken } from "../lib/cli-api-token.js";
|
|
15
7
|
import {
|
|
16
8
|
extractAttachmentBlocks,
|
|
@@ -19,6 +11,7 @@ import {
|
|
|
19
11
|
rewriteMediaReferences,
|
|
20
12
|
} from "../lib/site-media-parser.js";
|
|
21
13
|
import { parseFrontMatter as parseFrontMatterShared } from "../lib/hugo-markdown.js";
|
|
14
|
+
import { extractZipFile } from "../lib/zip-archive.js";
|
|
22
15
|
|
|
23
16
|
/**
|
|
24
17
|
* Parse front matter from a Markdown file.
|
|
@@ -32,22 +25,27 @@ async function parseToml(content) {
|
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
/**
|
|
35
|
-
*
|
|
36
|
-
* `static
|
|
37
|
-
* `
|
|
28
|
+
* Static directories of a Jant Hugo export, in Hugo's lookup order: the root
|
|
29
|
+
* `static/` (user overrides) before `themes/jant/static/`, where Jant writes
|
|
30
|
+
* `custom.css`, `favicon.ico`, and `apple-touch-icon.png`.
|
|
31
|
+
*/
|
|
32
|
+
const IMPORT_STATIC_DIRS = [["static"], ["themes", "jant", "static"]];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read custom.css from the first static directory of a Jant Hugo export that
|
|
36
|
+
* has one.
|
|
38
37
|
*/
|
|
39
38
|
async function readImportCustomCss(rootDir) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
for (const staticDir of IMPORT_STATIC_DIRS) {
|
|
40
|
+
const css = await readFile(
|
|
41
|
+
join(rootDir, ...staticDir, "custom.css"),
|
|
42
|
+
"utf-8",
|
|
43
|
+
).catch(() => null);
|
|
44
|
+
if (css !== null) {
|
|
45
|
+
return css;
|
|
46
|
+
}
|
|
46
47
|
}
|
|
47
|
-
return
|
|
48
|
-
join(rootDir, "themes", "jant", "static", "custom.css"),
|
|
49
|
-
"utf-8",
|
|
50
|
-
).catch(() => "");
|
|
48
|
+
return "";
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
function resolveImportUrl(url, siteConfig) {
|
|
@@ -148,26 +146,40 @@ async function resolveImportLocalAssetPath(rawUrl, siteConfig, sourceRootDir) {
|
|
|
148
146
|
return null;
|
|
149
147
|
}
|
|
150
148
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
for (const staticDir of IMPORT_STATIC_DIRS) {
|
|
150
|
+
const fullPath = join(sourceRootDir, ...staticDir, pathname);
|
|
151
|
+
const fileStat = await stat(fullPath).catch(() => null);
|
|
152
|
+
if (fileStat?.isFile()) {
|
|
153
|
+
return fullPath;
|
|
154
|
+
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
return
|
|
157
|
+
return null;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Read one asset as a Blob, from the export's files or its URL.
|
|
162
|
+
*
|
|
163
|
+
* A local file comes back file-backed (`openAsBlob`): an upload streams it
|
|
164
|
+
* from disk. Reading every file into memory first held gigabytes during the
|
|
165
|
+
* import of a real site, since Node does not count Blob memory toward the
|
|
166
|
+
* garbage collector's pressure.
|
|
167
|
+
*
|
|
168
|
+
* @param {{ sourceUrl?: string, sourceFilePath?: string | null, mimeType?: string, originalName?: string }} options
|
|
169
|
+
* @returns {Promise<{ blob: Blob, filename: string, contentType: string } | null>}
|
|
170
|
+
* The asset, or null when a remote source is missing or unreachable
|
|
171
|
+
*/
|
|
160
172
|
async function readImportAsset(options) {
|
|
161
173
|
const { sourceUrl, sourceFilePath, mimeType, originalName } = options;
|
|
162
174
|
|
|
163
175
|
if (sourceFilePath) {
|
|
164
|
-
const bytes = new Uint8Array(await readFile(sourceFilePath));
|
|
165
176
|
const filename =
|
|
166
177
|
originalName || basename(sourceFilePath) || getFilenameFromUrl(sourceUrl);
|
|
178
|
+
const contentType = mimeType || guessMimeType(filename);
|
|
167
179
|
return {
|
|
168
|
-
|
|
180
|
+
blob: await openAsBlob(sourceFilePath, { type: contentType }),
|
|
169
181
|
filename,
|
|
170
|
-
contentType
|
|
182
|
+
contentType,
|
|
171
183
|
};
|
|
172
184
|
}
|
|
173
185
|
|
|
@@ -182,23 +194,17 @@ async function readImportAsset(options) {
|
|
|
182
194
|
|
|
183
195
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
184
196
|
const filename = originalName || getFilenameFromUrl(sourceUrl) || "file";
|
|
197
|
+
const contentType =
|
|
198
|
+
mimeType ||
|
|
199
|
+
response.headers.get("content-type")?.split(";")[0] ||
|
|
200
|
+
guessMimeType(filename);
|
|
185
201
|
return {
|
|
186
|
-
bytes,
|
|
202
|
+
blob: new Blob([bytes], { type: contentType }),
|
|
187
203
|
filename,
|
|
188
|
-
contentType
|
|
189
|
-
mimeType ||
|
|
190
|
-
response.headers.get("content-type")?.split(";")[0] ||
|
|
191
|
-
guessMimeType(filename),
|
|
204
|
+
contentType,
|
|
192
205
|
};
|
|
193
206
|
}
|
|
194
207
|
|
|
195
|
-
function toArrayBuffer(bytes) {
|
|
196
|
-
return bytes.buffer.slice(
|
|
197
|
-
bytes.byteOffset,
|
|
198
|
-
bytes.byteOffset + bytes.byteLength,
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
208
|
async function readMediaSpecAsset(media, field = "src") {
|
|
203
209
|
if (field === "poster") {
|
|
204
210
|
return readImportAsset({
|
|
@@ -305,45 +311,60 @@ async function assertImportSlugAvailable(target, slug, label, kind) {
|
|
|
305
311
|
* Download a media file and upload it to the Jant API.
|
|
306
312
|
* Returns the new URL, or null on failure.
|
|
307
313
|
*/
|
|
314
|
+
/**
|
|
315
|
+
* Upload one media file through the site's API.
|
|
316
|
+
*
|
|
317
|
+
* Throws when the file can't be read or the site refuses it, so a caller
|
|
318
|
+
* decides whether that is fatal (the export's own media) or leaves a link in
|
|
319
|
+
* place (an image in the body). It used to return null for either, and a
|
|
320
|
+
* post was created without the file and without a word.
|
|
321
|
+
*
|
|
322
|
+
* @param {Record<string, unknown>} media - A normalized media spec
|
|
323
|
+
* @param {string} apiUrl - Target site URL
|
|
324
|
+
* @param {string} token - API token
|
|
325
|
+
* @returns {Promise<{ url: string, id: string }>} The uploaded media
|
|
326
|
+
*/
|
|
308
327
|
async function uploadRemoteMedia(media, apiUrl, token) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
328
|
+
const asset = await readMediaSpecAsset(media);
|
|
329
|
+
if (!asset) {
|
|
330
|
+
throw new Error(`Couldn't read ${media.src}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const formData = new FormData();
|
|
334
|
+
formData.append("file", asset.blob, asset.filename);
|
|
335
|
+
if (media.alt) formData.append("alt", media.alt);
|
|
336
|
+
if (media.summary) formData.append("summary", media.summary);
|
|
337
|
+
if (media.width) formData.append("width", String(media.width));
|
|
338
|
+
if (media.height) formData.append("height", String(media.height));
|
|
339
|
+
if (media.blurhash) formData.append("blurhash", media.blurhash);
|
|
340
|
+
if (media.waveform) formData.append("waveform", media.waveform);
|
|
341
|
+
if (media.durationSeconds) {
|
|
342
|
+
formData.append("durationSeconds", String(media.durationSeconds));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (media.poster) {
|
|
346
|
+
const posterAsset = await readMediaSpecAsset(media, "poster");
|
|
347
|
+
if (posterAsset) {
|
|
348
|
+
formData.append("poster", posterAsset.blob, posterAsset.filename);
|
|
349
|
+
} else {
|
|
350
|
+
console.warn(`Warning: couldn't read the poster ${media.poster}`);
|
|
333
351
|
}
|
|
352
|
+
}
|
|
334
353
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
354
|
+
const uploadResponse = await fetch(`${apiUrl}/api/upload`, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
357
|
+
body: formData,
|
|
358
|
+
});
|
|
340
359
|
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
360
|
+
if (!uploadResponse.ok) {
|
|
361
|
+
const detail = (await uploadResponse.text().catch(() => "")).slice(0, 300);
|
|
362
|
+
throw new Error(
|
|
363
|
+
`The site refused ${media.src}: HTTP ${uploadResponse.status}${detail ? ` ${detail}` : ""}`,
|
|
364
|
+
);
|
|
346
365
|
}
|
|
366
|
+
const data = await uploadResponse.json();
|
|
367
|
+
return { url: data.url, id: data.id };
|
|
347
368
|
}
|
|
348
369
|
|
|
349
370
|
function getFilenameFromUrl(fileUrl) {
|
|
@@ -394,33 +415,6 @@ function guessMimeType(filename) {
|
|
|
394
415
|
}
|
|
395
416
|
}
|
|
396
417
|
|
|
397
|
-
function generateImportedStorageKey(originalName) {
|
|
398
|
-
const id = typeidUnboxed("med");
|
|
399
|
-
const extension = extname(originalName) || "";
|
|
400
|
-
const filename = `${id}${extension}`;
|
|
401
|
-
return {
|
|
402
|
-
id,
|
|
403
|
-
filename,
|
|
404
|
-
storageKey: `media/${filename}`,
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function getMediaPublicUrl(storageKey, provider, appConfig) {
|
|
409
|
-
const base =
|
|
410
|
-
provider === "s3"
|
|
411
|
-
? appConfig.s3PublicUrl
|
|
412
|
-
: provider === "local"
|
|
413
|
-
? appConfig.localPublicUrl
|
|
414
|
-
: appConfig.r2PublicUrl;
|
|
415
|
-
|
|
416
|
-
if (base) {
|
|
417
|
-
return `${base.replace(/\/+$/, "")}/${storageKey}`;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const prefix = appConfig.sitePathPrefix || "";
|
|
421
|
-
return `${prefix}/${storageKey}`.replace(/\/{2,}/g, "/");
|
|
422
|
-
}
|
|
423
|
-
|
|
424
418
|
function normalizeImportedBodySegment(markdown) {
|
|
425
419
|
const extracted = extractAttachmentBlocks(markdown);
|
|
426
420
|
const normalized = normalizeImportedBody(extracted.markdown);
|
|
@@ -515,6 +509,10 @@ async function mediaSpecFromJantMedia(entry, sourceRootDir) {
|
|
|
515
509
|
waveform: typeof entry.waveform === "string" ? entry.waveform : undefined,
|
|
516
510
|
summary: typeof entry.summary === "string" ? entry.summary : undefined,
|
|
517
511
|
chars: typeof entry.chars === "number" ? entry.chars : undefined,
|
|
512
|
+
durationSeconds:
|
|
513
|
+
typeof entry.duration_seconds === "number"
|
|
514
|
+
? entry.duration_seconds
|
|
515
|
+
: undefined,
|
|
518
516
|
};
|
|
519
517
|
}
|
|
520
518
|
|
|
@@ -624,7 +622,7 @@ async function normalizeTextAttachmentSpec(spec, siteConfig, sourceRootDir) {
|
|
|
624
622
|
|
|
625
623
|
let markdown;
|
|
626
624
|
try {
|
|
627
|
-
markdown =
|
|
625
|
+
markdown = await asset.blob.text();
|
|
628
626
|
} catch {
|
|
629
627
|
return null;
|
|
630
628
|
}
|
|
@@ -667,7 +665,16 @@ async function buildImportedAttachments(
|
|
|
667
665
|
sourceRootDir,
|
|
668
666
|
);
|
|
669
667
|
if (!normalized || normalized.src.startsWith("data:")) continue;
|
|
670
|
-
|
|
668
|
+
let result;
|
|
669
|
+
try {
|
|
670
|
+
result = await target.uploadMedia(normalized);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
// The post's own attachment: creating the post without it would lose
|
|
673
|
+
// the file quietly.
|
|
674
|
+
throw new Error(`Couldn't upload ${spec.src}: ${err.message}`, {
|
|
675
|
+
cause: err,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
671
678
|
if (!result) continue;
|
|
672
679
|
attachments.push({
|
|
673
680
|
type: "media",
|
|
@@ -692,7 +699,14 @@ async function uploadMediaList(mediaSpecs, target, siteConfig, sourceRootDir) {
|
|
|
692
699
|
sourceRootDir,
|
|
693
700
|
);
|
|
694
701
|
if (!normalized || normalized.src.startsWith("data:")) continue;
|
|
695
|
-
|
|
702
|
+
let result;
|
|
703
|
+
try {
|
|
704
|
+
result = await target.uploadMedia(normalized);
|
|
705
|
+
} catch (err) {
|
|
706
|
+
// A body image may be a third party's; the Markdown keeps its URL.
|
|
707
|
+
console.warn(`Warning: kept ${spec.src} as a link. ${err.message}`);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
696
710
|
if (!result) continue;
|
|
697
711
|
// Key the rewrite map by the *original* URL as it appears in the body
|
|
698
712
|
// (e.g. `/media/...`). `normalized.src` has been resolved against
|
|
@@ -726,7 +740,16 @@ async function uploadBundleResources(resourceSpecs, target) {
|
|
|
726
740
|
for (const spec of resourceSpecs) {
|
|
727
741
|
if (!spec) continue;
|
|
728
742
|
if (!spec.srcFilePath && !isAbsoluteUrl(spec.src)) continue;
|
|
729
|
-
|
|
743
|
+
let result;
|
|
744
|
+
try {
|
|
745
|
+
result = await target.uploadMedia(spec);
|
|
746
|
+
} catch (err) {
|
|
747
|
+
// The post's own attachment: creating the post without it would lose
|
|
748
|
+
// the file quietly.
|
|
749
|
+
throw new Error(`Couldn't upload ${spec.src}: ${err.message}`, {
|
|
750
|
+
cause: err,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
730
753
|
if (!result) continue;
|
|
731
754
|
urlMap.set(spec.src, result.url);
|
|
732
755
|
mediaIds.push(result.id);
|
|
@@ -888,6 +911,9 @@ async function loadSiteConfig(rootDir) {
|
|
|
888
911
|
nav_exported: Array.isArray(jantData.nav),
|
|
889
912
|
collections_directory: directoryItems,
|
|
890
913
|
collections_directory_exported: directoryExported,
|
|
914
|
+
custom_urls: Array.isArray(jantData.custom_url)
|
|
915
|
+
? jantData.custom_url
|
|
916
|
+
: [],
|
|
891
917
|
},
|
|
892
918
|
jant_export: {
|
|
893
919
|
format:
|
|
@@ -908,8 +934,10 @@ function buildSettingsUpdatesFromConfig(siteConfig, customCss = "") {
|
|
|
908
934
|
SITE_DESCRIPTION: String(siteConfig?.description || ""),
|
|
909
935
|
SITE_LANGUAGE: String(siteConfig?.default_language || "en"),
|
|
910
936
|
SITE_FOOTER: String(jant.site_footer_markdown || ""),
|
|
911
|
-
SHOW_JANT_BRANDING_ON_HOME: jant.show_jant_branding_on_home
|
|
912
|
-
|
|
937
|
+
SHOW_JANT_BRANDING_ON_HOME: jant.show_jant_branding_on_home
|
|
938
|
+
? "true"
|
|
939
|
+
: "false",
|
|
940
|
+
NOINDEX: jant.noindex ? "true" : "false",
|
|
913
941
|
PUBLIC_API_ENABLED: jant.public_api_enabled === false ? "false" : "true",
|
|
914
942
|
RSS_FEEDS_ENABLED: jant.rss_feeds_enabled === false ? "false" : "true",
|
|
915
943
|
SHOW_HEADER_AVATAR: jant.show_header_avatar ? "true" : "",
|
|
@@ -946,6 +974,28 @@ function splitSettingsUpdatesForImport(updates) {
|
|
|
946
974
|
return { editable, internal };
|
|
947
975
|
}
|
|
948
976
|
|
|
977
|
+
const NAV_PLACEMENTS = new Set(["header", "more"]);
|
|
978
|
+
const NAV_ITEM_TYPES = new Set([
|
|
979
|
+
"system",
|
|
980
|
+
"link",
|
|
981
|
+
"collection",
|
|
982
|
+
"smart_collection",
|
|
983
|
+
"page",
|
|
984
|
+
]);
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* The slug a nav URL names, for exports older than `collection_slug` and
|
|
988
|
+
* `post_slug`: `/now` or `/now/` → `now`. Anything else names no slug.
|
|
989
|
+
*
|
|
990
|
+
* @param {unknown} url - The exported nav URL
|
|
991
|
+
* @returns {string | null} The slug, or null
|
|
992
|
+
*/
|
|
993
|
+
function getSlugFromNavUrl(url) {
|
|
994
|
+
if (typeof url !== "string") return null;
|
|
995
|
+
const match = url.match(/^\/([^/?#]+)\/?$/);
|
|
996
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
997
|
+
}
|
|
998
|
+
|
|
949
999
|
function normalizeImportedNavItems(siteConfig) {
|
|
950
1000
|
const jant = siteConfig?.extra?.jant || {};
|
|
951
1001
|
const navItems = jant.nav;
|
|
@@ -961,23 +1011,217 @@ function normalizeImportedNavItems(siteConfig) {
|
|
|
961
1011
|
items: navItems
|
|
962
1012
|
.map((item) => {
|
|
963
1013
|
if (!item || typeof item !== "object") return null;
|
|
964
|
-
const type = item.type
|
|
965
|
-
|
|
966
|
-
|
|
1014
|
+
const type = NAV_ITEM_TYPES.has(item.type) ? item.type : "link";
|
|
1015
|
+
const placement = NAV_PLACEMENTS.has(item.placement)
|
|
1016
|
+
? item.placement
|
|
1017
|
+
: "header";
|
|
1018
|
+
const label = typeof item.label === "string" ? item.label : "";
|
|
1019
|
+
const url = typeof item.url === "string" ? item.url : "";
|
|
1020
|
+
const customLabel =
|
|
1021
|
+
typeof item.custom_label === "string" && item.custom_label.trim()
|
|
1022
|
+
? item.custom_label
|
|
1023
|
+
: null;
|
|
1024
|
+
|
|
1025
|
+
if (type === "system") {
|
|
1026
|
+
if (typeof item.system_key !== "string" || !item.system_key) {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
return { type, systemKey: item.system_key, customLabel, placement };
|
|
967
1030
|
}
|
|
968
|
-
if (
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
return { type,
|
|
1031
|
+
if (type === "collection") {
|
|
1032
|
+
const slug =
|
|
1033
|
+
typeof item.collection_slug === "string"
|
|
1034
|
+
? item.collection_slug
|
|
1035
|
+
: getSlugFromNavUrl(url);
|
|
1036
|
+
return { type, slug, customLabel, label, url, placement };
|
|
974
1037
|
}
|
|
975
|
-
|
|
1038
|
+
if (type === "page") {
|
|
1039
|
+
const slug =
|
|
1040
|
+
typeof item.post_slug === "string"
|
|
1041
|
+
? item.post_slug
|
|
1042
|
+
: getSlugFromNavUrl(url);
|
|
1043
|
+
return { type, slug, customLabel, label, url, placement };
|
|
1044
|
+
}
|
|
1045
|
+
if (type === "smart_collection") {
|
|
1046
|
+
const slug =
|
|
1047
|
+
typeof item.smart_collection_slug === "string"
|
|
1048
|
+
? item.smart_collection_slug
|
|
1049
|
+
: getSlugFromNavUrl(url);
|
|
1050
|
+
return { type, slug, customLabel, label, url, placement };
|
|
1051
|
+
}
|
|
1052
|
+
if (!label || !url) return null;
|
|
1053
|
+
return { type: "link", label: customLabel ?? label, url, placement };
|
|
976
1054
|
})
|
|
977
1055
|
.filter(Boolean),
|
|
978
1056
|
};
|
|
979
1057
|
}
|
|
980
1058
|
|
|
1059
|
+
/**
|
|
1060
|
+
* The custom URLs an export lists that name no post or collection: redirects,
|
|
1061
|
+
* and legacy archive URLs the importer reports but can't recreate.
|
|
1062
|
+
*
|
|
1063
|
+
* @param {Record<string, unknown>} siteConfig - From `loadSiteConfig`
|
|
1064
|
+
* @returns {Array<{ path: string, kind: "redirect" | "archive", to?: string, status?: 301 | 302, archiveQuery?: string }>}
|
|
1065
|
+
*/
|
|
1066
|
+
function normalizeImportedCustomUrls(siteConfig) {
|
|
1067
|
+
const entries = siteConfig?.extra?.jant?.custom_urls;
|
|
1068
|
+
if (!Array.isArray(entries)) return [];
|
|
1069
|
+
|
|
1070
|
+
return entries
|
|
1071
|
+
.map((entry) => {
|
|
1072
|
+
if (!entry || typeof entry !== "object") return null;
|
|
1073
|
+
const path =
|
|
1074
|
+
typeof entry.path === "string" ? entry.path.replace(/^\/+/, "") : "";
|
|
1075
|
+
if (!path) return null;
|
|
1076
|
+
if (entry.kind === "redirect" && typeof entry.to === "string") {
|
|
1077
|
+
return {
|
|
1078
|
+
path,
|
|
1079
|
+
kind: "redirect",
|
|
1080
|
+
to: entry.to,
|
|
1081
|
+
status: entry.status === 302 ? 302 : 301,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
if (entry.kind === "archive") {
|
|
1085
|
+
return {
|
|
1086
|
+
path,
|
|
1087
|
+
kind: "archive",
|
|
1088
|
+
archiveQuery:
|
|
1089
|
+
typeof entry.archive_query === "string"
|
|
1090
|
+
? entry.archive_query
|
|
1091
|
+
: undefined,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
return null;
|
|
1095
|
+
})
|
|
1096
|
+
.filter(Boolean);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* The create request for one imported nav item, once the posts and
|
|
1101
|
+
* collections it may point at exist. A target that didn't come across stays
|
|
1102
|
+
* in the navigation as a plain link to the same address.
|
|
1103
|
+
*
|
|
1104
|
+
* @param {Record<string, unknown>} item - From `normalizeImportedNavItems`
|
|
1105
|
+
* @param {{ collectionSlugToId: Map<string, string>, smartCollectionSlugToId: Map<string, string>, postSlugToId: Map<string, string> }} targets
|
|
1106
|
+
* @returns {{ payload: Record<string, unknown>, customLabel: string | null, warning: string | null }}
|
|
1107
|
+
*/
|
|
1108
|
+
function buildNavItemCreateRequest(item, targets) {
|
|
1109
|
+
const { placement, customLabel } = item;
|
|
1110
|
+
|
|
1111
|
+
if (item.type === "system") {
|
|
1112
|
+
return {
|
|
1113
|
+
payload: { type: "system", systemKey: item.systemKey, placement },
|
|
1114
|
+
customLabel,
|
|
1115
|
+
warning: null,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
if (item.type === "link") {
|
|
1119
|
+
return {
|
|
1120
|
+
payload: { type: "link", label: item.label, url: item.url, placement },
|
|
1121
|
+
customLabel: null,
|
|
1122
|
+
warning: null,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const targetId =
|
|
1127
|
+
item.type === "collection"
|
|
1128
|
+
? targets.collectionSlugToId.get(item.slug)
|
|
1129
|
+
: item.type === "smart_collection"
|
|
1130
|
+
? targets.smartCollectionSlugToId.get(item.slug)
|
|
1131
|
+
: item.type === "page"
|
|
1132
|
+
? targets.postSlugToId.get(item.slug)
|
|
1133
|
+
: undefined;
|
|
1134
|
+
if (targetId) {
|
|
1135
|
+
const targetField =
|
|
1136
|
+
item.type === "collection"
|
|
1137
|
+
? "collectionId"
|
|
1138
|
+
: item.type === "smart_collection"
|
|
1139
|
+
? "smartCollectionId"
|
|
1140
|
+
: "postId";
|
|
1141
|
+
return {
|
|
1142
|
+
payload: {
|
|
1143
|
+
type: item.type,
|
|
1144
|
+
[targetField]: targetId,
|
|
1145
|
+
...(customLabel ? { label: customLabel } : {}),
|
|
1146
|
+
placement,
|
|
1147
|
+
},
|
|
1148
|
+
customLabel: null,
|
|
1149
|
+
warning: null,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
return {
|
|
1154
|
+
payload: {
|
|
1155
|
+
type: "link",
|
|
1156
|
+
label: customLabel ?? item.label,
|
|
1157
|
+
url: item.url,
|
|
1158
|
+
placement,
|
|
1159
|
+
},
|
|
1160
|
+
customLabel: null,
|
|
1161
|
+
warning: `The ${item.type.replace("_", " ")} behind "${item.label}" wasn't imported; it stays in the navigation as a link to ${item.url}.`,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* The create request for one exported smart collection.
|
|
1167
|
+
*
|
|
1168
|
+
* The conditions arrive as the export spells them, with the collection one
|
|
1169
|
+
* naming a slug; it becomes this site's ID. When that collection didn't come
|
|
1170
|
+
* across, the smart collection is skipped rather than created without the
|
|
1171
|
+
* condition, which would widen it to posts it never held.
|
|
1172
|
+
*
|
|
1173
|
+
* @param {{ slug: string, frontMatter: Record<string, unknown> }} bundle - A `type: smart_collection` section
|
|
1174
|
+
* @param {Map<string, string>} collectionSlugToId - Imported collections
|
|
1175
|
+
* @returns {{ payload: Record<string, unknown> | null, warning: string | null }}
|
|
1176
|
+
* @example
|
|
1177
|
+
* buildSmartCollectionCreateRequest(bundle, new Map([["ideas", "col_01…"]]));
|
|
1178
|
+
*/
|
|
1179
|
+
function buildSmartCollectionCreateRequest(bundle, collectionSlugToId) {
|
|
1180
|
+
const frontMatter = bundle.frontMatter;
|
|
1181
|
+
const exported = frontMatter.selection;
|
|
1182
|
+
const selection =
|
|
1183
|
+
exported && typeof exported === "object" && !Array.isArray(exported)
|
|
1184
|
+
? { ...exported }
|
|
1185
|
+
: {};
|
|
1186
|
+
|
|
1187
|
+
if (selection.collection !== undefined) {
|
|
1188
|
+
const collectionId =
|
|
1189
|
+
typeof selection.collection === "string"
|
|
1190
|
+
? collectionSlugToId.get(selection.collection)
|
|
1191
|
+
: undefined;
|
|
1192
|
+
if (!collectionId) {
|
|
1193
|
+
return {
|
|
1194
|
+
payload: null,
|
|
1195
|
+
warning: `skipped the smart collection /${bundle.slug}: the collection it filters by, "${selection.collection}", wasn't imported.`,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
selection.collection = [collectionId];
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const title =
|
|
1202
|
+
typeof frontMatter.title === "string" && frontMatter.title.trim()
|
|
1203
|
+
? frontMatter.title
|
|
1204
|
+
: bundle.slug;
|
|
1205
|
+
return {
|
|
1206
|
+
payload: {
|
|
1207
|
+
slug: bundle.slug,
|
|
1208
|
+
title,
|
|
1209
|
+
...(typeof frontMatter.summary_text === "string" &&
|
|
1210
|
+
frontMatter.summary_text.trim()
|
|
1211
|
+
? { description: frontMatter.summary_text }
|
|
1212
|
+
: {}),
|
|
1213
|
+
selection,
|
|
1214
|
+
...(typeof frontMatter.sort_order === "string"
|
|
1215
|
+
? { sort: frontMatter.sort_order }
|
|
1216
|
+
: {}),
|
|
1217
|
+
...(typeof frontMatter.display_layout === "string"
|
|
1218
|
+
? { layout: frontMatter.display_layout }
|
|
1219
|
+
: {}),
|
|
1220
|
+
},
|
|
1221
|
+
warning: null,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
981
1225
|
function normalizeImportedCollectionDirectory(siteConfig) {
|
|
982
1226
|
const jant = siteConfig?.extra?.jant || {};
|
|
983
1227
|
const directoryItems = jant.collections_directory;
|
|
@@ -995,12 +1239,12 @@ function normalizeImportedCollectionDirectory(siteConfig) {
|
|
|
995
1239
|
if (!item || typeof item !== "object") return null;
|
|
996
1240
|
|
|
997
1241
|
if (
|
|
998
|
-
item.type === "collection" &&
|
|
1242
|
+
(item.type === "collection" || item.type === "smart_collection") &&
|
|
999
1243
|
typeof item.slug === "string" &&
|
|
1000
1244
|
item.slug.trim()
|
|
1001
1245
|
) {
|
|
1002
1246
|
return {
|
|
1003
|
-
type:
|
|
1247
|
+
type: item.type,
|
|
1004
1248
|
slug: item.slug.trim(),
|
|
1005
1249
|
};
|
|
1006
1250
|
}
|
|
@@ -1021,6 +1265,10 @@ function normalizeImportedCollectionDirectory(siteConfig) {
|
|
|
1021
1265
|
type: "link",
|
|
1022
1266
|
label: item.label,
|
|
1023
1267
|
url: item.url,
|
|
1268
|
+
description:
|
|
1269
|
+
typeof item.description === "string" && item.description.trim()
|
|
1270
|
+
? item.description
|
|
1271
|
+
: null,
|
|
1024
1272
|
};
|
|
1025
1273
|
}
|
|
1026
1274
|
|
|
@@ -1149,6 +1397,7 @@ async function syncImportedCollectionDirectory(
|
|
|
1149
1397
|
target,
|
|
1150
1398
|
importedDirectory,
|
|
1151
1399
|
collectionSlugToId,
|
|
1400
|
+
smartCollectionSlugToId,
|
|
1152
1401
|
) {
|
|
1153
1402
|
if (!importedDirectory.exported) {
|
|
1154
1403
|
return { created: 0, deleted: 0, moved: 0 };
|
|
@@ -1156,8 +1405,12 @@ async function syncImportedCollectionDirectory(
|
|
|
1156
1405
|
|
|
1157
1406
|
let deleted = 0;
|
|
1158
1407
|
const existingItems = await target.listCollectionDirectoryItems();
|
|
1408
|
+
// Collections and smart collections get their row when they are created;
|
|
1409
|
+
// dividers and links exist only in the directory and are rebuilt from it.
|
|
1159
1410
|
for (const item of existingItems) {
|
|
1160
|
-
if (item.type === "collection")
|
|
1411
|
+
if (item.type === "collection" || item.type === "smart_collection") {
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1161
1414
|
const removed = await target.deleteCollectionDirectoryItem(item.id);
|
|
1162
1415
|
if (removed !== false) {
|
|
1163
1416
|
deleted += 1;
|
|
@@ -1165,24 +1418,33 @@ async function syncImportedCollectionDirectory(
|
|
|
1165
1418
|
}
|
|
1166
1419
|
|
|
1167
1420
|
let currentItems = await target.listCollectionDirectoryItems();
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1421
|
+
// Directory rows by what they list: `collection:<id>` or
|
|
1422
|
+
// `smart_collection:<id>`.
|
|
1423
|
+
const rowIdsByTarget = new Map();
|
|
1424
|
+
for (const item of currentItems) {
|
|
1425
|
+
if (item.type === "collection" && item.collectionId) {
|
|
1426
|
+
rowIdsByTarget.set(`collection:${item.collectionId}`, item.id);
|
|
1427
|
+
} else if (item.type === "smart_collection" && item.smartCollectionId) {
|
|
1428
|
+
rowIdsByTarget.set(`smart_collection:${item.smartCollectionId}`, item.id);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1173
1431
|
|
|
1174
1432
|
const desiredIds = [];
|
|
1175
|
-
const
|
|
1433
|
+
const seenTargets = new Set();
|
|
1176
1434
|
let created = 0;
|
|
1177
1435
|
|
|
1178
1436
|
for (const item of importedDirectory.items) {
|
|
1179
|
-
if (item.type === "collection") {
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1437
|
+
if (item.type === "collection" || item.type === "smart_collection") {
|
|
1438
|
+
const targetId =
|
|
1439
|
+
item.type === "collection"
|
|
1440
|
+
? collectionSlugToId.get(item.slug)
|
|
1441
|
+
: smartCollectionSlugToId.get(item.slug);
|
|
1442
|
+
const key = `${item.type}:${targetId}`;
|
|
1443
|
+
if (!targetId || seenTargets.has(key)) {
|
|
1182
1444
|
continue;
|
|
1183
1445
|
}
|
|
1184
|
-
|
|
1185
|
-
const directoryItemId =
|
|
1446
|
+
seenTargets.add(key);
|
|
1447
|
+
const directoryItemId = rowIdsByTarget.get(key);
|
|
1186
1448
|
if (directoryItemId) {
|
|
1187
1449
|
desiredIds.push(directoryItemId);
|
|
1188
1450
|
}
|
|
@@ -1199,6 +1461,7 @@ async function syncImportedCollectionDirectory(
|
|
|
1199
1461
|
type: "link",
|
|
1200
1462
|
label: item.label,
|
|
1201
1463
|
url: item.url,
|
|
1464
|
+
...(item.description ? { description: item.description } : {}),
|
|
1202
1465
|
},
|
|
1203
1466
|
);
|
|
1204
1467
|
desiredIds.push(createdItem.id);
|
|
@@ -1253,17 +1516,6 @@ async function getIncompleteSetupError(target, targetLabel) {
|
|
|
1253
1516
|
return buildIncompleteSetupError(targetLabel);
|
|
1254
1517
|
}
|
|
1255
1518
|
|
|
1256
|
-
function createUploadFile(name, type, bytes) {
|
|
1257
|
-
return {
|
|
1258
|
-
name,
|
|
1259
|
-
type,
|
|
1260
|
-
size: bytes.byteLength,
|
|
1261
|
-
stream() {
|
|
1262
|
-
return new Blob([bytes], { type }).stream();
|
|
1263
|
-
},
|
|
1264
|
-
};
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
1519
|
class ApiError extends Error {
|
|
1268
1520
|
constructor(status, text) {
|
|
1269
1521
|
super(`HTTP ${status}: ${text}`);
|
|
@@ -1290,10 +1542,12 @@ async function apiCall(method, path, apiUrl, token, body) {
|
|
|
1290
1542
|
cause === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" ||
|
|
1291
1543
|
cause?.includes("certificate")
|
|
1292
1544
|
) {
|
|
1293
|
-
console.error(`\
|
|
1294
|
-
console.error(
|
|
1295
|
-
|
|
1296
|
-
|
|
1545
|
+
console.error(`\nCertificate error connecting to ${apiUrl}`);
|
|
1546
|
+
console.error(
|
|
1547
|
+
"The certificate is not signed by a CA that Node trusts. For a local",
|
|
1548
|
+
);
|
|
1549
|
+
console.error("or self-signed target, trust this machine's CA store:");
|
|
1550
|
+
console.error(" NODE_OPTIONS=--use-system-ca jant site import ...");
|
|
1297
1551
|
process.exit(1);
|
|
1298
1552
|
}
|
|
1299
1553
|
throw new Error(
|
|
@@ -1344,6 +1598,12 @@ function createRemoteTarget(apiUrl, token) {
|
|
|
1344
1598
|
async deleteNavItem(id) {
|
|
1345
1599
|
return apiCall("DELETE", `/api/nav-items/${id}`, apiUrl, token);
|
|
1346
1600
|
},
|
|
1601
|
+
async updateNavItem(id, data) {
|
|
1602
|
+
return apiCall("PUT", `/api/nav-items/${id}`, apiUrl, token, data);
|
|
1603
|
+
},
|
|
1604
|
+
async createCustomUrl(data) {
|
|
1605
|
+
return apiCall("POST", "/api/custom-urls", apiUrl, token, data);
|
|
1606
|
+
},
|
|
1347
1607
|
async removeSiteAvatar() {
|
|
1348
1608
|
return apiCall("DELETE", "/api/settings/avatar", apiUrl, token);
|
|
1349
1609
|
},
|
|
@@ -1357,11 +1617,7 @@ function createRemoteTarget(apiUrl, token) {
|
|
|
1357
1617
|
}
|
|
1358
1618
|
|
|
1359
1619
|
const formData = new FormData();
|
|
1360
|
-
formData.append(
|
|
1361
|
-
"file",
|
|
1362
|
-
new Blob([avatarAsset.bytes], { type: avatarAsset.contentType }),
|
|
1363
|
-
avatarAsset.filename,
|
|
1364
|
-
);
|
|
1620
|
+
formData.append("file", avatarAsset.blob, avatarAsset.filename);
|
|
1365
1621
|
|
|
1366
1622
|
if (data.faviconUrl || data.faviconFilePath) {
|
|
1367
1623
|
const faviconAsset = await readImportAsset({
|
|
@@ -1371,13 +1627,7 @@ function createRemoteTarget(apiUrl, token) {
|
|
|
1371
1627
|
originalName: "favicon.ico",
|
|
1372
1628
|
});
|
|
1373
1629
|
if (faviconAsset) {
|
|
1374
|
-
formData.append(
|
|
1375
|
-
"favicon",
|
|
1376
|
-
new Blob([faviconAsset.bytes], {
|
|
1377
|
-
type: faviconAsset.contentType,
|
|
1378
|
-
}),
|
|
1379
|
-
faviconAsset.filename,
|
|
1380
|
-
);
|
|
1630
|
+
formData.append("favicon", faviconAsset.blob, faviconAsset.filename);
|
|
1381
1631
|
}
|
|
1382
1632
|
}
|
|
1383
1633
|
|
|
@@ -1389,9 +1639,7 @@ function createRemoteTarget(apiUrl, token) {
|
|
|
1389
1639
|
if (appleTouchAsset) {
|
|
1390
1640
|
formData.append(
|
|
1391
1641
|
"appleTouch",
|
|
1392
|
-
|
|
1393
|
-
type: appleTouchAsset.contentType,
|
|
1394
|
-
}),
|
|
1642
|
+
appleTouchAsset.blob,
|
|
1395
1643
|
appleTouchAsset.filename,
|
|
1396
1644
|
);
|
|
1397
1645
|
}
|
|
@@ -1427,6 +1675,25 @@ function createRemoteTarget(apiUrl, token) {
|
|
|
1427
1675
|
async createCollection(data) {
|
|
1428
1676
|
return apiCall("POST", "/api/collections", apiUrl, token, data);
|
|
1429
1677
|
},
|
|
1678
|
+
async listSmartCollections() {
|
|
1679
|
+
const result = await apiCall(
|
|
1680
|
+
"GET",
|
|
1681
|
+
"/api/smart-collections",
|
|
1682
|
+
apiUrl,
|
|
1683
|
+
token,
|
|
1684
|
+
);
|
|
1685
|
+
return result.smartCollections || [];
|
|
1686
|
+
},
|
|
1687
|
+
async createSmartCollection(data) {
|
|
1688
|
+
const result = await apiCall(
|
|
1689
|
+
"POST",
|
|
1690
|
+
"/api/smart-collections",
|
|
1691
|
+
apiUrl,
|
|
1692
|
+
token,
|
|
1693
|
+
data,
|
|
1694
|
+
);
|
|
1695
|
+
return result.smartCollection;
|
|
1696
|
+
},
|
|
1430
1697
|
async createCollectionDirectoryItem(data) {
|
|
1431
1698
|
return apiCall(
|
|
1432
1699
|
"POST",
|
|
@@ -1462,6 +1729,15 @@ function createRemoteTarget(apiUrl, token) {
|
|
|
1462
1729
|
toRemotePostPayload(data),
|
|
1463
1730
|
);
|
|
1464
1731
|
},
|
|
1732
|
+
async linkTranslation(postId, otherPostId) {
|
|
1733
|
+
return apiCall(
|
|
1734
|
+
"POST",
|
|
1735
|
+
`/api/posts/${postId}/translations`,
|
|
1736
|
+
apiUrl,
|
|
1737
|
+
token,
|
|
1738
|
+
{ postId: otherPostId },
|
|
1739
|
+
);
|
|
1740
|
+
},
|
|
1465
1741
|
async createAlias(path, targetSlug) {
|
|
1466
1742
|
return apiCall("POST", "/api/custom-urls", apiUrl, token, {
|
|
1467
1743
|
path,
|
|
@@ -1484,23 +1760,76 @@ function createRemoteTarget(apiUrl, token) {
|
|
|
1484
1760
|
};
|
|
1485
1761
|
}
|
|
1486
1762
|
|
|
1763
|
+
function getBundleTimestamp(bundle) {
|
|
1764
|
+
return parseImportTimestamp(bundle.frontMatter.date);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
function getBundleSourceId(bundle) {
|
|
1768
|
+
return typeof bundle.frontMatter.id === "string" ? bundle.frontMatter.id : "";
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* Order bundles by `date`, then by the original TypeID (time-sortable, so it
|
|
1773
|
+
* breaks a same-second tie the way the source site did), then by directory.
|
|
1774
|
+
*
|
|
1775
|
+
* @param {{ frontMatter: Record<string, unknown>, dir: string }} a
|
|
1776
|
+
* @param {{ frontMatter: Record<string, unknown>, dir: string }} b
|
|
1777
|
+
* @returns {number} Negative when `a` comes first
|
|
1778
|
+
*/
|
|
1779
|
+
function compareBundlesByDateThenId(a, b) {
|
|
1780
|
+
const aDate = getBundleTimestamp(a);
|
|
1781
|
+
const bDate = getBundleTimestamp(b);
|
|
1782
|
+
if (aDate !== null && bDate !== null && aDate !== bDate) return aDate - bDate;
|
|
1783
|
+
if (aDate !== null && bDate === null) return -1;
|
|
1784
|
+
if (aDate === null && bDate !== null) return 1;
|
|
1785
|
+
|
|
1786
|
+
const aId = getBundleSourceId(a);
|
|
1787
|
+
const bId = getBundleSourceId(b);
|
|
1788
|
+
if (aId && bId && aId !== bId) return aId < bId ? -1 : 1;
|
|
1789
|
+
|
|
1790
|
+
return basename(a.dir).localeCompare(basename(b.dir));
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Order reply bundles by their exported `weight` (position in the Thread),
|
|
1795
|
+
* falling back to {@link compareBundlesByDateThenId} for older exports.
|
|
1796
|
+
*
|
|
1797
|
+
* @param {{ frontMatter: Record<string, unknown>, dir: string }} a
|
|
1798
|
+
* @param {{ frontMatter: Record<string, unknown>, dir: string }} b
|
|
1799
|
+
* @returns {number} Negative when `a` comes first
|
|
1800
|
+
*/
|
|
1801
|
+
function compareReplyBundles(a, b) {
|
|
1802
|
+
const aWeight = a.frontMatter.weight;
|
|
1803
|
+
const bWeight = b.frontMatter.weight;
|
|
1804
|
+
if (
|
|
1805
|
+
typeof aWeight === "number" &&
|
|
1806
|
+
typeof bWeight === "number" &&
|
|
1807
|
+
aWeight !== bWeight
|
|
1808
|
+
) {
|
|
1809
|
+
return aWeight - bWeight;
|
|
1810
|
+
}
|
|
1811
|
+
return compareBundlesByDateThenId(a, b);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1487
1814
|
/**
|
|
1488
1815
|
* Walk `content/` and classify each `_index.md` / `index.md` bundle by its
|
|
1489
1816
|
* front-matter `type`. Returns ordered root-post bundles (with child reply
|
|
1490
|
-
* bundles attached) and
|
|
1817
|
+
* bundles attached), collection landing pages, and smart collection pages.
|
|
1491
1818
|
*
|
|
1492
1819
|
* Algorithm:
|
|
1493
1820
|
* 1. Recurse into `content/` collecting every directory that has either
|
|
1494
1821
|
* `_index.md` (branch bundle / section) or `index.md` (leaf bundle).
|
|
1495
1822
|
* 2. For each `_index.md`, read front matter. `type: "post"` (or a
|
|
1496
1823
|
* missing `type` with post-shaped keys) → root bundle. `type:
|
|
1497
|
-
* "collection"` → collection landing page
|
|
1824
|
+
* "collection"` → collection landing page, `type: "smart_collection"` →
|
|
1825
|
+
* smart collection conditions. Other known section types
|
|
1498
1826
|
* (`home`, `featured`, `archive`, `collections`) are recorded and
|
|
1499
1827
|
* skipped for post import.
|
|
1500
1828
|
* 3. For each root bundle, enumerate immediate child directories; any
|
|
1501
1829
|
* child dir containing `index.md` becomes a reply leaf bundle.
|
|
1502
|
-
* 4. Reply bundles are sorted by `
|
|
1503
|
-
*
|
|
1830
|
+
* 4. Reply bundles are sorted by `weight` (Thread position), then `date`,
|
|
1831
|
+
* then the original ID; root bundles by `date`, then the original ID.
|
|
1832
|
+
* Creating posts in that order keeps same-second ties as they were.
|
|
1504
1833
|
*/
|
|
1505
1834
|
async function walkHugoContent(rootDir) {
|
|
1506
1835
|
const contentDir = join(rootDir, "content");
|
|
@@ -1586,6 +1915,7 @@ async function walkHugoContent(rootDir) {
|
|
|
1586
1915
|
// Attach leaf children to their parent root bundles.
|
|
1587
1916
|
const rootBundles = [];
|
|
1588
1917
|
const collectionBundles = [];
|
|
1918
|
+
const smartCollectionBundles = [];
|
|
1589
1919
|
|
|
1590
1920
|
for (const record of dirs.values()) {
|
|
1591
1921
|
if (record.kind === "leaf") {
|
|
@@ -1600,6 +1930,10 @@ async function walkHugoContent(rootDir) {
|
|
|
1600
1930
|
collectionBundles.push(record);
|
|
1601
1931
|
continue;
|
|
1602
1932
|
}
|
|
1933
|
+
if (record.kind === "smart_collection") {
|
|
1934
|
+
smartCollectionBundles.push(record);
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1603
1937
|
if (record.kind === "post") {
|
|
1604
1938
|
rootBundles.push(record);
|
|
1605
1939
|
continue;
|
|
@@ -1607,27 +1941,19 @@ async function walkHugoContent(rootDir) {
|
|
|
1607
1941
|
// home / featured / archive / collections / anything else — skip.
|
|
1608
1942
|
}
|
|
1609
1943
|
|
|
1610
|
-
//
|
|
1944
|
+
// Replies in Thread order: the exported `weight`, which follows Jant's own
|
|
1945
|
+
// order (creation time, then ID). Older exports have no weight; `date` then
|
|
1946
|
+
// the original ID come next, since replies written in one second tie on
|
|
1947
|
+
// `date`, and the directory name last.
|
|
1611
1948
|
for (const root of rootBundles) {
|
|
1612
|
-
root.children.sort(
|
|
1613
|
-
const aDate =
|
|
1614
|
-
typeof a.frontMatter.date === "string"
|
|
1615
|
-
? Date.parse(a.frontMatter.date)
|
|
1616
|
-
: NaN;
|
|
1617
|
-
const bDate =
|
|
1618
|
-
typeof b.frontMatter.date === "string"
|
|
1619
|
-
? Date.parse(b.frontMatter.date)
|
|
1620
|
-
: NaN;
|
|
1621
|
-
if (Number.isFinite(aDate) && Number.isFinite(bDate) && aDate !== bDate) {
|
|
1622
|
-
return aDate - bDate;
|
|
1623
|
-
}
|
|
1624
|
-
if (Number.isFinite(aDate) && !Number.isFinite(bDate)) return -1;
|
|
1625
|
-
if (!Number.isFinite(aDate) && Number.isFinite(bDate)) return 1;
|
|
1626
|
-
return basename(a.dir).localeCompare(basename(b.dir));
|
|
1627
|
-
});
|
|
1949
|
+
root.children.sort(compareReplyBundles);
|
|
1628
1950
|
}
|
|
1629
1951
|
|
|
1630
|
-
|
|
1952
|
+
// Roots in creation order, oldest first, so the new IDs keep the original
|
|
1953
|
+
// order among posts that share a second: lists break those ties by ID.
|
|
1954
|
+
rootBundles.sort(compareBundlesByDateThenId);
|
|
1955
|
+
|
|
1956
|
+
return { rootBundles, collectionBundles, smartCollectionBundles };
|
|
1631
1957
|
}
|
|
1632
1958
|
|
|
1633
1959
|
/**
|
|
@@ -1759,6 +2085,45 @@ function shouldImportReplyQuietly(rootFrontMatter, replyFrontMatter) {
|
|
|
1759
2085
|
);
|
|
1760
2086
|
}
|
|
1761
2087
|
|
|
2088
|
+
/**
|
|
2089
|
+
* The post a Thread's next reply has to answer: the site's rule, the last post
|
|
2090
|
+
* in Thread order. The root comes first whatever its creation time, so the
|
|
2091
|
+
* first reply always ends the Thread; after that the newest reply by creation
|
|
2092
|
+
* time, then by ID, does.
|
|
2093
|
+
*
|
|
2094
|
+
* @param {{ id: string, createdAt: number, replyToId?: string | null }} tail - The current end
|
|
2095
|
+
* @param {{ id: string, createdAt: number, replyToId?: string | null } | null | undefined} created - The post just created
|
|
2096
|
+
* @returns {{ id: string, createdAt: number, replyToId?: string | null }} The new end
|
|
2097
|
+
* @example
|
|
2098
|
+
* getNextThreadTail(
|
|
2099
|
+
* { id: "root", createdAt: 20, replyToId: null },
|
|
2100
|
+
* { id: "r1", createdAt: 10, replyToId: "root" },
|
|
2101
|
+
* );
|
|
2102
|
+
* // r1: a reply older than the root still ends the Thread
|
|
2103
|
+
*/
|
|
2104
|
+
function getNextThreadTail(tail, created) {
|
|
2105
|
+
if (!created?.id) return tail;
|
|
2106
|
+
return compareThreadOrder(created, tail) > 0 ? created : tail;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/**
|
|
2110
|
+
* Compare two posts of one Thread the way the site orders them (`threadOrder`
|
|
2111
|
+
* in the post service): the root first, then creation time, then ID.
|
|
2112
|
+
*
|
|
2113
|
+
* @param {{ id: string, createdAt: number, replyToId?: string | null }} a
|
|
2114
|
+
* @param {{ id: string, createdAt: number, replyToId?: string | null }} b
|
|
2115
|
+
* @returns {number} Negative when `a` comes first, positive when `b` does
|
|
2116
|
+
* @example
|
|
2117
|
+
* compareThreadOrder({ id: "r1", createdAt: 10, replyToId: "root" },
|
|
2118
|
+
* { id: "root", createdAt: 20, replyToId: null }); // 1
|
|
2119
|
+
*/
|
|
2120
|
+
function compareThreadOrder(a, b) {
|
|
2121
|
+
const rank = (post) => (post.replyToId ? 1 : 0);
|
|
2122
|
+
if (rank(a) !== rank(b)) return rank(a) - rank(b);
|
|
2123
|
+
if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt;
|
|
2124
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
1762
2127
|
/**
|
|
1763
2128
|
* Build the payload for `target.createPost()` from a parsed bundle. Works
|
|
1764
2129
|
* for both root bundles (`forReply: false`) and reply leaf bundles — the
|
|
@@ -1814,6 +2179,16 @@ function buildPostPayloadFromBundle(bundle, options) {
|
|
|
1814
2179
|
status === "published" && typeof frontMatter.date === "string"
|
|
1815
2180
|
? Math.floor(new Date(frontMatter.date).getTime() / 1000)
|
|
1816
2181
|
: undefined,
|
|
2182
|
+
// `date` holds the publish time of a published post and the creation
|
|
2183
|
+
// time of a draft; `created` and `updated` are written when they differ.
|
|
2184
|
+
createdAt:
|
|
2185
|
+
parseImportTimestamp(frontMatter.created) ??
|
|
2186
|
+
parseImportTimestamp(frontMatter.date) ??
|
|
2187
|
+
undefined,
|
|
2188
|
+
updatedAt:
|
|
2189
|
+
parseImportTimestamp(frontMatter.updated) ??
|
|
2190
|
+
parseImportTimestamp(frontMatter.date) ??
|
|
2191
|
+
undefined,
|
|
1817
2192
|
featuredAt:
|
|
1818
2193
|
typeof frontMatter.featured_at === "string" && frontMatter.featured_at
|
|
1819
2194
|
? Math.floor(new Date(frontMatter.featured_at).getTime() / 1000)
|
|
@@ -1824,6 +2199,12 @@ function buildPostPayloadFromBundle(bundle, options) {
|
|
|
1824
2199
|
: undefined,
|
|
1825
2200
|
rating:
|
|
1826
2201
|
typeof frontMatter.rating === "number" ? frontMatter.rating : undefined,
|
|
2202
|
+
// Roots only: replies take the root's language from the service, and a
|
|
2203
|
+
// reply bundle has no language of its own in the export.
|
|
2204
|
+
language:
|
|
2205
|
+
!options.replyToId && typeof frontMatter.language === "string"
|
|
2206
|
+
? frontMatter.language
|
|
2207
|
+
: undefined,
|
|
1827
2208
|
quietReply: options.quietReply ? true : undefined,
|
|
1828
2209
|
};
|
|
1829
2210
|
|
|
@@ -1858,8 +2239,13 @@ export const __test__ = {
|
|
|
1858
2239
|
extractAttachmentBlocks,
|
|
1859
2240
|
buildImportedAttachments,
|
|
1860
2241
|
uploadMediaList,
|
|
2242
|
+
uploadBundleResources,
|
|
1861
2243
|
buildSettingsUpdatesFromConfig,
|
|
2244
|
+
splitSettingsUpdatesForImport,
|
|
1862
2245
|
normalizeImportedNavItems,
|
|
2246
|
+
buildNavItemCreateRequest,
|
|
2247
|
+
buildSmartCollectionCreateRequest,
|
|
2248
|
+
normalizeImportedCustomUrls,
|
|
1863
2249
|
normalizeImportedCollectionDirectory,
|
|
1864
2250
|
buildSiteAvatarImport,
|
|
1865
2251
|
reorderCollectionDirectoryItems,
|
|
@@ -1876,6 +2262,7 @@ export const __test__ = {
|
|
|
1876
2262
|
resolveThreadCollectionMemberships,
|
|
1877
2263
|
buildPostPayloadFromBundle,
|
|
1878
2264
|
shouldImportReplyQuietly,
|
|
2265
|
+
getNextThreadTail,
|
|
1879
2266
|
};
|
|
1880
2267
|
|
|
1881
2268
|
function printImportUsage() {
|
|
@@ -1961,6 +2348,8 @@ export async function run(argv) {
|
|
|
1961
2348
|
const apiUrl = url.replace(/\/$/, "");
|
|
1962
2349
|
const skipRemoteMedia = values["skip-remote-media"];
|
|
1963
2350
|
const target = dryRun ? null : createRemoteTarget(apiUrl, token);
|
|
2351
|
+
/** Exported translation-group ID → IDs of the posts created for it. */
|
|
2352
|
+
const translationGroups = new Map();
|
|
1964
2353
|
|
|
1965
2354
|
// 1. Read source — directory or ZIP
|
|
1966
2355
|
const inputPath = resolve(process.cwd(), values.path);
|
|
@@ -1976,21 +2365,14 @@ export async function run(argv) {
|
|
|
1976
2365
|
|
|
1977
2366
|
if (inputStat.isFile()) {
|
|
1978
2367
|
console.log(`Reading ZIP ${inputPath}...`);
|
|
1979
|
-
const zipData = await readFile(inputPath);
|
|
1980
|
-
const { unzipSync } = await import("fflate");
|
|
1981
|
-
const files = unzipSync(new Uint8Array(zipData));
|
|
1982
2368
|
tempSourceRootDir = await mkdtemp(join(tmpdir(), "jant-site-import-"));
|
|
1983
2369
|
sourceRootDir = tempSourceRootDir;
|
|
1984
|
-
|
|
1985
|
-
const fullPath = join(sourceRootDir, path);
|
|
1986
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
1987
|
-
await writeFile(fullPath, data);
|
|
1988
|
-
}
|
|
2370
|
+
await extractZipFile(inputPath, sourceRootDir);
|
|
1989
2371
|
} else {
|
|
1990
2372
|
console.log(`Reading directory ${inputPath}...`);
|
|
1991
2373
|
}
|
|
1992
2374
|
|
|
1993
|
-
const { rootBundles, collectionBundles } =
|
|
2375
|
+
const { rootBundles, collectionBundles, smartCollectionBundles } =
|
|
1994
2376
|
await walkHugoContent(sourceRootDir);
|
|
1995
2377
|
const siteConfig = await loadSiteConfig(sourceRootDir);
|
|
1996
2378
|
const customCss = await readImportCustomCss(sourceRootDir);
|
|
@@ -2001,11 +2383,17 @@ export async function run(argv) {
|
|
|
2001
2383
|
0,
|
|
2002
2384
|
);
|
|
2003
2385
|
console.log(
|
|
2004
|
-
`Found ${rootBundles.length} posts (+${replyCount} replies) and ${
|
|
2386
|
+
`Found ${rootBundles.length} posts (+${replyCount} replies), ${collectionBundles.length} collections, and ${smartCollectionBundles.length} smart collections`,
|
|
2005
2387
|
);
|
|
2006
2388
|
const importedCollectionDirectory = siteConfig
|
|
2007
2389
|
? normalizeImportedCollectionDirectory(siteConfig)
|
|
2008
2390
|
: { exported: false, items: [] };
|
|
2391
|
+
const importedNav = siteConfig
|
|
2392
|
+
? normalizeImportedNavItems(siteConfig)
|
|
2393
|
+
: { exported: false, items: [] };
|
|
2394
|
+
const importedCustomUrls = siteConfig
|
|
2395
|
+
? normalizeImportedCustomUrls(siteConfig)
|
|
2396
|
+
: [];
|
|
2009
2397
|
|
|
2010
2398
|
if (target) {
|
|
2011
2399
|
const setupError = await getIncompleteSetupError(
|
|
@@ -2025,7 +2413,6 @@ export async function run(argv) {
|
|
|
2025
2413
|
siteConfig,
|
|
2026
2414
|
customCss,
|
|
2027
2415
|
);
|
|
2028
|
-
const importedNav = normalizeImportedNavItems(siteConfig);
|
|
2029
2416
|
const avatarImport = await buildSiteAvatarImport(
|
|
2030
2417
|
siteConfig,
|
|
2031
2418
|
sourceRootDir,
|
|
@@ -2078,21 +2465,6 @@ export async function run(argv) {
|
|
|
2078
2465
|
}
|
|
2079
2466
|
}
|
|
2080
2467
|
|
|
2081
|
-
if (importedNav.exported) {
|
|
2082
|
-
try {
|
|
2083
|
-
const existingNavItems = await target.listNavItems();
|
|
2084
|
-
for (const item of existingNavItems) {
|
|
2085
|
-
await target.deleteNavItem(item.id);
|
|
2086
|
-
}
|
|
2087
|
-
for (const item of importedNav.items) {
|
|
2088
|
-
await target.createNavItem(item);
|
|
2089
|
-
}
|
|
2090
|
-
} catch (err) {
|
|
2091
|
-
console.error(`Error importing navigation: ${err.message}`);
|
|
2092
|
-
process.exit(1);
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
2468
|
if (avatarImport) {
|
|
2097
2469
|
try {
|
|
2098
2470
|
await target.syncSiteAvatar(
|
|
@@ -2113,6 +2485,8 @@ export async function run(argv) {
|
|
|
2113
2485
|
|
|
2114
2486
|
// 3. Fetch existing collections and create missing ones
|
|
2115
2487
|
const collectionSlugToId = new Map();
|
|
2488
|
+
// Root posts by slug, for navigation entries that point at a page.
|
|
2489
|
+
const postSlugToId = new Map();
|
|
2116
2490
|
|
|
2117
2491
|
if (!dryRun) {
|
|
2118
2492
|
try {
|
|
@@ -2165,6 +2539,60 @@ export async function run(argv) {
|
|
|
2165
2539
|
}
|
|
2166
2540
|
}
|
|
2167
2541
|
|
|
2542
|
+
// Smart collections after collections, which their conditions may name.
|
|
2543
|
+
// They hold no posts, so nothing about them waits for the posts below.
|
|
2544
|
+
const smartCollectionSlugToId = new Map();
|
|
2545
|
+
if (!dryRun) {
|
|
2546
|
+
try {
|
|
2547
|
+
for (const existing of await target.listSmartCollections()) {
|
|
2548
|
+
smartCollectionSlugToId.set(existing.slug, existing.id);
|
|
2549
|
+
}
|
|
2550
|
+
} catch (err) {
|
|
2551
|
+
console.error(
|
|
2552
|
+
`Error fetching existing smart collections: ${err.message}`,
|
|
2553
|
+
);
|
|
2554
|
+
process.exit(1);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
for (const bundle of smartCollectionBundles) {
|
|
2559
|
+
const slug = bundle.slug;
|
|
2560
|
+
if (smartCollectionSlugToId.has(slug)) {
|
|
2561
|
+
console.error(
|
|
2562
|
+
`Import conflict: smart collection slug "${slug}" is already in use. Import into an empty site or remove the existing smart collection first.`,
|
|
2563
|
+
);
|
|
2564
|
+
process.exit(1);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
const request = buildSmartCollectionCreateRequest(
|
|
2568
|
+
bundle,
|
|
2569
|
+
collectionSlugToId,
|
|
2570
|
+
);
|
|
2571
|
+
if (!request.payload) {
|
|
2572
|
+
console.warn(`Warning: ${request.warning}`);
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
if (dryRun) {
|
|
2577
|
+
console.log(
|
|
2578
|
+
`[dry-run] Would create smart collection: ${request.payload.title}`,
|
|
2579
|
+
);
|
|
2580
|
+
smartCollectionSlugToId.set(slug, `dry-run-${slug}`);
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
try {
|
|
2585
|
+
const created = await target.createSmartCollection(request.payload);
|
|
2586
|
+
smartCollectionSlugToId.set(slug, created.id);
|
|
2587
|
+
console.log(`Created smart collection: ${request.payload.title}`);
|
|
2588
|
+
} catch (err) {
|
|
2589
|
+
console.error(
|
|
2590
|
+
`Error creating smart collection "${slug}": ${err.message}`,
|
|
2591
|
+
);
|
|
2592
|
+
process.exit(1);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2168
2596
|
if (importedCollectionDirectory.exported) {
|
|
2169
2597
|
if (dryRun) {
|
|
2170
2598
|
console.log(
|
|
@@ -2176,6 +2604,7 @@ export async function run(argv) {
|
|
|
2176
2604
|
target,
|
|
2177
2605
|
importedCollectionDirectory,
|
|
2178
2606
|
collectionSlugToId,
|
|
2607
|
+
smartCollectionSlugToId,
|
|
2179
2608
|
);
|
|
2180
2609
|
} catch (err) {
|
|
2181
2610
|
console.error(
|
|
@@ -2325,6 +2754,19 @@ export async function run(argv) {
|
|
|
2325
2754
|
try {
|
|
2326
2755
|
post = await target.createPost(postData);
|
|
2327
2756
|
postsCreated++;
|
|
2757
|
+
if (post?.id && post.slug) postSlugToId.set(post.slug, post.id);
|
|
2758
|
+
// Translation groups are rebuilt after every post exists: the group ID
|
|
2759
|
+
// in the export is opaque and its members can appear in any order, so
|
|
2760
|
+
// there is nothing to link to until the whole run is done.
|
|
2761
|
+
const groupKey =
|
|
2762
|
+
typeof rootFm.translation_group === "string"
|
|
2763
|
+
? rootFm.translation_group.trim()
|
|
2764
|
+
: "";
|
|
2765
|
+
if (groupKey && post?.id) {
|
|
2766
|
+
const members = translationGroups.get(groupKey) ?? [];
|
|
2767
|
+
members.push(post.id);
|
|
2768
|
+
translationGroups.set(groupKey, members);
|
|
2769
|
+
}
|
|
2328
2770
|
const replyInfo =
|
|
2329
2771
|
rootBundle.children.length > 0
|
|
2330
2772
|
? ` (+${rootBundle.children.length} replies)`
|
|
@@ -2338,10 +2780,14 @@ export async function run(argv) {
|
|
|
2338
2780
|
// Create replies before aliases so reply slugs can claim their paths.
|
|
2339
2781
|
if (!post) continue;
|
|
2340
2782
|
const replySlugPaths = new Set();
|
|
2341
|
-
// Jant threads are linear:
|
|
2342
|
-
//
|
|
2343
|
-
//
|
|
2344
|
-
|
|
2783
|
+
// Jant threads are linear: a reply must point at the current end of the
|
|
2784
|
+
// thread, which the site reads as its last post in Thread order — the
|
|
2785
|
+
// root first, then replies by creation time, then ID. Creation times
|
|
2786
|
+
// are restored from the export (or taken from `date` in an older one),
|
|
2787
|
+
// so a reply sent after another can carry the earlier time; the end is
|
|
2788
|
+
// then the one with the later time, not the one sent last. Track it by
|
|
2789
|
+
// the site's rule.
|
|
2790
|
+
let threadTail = post;
|
|
2345
2791
|
for (const replyBundle of rootBundle.children) {
|
|
2346
2792
|
const replyFm = replyBundle.frontMatter;
|
|
2347
2793
|
const replySlug = replyBundle.slug;
|
|
@@ -2440,16 +2886,14 @@ export async function run(argv) {
|
|
|
2440
2886
|
bodyMarkdown: replyBody,
|
|
2441
2887
|
attachments: replyAttachments,
|
|
2442
2888
|
memberships: { entries: [], ids: [] },
|
|
2443
|
-
replyToId:
|
|
2889
|
+
replyToId: threadTail.id,
|
|
2444
2890
|
quietReply: shouldImportReplyQuietly(rootFm, replyFm),
|
|
2445
2891
|
});
|
|
2446
2892
|
|
|
2447
2893
|
try {
|
|
2448
2894
|
const createdReply = await target.createPost(replyData);
|
|
2449
2895
|
repliesCreated++;
|
|
2450
|
-
|
|
2451
|
-
threadTailId = createdReply.id;
|
|
2452
|
-
}
|
|
2896
|
+
threadTail = getNextThreadTail(threadTail, createdReply);
|
|
2453
2897
|
} catch (err) {
|
|
2454
2898
|
console.error(` Error creating reply: ${err.message}`);
|
|
2455
2899
|
process.exit(1);
|
|
@@ -2483,6 +2927,85 @@ export async function run(argv) {
|
|
|
2483
2927
|
}
|
|
2484
2928
|
}
|
|
2485
2929
|
|
|
2930
|
+
// Rebuild translation groups. Each member is linked to the first one, so a
|
|
2931
|
+
// group of N takes N-1 calls and never asks the server to merge two groups
|
|
2932
|
+
// — which it refuses, on purpose.
|
|
2933
|
+
let translationsLinked = 0;
|
|
2934
|
+
for (const [groupKey, members] of translationGroups) {
|
|
2935
|
+
if (members.length < 2) continue;
|
|
2936
|
+
const [anchor, ...rest] = members;
|
|
2937
|
+
for (const memberId of rest) {
|
|
2938
|
+
try {
|
|
2939
|
+
await target.linkTranslation(anchor, memberId);
|
|
2940
|
+
translationsLinked++;
|
|
2941
|
+
} catch (err) {
|
|
2942
|
+
// A clash here means the export itself held two posts in the same
|
|
2943
|
+
// language for one group. Say so and keep going: the posts are
|
|
2944
|
+
// imported either way, they are just not linked.
|
|
2945
|
+
console.warn(
|
|
2946
|
+
`Warning: could not link a translation in group "${groupKey}": ${err.message}`,
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
// Navigation and custom URLs go last: they point at posts and
|
|
2953
|
+
// collections, which exist by now.
|
|
2954
|
+
let customUrlsCreated = 0;
|
|
2955
|
+
if (!dryRun && target) {
|
|
2956
|
+
if (importedNav.exported) {
|
|
2957
|
+
try {
|
|
2958
|
+
const existingNavItems = await target.listNavItems();
|
|
2959
|
+
for (const item of existingNavItems) {
|
|
2960
|
+
await target.deleteNavItem(item.id);
|
|
2961
|
+
}
|
|
2962
|
+
for (const item of importedNav.items) {
|
|
2963
|
+
const request = buildNavItemCreateRequest(item, {
|
|
2964
|
+
collectionSlugToId,
|
|
2965
|
+
smartCollectionSlugToId,
|
|
2966
|
+
postSlugToId,
|
|
2967
|
+
});
|
|
2968
|
+
if (request.warning) console.warn(`Warning: ${request.warning}`);
|
|
2969
|
+
const created = await target.createNavItem(request.payload);
|
|
2970
|
+
if (request.customLabel && created?.id) {
|
|
2971
|
+
await target.updateNavItem(created.id, {
|
|
2972
|
+
label: request.customLabel,
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
} catch (err) {
|
|
2977
|
+
console.error(`Error importing navigation: ${err.message}`);
|
|
2978
|
+
process.exit(1);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
for (const customUrl of importedCustomUrls) {
|
|
2983
|
+
if (customUrl.kind === "archive") {
|
|
2984
|
+
console.warn(
|
|
2985
|
+
`Warning: skipped /${customUrl.path}, an archive URL (${customUrl.archiveQuery ?? "no filter"}). Archive URLs can no longer be created. Create a smart collection at /${customUrl.path} with the same conditions, here or on the source site before exporting.`,
|
|
2986
|
+
);
|
|
2987
|
+
continue;
|
|
2988
|
+
}
|
|
2989
|
+
try {
|
|
2990
|
+
await target.createCustomUrl({
|
|
2991
|
+
path: customUrl.path,
|
|
2992
|
+
targetType: "redirect",
|
|
2993
|
+
toPath: customUrl.to,
|
|
2994
|
+
redirectType: String(customUrl.status),
|
|
2995
|
+
});
|
|
2996
|
+
customUrlsCreated++;
|
|
2997
|
+
} catch (err) {
|
|
2998
|
+
console.warn(
|
|
2999
|
+
`Warning: couldn't recreate the redirect /${customUrl.path} → ${customUrl.to}: ${err.message}`,
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
} else if (dryRun && importedCustomUrls.length > 0) {
|
|
3004
|
+
console.log(
|
|
3005
|
+
`[dry-run] Would recreate ${importedCustomUrls.length} custom URLs`,
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
|
|
2486
3009
|
await target?.close();
|
|
2487
3010
|
|
|
2488
3011
|
// 5. Summary
|
|
@@ -2491,9 +3014,15 @@ export async function run(argv) {
|
|
|
2491
3014
|
console.log(` Posts created: ${postsCreated}`);
|
|
2492
3015
|
console.log(` Replies created: ${repliesCreated}`);
|
|
2493
3016
|
console.log(` Media uploaded: ${mediaUploaded}`);
|
|
3017
|
+
if (translationsLinked > 0) {
|
|
3018
|
+
console.log(` Translations linked: ${translationsLinked}`);
|
|
3019
|
+
}
|
|
2494
3020
|
if (aliasesCreated > 0) {
|
|
2495
3021
|
console.log(` Aliases created: ${aliasesCreated}`);
|
|
2496
3022
|
}
|
|
3023
|
+
if (customUrlsCreated > 0) {
|
|
3024
|
+
console.log(` Redirects created: ${customUrlsCreated}`);
|
|
3025
|
+
}
|
|
2497
3026
|
if (dryRun) {
|
|
2498
3027
|
console.log(" (dry-run mode — no changes were made)");
|
|
2499
3028
|
}
|