@contentrain/wp-import 0.6.0 → 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 +30 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +374 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -102,6 +102,36 @@ these before claiming a complete migration. ACF field/group records and their
|
|
|
102
102
|
cross-model parents remain in the content store; route discovery decides which
|
|
103
103
|
records become public pages. Importing configuration does not execute a plugin.
|
|
104
104
|
|
|
105
|
+
### Site name over REST
|
|
106
|
+
|
|
107
|
+
`fetchRestRawIR` reads the `/wp-json/` index for the site's name and tagline
|
|
108
|
+
(`RawIR.site.title` / `description`, the store's `site` singleton) and its
|
|
109
|
+
install and public addresses (`base_site_url` / `base_blog_url`). An index that
|
|
110
|
+
does not answer leaves them out.
|
|
111
|
+
|
|
112
|
+
### Menus over REST
|
|
113
|
+
|
|
114
|
+
With an Application Password, `fetchRestRawIR` reads the site's menus into
|
|
115
|
+
`RawIR.menus`: classic menus (`/wp/v2/menus` + `/wp/v2/menu-items`, with the
|
|
116
|
+
theme locations each is assigned to) and a block theme's published
|
|
117
|
+
`wp_navigation` posts (links, submenus, page lists, home links). A block menu's
|
|
118
|
+
`locations` are the template-part areas (`header`, `footer`) whose navigation
|
|
119
|
+
block refers to it; an empty navigation block without a `ref` shows the most
|
|
120
|
+
recent published one, as WordPress does. Block menu items have no WordPress id
|
|
121
|
+
and get negative ids; the store claims no `wp_id` for them.
|
|
122
|
+
|
|
123
|
+
A menu item that is itself a draft, or whose post target is not proven public
|
|
124
|
+
— a draft, pending, private, scheduled or password-protected post, or one this
|
|
125
|
+
import never read (a type outside REST, a listing past a page cap) — is left
|
|
126
|
+
out, fail-closed: its label is often that post's title. Its children move up to the
|
|
127
|
+
nearest kept ancestor; `warnings` gives only the count.
|
|
128
|
+
|
|
129
|
+
Both need `edit_theme_options`. Without a credential, with a rejected one, or
|
|
130
|
+
with a user who lacks that right, no menus are read and the result's `gaps`
|
|
131
|
+
contains `menus_require_auth` — an empty menu list is never presented as the
|
|
132
|
+
site having none. The `menus` model gains a `locations` field only when the
|
|
133
|
+
source named them.
|
|
134
|
+
|
|
105
135
|
|
|
106
136
|
### Publication and translation identity
|
|
107
137
|
|
package/dist/index.d.mts
CHANGED
|
@@ -67,6 +67,12 @@ interface RestImportResult {
|
|
|
67
67
|
status: 'none' | 'accepted' | 'rejected';
|
|
68
68
|
fell_back: string[];
|
|
69
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* What this rung could not read, as codes a caller can act on (the text is in `warnings`).
|
|
72
|
+
* `menus_require_auth`: menus and block navigation are only readable with an application password
|
|
73
|
+
* of a user who may edit theme options — none was given, the site rejected it, or it lacks that right.
|
|
74
|
+
*/
|
|
75
|
+
gaps: string[];
|
|
70
76
|
}
|
|
71
77
|
declare function fetchRestRawIR(options: RestImportOptions): Promise<RestImportResult>;
|
|
72
78
|
//#endregion
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/wxr.ts","../src/rest.ts","../src/contentrain.ts","../src/comments.ts","../src/php-unserialize.ts","../src/core.ts","../src/delta.ts"],"mappings":";;;;UA2BiB,QAAA;EACf,UAAA;IAAc,QAAA;IAAkB,OAAA;IAAiB,MAAA;IAAgB,IAAA;EAAA;EACjE,UAAA;EACA,gBAAA;EACA,MAAA,EAAQ,MAAA;AAAA;AAAA,UAGO,cAAA;EACf,GAAA,EAAK,KAAA;EACL,KAAA,EAAO,QAAA;AAAA;AAAA,iBAiBa,QAAA,CAAS,KAAA,WAAgB,QAAA,EAAU,IAAA;EAAS,IAAA;AAAA,IAAkB,OAAA,CAAQ,cAAA;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/wxr.ts","../src/rest.ts","../src/contentrain.ts","../src/comments.ts","../src/php-unserialize.ts","../src/core.ts","../src/delta.ts"],"mappings":";;;;UA2BiB,QAAA;EACf,UAAA;IAAc,QAAA;IAAkB,OAAA;IAAiB,MAAA;IAAgB,IAAA;EAAA;EACjE,UAAA;EACA,gBAAA;EACA,MAAA,EAAQ,MAAA;AAAA;AAAA,UAGO,cAAA;EACf,GAAA,EAAK,KAAA;EACL,KAAA,EAAO,QAAA;AAAA;AAAA,iBAiBa,QAAA,CAAS,KAAA,WAAgB,QAAA,EAAU,IAAA;EAAS,IAAA;AAAA,IAAkB,OAAA,CAAQ,cAAA;;;UCrC3E,iBAAA;EACf,MAAA;;EAEA,SAAA,UAAmB,KAAA;EDQI;;;;;;;;ECCvB,IAAA;IAAS,IAAA;IAAc,WAAA;EAAA;EACvB,OAAA;EDEc;AAGhB;;;;ECCE,WAAA;EDAK;;;;ECKL,QAAA;EACA,IAAA;AAAA;AAAA,UAGe,gBAAA;EACf,GAAA,EAAK,KAAA;EACL,QAAA;EDOkF;;;;;;;;ECElF,UAAA;IAAc,MAAA;IAA0C,SAAA;EAAA;;;AAvC1D;;;EA6CE,IAAA;AAAA;AAAA,iBAkDoB,cAAA,CAAe,OAAA,EAAS,iBAAA,GAAoB,OAAA,CAAQ,gBAAA;;;UCpFzD,YAAA;EACf,cAAA;EACA,aAAA;EACA,cAAA;EACA,WAAA,EAAa,MAAA;EACb,UAAA,EAAY,MAAA;EACZ,aAAA;EACA,iBAAA;EACA,MAAA,EAAQ,MAAA;IAAiB,IAAA;IAAc,MAAA;IAAgB,MAAA;IAAgB,OAAA;EAAA;EFJvE;EEMA,OAAA;EFNc;EEQd,kBAAA;EFLe;;;;;;EEYf,yBAAA;AAAA;AAAA,UAGe,iBAAA;EACf,KAAA,EAAO,MAAA;EACP,gBAAA,EAAkB,cAAA;EAClB,MAAA,EAAQ,YAAA;AAAA;AAAA,iBAoCM,gBAAA,CAAiB,GAAA,EAAK,KAAA,EAAO,IAAA;EAAS,SAAA;AAAA,IAAuB,iBAAA;;;;;;AF7D7E;;;iBGbgB,cAAA,CAAe,GAAA,EAAK,KAAA;EAAU,QAAA,EAAU,UAAA;EAAc,QAAA,EAAU,WAAA,CAAY,cAAA;AAAA;AAAA,iBAgB5E,mBAAA,CAAoB,GAAA,EAAK,KAAA,EAAO,OAAA,EAAS,cAAA,EAAgB,IAAA;EAAS,YAAA;AAAA,IAA0B,cAAA;;iBAiB5F,iBAAA,CAAkB,GAAA,EAAK,cAAA,GAAiB,eAAA;;;UC3CvC,iBAAA;EACf,EAAA;EACA,UAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,iBAGc,eAAA,CAAgB,CAAA,YAAa,CAAA;AAAA,iBAI7B,cAAA,CAAe,GAAA;;iBAyFf,cAAA,CAAe,CAAA,WAAY,iBAAA;;;cC9F9B,KAAA,GAAS,CAAA;;cAST,KAAA,GAAS,CAAA;;cAcT,KAAA,GAAS,CAAA;;;;;;;;;ALCtB;cK8Ba,UAAA,EAAU,MAAA;;cAIV,WAAA,EAAW,MAAA;;cAOX,UAAA,GAAc,CAAA;AAAA,cAGd,OAAA,GAAW,CAAA;;;;UC3DP,UAAA;EACf,KAAA,EAAO,MAAA;EACP,gBAAA,EAAkB,cAAA;AAAA;AAAA,UAGH,oBAAA;ENOD;EMLd,KAAA,EAAO,eAAA;ENEO;EMAd,KAAA,EAAO,UAAA;ENA0C;EMEjD,QAAA,GAAW,UAAA;ENDX;;;;;EMOA,UAAA;ENFe;;;;EMOf,SAAA;AAAA;AAAA,cAGW,kBAAA;AAAA,cACA,iBAAA;;;ANQb;;;;;;;;;;iBM8NgB,eAAA,CAAgB,KAAA,EAAO,oBAAA,GAAuB,eAAA;;iBAkC9C,uBAAA,CAAwB,IAAA,EAAM,eAAA"}
|
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";
|
|
@@ -695,6 +954,8 @@ const DENIED = new Set([
|
|
|
695
954
|
]);
|
|
696
955
|
/** A comment listing's name in `credential.fell_back`: `comments` (approved) or `comments:hold`. */
|
|
697
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;
|
|
698
959
|
const positiveInteger = (name, value) => {
|
|
699
960
|
if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`${name} must be a positive safe integer`);
|
|
700
961
|
return value;
|
|
@@ -790,6 +1051,18 @@ async function fetchRestRawIR(options) {
|
|
|
790
1051
|
const rest = await Promise.all(Array.from({ length: wanted - 1 }, (_v, i) => pageData(i + 2).then((r) => r.items)));
|
|
791
1052
|
return firstPage.concat(...rest);
|
|
792
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
|
+
});
|
|
793
1066
|
const typesResp = await doFetch(`${origin}/wp-json/wp/v2/types`, { headers });
|
|
794
1067
|
const types = typesResp.ok ? await typesResp.json() : {};
|
|
795
1068
|
if (!typesResp.ok) warnings.push(`types: HTTP ${typesResp.status} — importing posts and pages only`);
|
|
@@ -915,6 +1188,87 @@ async function fetchRestRawIR(options) {
|
|
|
915
1188
|
parent_resolved: m.post ? posts.some((p) => p.id === m.post) : null,
|
|
916
1189
|
date: iso(m.date_gmt)
|
|
917
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
|
+
}
|
|
918
1272
|
const postIds = new Set(posts.map((p) => p.id));
|
|
919
1273
|
const commentIds = new Set(restComments.map((c) => c.id));
|
|
920
1274
|
const comments = restComments.map((c) => ({
|
|
@@ -941,7 +1295,13 @@ async function fetchRestRawIR(options) {
|
|
|
941
1295
|
kind: authed ? "rest_auth" : "rest_public",
|
|
942
1296
|
tool: options.tool ?? "@contentrain/wp-import"
|
|
943
1297
|
},
|
|
944
|
-
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
|
+
},
|
|
945
1305
|
authors: users.map((u) => ({
|
|
946
1306
|
id: u.id,
|
|
947
1307
|
login: u.slug,
|
|
@@ -952,13 +1312,15 @@ async function fetchRestRawIR(options) {
|
|
|
952
1312
|
posts,
|
|
953
1313
|
attachments,
|
|
954
1314
|
comments,
|
|
1315
|
+
...menus.length ? { menus } : {},
|
|
955
1316
|
...languagePairs.length ? { language_pairs: languagePairs } : {}
|
|
956
1317
|
},
|
|
957
1318
|
warnings,
|
|
958
1319
|
credential: {
|
|
959
1320
|
status: !options.auth ? "none" : fellBack.size ? "rejected" : "accepted",
|
|
960
1321
|
fell_back: [...fellBack].toSorted()
|
|
961
|
-
}
|
|
1322
|
+
},
|
|
1323
|
+
gaps
|
|
962
1324
|
};
|
|
963
1325
|
}
|
|
964
1326
|
//#endregion
|
|
@@ -1537,7 +1899,13 @@ function rawToContentrain(raw, opts) {
|
|
|
1537
1899
|
type: "integer",
|
|
1538
1900
|
label: "WP term ID",
|
|
1539
1901
|
order: 40
|
|
1540
|
-
}
|
|
1902
|
+
},
|
|
1903
|
+
...menus.some((m) => m.locations?.length) ? { locations: {
|
|
1904
|
+
type: "array",
|
|
1905
|
+
items: "string",
|
|
1906
|
+
label: "Locations",
|
|
1907
|
+
order: 50
|
|
1908
|
+
} } : {}
|
|
1541
1909
|
}
|
|
1542
1910
|
});
|
|
1543
1911
|
const toTarget = relationOver([...contentModelIds, ...taxonomies.map(taxModelId)]);
|
|
@@ -1625,7 +1993,8 @@ function rawToContentrain(raw, opts) {
|
|
|
1625
1993
|
name: strip(m.name),
|
|
1626
1994
|
slug: slugify(m.slug) || `menu-${m.id}`,
|
|
1627
1995
|
items: m.items.map((i) => itemRef(i.id)),
|
|
1628
|
-
wp_id: m.id
|
|
1996
|
+
wp_id: m.id,
|
|
1997
|
+
...m.locations?.length ? { locations: m.locations } : {}
|
|
1629
1998
|
};
|
|
1630
1999
|
metas.menus[mid] = importMeta("published");
|
|
1631
2000
|
for (const i of m.items) {
|
|
@@ -1634,7 +2003,7 @@ function rawToContentrain(raw, opts) {
|
|
|
1634
2003
|
menu: mid,
|
|
1635
2004
|
order: i.order,
|
|
1636
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",
|
|
1637
|
-
wp_id: i.id,
|
|
2006
|
+
...i.id > 0 ? { wp_id: i.id } : {},
|
|
1638
2007
|
open_in_new_tab: i.target_attr === "_blank"
|
|
1639
2008
|
};
|
|
1640
2009
|
if (i.parent && !i.parent_unresolved) e.parent = itemRef(i.parent);
|