emdash 0.0.0-a → 0.0.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 +87 -1
- package/dist/adapters-BLMa4JGD.d.mts +106 -0
- package/dist/adapters-BLMa4JGD.d.mts.map +1 -0
- package/dist/apply-Bjfq_b4-.mjs +1293 -0
- package/dist/apply-Bjfq_b4-.mjs.map +1 -0
- package/dist/astro/index.d.mts +51 -0
- package/dist/astro/index.d.mts.map +1 -0
- package/dist/astro/index.mjs +1333 -0
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/middleware/auth.d.mts +31 -0
- package/dist/astro/middleware/auth.d.mts.map +1 -0
- package/dist/astro/middleware/auth.mjs +654 -0
- package/dist/astro/middleware/auth.mjs.map +1 -0
- package/dist/astro/middleware/redirect.d.mts +22 -0
- package/dist/astro/middleware/redirect.d.mts.map +1 -0
- package/dist/astro/middleware/redirect.mjs +63 -0
- package/dist/astro/middleware/redirect.mjs.map +1 -0
- package/dist/astro/middleware/request-context.d.mts +18 -0
- package/dist/astro/middleware/request-context.d.mts.map +1 -0
- package/dist/astro/middleware/request-context.mjs +1310 -0
- package/dist/astro/middleware/request-context.mjs.map +1 -0
- package/dist/astro/middleware/setup.d.mts +20 -0
- package/dist/astro/middleware/setup.d.mts.map +1 -0
- package/dist/astro/middleware/setup.mjs +47 -0
- package/dist/astro/middleware/setup.mjs.map +1 -0
- package/dist/astro/middleware.d.mts +13 -0
- package/dist/astro/middleware.d.mts.map +1 -0
- package/dist/astro/middleware.mjs +1613 -0
- package/dist/astro/middleware.mjs.map +1 -0
- package/dist/astro/types.d.mts +250 -0
- package/dist/astro/types.d.mts.map +1 -0
- package/dist/astro/types.mjs +1 -0
- package/dist/base64-MBPo9ozB.mjs +59 -0
- package/dist/base64-MBPo9ozB.mjs.map +1 -0
- package/dist/byline-CL847F26.mjs +213 -0
- package/dist/byline-CL847F26.mjs.map +1 -0
- package/dist/bylines-C2a-2TGt.mjs +136 -0
- package/dist/bylines-C2a-2TGt.mjs.map +1 -0
- package/dist/chunk-ClPoSABd.mjs +21 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +3909 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/client/cf-access.d.mts +60 -0
- package/dist/client/cf-access.d.mts.map +1 -0
- package/dist/client/cf-access.mjs +179 -0
- package/dist/client/cf-access.mjs.map +1 -0
- package/dist/client/index.d.mts +398 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +346 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/config-CKE8p9xM.mjs +55 -0
- package/dist/config-CKE8p9xM.mjs.map +1 -0
- package/dist/connection-B4zVnQIa.mjs +40 -0
- package/dist/connection-B4zVnQIa.mjs.map +1 -0
- package/dist/content-D6C2WsZC.mjs +824 -0
- package/dist/content-D6C2WsZC.mjs.map +1 -0
- package/dist/db/index.d.mts +4 -0
- package/dist/db/index.mjs +62 -0
- package/dist/db/index.mjs.map +1 -0
- package/dist/db/libsql.d.mts +11 -0
- package/dist/db/libsql.d.mts.map +1 -0
- package/dist/db/libsql.mjs +17 -0
- package/dist/db/libsql.mjs.map +1 -0
- package/dist/db/postgres.d.mts +11 -0
- package/dist/db/postgres.d.mts.map +1 -0
- package/dist/db/postgres.mjs +30 -0
- package/dist/db/postgres.mjs.map +1 -0
- package/dist/db/sqlite.d.mts +11 -0
- package/dist/db/sqlite.d.mts.map +1 -0
- package/dist/db/sqlite.mjs +16 -0
- package/dist/db/sqlite.mjs.map +1 -0
- package/dist/default-Cyi4aAxu.mjs +81 -0
- package/dist/default-Cyi4aAxu.mjs.map +1 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs +90 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs.map +1 -0
- package/dist/error-Cxz0tQeO.mjs +27 -0
- package/dist/error-Cxz0tQeO.mjs.map +1 -0
- package/dist/index-C1xF3OGh.d.mts +4527 -0
- package/dist/index-C1xF3OGh.d.mts.map +1 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +30 -0
- package/dist/load-yOOlckBj.mjs +28 -0
- package/dist/load-yOOlckBj.mjs.map +1 -0
- package/dist/loader-fz8Q_3EO.mjs +447 -0
- package/dist/loader-fz8Q_3EO.mjs.map +1 -0
- package/dist/manifest-schema-Dcl0R6nM.mjs +184 -0
- package/dist/manifest-schema-Dcl0R6nM.mjs.map +1 -0
- package/dist/media/index.d.mts +26 -0
- package/dist/media/index.d.mts.map +1 -0
- package/dist/media/index.mjs +55 -0
- package/dist/media/index.mjs.map +1 -0
- package/dist/media/local-runtime.d.mts +39 -0
- package/dist/media/local-runtime.d.mts.map +1 -0
- package/dist/media/local-runtime.mjs +133 -0
- package/dist/media/local-runtime.mjs.map +1 -0
- package/dist/media-DqHVh136.mjs +200 -0
- package/dist/media-DqHVh136.mjs.map +1 -0
- package/dist/mode-C2EzN1uE.mjs +23 -0
- package/dist/mode-C2EzN1uE.mjs.map +1 -0
- package/dist/page/index.d.mts +140 -0
- package/dist/page/index.d.mts.map +1 -0
- package/dist/page/index.mjs +416 -0
- package/dist/page/index.mjs.map +1 -0
- package/dist/placeholder-CmGAmqeO.d.mts +276 -0
- package/dist/placeholder-CmGAmqeO.d.mts.map +1 -0
- package/dist/placeholder-SmpOx-_v.mjs +243 -0
- package/dist/placeholder-SmpOx-_v.mjs.map +1 -0
- package/dist/plugin-utils.d.mts +58 -0
- package/dist/plugin-utils.d.mts.map +1 -0
- package/dist/plugin-utils.mjs +78 -0
- package/dist/plugin-utils.mjs.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +22 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.mjs +113 -0
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -0
- package/dist/query-CS_iSj34.mjs +460 -0
- package/dist/query-CS_iSj34.mjs.map +1 -0
- package/dist/redirect-DIfIni3r.mjs +329 -0
- package/dist/redirect-DIfIni3r.mjs.map +1 -0
- package/dist/registry-D_w5HW4G.mjs +863 -0
- package/dist/registry-D_w5HW4G.mjs.map +1 -0
- package/dist/request-context.d.mts +49 -0
- package/dist/request-context.d.mts.map +1 -0
- package/dist/request-context.mjs +43 -0
- package/dist/request-context.mjs.map +1 -0
- package/dist/runner-B-u2F2b6.mjs +1412 -0
- package/dist/runner-B-u2F2b6.mjs.map +1 -0
- package/dist/runner-EAtf0ZIe.d.mts +27 -0
- package/dist/runner-EAtf0ZIe.d.mts.map +1 -0
- package/dist/runtime.d.mts +26 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +42 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/search-DG603UrT.mjs +9211 -0
- package/dist/search-DG603UrT.mjs.map +1 -0
- package/dist/seed/index.d.mts +3 -0
- package/dist/seed/index.mjs +15 -0
- package/dist/seo/index.d.mts +70 -0
- package/dist/seo/index.d.mts.map +1 -0
- package/dist/seo/index.mjs +70 -0
- package/dist/seo/index.mjs.map +1 -0
- package/dist/storage/local.d.mts +39 -0
- package/dist/storage/local.d.mts.map +1 -0
- package/dist/storage/local.mjs +166 -0
- package/dist/storage/local.mjs.map +1 -0
- package/dist/storage/s3.d.mts +32 -0
- package/dist/storage/s3.d.mts.map +1 -0
- package/dist/storage/s3.mjs +175 -0
- package/dist/storage/s3.mjs.map +1 -0
- package/dist/tokens-DpgrkrXK.mjs +171 -0
- package/dist/tokens-DpgrkrXK.mjs.map +1 -0
- package/dist/transport-BFGblqwG.d.mts +42 -0
- package/dist/transport-BFGblqwG.d.mts.map +1 -0
- package/dist/transport-yxiQsi8I.mjs +418 -0
- package/dist/transport-yxiQsi8I.mjs.map +1 -0
- package/dist/types-BRuPJGdV.d.mts +102 -0
- package/dist/types-BRuPJGdV.d.mts.map +1 -0
- package/dist/types-C4-fAxN3.d.mts +182 -0
- package/dist/types-C4-fAxN3.d.mts.map +1 -0
- package/dist/types-CMMN0pNg.mjs +31 -0
- package/dist/types-CMMN0pNg.mjs.map +1 -0
- package/dist/types-CUBbjgmP.mjs +16 -0
- package/dist/types-CUBbjgmP.mjs.map +1 -0
- package/dist/types-DRjfYOEv.d.mts +426 -0
- package/dist/types-DRjfYOEv.d.mts.map +1 -0
- package/dist/types-DY5zk5HN.mjs +73 -0
- package/dist/types-DY5zk5HN.mjs.map +1 -0
- package/dist/types-DaNLHo_T.d.mts +184 -0
- package/dist/types-DaNLHo_T.d.mts.map +1 -0
- package/dist/types-DvhsUmSJ.d.mts +1111 -0
- package/dist/types-DvhsUmSJ.d.mts.map +1 -0
- package/dist/validate-CpBtVMsD.d.mts +378 -0
- package/dist/validate-CpBtVMsD.d.mts.map +1 -0
- package/dist/validate-CqRJb_xU.mjs +97 -0
- package/dist/validate-CqRJb_xU.mjs.map +1 -0
- package/dist/validate-O7PWmlnq.mjs +328 -0
- package/dist/validate-O7PWmlnq.mjs.map +1 -0
- package/locals.d.ts +46 -0
- package/package.json +233 -19
- package/src/api/authorize.ts +63 -0
- package/src/api/csrf.ts +48 -0
- package/src/api/error.ts +99 -0
- package/src/api/errors.ts +445 -0
- package/src/api/escape.ts +9 -0
- package/src/api/handlers/api-tokens.ts +240 -0
- package/src/api/handlers/comments.ts +314 -0
- package/src/api/handlers/content.ts +1315 -0
- package/src/api/handlers/dashboard.ts +205 -0
- package/src/api/handlers/device-flow.ts +687 -0
- package/src/api/handlers/index.ts +163 -0
- package/src/api/handlers/manifest.ts +158 -0
- package/src/api/handlers/marketplace.ts +930 -0
- package/src/api/handlers/media.ts +207 -0
- package/src/api/handlers/menus.ts +493 -0
- package/src/api/handlers/oauth-authorization.ts +429 -0
- package/src/api/handlers/oauth-clients.ts +353 -0
- package/src/api/handlers/oauth-user-lookup.ts +39 -0
- package/src/api/handlers/plugins.ts +254 -0
- package/src/api/handlers/redirects.ts +360 -0
- package/src/api/handlers/revision.ts +145 -0
- package/src/api/handlers/schema.ts +534 -0
- package/src/api/handlers/sections.ts +289 -0
- package/src/api/handlers/seo.ts +115 -0
- package/src/api/handlers/settings.ts +49 -0
- package/src/api/handlers/snapshot.ts +350 -0
- package/src/api/handlers/taxonomies.ts +523 -0
- package/src/api/index.ts +6 -0
- package/src/api/openapi/document.ts +2368 -0
- package/src/api/openapi/index.ts +1 -0
- package/src/api/parse.ts +139 -0
- package/src/api/redirect.ts +14 -0
- package/src/api/rev.ts +67 -0
- package/src/api/schemas/auth.ts +112 -0
- package/src/api/schemas/bylines.ts +85 -0
- package/src/api/schemas/comments.ts +117 -0
- package/src/api/schemas/common.ts +89 -0
- package/src/api/schemas/content.ts +191 -0
- package/src/api/schemas/import.ts +52 -0
- package/src/api/schemas/index.ts +17 -0
- package/src/api/schemas/media.ts +116 -0
- package/src/api/schemas/menus.ts +111 -0
- package/src/api/schemas/redirects.ts +155 -0
- package/src/api/schemas/schema.ts +203 -0
- package/src/api/schemas/search.ts +63 -0
- package/src/api/schemas/sections.ts +67 -0
- package/src/api/schemas/settings.ts +63 -0
- package/src/api/schemas/setup.ts +37 -0
- package/src/api/schemas/taxonomies.ts +113 -0
- package/src/api/schemas/users.ts +96 -0
- package/src/api/schemas/widgets.ts +80 -0
- package/src/api/site-url.ts +25 -0
- package/src/api/types.ts +82 -0
- package/src/astro/index.ts +27 -0
- package/src/astro/integration/index.ts +303 -0
- package/src/astro/integration/routes.ts +834 -0
- package/src/astro/integration/runtime.ts +338 -0
- package/src/astro/integration/virtual-modules.ts +469 -0
- package/src/astro/integration/vite-config.ts +328 -0
- package/src/astro/middleware/auth.ts +743 -0
- package/src/astro/middleware/redirect.ts +89 -0
- package/src/astro/middleware/request-context.ts +129 -0
- package/src/astro/middleware/setup.ts +89 -0
- package/src/astro/middleware.ts +398 -0
- package/src/astro/routes/PluginRegistry.tsx +15 -0
- package/src/astro/routes/admin.astro +81 -0
- package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
- package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
- package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
- package/src/astro/routes/api/admin/bylines/index.ts +72 -0
- package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
- package/src/astro/routes/api/admin/comments/[id].ts +64 -0
- package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
- package/src/astro/routes/api/admin/comments/counts.ts +30 -0
- package/src/astro/routes/api/admin/comments/index.ts +46 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
- package/src/astro/routes/api/admin/plugins/index.ts +32 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +61 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +62 -0
- package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +61 -0
- package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
- package/src/astro/routes/api/admin/users/index.ts +66 -0
- package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
- package/src/astro/routes/api/auth/invite/accept.ts +52 -0
- package/src/astro/routes/api/auth/invite/complete.ts +84 -0
- package/src/astro/routes/api/auth/invite/index.ts +99 -0
- package/src/astro/routes/api/auth/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
- package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
- package/src/astro/routes/api/auth/me.ts +60 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +219 -0
- package/src/astro/routes/api/auth/oauth/[provider].ts +119 -0
- package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
- package/src/astro/routes/api/auth/passkey/index.ts +54 -0
- package/src/astro/routes/api/auth/passkey/options.ts +82 -0
- package/src/astro/routes/api/auth/passkey/register/options.ts +86 -0
- package/src/astro/routes/api/auth/passkey/register/verify.ts +117 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +66 -0
- package/src/astro/routes/api/auth/signup/complete.ts +85 -0
- package/src/astro/routes/api/auth/signup/request.ts +77 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +312 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +105 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
- package/src/astro/routes/api/content/[collection]/index.ts +59 -0
- package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
- package/src/astro/routes/api/dashboard.ts +32 -0
- package/src/astro/routes/api/dev/emails.ts +36 -0
- package/src/astro/routes/api/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +510 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +283 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
- package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +347 -0
- package/src/astro/routes/api/manifest.ts +62 -0
- package/src/astro/routes/api/mcp.ts +124 -0
- package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
- package/src/astro/routes/api/media/[id].ts +145 -0
- package/src/astro/routes/api/media/file/[key].ts +79 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
- package/src/astro/routes/api/media/providers/index.ts +30 -0
- package/src/astro/routes/api/media/upload-url.ts +137 -0
- package/src/astro/routes/api/media.ts +190 -0
- package/src/astro/routes/api/menus/[name]/items.ts +87 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
- package/src/astro/routes/api/menus/[name].ts +65 -0
- package/src/astro/routes/api/menus/index.ts +47 -0
- package/src/astro/routes/api/oauth/authorize.ts +412 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +51 -0
- package/src/astro/routes/api/oauth/device/token.ts +69 -0
- package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
- package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
- package/src/astro/routes/api/oauth/token.ts +184 -0
- package/src/astro/routes/api/openapi.json.ts +32 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
- package/src/astro/routes/api/redirects/404s/index.ts +72 -0
- package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
- package/src/astro/routes/api/redirects/[id].ts +84 -0
- package/src/astro/routes/api/redirects/index.ts +52 -0
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
- package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
- package/src/astro/routes/api/schema/collections/index.ts +47 -0
- package/src/astro/routes/api/schema/index.ts +109 -0
- package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
- package/src/astro/routes/api/schema/orphans/index.ts +26 -0
- package/src/astro/routes/api/search/enable.ts +64 -0
- package/src/astro/routes/api/search/index.ts +55 -0
- package/src/astro/routes/api/search/rebuild.ts +72 -0
- package/src/astro/routes/api/search/stats.ts +35 -0
- package/src/astro/routes/api/search/suggest.ts +53 -0
- package/src/astro/routes/api/sections/[slug].ts +84 -0
- package/src/astro/routes/api/sections/index.ts +52 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +67 -0
- package/src/astro/routes/api/setup/admin-verify.ts +100 -0
- package/src/astro/routes/api/setup/admin.ts +94 -0
- package/src/astro/routes/api/setup/dev-bypass.ts +199 -0
- package/src/astro/routes/api/setup/dev-reset.ts +40 -0
- package/src/astro/routes/api/setup/index.ts +126 -0
- package/src/astro/routes/api/setup/status.ts +122 -0
- package/src/astro/routes/api/snapshot.ts +75 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
- package/src/astro/routes/api/taxonomies/index.ts +59 -0
- package/src/astro/routes/api/themes/preview.ts +77 -0
- package/src/astro/routes/api/typegen.ts +114 -0
- package/src/astro/routes/api/well-known/auth.ts +68 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +44 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +37 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +72 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
- package/src/astro/routes/api/widget-areas/[name].ts +87 -0
- package/src/astro/routes/api/widget-areas/index.ts +99 -0
- package/src/astro/routes/api/widget-components.ts +22 -0
- package/src/astro/routes/robots.txt.ts +77 -0
- package/src/astro/routes/sitemap.xml.ts +97 -0
- package/src/astro/storage/adapters.ts +74 -0
- package/src/astro/storage/index.ts +19 -0
- package/src/astro/storage/types.ts +60 -0
- package/src/astro/types.ts +346 -0
- package/src/auth/api-tokens.ts +25 -0
- package/src/auth/challenge-store.ts +80 -0
- package/src/auth/mode.ts +96 -0
- package/src/auth/oauth-state-store.ts +96 -0
- package/src/auth/passkey-config.ts +27 -0
- package/src/auth/rate-limit.ts +158 -0
- package/src/auth/scopes.ts +33 -0
- package/src/auth/types.ts +104 -0
- package/src/aws-sdk.d.ts +100 -0
- package/src/bylines/index.ts +237 -0
- package/src/cleanup.ts +153 -0
- package/src/cli/client-factory.ts +100 -0
- package/src/cli/commands/auth.ts +46 -0
- package/src/cli/commands/bundle-utils.ts +247 -0
- package/src/cli/commands/bundle.ts +609 -0
- package/src/cli/commands/content.ts +442 -0
- package/src/cli/commands/dev.ts +191 -0
- package/src/cli/commands/doctor.ts +211 -0
- package/src/cli/commands/export-seed.ts +630 -0
- package/src/cli/commands/import/wordpress.ts +1056 -0
- package/src/cli/commands/init.ts +192 -0
- package/src/cli/commands/login.ts +547 -0
- package/src/cli/commands/media.ts +165 -0
- package/src/cli/commands/menu.ts +67 -0
- package/src/cli/commands/plugin-init.ts +291 -0
- package/src/cli/commands/plugin-validate.ts +31 -0
- package/src/cli/commands/plugin.ts +33 -0
- package/src/cli/commands/publish.ts +699 -0
- package/src/cli/commands/schema.ts +233 -0
- package/src/cli/commands/search-cmd.ts +54 -0
- package/src/cli/commands/seed.ts +288 -0
- package/src/cli/commands/taxonomy.ts +128 -0
- package/src/cli/commands/types.ts +68 -0
- package/src/cli/credentials.ts +236 -0
- package/src/cli/index.ts +70 -0
- package/src/cli/output.ts +75 -0
- package/src/cli/wxr/parser.ts +969 -0
- package/src/client/cf-access.ts +193 -0
- package/src/client/index.ts +854 -0
- package/src/client/portable-text.ts +413 -0
- package/src/client/transport.ts +200 -0
- package/src/comments/moderator.ts +46 -0
- package/src/comments/notifications.ts +144 -0
- package/src/comments/query.ts +105 -0
- package/src/comments/service.ts +213 -0
- package/src/components/Break.astro +45 -0
- package/src/components/Button.astro +71 -0
- package/src/components/Buttons.astro +49 -0
- package/src/components/Code.astro +59 -0
- package/src/components/Columns.astro +59 -0
- package/src/components/CommentForm.astro +315 -0
- package/src/components/Comments.astro +232 -0
- package/src/components/Cover.astro +128 -0
- package/src/components/EmDashBodyEnd.astro +32 -0
- package/src/components/EmDashBodyStart.astro +32 -0
- package/src/components/EmDashHead.astro +53 -0
- package/src/components/EmDashImage.astro +178 -0
- package/src/components/EmDashMedia.astro +167 -0
- package/src/components/Embed.astro +128 -0
- package/src/components/File.astro +122 -0
- package/src/components/Gallery.astro +93 -0
- package/src/components/HtmlBlock.astro +33 -0
- package/src/components/Image.astro +178 -0
- package/src/components/InlineEditor.astro +27 -0
- package/src/components/InlinePortableTextEditor.tsx +1905 -0
- package/src/components/LiveSearch.astro +614 -0
- package/src/components/PortableText.astro +51 -0
- package/src/components/Pullquote.astro +51 -0
- package/src/components/Table.astro +108 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +116 -0
- package/src/components/marks/Link.astro +31 -0
- package/src/components/marks/StrikeThrough.astro +7 -0
- package/src/components/marks/Subscript.astro +7 -0
- package/src/components/marks/Superscript.astro +7 -0
- package/src/components/marks/Underline.astro +7 -0
- package/src/components/widgets/Archives.astro +65 -0
- package/src/components/widgets/Categories.astro +35 -0
- package/src/components/widgets/RecentPosts.astro +51 -0
- package/src/components/widgets/Search.astro +18 -0
- package/src/components/widgets/Tags.astro +38 -0
- package/src/content/converters/index.ts +9 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +385 -0
- package/src/content/converters/prosemirror-to-portable-text.ts +413 -0
- package/src/content/converters/types.ts +120 -0
- package/src/content/index.ts +5 -0
- package/src/database/connection.ts +67 -0
- package/src/database/dialect-helpers.ts +138 -0
- package/src/database/index.ts +5 -0
- package/src/database/migrations/001_initial.ts +136 -0
- package/src/database/migrations/002_media_status.ts +26 -0
- package/src/database/migrations/003_schema_registry.ts +79 -0
- package/src/database/migrations/004_plugins.ts +62 -0
- package/src/database/migrations/005_menus.ts +67 -0
- package/src/database/migrations/006_taxonomy_defs.ts +51 -0
- package/src/database/migrations/007_widgets.ts +42 -0
- package/src/database/migrations/008_auth.ts +194 -0
- package/src/database/migrations/009_user_disabled.ts +27 -0
- package/src/database/migrations/011_sections.ts +65 -0
- package/src/database/migrations/012_search.ts +25 -0
- package/src/database/migrations/013_scheduled_publishing.ts +51 -0
- package/src/database/migrations/014_draft_revisions.ts +72 -0
- package/src/database/migrations/015_indexes.ts +82 -0
- package/src/database/migrations/016_api_tokens.ts +89 -0
- package/src/database/migrations/017_authorization_codes.ts +45 -0
- package/src/database/migrations/018_seo.ts +56 -0
- package/src/database/migrations/019_i18n.ts +618 -0
- package/src/database/migrations/020_collection_url_pattern.ts +23 -0
- package/src/database/migrations/021_remove_section_categories.ts +43 -0
- package/src/database/migrations/022_marketplace_plugin_state.ts +46 -0
- package/src/database/migrations/023_plugin_metadata.ts +33 -0
- package/src/database/migrations/024_media_placeholders.ts +32 -0
- package/src/database/migrations/025_oauth_clients.ts +28 -0
- package/src/database/migrations/026_cron_tasks.ts +49 -0
- package/src/database/migrations/027_comments.ts +87 -0
- package/src/database/migrations/028_drop_author_url.ts +9 -0
- package/src/database/migrations/029_redirects.ts +67 -0
- package/src/database/migrations/030_widen_scheduled_index.ts +48 -0
- package/src/database/migrations/031_bylines.ts +90 -0
- package/src/database/migrations/032_rate_limits.ts +42 -0
- package/src/database/migrations/runner.ts +170 -0
- package/src/database/repositories/audit.ts +294 -0
- package/src/database/repositories/byline.ts +387 -0
- package/src/database/repositories/comment.ts +458 -0
- package/src/database/repositories/content.ts +1144 -0
- package/src/database/repositories/index.ts +30 -0
- package/src/database/repositories/media.ts +347 -0
- package/src/database/repositories/options.ts +150 -0
- package/src/database/repositories/plugin-storage.ts +373 -0
- package/src/database/repositories/redirect.ts +480 -0
- package/src/database/repositories/revision.ts +200 -0
- package/src/database/repositories/seo.ts +176 -0
- package/src/database/repositories/taxonomy.ts +294 -0
- package/src/database/repositories/types.ts +132 -0
- package/src/database/repositories/user.ts +258 -0
- package/src/database/transaction.ts +54 -0
- package/src/database/types.ts +501 -0
- package/src/database/validate.ts +138 -0
- package/src/db/adapters.ts +125 -0
- package/src/db/index.ts +37 -0
- package/src/db/libsql.ts +23 -0
- package/src/db/postgres.ts +30 -0
- package/src/db/sqlite.ts +27 -0
- package/src/emdash-runtime.ts +2096 -0
- package/src/fields/boolean.ts +34 -0
- package/src/fields/datetime.ts +44 -0
- package/src/fields/file.ts +41 -0
- package/src/fields/image.ts +34 -0
- package/src/fields/index.ts +42 -0
- package/src/fields/integer.ts +50 -0
- package/src/fields/json.ts +37 -0
- package/src/fields/multiselect.ts +48 -0
- package/src/fields/number.ts +52 -0
- package/src/fields/portable-text.ts +33 -0
- package/src/fields/reference.ts +29 -0
- package/src/fields/richtext.ts +31 -0
- package/src/fields/select.ts +46 -0
- package/src/fields/slug.ts +38 -0
- package/src/fields/text.ts +55 -0
- package/src/fields/textarea.ts +52 -0
- package/src/fields/types.ts +64 -0
- package/src/i18n/config.ts +68 -0
- package/src/import/index.ts +90 -0
- package/src/import/menus.ts +436 -0
- package/src/import/registry.ts +111 -0
- package/src/import/sections.ts +103 -0
- package/src/import/settings.ts +281 -0
- package/src/import/sources/wordpress-plugin.ts +641 -0
- package/src/import/sources/wordpress-rest.ts +191 -0
- package/src/import/sources/wxr.ts +330 -0
- package/src/import/ssrf.ts +260 -0
- package/src/import/types.ts +418 -0
- package/src/import/utils.ts +412 -0
- package/src/index.ts +481 -0
- package/src/loader.ts +770 -0
- package/src/mcp/server.ts +1463 -0
- package/src/media/index.ts +32 -0
- package/src/media/local-runtime.ts +213 -0
- package/src/media/local.ts +46 -0
- package/src/media/normalize.ts +190 -0
- package/src/media/placeholder.ts +150 -0
- package/src/media/provider-loader.ts +78 -0
- package/src/media/types.ts +279 -0
- package/src/menus/index.ts +324 -0
- package/src/menus/types.ts +112 -0
- package/src/page/context.ts +93 -0
- package/src/page/fragments.ts +89 -0
- package/src/page/index.ts +58 -0
- package/src/page/jsonld.ts +94 -0
- package/src/page/metadata.ts +185 -0
- package/src/page/seo-contributions.ts +136 -0
- package/src/plugin-utils.ts +80 -0
- package/src/plugins/adapt-sandbox-entry.ts +207 -0
- package/src/plugins/context.ts +833 -0
- package/src/plugins/cron.ts +361 -0
- package/src/plugins/define-plugin.ts +259 -0
- package/src/plugins/email-console.ts +73 -0
- package/src/plugins/email.ts +209 -0
- package/src/plugins/hooks.ts +1273 -0
- package/src/plugins/index.ts +193 -0
- package/src/plugins/manager.ts +595 -0
- package/src/plugins/manifest-schema.ts +230 -0
- package/src/plugins/marketplace.ts +460 -0
- package/src/plugins/request-meta.ts +139 -0
- package/src/plugins/routes.ts +302 -0
- package/src/plugins/sandbox/index.ts +18 -0
- package/src/plugins/sandbox/noop.ts +76 -0
- package/src/plugins/sandbox/types.ts +173 -0
- package/src/plugins/scheduler/node.ts +122 -0
- package/src/plugins/scheduler/piggyback.ts +71 -0
- package/src/plugins/scheduler/types.ts +27 -0
- package/src/plugins/state.ts +208 -0
- package/src/plugins/storage-indexes.ts +326 -0
- package/src/plugins/storage-query.ts +240 -0
- package/src/plugins/types.ts +1284 -0
- package/src/preview/helpers.ts +27 -0
- package/src/preview/index.ts +40 -0
- package/src/preview/tokens.ts +279 -0
- package/src/preview/urls.ts +118 -0
- package/src/query.ts +674 -0
- package/src/redirects/patterns.ts +224 -0
- package/src/request-context.ts +67 -0
- package/src/runtime.ts +21 -0
- package/src/schema/index.ts +29 -0
- package/src/schema/query.ts +44 -0
- package/src/schema/registry.ts +965 -0
- package/src/schema/types.ts +276 -0
- package/src/schema/zod-generator.ts +413 -0
- package/src/search/fts-manager.ts +452 -0
- package/src/search/index.ts +26 -0
- package/src/search/query.ts +396 -0
- package/src/search/text-extraction.ts +162 -0
- package/src/search/types.ts +114 -0
- package/src/sections/index.ts +226 -0
- package/src/sections/types.ts +86 -0
- package/src/seed/apply.ts +1141 -0
- package/src/seed/default.ts +86 -0
- package/src/seed/index.ts +28 -0
- package/src/seed/load.ts +35 -0
- package/src/seed/types.ts +341 -0
- package/src/seed/validate.ts +642 -0
- package/src/seo/index.ts +179 -0
- package/src/settings/index.ts +203 -0
- package/src/settings/types.ts +58 -0
- package/src/storage/index.ts +28 -0
- package/src/storage/local.ts +253 -0
- package/src/storage/s3.ts +271 -0
- package/src/storage/types.ts +204 -0
- package/src/taxonomies/index.ts +309 -0
- package/src/taxonomies/types.ts +61 -0
- package/src/ui.ts +75 -0
- package/src/utils/base64.ts +73 -0
- package/src/utils/hash.ts +36 -0
- package/src/utils/sanitize.ts +20 -0
- package/src/utils/slugify.ts +29 -0
- package/src/utils/url.ts +48 -0
- package/src/virtual-modules.d.ts +111 -0
- package/src/visual-editing/editable.ts +108 -0
- package/src/visual-editing/toolbar.ts +1229 -0
- package/src/widgets/components.ts +105 -0
- package/src/widgets/index.ts +131 -0
- package/src/widgets/types.ts +81 -0
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress WXR import implementation
|
|
3
|
+
*
|
|
4
|
+
* Two-phase import process:
|
|
5
|
+
* 1. Prepare: Analyze WXR, generate config and suggested live.config.ts
|
|
6
|
+
* 2. Execute: Import content using the generated/edited config
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createReadStream } from "node:fs";
|
|
10
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
import { gutenbergToPortableText } from "@emdash-cms/gutenberg-to-portable-text";
|
|
14
|
+
import pc from "picocolors";
|
|
15
|
+
|
|
16
|
+
import { slugify } from "#utils/slugify.js";
|
|
17
|
+
|
|
18
|
+
import { validateExternalUrl, ssrfSafeFetch } from "../../../import/ssrf.js";
|
|
19
|
+
import { parseWxr, type WxrData, type WxrPost, type WxrAttachment } from "../../wxr/parser.js";
|
|
20
|
+
|
|
21
|
+
// Regex patterns for WordPress import
|
|
22
|
+
const NUMBER_PATTERN = /^-?\d+(\.\d+)?$/;
|
|
23
|
+
const DOT_PATTERN = /\./g;
|
|
24
|
+
const NON_ALPHANUMERIC_UNDERSCORE_PATTERN = /[^a-zA-Z0-9_]/g;
|
|
25
|
+
const TRAILING_SLASH_PATTERN = /\/$/;
|
|
26
|
+
const PHP_STRING_PATTERN = /s:\d+:"(.*)";/;
|
|
27
|
+
const PHP_ARRAY_PATTERN = /s:(\d+):"([^"]+)";(?:s:(\d+):"([^"]+)"|i:(\d+)|b:([01]))/g;
|
|
28
|
+
|
|
29
|
+
/** Type guard for Record<string, unknown> */
|
|
30
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Types
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
export interface MigrationConfig {
|
|
39
|
+
/** WordPress site info */
|
|
40
|
+
site: {
|
|
41
|
+
title: string;
|
|
42
|
+
url: string;
|
|
43
|
+
};
|
|
44
|
+
/** Map WordPress post types to EmDash collections */
|
|
45
|
+
collections: Record<string, CollectionMapping>;
|
|
46
|
+
/** Map WordPress meta keys to EmDash field names */
|
|
47
|
+
fields: Record<string, FieldMapping>;
|
|
48
|
+
/** Post types to skip */
|
|
49
|
+
skipPostTypes: string[];
|
|
50
|
+
/** Meta keys to skip (internal WP fields) */
|
|
51
|
+
skipMetaKeys: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface CollectionMapping {
|
|
55
|
+
/** EmDash collection name */
|
|
56
|
+
collection: string;
|
|
57
|
+
/** Whether to import this type */
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
/** Number of items found */
|
|
60
|
+
count: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface FieldMapping {
|
|
64
|
+
/** EmDash field name (supports dot notation for nested) */
|
|
65
|
+
field: string;
|
|
66
|
+
/** Field type hint */
|
|
67
|
+
type: "string" | "number" | "boolean" | "date" | "reference" | "json";
|
|
68
|
+
/** Whether to import this field */
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
/** Number of posts with this field */
|
|
71
|
+
count: number;
|
|
72
|
+
/** Sample values for reference */
|
|
73
|
+
samples: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PrepareOptions {
|
|
77
|
+
outputDir: string;
|
|
78
|
+
configPath: string;
|
|
79
|
+
verbose: boolean;
|
|
80
|
+
dryRun: boolean;
|
|
81
|
+
json: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ExecuteOptions {
|
|
85
|
+
outputDir: string;
|
|
86
|
+
mediaDir?: string;
|
|
87
|
+
configPath: string;
|
|
88
|
+
skipMedia: boolean;
|
|
89
|
+
verbose: boolean;
|
|
90
|
+
dryRun: boolean;
|
|
91
|
+
json: boolean;
|
|
92
|
+
resume: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Progress tracking for resumable imports */
|
|
96
|
+
export interface ImportProgress {
|
|
97
|
+
/** ISO timestamp when import started */
|
|
98
|
+
startedAt: string;
|
|
99
|
+
/** ISO timestamp of last update */
|
|
100
|
+
updatedAt: string;
|
|
101
|
+
/** Source WXR file path */
|
|
102
|
+
sourceFile: string;
|
|
103
|
+
/** Config file used */
|
|
104
|
+
configFile: string;
|
|
105
|
+
/** Posts successfully imported (by WP ID) */
|
|
106
|
+
importedPosts: number[];
|
|
107
|
+
/** Attachments successfully downloaded (by WP ID) */
|
|
108
|
+
downloadedMedia: number[];
|
|
109
|
+
/** Items that failed with error messages */
|
|
110
|
+
errors: Array<{ id: number; type: string; error: string }>;
|
|
111
|
+
/** Summary stats */
|
|
112
|
+
stats: {
|
|
113
|
+
totalPosts: number;
|
|
114
|
+
totalMedia: number;
|
|
115
|
+
importedPosts: number;
|
|
116
|
+
downloadedMedia: number;
|
|
117
|
+
skippedPosts: number;
|
|
118
|
+
errorCount: number;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Structured result for agent-friendly output */
|
|
123
|
+
export interface ImportResult {
|
|
124
|
+
success: boolean;
|
|
125
|
+
phase: "prepare" | "execute";
|
|
126
|
+
dryRun: boolean;
|
|
127
|
+
/** Summary of what was/would be done */
|
|
128
|
+
summary: {
|
|
129
|
+
postsAnalyzed?: number;
|
|
130
|
+
postsImported?: number;
|
|
131
|
+
postsSkipped?: number;
|
|
132
|
+
mediaDownloaded?: number;
|
|
133
|
+
mediaSkipped?: number;
|
|
134
|
+
errors: number;
|
|
135
|
+
};
|
|
136
|
+
/** Files created/would be created */
|
|
137
|
+
files: Array<{
|
|
138
|
+
path: string;
|
|
139
|
+
action: "created" | "skipped" | "would_create";
|
|
140
|
+
}>;
|
|
141
|
+
/** Errors encountered */
|
|
142
|
+
errors: Array<{ id?: number; message: string }>;
|
|
143
|
+
/** Next steps for the user/agent */
|
|
144
|
+
nextSteps: string[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Phase 1: Prepare
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
export async function prepareWordPressImport(
|
|
152
|
+
filePath: string,
|
|
153
|
+
options: PrepareOptions,
|
|
154
|
+
): Promise<ImportResult> {
|
|
155
|
+
const result: ImportResult = {
|
|
156
|
+
success: true,
|
|
157
|
+
phase: "prepare",
|
|
158
|
+
dryRun: options.dryRun,
|
|
159
|
+
summary: { errors: 0 },
|
|
160
|
+
files: [],
|
|
161
|
+
errors: [],
|
|
162
|
+
nextSteps: [],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const log = (msg: string) => !options.json && console.log(msg);
|
|
166
|
+
|
|
167
|
+
if (options.dryRun) {
|
|
168
|
+
log(pc.yellow("[DRY RUN] ") + pc.cyan("Analyzing WordPress export...\n"));
|
|
169
|
+
} else {
|
|
170
|
+
log(pc.cyan("Analyzing WordPress export...\n"));
|
|
171
|
+
}
|
|
172
|
+
log(pc.dim(`File: ${filePath}`));
|
|
173
|
+
|
|
174
|
+
// Parse WXR
|
|
175
|
+
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
176
|
+
const wxr = await parseWxr(stream);
|
|
177
|
+
|
|
178
|
+
// Analyze content
|
|
179
|
+
const analysis = analyzeWxrContent(wxr, options.verbose);
|
|
180
|
+
|
|
181
|
+
// Generate migration config
|
|
182
|
+
const config = generateMigrationConfig(wxr, analysis);
|
|
183
|
+
|
|
184
|
+
result.summary.postsAnalyzed = wxr.posts.length;
|
|
185
|
+
|
|
186
|
+
// Write config file (or report what would be written)
|
|
187
|
+
if (options.dryRun) {
|
|
188
|
+
log(pc.yellow(`\n[DRY RUN] Would write: ${options.configPath}`));
|
|
189
|
+
result.files.push({ path: options.configPath, action: "would_create" });
|
|
190
|
+
} else {
|
|
191
|
+
await mkdir(dirname(options.configPath), { recursive: true });
|
|
192
|
+
await writeFile(options.configPath, JSON.stringify(config, null, 2));
|
|
193
|
+
log(pc.green(`\nWrote migration config: ${options.configPath}`));
|
|
194
|
+
result.files.push({ path: options.configPath, action: "created" });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Generate suggested live.config.ts
|
|
198
|
+
const liveConfigPath = join(options.outputDir, "suggested-live.config.ts");
|
|
199
|
+
const liveConfigContent = generateLiveConfig(config, analysis);
|
|
200
|
+
|
|
201
|
+
if (options.dryRun) {
|
|
202
|
+
log(pc.yellow(`[DRY RUN] Would write: ${liveConfigPath}`));
|
|
203
|
+
result.files.push({ path: liveConfigPath, action: "would_create" });
|
|
204
|
+
} else {
|
|
205
|
+
await mkdir(dirname(liveConfigPath), { recursive: true });
|
|
206
|
+
await writeFile(liveConfigPath, liveConfigContent);
|
|
207
|
+
log(pc.green(`Wrote suggested config: ${liveConfigPath}`));
|
|
208
|
+
result.files.push({ path: liveConfigPath, action: "created" });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Summary
|
|
212
|
+
log(pc.cyan("\n=== Analysis Summary ===\n"));
|
|
213
|
+
|
|
214
|
+
log(pc.bold("Post Types:"));
|
|
215
|
+
for (const [type, mapping] of Object.entries(config.collections)) {
|
|
216
|
+
const status = mapping.enabled ? pc.green("enabled") : pc.yellow("disabled");
|
|
217
|
+
log(` ${type} → ${mapping.collection} (${mapping.count} items) [${status}]`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
log(pc.bold("\nCustom Fields:"));
|
|
221
|
+
const enabledFields = Object.entries(config.fields).filter(([_, m]) => m.enabled);
|
|
222
|
+
const disabledFields = Object.entries(config.fields).filter(([_, m]) => !m.enabled);
|
|
223
|
+
|
|
224
|
+
for (const [key, mapping] of enabledFields.slice(0, 10)) {
|
|
225
|
+
log(` ${key} → ${mapping.field} (${mapping.type}, ${mapping.count} posts)`);
|
|
226
|
+
}
|
|
227
|
+
if (enabledFields.length > 10) {
|
|
228
|
+
log(pc.dim(` ... and ${enabledFields.length - 10} more`));
|
|
229
|
+
}
|
|
230
|
+
if (disabledFields.length > 0) {
|
|
231
|
+
log(pc.dim(` (${disabledFields.length} internal fields hidden)`));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Next steps
|
|
235
|
+
result.nextSteps = [
|
|
236
|
+
`Review and edit: ${options.configPath}`,
|
|
237
|
+
`Review suggested config: ${liveConfigPath}`,
|
|
238
|
+
"Copy relevant parts to your src/live.config.ts",
|
|
239
|
+
`Run: emdash import wordpress ${filePath} --execute`,
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
log(pc.cyan("\n=== Next Steps ===\n"));
|
|
243
|
+
for (const step of result.nextSteps) {
|
|
244
|
+
log(` ${step}`);
|
|
245
|
+
}
|
|
246
|
+
log("");
|
|
247
|
+
|
|
248
|
+
// JSON output for agents
|
|
249
|
+
if (options.json) {
|
|
250
|
+
console.log(JSON.stringify(result, null, 2));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
interface ContentAnalysis {
|
|
257
|
+
postTypes: Map<string, number>;
|
|
258
|
+
metaKeys: Map<string, MetaKeyInfo>;
|
|
259
|
+
categories: number;
|
|
260
|
+
tags: number;
|
|
261
|
+
attachments: number;
|
|
262
|
+
authors: string[];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
interface MetaKeyInfo {
|
|
266
|
+
count: number;
|
|
267
|
+
samples: string[];
|
|
268
|
+
isInternal: boolean;
|
|
269
|
+
inferredType: "string" | "number" | "boolean" | "date" | "reference" | "json";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function analyzeWxrContent(wxr: WxrData, _verbose: boolean): ContentAnalysis {
|
|
273
|
+
const postTypes = new Map<string, number>();
|
|
274
|
+
const metaKeys = new Map<string, MetaKeyInfo>();
|
|
275
|
+
|
|
276
|
+
// Analyze posts
|
|
277
|
+
for (const post of wxr.posts) {
|
|
278
|
+
// Count post types
|
|
279
|
+
const type = post.postType || "post";
|
|
280
|
+
postTypes.set(type, (postTypes.get(type) || 0) + 1);
|
|
281
|
+
|
|
282
|
+
// Analyze meta keys
|
|
283
|
+
for (const [key, value] of post.meta) {
|
|
284
|
+
const existing = metaKeys.get(key);
|
|
285
|
+
if (existing) {
|
|
286
|
+
existing.count++;
|
|
287
|
+
if (existing.samples.length < 3 && value && !existing.samples.includes(value)) {
|
|
288
|
+
existing.samples.push(value.slice(0, 100));
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
metaKeys.set(key, {
|
|
292
|
+
count: 1,
|
|
293
|
+
samples: value ? [value.slice(0, 100)] : [],
|
|
294
|
+
isInternal: isInternalMetaKey(key),
|
|
295
|
+
inferredType: inferMetaType(key, value),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Analyze attachments
|
|
302
|
+
for (const attachment of wxr.attachments) {
|
|
303
|
+
for (const [key, value] of attachment.meta) {
|
|
304
|
+
const existing = metaKeys.get(key);
|
|
305
|
+
if (existing) {
|
|
306
|
+
existing.count++;
|
|
307
|
+
} else {
|
|
308
|
+
metaKeys.set(key, {
|
|
309
|
+
count: 1,
|
|
310
|
+
samples: value ? [value.slice(0, 100)] : [],
|
|
311
|
+
isInternal: isInternalMetaKey(key),
|
|
312
|
+
inferredType: inferMetaType(key, value),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
postTypes,
|
|
320
|
+
metaKeys,
|
|
321
|
+
categories: wxr.categories.length,
|
|
322
|
+
tags: wxr.tags.length,
|
|
323
|
+
attachments: wxr.attachments.length,
|
|
324
|
+
authors: wxr.authors.map((a) => a.displayName || a.login || "Unknown"),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function isInternalMetaKey(key: string): boolean {
|
|
329
|
+
// WordPress internal keys
|
|
330
|
+
if (key.startsWith("_edit_")) return true;
|
|
331
|
+
if (key.startsWith("_wp_")) return true;
|
|
332
|
+
if (key === "_edit_last" || key === "_edit_lock") return true;
|
|
333
|
+
if (key === "_pingme" || key === "_encloseme") return true;
|
|
334
|
+
|
|
335
|
+
// But keep these useful ones
|
|
336
|
+
if (key === "_thumbnail_id") return false;
|
|
337
|
+
if (key.startsWith("_yoast_")) return false;
|
|
338
|
+
if (key.startsWith("_rank_math_")) return false;
|
|
339
|
+
if (key.startsWith("_aioseop_")) return false;
|
|
340
|
+
|
|
341
|
+
// Other underscore prefixes are usually internal
|
|
342
|
+
if (key.startsWith("_") && !key.startsWith("_yoast") && !key.startsWith("_thumbnail")) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function inferMetaType(key: string, value: string | undefined): MetaKeyInfo["inferredType"] {
|
|
350
|
+
// Known patterns
|
|
351
|
+
if (key.endsWith("_id") || key === "_thumbnail_id") return "reference";
|
|
352
|
+
if (key.endsWith("_date") || key.endsWith("_time")) return "date";
|
|
353
|
+
if (key.endsWith("_count") || key.endsWith("_number") || key === "price") return "number";
|
|
354
|
+
|
|
355
|
+
// Check value
|
|
356
|
+
if (!value) return "string";
|
|
357
|
+
|
|
358
|
+
// Serialized PHP
|
|
359
|
+
if (value.startsWith("a:") || value.startsWith("O:") || value.startsWith("s:")) {
|
|
360
|
+
return "json";
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// JSON
|
|
364
|
+
if (
|
|
365
|
+
(value.startsWith("{") && value.endsWith("}")) ||
|
|
366
|
+
(value.startsWith("[") && value.endsWith("]"))
|
|
367
|
+
) {
|
|
368
|
+
return "json";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Number
|
|
372
|
+
if (NUMBER_PATTERN.test(value)) return "number";
|
|
373
|
+
|
|
374
|
+
// Boolean
|
|
375
|
+
if (value === "0" || value === "1" || value === "true" || value === "false") {
|
|
376
|
+
return "boolean";
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return "string";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function generateMigrationConfig(wxr: WxrData, analysis: ContentAnalysis): MigrationConfig {
|
|
383
|
+
const collections: Record<string, CollectionMapping> = {};
|
|
384
|
+
const fields: Record<string, FieldMapping> = {};
|
|
385
|
+
|
|
386
|
+
// Map post types to collections
|
|
387
|
+
for (const [type, count] of analysis.postTypes) {
|
|
388
|
+
// Skip internal types (see INTERNAL_POST_TYPES in utils.ts)
|
|
389
|
+
const skip = [
|
|
390
|
+
"revision",
|
|
391
|
+
"nav_menu_item",
|
|
392
|
+
"custom_css",
|
|
393
|
+
"customize_changeset",
|
|
394
|
+
"oembed_cache",
|
|
395
|
+
"wp_global_styles",
|
|
396
|
+
"wp_navigation",
|
|
397
|
+
"wp_template",
|
|
398
|
+
"wp_template_part",
|
|
399
|
+
"attachment", // Handled separately as media
|
|
400
|
+
"wp_block", // Handled separately as sections (reusable blocks)
|
|
401
|
+
].includes(type);
|
|
402
|
+
|
|
403
|
+
collections[type] = {
|
|
404
|
+
collection: mapPostTypeToCollection(type),
|
|
405
|
+
enabled: !skip,
|
|
406
|
+
count,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Map meta keys to fields
|
|
411
|
+
for (const [key, info] of analysis.metaKeys) {
|
|
412
|
+
fields[key] = {
|
|
413
|
+
field: mapMetaKeyToField(key),
|
|
414
|
+
type: info.inferredType,
|
|
415
|
+
enabled: !info.isInternal,
|
|
416
|
+
count: info.count,
|
|
417
|
+
samples: info.samples,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
site: {
|
|
423
|
+
title: wxr.site.title || "WordPress Site",
|
|
424
|
+
url: wxr.site.link || "",
|
|
425
|
+
},
|
|
426
|
+
collections,
|
|
427
|
+
fields,
|
|
428
|
+
skipPostTypes: [
|
|
429
|
+
"revision",
|
|
430
|
+
"nav_menu_item",
|
|
431
|
+
"custom_css",
|
|
432
|
+
"customize_changeset",
|
|
433
|
+
"oembed_cache",
|
|
434
|
+
"wp_global_styles",
|
|
435
|
+
"wp_navigation",
|
|
436
|
+
"wp_template",
|
|
437
|
+
"wp_template_part",
|
|
438
|
+
"attachment",
|
|
439
|
+
"wp_block",
|
|
440
|
+
],
|
|
441
|
+
skipMetaKeys: ["_edit_last", "_edit_lock", "_pingme", "_encloseme"],
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function mapPostTypeToCollection(postType: string): string {
|
|
446
|
+
const mapping: Record<string, string> = {
|
|
447
|
+
post: "posts",
|
|
448
|
+
page: "pages",
|
|
449
|
+
attachment: "media",
|
|
450
|
+
product: "products",
|
|
451
|
+
portfolio: "portfolio",
|
|
452
|
+
testimonial: "testimonials",
|
|
453
|
+
team: "team",
|
|
454
|
+
event: "events",
|
|
455
|
+
faq: "faqs",
|
|
456
|
+
};
|
|
457
|
+
return mapping[postType] || postType;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function mapMetaKeyToField(key: string): string {
|
|
461
|
+
// SEO plugins
|
|
462
|
+
if (key === "_yoast_wpseo_title") return "seo.title";
|
|
463
|
+
if (key === "_yoast_wpseo_metadesc") return "seo.description";
|
|
464
|
+
if (key === "_yoast_wpseo_focuskw") return "seo.keywords";
|
|
465
|
+
if (key === "_rank_math_title") return "seo.title";
|
|
466
|
+
if (key === "_rank_math_description") return "seo.description";
|
|
467
|
+
if (key === "_aioseop_title") return "seo.title";
|
|
468
|
+
if (key === "_aioseop_description") return "seo.description";
|
|
469
|
+
|
|
470
|
+
// Featured image
|
|
471
|
+
if (key === "_thumbnail_id") return "featuredImage";
|
|
472
|
+
|
|
473
|
+
// Remove leading underscore for others
|
|
474
|
+
if (key.startsWith("_")) {
|
|
475
|
+
return key.slice(1);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return key;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function generateLiveConfig(config: MigrationConfig, _analysis: ContentAnalysis): string {
|
|
482
|
+
const lines: string[] = [
|
|
483
|
+
"/**",
|
|
484
|
+
" * Suggested EmDash collections",
|
|
485
|
+
` * Generated from: ${config.site.title}`,
|
|
486
|
+
" *",
|
|
487
|
+
" * Create these collections in the EmDash admin UI:",
|
|
488
|
+
" * 1. Go to /_emdash/admin/content-types",
|
|
489
|
+
" * 2. Click 'New Content Type'",
|
|
490
|
+
" * 3. Create the collections listed below with their fields",
|
|
491
|
+
" */",
|
|
492
|
+
"",
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
// Generate collection suggestions for each enabled post type
|
|
496
|
+
for (const [type, mapping] of Object.entries(config.collections)) {
|
|
497
|
+
if (!mapping.enabled) continue;
|
|
498
|
+
|
|
499
|
+
lines.push(`// ${type} → "${mapping.collection}" (${mapping.count} items)`);
|
|
500
|
+
lines.push(`// Label: "${capitalize(mapping.collection)}"`);
|
|
501
|
+
lines.push(`// Label Singular: "${capitalize(singularize(mapping.collection))}"`);
|
|
502
|
+
lines.push("// Suggested fields:");
|
|
503
|
+
lines.push("// - title (string)");
|
|
504
|
+
lines.push("// - content (portableText)");
|
|
505
|
+
lines.push("// - excerpt (string, optional)");
|
|
506
|
+
|
|
507
|
+
// Add fields for this collection
|
|
508
|
+
const collectionFields = Object.entries(config.fields)
|
|
509
|
+
.filter(([_, m]) => m.enabled)
|
|
510
|
+
.slice(0, 10); // Limit to avoid huge output
|
|
511
|
+
|
|
512
|
+
for (const [key, fieldMapping] of collectionFields) {
|
|
513
|
+
lines.push(
|
|
514
|
+
`// - ${sanitizeFieldName(fieldMapping.field)} (${fieldMapping.type}) // from: ${key}`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
lines.push("");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return lines.join("\n");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function sanitizeFieldName(name: string): string {
|
|
525
|
+
// Handle nested fields like seo.title → seo: { title }
|
|
526
|
+
// For now, just flatten with underscore
|
|
527
|
+
return name.replace(DOT_PATTERN, "_").replace(NON_ALPHANUMERIC_UNDERSCORE_PATTERN, "");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function capitalize(str: string): string {
|
|
531
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function singularize(str: string): string {
|
|
535
|
+
if (str.endsWith("ies")) return str.slice(0, -3) + "y";
|
|
536
|
+
if (str.endsWith("s")) return str.slice(0, -1);
|
|
537
|
+
return str;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ============================================================================
|
|
541
|
+
// Phase 2: Execute
|
|
542
|
+
// ============================================================================
|
|
543
|
+
|
|
544
|
+
export async function executeWordPressImport(
|
|
545
|
+
filePath: string,
|
|
546
|
+
options: ExecuteOptions,
|
|
547
|
+
): Promise<ImportResult> {
|
|
548
|
+
const result: ImportResult = {
|
|
549
|
+
success: true,
|
|
550
|
+
phase: "execute",
|
|
551
|
+
dryRun: options.dryRun,
|
|
552
|
+
summary: {
|
|
553
|
+
postsImported: 0,
|
|
554
|
+
postsSkipped: 0,
|
|
555
|
+
mediaDownloaded: 0,
|
|
556
|
+
mediaSkipped: 0,
|
|
557
|
+
errors: 0,
|
|
558
|
+
},
|
|
559
|
+
files: [],
|
|
560
|
+
errors: [],
|
|
561
|
+
nextSteps: [],
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const log = (msg: string) => !options.json && console.log(msg);
|
|
565
|
+
const progressPath = join(options.outputDir, ".wp-migration-progress.json");
|
|
566
|
+
|
|
567
|
+
if (options.dryRun) {
|
|
568
|
+
log(pc.yellow("[DRY RUN] ") + pc.cyan("Importing WordPress content...\n"));
|
|
569
|
+
} else {
|
|
570
|
+
log(pc.cyan("Importing WordPress content...\n"));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Load config
|
|
574
|
+
let config: MigrationConfig;
|
|
575
|
+
try {
|
|
576
|
+
const configContent = await readFile(options.configPath, "utf-8");
|
|
577
|
+
config = JSON.parse(configContent);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
const msg = `Failed to load migration config: ${options.configPath}`;
|
|
580
|
+
log(pc.red(msg));
|
|
581
|
+
log(pc.dim("Run with --prepare first to generate the config."));
|
|
582
|
+
result.success = false;
|
|
583
|
+
result.errors.push({ message: msg });
|
|
584
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
log(pc.dim(`Using config: ${options.configPath}`));
|
|
589
|
+
log(pc.dim(`File: ${filePath}`));
|
|
590
|
+
if (options.resume) {
|
|
591
|
+
log(pc.dim(`Resume mode: will skip already-imported items`));
|
|
592
|
+
}
|
|
593
|
+
log("");
|
|
594
|
+
|
|
595
|
+
// Load or initialize progress tracking
|
|
596
|
+
let progress: ImportProgress;
|
|
597
|
+
if (options.resume) {
|
|
598
|
+
try {
|
|
599
|
+
const progressContent = await readFile(progressPath, "utf-8");
|
|
600
|
+
progress = JSON.parse(progressContent);
|
|
601
|
+
log(
|
|
602
|
+
pc.dim(
|
|
603
|
+
`Resuming from previous run (${progress.stats.importedPosts} posts already imported)`,
|
|
604
|
+
),
|
|
605
|
+
);
|
|
606
|
+
} catch {
|
|
607
|
+
progress = createFreshProgress(filePath, options.configPath);
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
progress = createFreshProgress(filePath, options.configPath);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const alreadyImported = new Set(progress.importedPosts);
|
|
614
|
+
const alreadyDownloaded = new Set(progress.downloadedMedia);
|
|
615
|
+
|
|
616
|
+
// Parse WXR
|
|
617
|
+
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
618
|
+
const wxr = await parseWxr(stream);
|
|
619
|
+
|
|
620
|
+
// Update totals in progress
|
|
621
|
+
progress.stats.totalPosts = wxr.posts.length;
|
|
622
|
+
progress.stats.totalMedia = wxr.attachments.length;
|
|
623
|
+
|
|
624
|
+
// Build media map
|
|
625
|
+
const mediaMap = new Map<number, string>();
|
|
626
|
+
for (const attachment of wxr.attachments) {
|
|
627
|
+
if (attachment.id) {
|
|
628
|
+
mediaMap.set(attachment.id, `media-${attachment.id}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Stats
|
|
633
|
+
const stats = {
|
|
634
|
+
imported: 0,
|
|
635
|
+
skipped: 0,
|
|
636
|
+
resumed: 0,
|
|
637
|
+
errors: 0,
|
|
638
|
+
byCollection: new Map<string, number>(),
|
|
639
|
+
};
|
|
640
|
+
const redirects = new Map<string, string>();
|
|
641
|
+
|
|
642
|
+
// Process posts
|
|
643
|
+
for (const post of wxr.posts) {
|
|
644
|
+
const postType = post.postType || "post";
|
|
645
|
+
const mapping = config.collections[postType];
|
|
646
|
+
|
|
647
|
+
// Skip if not mapped or disabled
|
|
648
|
+
if (!mapping || !mapping.enabled) {
|
|
649
|
+
stats.skipped++;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Skip if already imported (resume mode)
|
|
654
|
+
if (post.id && alreadyImported.has(post.id)) {
|
|
655
|
+
stats.resumed++;
|
|
656
|
+
if (options.verbose) {
|
|
657
|
+
log(pc.dim(` [skip] ${mapping.collection}/${post.title} (already imported)`));
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
const converted = convertPostWithConfig(post, mapping.collection, config, mediaMap);
|
|
664
|
+
|
|
665
|
+
const outputPath = join(options.outputDir, converted.collection, `${converted.slug}.json`);
|
|
666
|
+
|
|
667
|
+
if (options.dryRun) {
|
|
668
|
+
if (options.verbose) {
|
|
669
|
+
log(pc.yellow(` [would create] ${outputPath}`));
|
|
670
|
+
}
|
|
671
|
+
result.files.push({ path: outputPath, action: "would_create" });
|
|
672
|
+
} else {
|
|
673
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
674
|
+
await writeFile(outputPath, JSON.stringify(converted.data, null, 2));
|
|
675
|
+
result.files.push({ path: outputPath, action: "created" });
|
|
676
|
+
|
|
677
|
+
// Update progress
|
|
678
|
+
if (post.id) {
|
|
679
|
+
progress.importedPosts.push(post.id);
|
|
680
|
+
progress.stats.importedPosts++;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (options.verbose) {
|
|
684
|
+
log(pc.green(` ${mapping.collection}/${converted.slug}`));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
stats.imported++;
|
|
689
|
+
stats.byCollection.set(
|
|
690
|
+
mapping.collection,
|
|
691
|
+
(stats.byCollection.get(mapping.collection) || 0) + 1,
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
// Track redirect
|
|
695
|
+
if (post.link && converted.slug) {
|
|
696
|
+
redirects.set(post.link, `/${converted.collection}/${converted.slug}`);
|
|
697
|
+
}
|
|
698
|
+
} catch (error) {
|
|
699
|
+
stats.errors++;
|
|
700
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
701
|
+
result.errors.push({
|
|
702
|
+
id: post.id,
|
|
703
|
+
message: `${post.title}: ${errorMsg}`,
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
if (post.id) {
|
|
707
|
+
progress.errors.push({ id: post.id, type: "post", error: errorMsg });
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (options.verbose) {
|
|
711
|
+
log(pc.red(` Failed: ${post.title} - ${errorMsg}`));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Save progress periodically (every 50 items)
|
|
716
|
+
if (!options.dryRun && stats.imported % 50 === 0) {
|
|
717
|
+
progress.updatedAt = new Date().toISOString();
|
|
718
|
+
await writeFile(progressPath, JSON.stringify(progress, null, 2));
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Download media
|
|
723
|
+
let mediaDownloaded = 0;
|
|
724
|
+
let mediaSkipped = 0;
|
|
725
|
+
if (!options.skipMedia && options.mediaDir && wxr.attachments.length > 0) {
|
|
726
|
+
log(pc.dim("\nDownloading media..."));
|
|
727
|
+
for (const attachment of wxr.attachments) {
|
|
728
|
+
// Skip if already downloaded (resume mode)
|
|
729
|
+
if (attachment.id && alreadyDownloaded.has(attachment.id)) {
|
|
730
|
+
mediaSkipped++;
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
if (options.dryRun) {
|
|
736
|
+
if (options.verbose) {
|
|
737
|
+
log(pc.yellow(` [would download] ${attachment.url}`));
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
await downloadMedia(attachment, options.mediaDir);
|
|
741
|
+
|
|
742
|
+
if (attachment.id) {
|
|
743
|
+
progress.downloadedMedia.push(attachment.id);
|
|
744
|
+
progress.stats.downloadedMedia++;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (options.verbose) {
|
|
748
|
+
log(pc.green(` ${attachment.url}`));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
mediaDownloaded++;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
754
|
+
result.errors.push({
|
|
755
|
+
id: attachment.id,
|
|
756
|
+
message: `Media ${attachment.url}: ${errorMsg}`,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
if (attachment.id) {
|
|
760
|
+
progress.errors.push({
|
|
761
|
+
id: attachment.id,
|
|
762
|
+
type: "media",
|
|
763
|
+
error: errorMsg,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (options.verbose) {
|
|
768
|
+
log(pc.red(` Failed: ${attachment.url}`));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
log(pc.dim(`Downloaded ${mediaDownloaded} media files`));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Write redirects
|
|
776
|
+
const redirectPath = join(options.outputDir, "_redirects.json");
|
|
777
|
+
if (redirects.size > 0) {
|
|
778
|
+
if (options.dryRun) {
|
|
779
|
+
result.files.push({ path: redirectPath, action: "would_create" });
|
|
780
|
+
} else {
|
|
781
|
+
await writeFile(redirectPath, JSON.stringify(Object.fromEntries(redirects), null, 2));
|
|
782
|
+
result.files.push({ path: redirectPath, action: "created" });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Save final progress
|
|
787
|
+
if (!options.dryRun) {
|
|
788
|
+
progress.updatedAt = new Date().toISOString();
|
|
789
|
+
progress.stats.skippedPosts = stats.skipped;
|
|
790
|
+
progress.stats.errorCount = stats.errors;
|
|
791
|
+
await writeFile(progressPath, JSON.stringify(progress, null, 2));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Update result summary
|
|
795
|
+
result.summary.postsImported = stats.imported;
|
|
796
|
+
result.summary.postsSkipped = stats.skipped + stats.resumed;
|
|
797
|
+
result.summary.mediaDownloaded = mediaDownloaded;
|
|
798
|
+
result.summary.mediaSkipped = mediaSkipped;
|
|
799
|
+
result.summary.errors = stats.errors + result.errors.length;
|
|
800
|
+
|
|
801
|
+
// Summary
|
|
802
|
+
const prefix = options.dryRun ? "[DRY RUN] " : "";
|
|
803
|
+
log(pc.cyan(`\n=== ${prefix}Import ${options.dryRun ? "Preview" : "Complete"} ===\n`));
|
|
804
|
+
log(`${options.dryRun ? "Would import" : "Imported"}: ${pc.green(stats.imported.toString())}`);
|
|
805
|
+
if (stats.resumed > 0) {
|
|
806
|
+
log(`Resumed (skipped): ${pc.blue(stats.resumed.toString())}`);
|
|
807
|
+
}
|
|
808
|
+
log(`Skipped (disabled): ${pc.yellow(stats.skipped.toString())}`);
|
|
809
|
+
if (stats.errors > 0) {
|
|
810
|
+
log(`Errors: ${pc.red(stats.errors.toString())}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
log(pc.bold("\nBy collection:"));
|
|
814
|
+
for (const [collection, count] of stats.byCollection) {
|
|
815
|
+
log(` ${collection}: ${count}`);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (redirects.size > 0 && !options.dryRun) {
|
|
819
|
+
log(pc.dim(`\nRedirect map written to: ${redirectPath}`));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Next steps
|
|
823
|
+
if (options.dryRun) {
|
|
824
|
+
result.nextSteps = [
|
|
825
|
+
`Run without --dry-run to perform the import`,
|
|
826
|
+
`emdash import wordpress ${filePath} --execute`,
|
|
827
|
+
];
|
|
828
|
+
} else if (stats.errors > 0) {
|
|
829
|
+
result.nextSteps = [
|
|
830
|
+
`Fix errors and run with --resume to continue`,
|
|
831
|
+
`emdash import wordpress ${filePath} --execute --resume`,
|
|
832
|
+
];
|
|
833
|
+
} else {
|
|
834
|
+
result.nextSteps = [
|
|
835
|
+
`Verify import: emdash migrate:verify --source ${filePath}`,
|
|
836
|
+
`Progress saved to: ${progressPath}`,
|
|
837
|
+
];
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (!options.dryRun) {
|
|
841
|
+
log(pc.dim(`\nProgress saved to: ${progressPath}`));
|
|
842
|
+
log(pc.dim(`Run with --resume to continue from where you left off.`));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// JSON output for agents
|
|
846
|
+
if (options.json) {
|
|
847
|
+
console.log(JSON.stringify(result, null, 2));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function createFreshProgress(sourceFile: string, configFile: string): ImportProgress {
|
|
854
|
+
return {
|
|
855
|
+
startedAt: new Date().toISOString(),
|
|
856
|
+
updatedAt: new Date().toISOString(),
|
|
857
|
+
sourceFile: resolve(sourceFile),
|
|
858
|
+
configFile: resolve(configFile),
|
|
859
|
+
importedPosts: [],
|
|
860
|
+
downloadedMedia: [],
|
|
861
|
+
errors: [],
|
|
862
|
+
stats: {
|
|
863
|
+
totalPosts: 0,
|
|
864
|
+
totalMedia: 0,
|
|
865
|
+
importedPosts: 0,
|
|
866
|
+
downloadedMedia: 0,
|
|
867
|
+
skippedPosts: 0,
|
|
868
|
+
errorCount: 0,
|
|
869
|
+
},
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
interface ConvertedContent {
|
|
874
|
+
slug: string;
|
|
875
|
+
collection: string;
|
|
876
|
+
data: Record<string, unknown>;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function convertPostWithConfig(
|
|
880
|
+
post: WxrPost,
|
|
881
|
+
collection: string,
|
|
882
|
+
config: MigrationConfig,
|
|
883
|
+
mediaMap: Map<number, string>,
|
|
884
|
+
): ConvertedContent {
|
|
885
|
+
// Convert content to Portable Text
|
|
886
|
+
const content = gutenbergToPortableText(post.content || "", { mediaMap });
|
|
887
|
+
|
|
888
|
+
// Extract slug
|
|
889
|
+
const slug = extractSlug(post.link) || slugify(post.title || "untitled");
|
|
890
|
+
|
|
891
|
+
// Build data object
|
|
892
|
+
const data: Record<string, unknown> = {
|
|
893
|
+
title: post.title,
|
|
894
|
+
content,
|
|
895
|
+
status: mapStatus(post.status),
|
|
896
|
+
publishedAt: post.pubDate ? new Date(post.pubDate).toISOString() : null,
|
|
897
|
+
createdAt: post.postDate ? new Date(post.postDate).toISOString() : null,
|
|
898
|
+
author: post.creator,
|
|
899
|
+
excerpt: post.excerpt,
|
|
900
|
+
categories: post.categories,
|
|
901
|
+
tags: post.tags,
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// Map custom fields
|
|
905
|
+
for (const [wpKey, value] of post.meta) {
|
|
906
|
+
const fieldConfig = config.fields[wpKey];
|
|
907
|
+
if (!fieldConfig || !fieldConfig.enabled) continue;
|
|
908
|
+
|
|
909
|
+
const fieldName = fieldConfig.field;
|
|
910
|
+
let fieldValue: unknown = value;
|
|
911
|
+
|
|
912
|
+
// Type conversion
|
|
913
|
+
switch (fieldConfig.type) {
|
|
914
|
+
case "number":
|
|
915
|
+
fieldValue = parseFloat(value) || 0;
|
|
916
|
+
break;
|
|
917
|
+
case "boolean":
|
|
918
|
+
fieldValue = value === "1" || value === "true";
|
|
919
|
+
break;
|
|
920
|
+
case "date":
|
|
921
|
+
fieldValue = new Date(value).toISOString();
|
|
922
|
+
break;
|
|
923
|
+
case "reference":
|
|
924
|
+
// Map WordPress ID to new reference
|
|
925
|
+
const wpId = parseInt(value, 10);
|
|
926
|
+
fieldValue = mediaMap.get(wpId) || value;
|
|
927
|
+
break;
|
|
928
|
+
case "json":
|
|
929
|
+
try {
|
|
930
|
+
// Try PHP unserialize first
|
|
931
|
+
if (value.startsWith("a:") || value.startsWith("O:")) {
|
|
932
|
+
fieldValue = unserializePhp(value);
|
|
933
|
+
} else {
|
|
934
|
+
fieldValue = JSON.parse(value);
|
|
935
|
+
}
|
|
936
|
+
} catch {
|
|
937
|
+
fieldValue = value;
|
|
938
|
+
}
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Handle nested fields (e.g., seo.title)
|
|
943
|
+
if (fieldName.includes(".")) {
|
|
944
|
+
const parts = fieldName.split(".");
|
|
945
|
+
let obj: Record<string, unknown> = data;
|
|
946
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
947
|
+
const part = parts[i];
|
|
948
|
+
const nested = obj[part];
|
|
949
|
+
if (isRecord(nested)) {
|
|
950
|
+
obj = nested;
|
|
951
|
+
} else {
|
|
952
|
+
const newObj: Record<string, unknown> = {};
|
|
953
|
+
obj[part] = newObj;
|
|
954
|
+
obj = newObj;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
obj[parts.at(-1)!] = fieldValue;
|
|
958
|
+
} else {
|
|
959
|
+
data[fieldName] = fieldValue;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Original WP metadata for reference
|
|
964
|
+
data._wp = {
|
|
965
|
+
id: post.id,
|
|
966
|
+
link: post.link,
|
|
967
|
+
guid: post.guid,
|
|
968
|
+
postType: post.postType,
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
return { slug, collection, data };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async function downloadMedia(attachment: WxrAttachment, mediaDir: string): Promise<void> {
|
|
975
|
+
if (!attachment.url) return;
|
|
976
|
+
|
|
977
|
+
// Validate URL is not targeting internal/private addresses
|
|
978
|
+
const parsed = validateExternalUrl(attachment.url);
|
|
979
|
+
const filename = parsed.pathname.split("/").pop() || `media-${attachment.id}`;
|
|
980
|
+
const filePath = join(mediaDir, filename);
|
|
981
|
+
|
|
982
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
983
|
+
|
|
984
|
+
const response = await ssrfSafeFetch(attachment.url);
|
|
985
|
+
if (!response.ok) {
|
|
986
|
+
throw new Error(`HTTP ${response.status}`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const buffer = await response.arrayBuffer();
|
|
990
|
+
await writeFile(filePath, Buffer.from(buffer));
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function extractSlug(link: string | undefined): string | undefined {
|
|
994
|
+
if (!link) return undefined;
|
|
995
|
+
try {
|
|
996
|
+
const url = new URL(link);
|
|
997
|
+
const path = url.pathname.replace(TRAILING_SLASH_PATTERN, "");
|
|
998
|
+
const segments = path.split("/").filter(Boolean);
|
|
999
|
+
return segments.pop();
|
|
1000
|
+
} catch {
|
|
1001
|
+
return undefined;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function mapStatus(wpStatus: string | undefined): string {
|
|
1006
|
+
switch (wpStatus) {
|
|
1007
|
+
case "publish":
|
|
1008
|
+
return "published";
|
|
1009
|
+
case "draft":
|
|
1010
|
+
return "draft";
|
|
1011
|
+
case "pending":
|
|
1012
|
+
return "pending";
|
|
1013
|
+
case "private":
|
|
1014
|
+
return "private";
|
|
1015
|
+
case "trash":
|
|
1016
|
+
return "archived";
|
|
1017
|
+
default:
|
|
1018
|
+
return "draft";
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Basic PHP unserialize for simple arrays/strings
|
|
1024
|
+
* Not a full implementation, but handles common cases
|
|
1025
|
+
*/
|
|
1026
|
+
function unserializePhp(str: string): unknown {
|
|
1027
|
+
// This is a simplified parser - for production, use a proper library
|
|
1028
|
+
try {
|
|
1029
|
+
if (str.startsWith("a:")) {
|
|
1030
|
+
// Array - extract key/value pairs
|
|
1031
|
+
const result: Record<string, unknown> = {};
|
|
1032
|
+
// Match pattern: s:4:"key";s:5:"value";
|
|
1033
|
+
const matches = str.matchAll(PHP_ARRAY_PATTERN);
|
|
1034
|
+
for (const match of matches) {
|
|
1035
|
+
const key = match[2];
|
|
1036
|
+
const strVal = match[4];
|
|
1037
|
+
const intVal = match[5];
|
|
1038
|
+
const boolVal = match[6];
|
|
1039
|
+
if (key) {
|
|
1040
|
+
if (strVal !== undefined) result[key] = strVal;
|
|
1041
|
+
else if (intVal !== undefined) result[key] = parseInt(intVal, 10);
|
|
1042
|
+
else if (boolVal !== undefined) result[key] = boolVal === "1";
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return result;
|
|
1046
|
+
}
|
|
1047
|
+
if (str.startsWith("s:")) {
|
|
1048
|
+
// Simple string
|
|
1049
|
+
const match = str.match(PHP_STRING_PATTERN);
|
|
1050
|
+
return match?.[1] || str;
|
|
1051
|
+
}
|
|
1052
|
+
return str;
|
|
1053
|
+
} catch {
|
|
1054
|
+
return str;
|
|
1055
|
+
}
|
|
1056
|
+
}
|