@contentrain/wp-import 0.5.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/index.d.mts +30 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +453 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -675,6 +675,265 @@ function guessMime(u) {
|
|
|
675
675
|
return MIME[(u.split("?")[0]?.split(".").pop() ?? "").toLowerCase()] ?? null;
|
|
676
676
|
}
|
|
677
677
|
//#endregion
|
|
678
|
+
//#region src/rest-menus.ts
|
|
679
|
+
const targetOf = (kind, object, id, url, ctx) => {
|
|
680
|
+
const objectId = id && id > 0 ? id : null;
|
|
681
|
+
if (kind === "post_type" && object) {
|
|
682
|
+
const slug = objectId ? ctx.postSlug(objectId) : void 0;
|
|
683
|
+
return {
|
|
684
|
+
kind: "post",
|
|
685
|
+
post_type: object,
|
|
686
|
+
id: objectId,
|
|
687
|
+
slug: slug ?? null,
|
|
688
|
+
resolved: slug !== void 0
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
if (kind === "taxonomy" && object) {
|
|
692
|
+
const slug = objectId ? ctx.termSlug(object, objectId) : void 0;
|
|
693
|
+
return {
|
|
694
|
+
kind: "term",
|
|
695
|
+
taxonomy: object,
|
|
696
|
+
id: objectId,
|
|
697
|
+
slug: slug ?? null,
|
|
698
|
+
resolved: slug !== void 0
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
if (kind === "post_type_archive" && object) return {
|
|
702
|
+
kind: "archive",
|
|
703
|
+
post_type: object,
|
|
704
|
+
resolved: true
|
|
705
|
+
};
|
|
706
|
+
if (url) return {
|
|
707
|
+
kind: "url",
|
|
708
|
+
url,
|
|
709
|
+
resolved: true
|
|
710
|
+
};
|
|
711
|
+
return {
|
|
712
|
+
kind: "unknown",
|
|
713
|
+
resolved: false
|
|
714
|
+
};
|
|
715
|
+
};
|
|
716
|
+
/**
|
|
717
|
+
* Items that are left out: a draft item, or one whose post target is not proven visible. Their children move up
|
|
718
|
+
* to the nearest kept ancestor, as WordPress's walker shows them.
|
|
719
|
+
*/
|
|
720
|
+
function hiddenItems(items, drop) {
|
|
721
|
+
const parentOf = new Map(items.filter(drop).map((i) => [i.id, i.parent || null]));
|
|
722
|
+
const lift = (parent) => {
|
|
723
|
+
const seen = /* @__PURE__ */ new Set();
|
|
724
|
+
let p = parent;
|
|
725
|
+
while (p !== null && parentOf.has(p) && !seen.has(p)) {
|
|
726
|
+
seen.add(p);
|
|
727
|
+
p = parentOf.get(p) ?? null;
|
|
728
|
+
}
|
|
729
|
+
return p;
|
|
730
|
+
};
|
|
731
|
+
return {
|
|
732
|
+
gone: new Set(parentOf.keys()),
|
|
733
|
+
lift
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
/** Classic menus and their items; each menu names the theme locations it is assigned to. `dropped` counts left-out items. */
|
|
737
|
+
function classicMenus(menus, items, ctx, dropped = { count: 0 }) {
|
|
738
|
+
const out = [];
|
|
739
|
+
for (const m of [...menus].toSorted((a, b) => a.id - b.id)) {
|
|
740
|
+
const all = items.filter((i) => i.menus === m.id).toSorted((a, b) => (a.menu_order ?? 0) - (b.menu_order ?? 0) || a.id - b.id);
|
|
741
|
+
const { gone, lift } = hiddenItems(all, (i) => (i.status ?? "publish") !== "publish" || i.type === "post_type" && !(i.object_id && ctx.postSlug(i.object_id)));
|
|
742
|
+
dropped.count += gone.size;
|
|
743
|
+
const own = all.filter((i) => !gone.has(i.id));
|
|
744
|
+
const ids = new Set(own.map((i) => i.id));
|
|
745
|
+
const menu = {
|
|
746
|
+
id: m.id,
|
|
747
|
+
slug: m.slug ?? "",
|
|
748
|
+
name: strip(m.name ?? m.slug ?? ""),
|
|
749
|
+
items: own.map((i) => {
|
|
750
|
+
const item = {
|
|
751
|
+
id: i.id,
|
|
752
|
+
title: i.title?.raw || i.title?.rendered || "",
|
|
753
|
+
order: i.menu_order ?? 0,
|
|
754
|
+
parent: lift(i.parent || null),
|
|
755
|
+
url: i.url || null,
|
|
756
|
+
target: targetOf(i.type, i.object, i.object_id, i.url, ctx),
|
|
757
|
+
target_attr: i.target || null,
|
|
758
|
+
classes: (i.classes ?? []).filter(Boolean),
|
|
759
|
+
description: i.description ?? "",
|
|
760
|
+
status: i.status ?? "publish"
|
|
761
|
+
};
|
|
762
|
+
if (item.parent && !ids.has(item.parent)) item.parent_unresolved = true;
|
|
763
|
+
return item;
|
|
764
|
+
})
|
|
765
|
+
};
|
|
766
|
+
const locations = (m.locations ?? []).filter(Boolean).toSorted();
|
|
767
|
+
if (locations.length) menu.locations = locations;
|
|
768
|
+
out.push(menu);
|
|
769
|
+
}
|
|
770
|
+
return out;
|
|
771
|
+
}
|
|
772
|
+
/** Serialized block comments (`<!-- wp:name {attrs} /-->`, `<!-- /wp:name -->`). Same grammar as the facts builder parser. */
|
|
773
|
+
const BLOCK_TOKEN = /<!--\s+(\/)?wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+({(?:(?!}\s+\/?-->)[\s\S])*?}\s+)?(\/)?-->/g;
|
|
774
|
+
function parseBlocks(content) {
|
|
775
|
+
const root = {
|
|
776
|
+
name: "root",
|
|
777
|
+
attrs: {},
|
|
778
|
+
children: []
|
|
779
|
+
};
|
|
780
|
+
const stack = [root];
|
|
781
|
+
const token = new RegExp(BLOCK_TOKEN.source, "g");
|
|
782
|
+
let m;
|
|
783
|
+
while (m = token.exec(content)) {
|
|
784
|
+
const [, closer, ns, local, json, selfClose] = m;
|
|
785
|
+
if (closer) {
|
|
786
|
+
if (stack.length > 1) stack.pop();
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
let attrs = {};
|
|
790
|
+
if (json) try {
|
|
791
|
+
attrs = JSON.parse(json);
|
|
792
|
+
} catch {
|
|
793
|
+
attrs = {};
|
|
794
|
+
}
|
|
795
|
+
const block = {
|
|
796
|
+
name: `${ns ? ns.slice(0, -1) : "core"}/${local}`,
|
|
797
|
+
attrs,
|
|
798
|
+
children: []
|
|
799
|
+
};
|
|
800
|
+
stack[stack.length - 1].children.push(block);
|
|
801
|
+
if (!selfClose) stack.push(block);
|
|
802
|
+
}
|
|
803
|
+
return root.children;
|
|
804
|
+
}
|
|
805
|
+
const str = (v) => typeof v === "string" && v ? v : void 0;
|
|
806
|
+
const absolute = (url, origin) => {
|
|
807
|
+
if (!url) return void 0;
|
|
808
|
+
try {
|
|
809
|
+
return new URL(url, `${origin}/`).href;
|
|
810
|
+
} catch {
|
|
811
|
+
return url;
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
/** The block editor's link kinds → the classic menu item types. */
|
|
815
|
+
const KIND = {
|
|
816
|
+
"post-type": "post_type",
|
|
817
|
+
taxonomy: "taxonomy",
|
|
818
|
+
custom: "custom"
|
|
819
|
+
};
|
|
820
|
+
/**
|
|
821
|
+
* Menu items of one `wp_navigation` body. Links and submenus become items; a page list becomes the
|
|
822
|
+
* published pages it lists; a home link points at `/`. Other blocks (search, social icons, spacers) are not links.
|
|
823
|
+
*/
|
|
824
|
+
function navigationItems(content, ctx, nextId, dropped = { count: 0 }) {
|
|
825
|
+
const items = [];
|
|
826
|
+
const push = (b, parent) => {
|
|
827
|
+
const id = nextId();
|
|
828
|
+
const url = absolute(b.url, ctx.origin);
|
|
829
|
+
items.push({
|
|
830
|
+
id,
|
|
831
|
+
title: b.title,
|
|
832
|
+
order: items.length + 1,
|
|
833
|
+
parent,
|
|
834
|
+
url: url ?? null,
|
|
835
|
+
target: targetOf(b.kind, b.object, b.id, url, ctx),
|
|
836
|
+
target_attr: b.newTab ? "_blank" : null,
|
|
837
|
+
classes: (b.className ?? "").split(/\s+/).filter(Boolean),
|
|
838
|
+
description: b.description ?? "",
|
|
839
|
+
status: "publish"
|
|
840
|
+
});
|
|
841
|
+
return id;
|
|
842
|
+
};
|
|
843
|
+
const pageList = (parentPage, parent) => {
|
|
844
|
+
const kids = ctx.pages.filter((p) => (p.parent ?? 0) === parentPage).toSorted((a, b) => a.menu_order - b.menu_order || a.title.localeCompare(b.title));
|
|
845
|
+
for (const p of kids) {
|
|
846
|
+
const id = push({
|
|
847
|
+
title: p.title,
|
|
848
|
+
url: p.link ?? void 0,
|
|
849
|
+
kind: "post_type",
|
|
850
|
+
object: "page",
|
|
851
|
+
id: p.id
|
|
852
|
+
}, parent);
|
|
853
|
+
pageList(p.id, id);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
const walk = (blocks, parent) => {
|
|
857
|
+
for (const b of blocks) {
|
|
858
|
+
const a = b.attrs;
|
|
859
|
+
if (b.name === "core/navigation-link" || b.name === "core/navigation-submenu") {
|
|
860
|
+
const kind = KIND[str(a.kind) ?? ""] ?? (str(a.type) === "category" || str(a.type) === "tag" || str(a.type) === "post_tag" ? "taxonomy" : a.id ? "post_type" : "custom");
|
|
861
|
+
const object = kind === "taxonomy" && str(a.type) === "tag" ? "post_tag" : str(a.type);
|
|
862
|
+
if (kind === "post_type" && typeof a.id === "number" && !ctx.postSlug(a.id)) {
|
|
863
|
+
dropped.count++;
|
|
864
|
+
walk(b.children, parent);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const id = push({
|
|
868
|
+
title: strip(a.label),
|
|
869
|
+
url: str(a.url),
|
|
870
|
+
kind,
|
|
871
|
+
object,
|
|
872
|
+
id: typeof a.id === "number" ? a.id : void 0,
|
|
873
|
+
newTab: a.opensInNewTab === true,
|
|
874
|
+
className: str(a.className),
|
|
875
|
+
description: str(a.description)
|
|
876
|
+
}, parent);
|
|
877
|
+
walk(b.children, id);
|
|
878
|
+
} else if (b.name === "core/home-link") push({
|
|
879
|
+
title: strip(a.label) || "Home",
|
|
880
|
+
url: "/",
|
|
881
|
+
className: str(a.className)
|
|
882
|
+
}, parent);
|
|
883
|
+
else if (b.name === "core/page-list") pageList(typeof a.parentPageID === "number" ? a.parentPageID : 0, parent);
|
|
884
|
+
else walk(b.children, parent);
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
walk(parseBlocks(content), null);
|
|
888
|
+
return items;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Where each navigation is shown: the template parts (`header`, `footer`) whose navigation block
|
|
892
|
+
* refers to it. An empty navigation block without a `ref` shows the fallback — the most recent published one;
|
|
893
|
+
* one with links of its own shows those (an inline menu, not a `wp_navigation` post).
|
|
894
|
+
*/
|
|
895
|
+
function navigationLocations(parts, navigations) {
|
|
896
|
+
const out = /* @__PURE__ */ new Map();
|
|
897
|
+
const fallback = [...navigations].toSorted((a, b) => (b.date_gmt ?? "").localeCompare(a.date_gmt ?? "") || b.id - a.id)[0]?.id;
|
|
898
|
+
const find = (blocks, area) => {
|
|
899
|
+
for (const b of blocks) {
|
|
900
|
+
if (b.name === "core/navigation") {
|
|
901
|
+
const ref = typeof b.attrs.ref === "number" ? b.attrs.ref : b.children.length ? void 0 : fallback;
|
|
902
|
+
if (ref !== void 0) (out.get(ref) ?? out.set(ref, /* @__PURE__ */ new Set()).get(ref)).add(area);
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
find(b.children, area);
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
for (const p of parts) {
|
|
909
|
+
const area = p.area && p.area !== "uncategorized" ? p.area : void 0;
|
|
910
|
+
if (area && p.content?.raw) find(parseBlocks(p.content.raw), area);
|
|
911
|
+
}
|
|
912
|
+
return new Map([...out].map(([id, set]) => [id, [...set].toSorted()]));
|
|
913
|
+
}
|
|
914
|
+
/** Published `wp_navigation` posts → RawMenu[], slugs kept unique against the classic menus already read. */
|
|
915
|
+
function blockMenus(navigations, parts, ctx, taken, dropped = { count: 0 }) {
|
|
916
|
+
const published = navigations.filter((n) => (n.status ?? "publish") === "publish").toSorted((a, b) => a.id - b.id);
|
|
917
|
+
const locations = navigationLocations(parts, published);
|
|
918
|
+
let next = 0;
|
|
919
|
+
const out = [];
|
|
920
|
+
for (const n of published) {
|
|
921
|
+
let slug = n.slug || `navigation-${n.id}`;
|
|
922
|
+
while (taken.has(slug)) slug = `${slug}-nav`;
|
|
923
|
+
taken.add(slug);
|
|
924
|
+
const menu = {
|
|
925
|
+
id: n.id,
|
|
926
|
+
slug,
|
|
927
|
+
name: strip(n.title?.raw || n.title?.rendered || "") || slug,
|
|
928
|
+
items: navigationItems(n.content?.raw ?? "", ctx, () => -++next, dropped)
|
|
929
|
+
};
|
|
930
|
+
const where = locations.get(n.id);
|
|
931
|
+
if (where?.length) menu.locations = where;
|
|
932
|
+
out.push(menu);
|
|
933
|
+
}
|
|
934
|
+
return out;
|
|
935
|
+
}
|
|
936
|
+
//#endregion
|
|
678
937
|
//#region src/rest.ts
|
|
679
938
|
const iso = (gmt) => gmt ? `${gmt}Z` : null;
|
|
680
939
|
const approvedOf = (status) => status === "approved" ? "1" : status === "hold" ? "0" : status === "spam" ? "spam" : status === "trash" ? "trash" : status ?? "1";
|
|
@@ -693,6 +952,10 @@ const DENIED = new Set([
|
|
|
693
952
|
401,
|
|
694
953
|
403
|
|
695
954
|
]);
|
|
955
|
+
/** A comment listing's name in `credential.fell_back`: `comments` (approved) or `comments:hold`. */
|
|
956
|
+
const commentKey = (status) => status === "approve" ? "comments" : `comments:${status}`;
|
|
957
|
+
/** What a visitor can open: published, not behind a post password. */
|
|
958
|
+
const visible = (p) => p.status === "publish" && !p.password;
|
|
696
959
|
const positiveInteger = (name, value) => {
|
|
697
960
|
if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`${name} must be a positive safe integer`);
|
|
698
961
|
return value;
|
|
@@ -721,20 +984,36 @@ async function fetchRestRawIR(options) {
|
|
|
721
984
|
});
|
|
722
985
|
}
|
|
723
986
|
};
|
|
724
|
-
const
|
|
725
|
-
|
|
987
|
+
const anonymous = { accept: "application/json" };
|
|
988
|
+
let headers = anonymous;
|
|
989
|
+
if (options.auth) headers = {
|
|
990
|
+
...anonymous,
|
|
991
|
+
authorization: `Basic ${Buffer.from(`${options.auth.user}:${options.auth.appPassword}`).toString("base64")}`
|
|
992
|
+
};
|
|
993
|
+
const fellBack = /* @__PURE__ */ new Set();
|
|
994
|
+
let rejected = false;
|
|
995
|
+
if (options.auth) {
|
|
996
|
+
const me = await doFetch(`${origin}/wp-json/wp/v2/users/me`, { headers });
|
|
997
|
+
await me.body?.cancel();
|
|
998
|
+
if (me.status === 401 || me.status === 403) {
|
|
999
|
+
rejected = true;
|
|
1000
|
+
headers = anonymous;
|
|
1001
|
+
warnings.push(`credential: HTTP ${me.status} on users/me — the site rejected it; imported the public listings only`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
726
1004
|
/**
|
|
727
1005
|
* Every page of `path`. With `fallback`, a first page the site refuses
|
|
728
1006
|
* (400/401/403 — the credential may not list those statuses or the edit
|
|
729
1007
|
* context) is retried as `fallback` instead of dropping the collection;
|
|
730
1008
|
* an empty `fallback` means there is nothing public to retry, so it is
|
|
731
|
-
* skipped with that warning.
|
|
1009
|
+
* skipped with that warning. The retry is anonymous, and `key` goes into
|
|
1010
|
+
* `credential.fell_back`.
|
|
732
1011
|
*/
|
|
733
|
-
const getAll = async (path, fallback) => {
|
|
1012
|
+
const getAll = async (path, fallback, key, send = headers) => {
|
|
734
1013
|
const label = path.split("?")[0];
|
|
735
1014
|
const url = (page) => `${origin}/wp-json/wp/v2/${path}${path.includes("?") ? "&" : "?"}per_page=${perPage}&page=${page}`;
|
|
736
1015
|
const pageData = (page) => schedule(async () => {
|
|
737
|
-
const response = await doFetch(url(page), { headers });
|
|
1016
|
+
const response = await doFetch(url(page), { headers: send });
|
|
738
1017
|
if (!response.ok) {
|
|
739
1018
|
await response.body?.cancel();
|
|
740
1019
|
if (page === 1 && fallback !== void 0 && DENIED.has(response.status)) return {
|
|
@@ -761,7 +1040,8 @@ async function fetchRestRawIR(options) {
|
|
|
761
1040
|
const first = await pageData(1);
|
|
762
1041
|
if (first.denied !== void 0) {
|
|
763
1042
|
warnings.push(`${label}: HTTP ${first.denied} for ${path.split("?")[1]} with the credential — ${fallback ? "fell back to the public listing" : "skipped (no public listing)"}`);
|
|
764
|
-
|
|
1043
|
+
fellBack.add(key ?? label);
|
|
1044
|
+
return fallback ? getAll(fallback, void 0, void 0, anonymous) : [];
|
|
765
1045
|
}
|
|
766
1046
|
const totalPages = first.pages;
|
|
767
1047
|
const firstPage = first.items;
|
|
@@ -771,6 +1051,18 @@ async function fetchRestRawIR(options) {
|
|
|
771
1051
|
const rest = await Promise.all(Array.from({ length: wanted - 1 }, (_v, i) => pageData(i + 2).then((r) => r.items)));
|
|
772
1052
|
return firstPage.concat(...rest);
|
|
773
1053
|
};
|
|
1054
|
+
const about = await schedule(async () => {
|
|
1055
|
+
try {
|
|
1056
|
+
const index = await doFetch(`${origin}/wp-json/`, { headers });
|
|
1057
|
+
if (!index.ok) {
|
|
1058
|
+
await index.body?.cancel();
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
return await index.json();
|
|
1062
|
+
} catch {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
774
1066
|
const typesResp = await doFetch(`${origin}/wp-json/wp/v2/types`, { headers });
|
|
775
1067
|
const types = typesResp.ok ? await typesResp.json() : {};
|
|
776
1068
|
if (!typesResp.ok) warnings.push(`types: HTTP ${typesResp.status} — importing posts and pages only`);
|
|
@@ -785,10 +1077,15 @@ async function fetchRestRawIR(options) {
|
|
|
785
1077
|
slug: "page",
|
|
786
1078
|
base: "pages"
|
|
787
1079
|
}];
|
|
788
|
-
|
|
1080
|
+
if (rejected) {
|
|
1081
|
+
for (const b of bases) fellBack.add(b.base);
|
|
1082
|
+
for (const status of AUTH_COMMENT_STATUSES) fellBack.add(commentKey(status));
|
|
1083
|
+
}
|
|
1084
|
+
const authed = options.auth !== void 0 && !rejected;
|
|
1085
|
+
const postListing = (base) => authed ? getAll(`${base}?status=${AUTH_POST_STATUSES.join(",")}&context=edit`, base, base) : getAll(base);
|
|
789
1086
|
const commentListing = async () => {
|
|
790
|
-
if (!
|
|
791
|
-
const lists = await Promise.all(AUTH_COMMENT_STATUSES.map((status) => getAll(`comments?status=${status}&context=edit`, status === "approve" ? "comments" : "")));
|
|
1087
|
+
if (!authed) return getAll("comments");
|
|
1088
|
+
const lists = await Promise.all(AUTH_COMMENT_STATUSES.map((status) => getAll(`comments?status=${status}&context=edit`, status === "approve" ? "comments" : "", commentKey(status))));
|
|
792
1089
|
const byId = /* @__PURE__ */ new Map();
|
|
793
1090
|
for (const c of lists.flat()) if (!byId.has(c.id)) byId.set(c.id, c);
|
|
794
1091
|
return [...byId.values()];
|
|
@@ -891,6 +1188,87 @@ async function fetchRestRawIR(options) {
|
|
|
891
1188
|
parent_resolved: m.post ? posts.some((p) => p.id === m.post) : null,
|
|
892
1189
|
date: iso(m.date_gmt)
|
|
893
1190
|
}));
|
|
1191
|
+
const gaps = [];
|
|
1192
|
+
let menus = [];
|
|
1193
|
+
if (!authed) gaps.push("menus_require_auth");
|
|
1194
|
+
else {
|
|
1195
|
+
const listing = async (path) => {
|
|
1196
|
+
const url = (page) => `${origin}/wp-json/wp/v2/${path}${path.includes("?") ? "&" : "?"}per_page=${perPage}&page=${page}`;
|
|
1197
|
+
const first = await schedule(async () => {
|
|
1198
|
+
const r = await doFetch(url(1), { headers });
|
|
1199
|
+
if (!r.ok) {
|
|
1200
|
+
await r.body?.cancel();
|
|
1201
|
+
return {
|
|
1202
|
+
items: [],
|
|
1203
|
+
status: r.status,
|
|
1204
|
+
pages: 1
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
const items = await r.json();
|
|
1208
|
+
return {
|
|
1209
|
+
items: Array.isArray(items) ? items : [],
|
|
1210
|
+
status: r.status,
|
|
1211
|
+
pages: Math.max(1, Number(r.headers.get("x-wp-totalpages") ?? "1") || 1)
|
|
1212
|
+
};
|
|
1213
|
+
});
|
|
1214
|
+
const wanted = maxPages !== void 0 ? Math.min(first.pages, maxPages) : first.pages;
|
|
1215
|
+
const rest = await Promise.all(Array.from({ length: wanted - 1 }, (_v, i) => schedule(async () => {
|
|
1216
|
+
const r = await doFetch(url(i + 2), { headers });
|
|
1217
|
+
if (!r.ok) {
|
|
1218
|
+
await r.body?.cancel();
|
|
1219
|
+
warnings.push(`${path.split("?")[0]}: page ${i + 2} HTTP ${r.status} — skipped`);
|
|
1220
|
+
return [];
|
|
1221
|
+
}
|
|
1222
|
+
const items = await r.json();
|
|
1223
|
+
return Array.isArray(items) ? items : [];
|
|
1224
|
+
})));
|
|
1225
|
+
return {
|
|
1226
|
+
items: first.items.concat(...rest),
|
|
1227
|
+
status: first.status
|
|
1228
|
+
};
|
|
1229
|
+
};
|
|
1230
|
+
const [classic, items, navs, parts] = await Promise.all([
|
|
1231
|
+
listing("menus?context=edit"),
|
|
1232
|
+
listing("menu-items?context=edit"),
|
|
1233
|
+
listing("navigation?context=edit&status=publish"),
|
|
1234
|
+
listing("template-parts?context=edit")
|
|
1235
|
+
]);
|
|
1236
|
+
const denied = [
|
|
1237
|
+
classic,
|
|
1238
|
+
items,
|
|
1239
|
+
navs
|
|
1240
|
+
].filter((l) => DENIED.has(l.status));
|
|
1241
|
+
if (denied.length) {
|
|
1242
|
+
gaps.push("menus_require_auth");
|
|
1243
|
+
warnings.push(`menus: HTTP ${denied[0].status} with the credential — the user may not edit theme options; menus not read`);
|
|
1244
|
+
}
|
|
1245
|
+
for (const [name, l] of [
|
|
1246
|
+
["menus", classic],
|
|
1247
|
+
["menu-items", items],
|
|
1248
|
+
["navigation", navs]
|
|
1249
|
+
]) if (!l.status || l.status >= 400 && !DENIED.has(l.status) && l.status !== 404) warnings.push(`${name}: HTTP ${l.status} — skipped`);
|
|
1250
|
+
const slugOf = new Map(posts.filter(visible).map((p) => [p.id, p.slug]));
|
|
1251
|
+
const ctx = {
|
|
1252
|
+
origin,
|
|
1253
|
+
postSlug: (id) => slugOf.get(id),
|
|
1254
|
+
termSlug: (taxonomy, id) => {
|
|
1255
|
+
const t = termsById.get(id);
|
|
1256
|
+
return t && t.taxonomy === taxonomy ? t.slug : void 0;
|
|
1257
|
+
},
|
|
1258
|
+
pages: posts.filter((p) => p.type === "page" && p.status === "publish" && !p.password).map((p) => ({
|
|
1259
|
+
id: p.id,
|
|
1260
|
+
parent: p.parent ?? null,
|
|
1261
|
+
menu_order: p.menu_order ?? 0,
|
|
1262
|
+
title: strip(p.title),
|
|
1263
|
+
link: p.link ?? null,
|
|
1264
|
+
slug: p.slug
|
|
1265
|
+
}))
|
|
1266
|
+
};
|
|
1267
|
+
const dropped = { count: 0 };
|
|
1268
|
+
menus = classicMenus(DENIED.has(classic.status) ? [] : classic.items, DENIED.has(items.status) ? [] : items.items, ctx, dropped);
|
|
1269
|
+
menus.push(...blockMenus(DENIED.has(navs.status) ? [] : navs.items, parts.status < 400 ? parts.items : [], ctx, new Set(menus.map((m) => m.slug)), dropped));
|
|
1270
|
+
if (dropped.count) warnings.push(`menus: ${dropped.count} item(s) are drafts or point at content not proven public (unpublished, password-protected, or not read by this import) — left out`);
|
|
1271
|
+
}
|
|
894
1272
|
const postIds = new Set(posts.map((p) => p.id));
|
|
895
1273
|
const commentIds = new Set(restComments.map((c) => c.id));
|
|
896
1274
|
const comments = restComments.map((c) => ({
|
|
@@ -914,10 +1292,16 @@ async function fetchRestRawIR(options) {
|
|
|
914
1292
|
raw: {
|
|
915
1293
|
version: MIGRATION_CONTRACT_VERSION,
|
|
916
1294
|
provenance: {
|
|
917
|
-
kind:
|
|
1295
|
+
kind: authed ? "rest_auth" : "rest_public",
|
|
918
1296
|
tool: options.tool ?? "@contentrain/wp-import"
|
|
919
1297
|
},
|
|
920
|
-
site: {
|
|
1298
|
+
site: {
|
|
1299
|
+
url: origin,
|
|
1300
|
+
...typeof about?.name === "string" && strip(about.name) ? { title: strip(about.name) } : {},
|
|
1301
|
+
...typeof about?.description === "string" && strip(about.description) ? { description: strip(about.description) } : {},
|
|
1302
|
+
...typeof about?.url === "string" && about.url ? { base_site_url: about.url } : {},
|
|
1303
|
+
...typeof about?.home === "string" && about.home ? { base_blog_url: about.home } : {}
|
|
1304
|
+
},
|
|
921
1305
|
authors: users.map((u) => ({
|
|
922
1306
|
id: u.id,
|
|
923
1307
|
login: u.slug,
|
|
@@ -928,9 +1312,15 @@ async function fetchRestRawIR(options) {
|
|
|
928
1312
|
posts,
|
|
929
1313
|
attachments,
|
|
930
1314
|
comments,
|
|
1315
|
+
...menus.length ? { menus } : {},
|
|
931
1316
|
...languagePairs.length ? { language_pairs: languagePairs } : {}
|
|
932
1317
|
},
|
|
933
|
-
warnings
|
|
1318
|
+
warnings,
|
|
1319
|
+
credential: {
|
|
1320
|
+
status: !options.auth ? "none" : fellBack.size ? "rejected" : "accepted",
|
|
1321
|
+
fell_back: [...fellBack].toSorted()
|
|
1322
|
+
},
|
|
1323
|
+
gaps
|
|
934
1324
|
};
|
|
935
1325
|
}
|
|
936
1326
|
//#endregion
|
|
@@ -1509,7 +1899,13 @@ function rawToContentrain(raw, opts) {
|
|
|
1509
1899
|
type: "integer",
|
|
1510
1900
|
label: "WP term ID",
|
|
1511
1901
|
order: 40
|
|
1512
|
-
}
|
|
1902
|
+
},
|
|
1903
|
+
...menus.some((m) => m.locations?.length) ? { locations: {
|
|
1904
|
+
type: "array",
|
|
1905
|
+
items: "string",
|
|
1906
|
+
label: "Locations",
|
|
1907
|
+
order: 50
|
|
1908
|
+
} } : {}
|
|
1513
1909
|
}
|
|
1514
1910
|
});
|
|
1515
1911
|
const toTarget = relationOver([...contentModelIds, ...taxonomies.map(taxModelId)]);
|
|
@@ -1597,7 +1993,8 @@ function rawToContentrain(raw, opts) {
|
|
|
1597
1993
|
name: strip(m.name),
|
|
1598
1994
|
slug: slugify(m.slug) || `menu-${m.id}`,
|
|
1599
1995
|
items: m.items.map((i) => itemRef(i.id)),
|
|
1600
|
-
wp_id: m.id
|
|
1996
|
+
wp_id: m.id,
|
|
1997
|
+
...m.locations?.length ? { locations: m.locations } : {}
|
|
1601
1998
|
};
|
|
1602
1999
|
metas.menus[mid] = importMeta("published");
|
|
1603
2000
|
for (const i of m.items) {
|
|
@@ -1606,7 +2003,7 @@ function rawToContentrain(raw, opts) {
|
|
|
1606
2003
|
menu: mid,
|
|
1607
2004
|
order: i.order,
|
|
1608
2005
|
type: i.target.kind === "url" ? "custom" : i.target.kind === "post" ? "post_type" : i.target.kind === "term" ? "taxonomy" : i.target.kind === "archive" ? "post_type_archive" : "custom",
|
|
1609
|
-
wp_id: i.id,
|
|
2006
|
+
...i.id > 0 ? { wp_id: i.id } : {},
|
|
1610
2007
|
open_in_new_tab: i.target_attr === "_blank"
|
|
1611
2008
|
};
|
|
1612
2009
|
if (i.parent && !i.parent_unresolved) e.parent = itemRef(i.parent);
|
|
@@ -1837,8 +2234,44 @@ function rawToContentrain(raw, opts) {
|
|
|
1837
2234
|
}
|
|
1838
2235
|
//#endregion
|
|
1839
2236
|
//#region src/comments.ts
|
|
2237
|
+
/**
|
|
2238
|
+
* The comments that may leave the source site (see `CommentsExport.comments`). An allowlist, so it
|
|
2239
|
+
* fails closed: a comment travels only when its entry is in this RawIR and public, and its status is
|
|
2240
|
+
* approved (`'1'`, or none) or pending (`'0'`). Everything else is counted in `excluded`: spam, trash,
|
|
2241
|
+
* WordPress's `post-trashed`, a plugin's own status, a post this RawIR does not hold.
|
|
2242
|
+
*/
|
|
2243
|
+
function selectComments(raw) {
|
|
2244
|
+
const posts = new Map(raw.posts.map((p) => [p.id, p]));
|
|
2245
|
+
const excluded = {};
|
|
2246
|
+
const count = (key) => {
|
|
2247
|
+
excluded[key] = (excluded[key] ?? 0) + 1;
|
|
2248
|
+
};
|
|
2249
|
+
const comments = [];
|
|
2250
|
+
for (const c of raw.comments ?? []) {
|
|
2251
|
+
const post = posts.get(c.post);
|
|
2252
|
+
if (!post) {
|
|
2253
|
+
count("unknown_entry");
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
if (post.status !== "publish" || post.password !== null && post.password !== void 0 && post.password !== "") {
|
|
2257
|
+
count("non_public_entry");
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
const status = c.approved ?? "1";
|
|
2261
|
+
if (status === "1" || status === "0") {
|
|
2262
|
+
comments.push(c);
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
count(status === "spam" ? "spam" : status === "trash" ? "trash" : "other_status");
|
|
2266
|
+
}
|
|
2267
|
+
return {
|
|
2268
|
+
comments,
|
|
2269
|
+
excluded
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
1840
2272
|
function buildCommentsExport(raw, entries, opts) {
|
|
1841
2273
|
const threadsClosed = raw.posts.filter((p) => p.comment_status && p.comment_status !== "open").map((p) => p.id);
|
|
2274
|
+
const { comments, excluded } = selectComments(raw);
|
|
1842
2275
|
return {
|
|
1843
2276
|
version: MIGRATION_CONTRACT_VERSION,
|
|
1844
2277
|
format: COMMENTS_EXPORT_FORMAT,
|
|
@@ -1847,7 +2280,8 @@ function buildCommentsExport(raw, entries, opts) {
|
|
|
1847
2280
|
generated_at: opts?.generated_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1848
2281
|
entries,
|
|
1849
2282
|
threads_closed: threadsClosed.length ? threadsClosed : void 0,
|
|
1850
|
-
comments
|
|
2283
|
+
comments,
|
|
2284
|
+
excluded: Object.keys(excluded).length ? excluded : void 0
|
|
1851
2285
|
};
|
|
1852
2286
|
}
|
|
1853
2287
|
/** Handoff summary for the export: counts + closed threads + what could not be addressed. */
|
|
@@ -1870,7 +2304,8 @@ function summarizeComments(exp) {
|
|
|
1870
2304
|
types: byType,
|
|
1871
2305
|
export: { format: exp.format },
|
|
1872
2306
|
threads_closed: exp.threads_closed,
|
|
1873
|
-
unresolved: unresolved.length ? unresolved : void 0
|
|
2307
|
+
unresolved: unresolved.length ? unresolved : void 0,
|
|
2308
|
+
excluded: exp.excluded
|
|
1874
2309
|
};
|
|
1875
2310
|
}
|
|
1876
2311
|
//#endregion
|
|
@@ -2124,6 +2559,6 @@ function formatSourceDeltaReport(plan) {
|
|
|
2124
2559
|
return `${lines.join("\n")}\n`;
|
|
2125
2560
|
}
|
|
2126
2561
|
//#endregion
|
|
2127
|
-
export { DEFAULT_IMPORTERS, DEFAULT_TAXONOMIES, PLUGIN_META, SKIP_TYPES, buildCommentsExport, canon, fetchRestRawIR, formatSourceDeltaReport, hexId, looksSerialized, parseWxr, phpUnserialize, planSourceDelta, rawToContentrain, slugify, strip, summarizeComments, taxModelId, tryUnserialize };
|
|
2562
|
+
export { DEFAULT_IMPORTERS, DEFAULT_TAXONOMIES, PLUGIN_META, SKIP_TYPES, buildCommentsExport, canon, fetchRestRawIR, formatSourceDeltaReport, hexId, looksSerialized, parseWxr, phpUnserialize, planSourceDelta, rawToContentrain, selectComments, slugify, strip, summarizeComments, taxModelId, tryUnserialize };
|
|
2128
2563
|
|
|
2129
2564
|
//# sourceMappingURL=index.mjs.map
|