wp-migrate-core 0.1.0-demo → 0.1.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/CHANGELOG.md +18 -0
- package/README.md +4 -3
- package/dist/src/adapters.js +1 -1
- package/dist/src/cli.js +25 -24
- package/dist/src/core.d.ts +1 -1
- package/dist/src/core.js +51 -12
- package/dist/src/generate.js +2 -2
- package/dist/src/report.js +2 -2
- package/dist/src/types.d.ts +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1] - 2026-09-03
|
|
4
|
+
|
|
5
|
+
This release makes inspection safer when an export or output path is not quite what the CLI expects.
|
|
6
|
+
|
|
7
|
+
- Writes the inspection plan and repair report as one staged handoff, then moves them into place together.
|
|
8
|
+
- Refuses an existing inspection output path without changing its files.
|
|
9
|
+
- Skips missing, invalid, and duplicate WordPress post IDs and explains each skipped item in the repair queue.
|
|
10
|
+
- Keeps malformed numeric XML entities intact instead of letting them crash WXR parsing.
|
|
11
|
+
- Adds regression coverage for the parser edge cases and the CLI's no-clobber behavior.
|
|
12
|
+
|
|
13
|
+
## [0.1.0-demo] - 2026-09-02
|
|
14
|
+
|
|
15
|
+
The first public demo: inspect a WordPress WXR export, surface unsupported migration work, and generate a deliberately private Astro handoff for human review.
|
|
16
|
+
|
|
17
|
+
[0.1.1]: https://github.com/lame13/wp-migrate-core/compare/v0.1.0-demo...v0.1.1
|
|
18
|
+
[0.1.0-demo]: https://github.com/lame13/wp-migrate-core/releases/tag/v0.1.0-demo
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wp-migrate-core
|
|
2
2
|
|
|
3
|
-
`wp-migrate-core` is an intentionally limited, local CLI prototype for inspecting a WordPress WXR export and creating an Astro migration handoff. Version `0.1.
|
|
3
|
+
`wp-migrate-core` is an intentionally limited, local CLI prototype for inspecting a WordPress WXR export and creating an Astro migration handoff. Version `0.1.1` uses npm's `demo` tag.
|
|
4
4
|
|
|
5
5
|
It can help expose what must be rebuilt. It does not produce a finished or production-ready WordPress replacement.
|
|
6
6
|
|
|
@@ -29,9 +29,9 @@ wp-migrate-core report <export.xml> [--out migration-report.html]
|
|
|
29
29
|
wp-migrate-core demo [--out wp-migrate-core-demo]
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
`inspect`
|
|
32
|
+
`inspect` validates the WXR file before writing. It creates `migration-plan.json` and a local repair report as a pair in a new output directory: if either output cannot be created, it leaves neither newly created file behind and never overwrites an existing `--out` path. `convert` writes an Astro-shaped project plus its migration manifest and repair report. `report` writes only the repair report. `demo` runs both inspection and conversion against the bundled fixture.
|
|
33
33
|
|
|
34
|
-
The `astro` target is the only enabled target
|
|
34
|
+
The `astro` target is the only enabled target. `next` and `nuxt` are registered as planned targets and deliberately fail when selected.
|
|
35
35
|
|
|
36
36
|
## What the demo handles
|
|
37
37
|
|
|
@@ -40,6 +40,7 @@ The `astro` target is the only enabled target in `0.1.0-demo`. `next` and `nuxt`
|
|
|
40
40
|
- A bounded subset of serialized Gutenberg core blocks.
|
|
41
41
|
- Elementor `_elementor_data` in WXR post metadata, including simple containers, headings, text, images, buttons, dividers, and spacers.
|
|
42
42
|
- An explicit repair queue for dynamic Gutenberg blocks, unsupported shortcodes, Elementor forms and queries, and unknown Elementor widgets.
|
|
43
|
+
- Items with missing, invalid, or duplicate `wp:post_id` values are skipped and added to the repair report. Correct the export before relying on the handoff.
|
|
43
44
|
|
|
44
45
|
Each detected construct is marked as `native`, `legacy-html`, `manual`, or `blocked`. Unsupported behavior stays visible in the generated output and repair queue; it is not counted as a successful conversion.
|
|
45
46
|
|
package/dist/src/adapters.js
CHANGED
|
@@ -6,6 +6,6 @@ export const targetAvailability = {
|
|
|
6
6
|
export function assertTargetEnabled(target) {
|
|
7
7
|
const availability = targetAvailability[target];
|
|
8
8
|
if (!availability.enabled) {
|
|
9
|
-
throw new Error(`${availability.label} is planned but
|
|
9
|
+
throw new Error(`${availability.label} is planned but not available in this release. Use --target astro.`);
|
|
10
10
|
}
|
|
11
11
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { lstat, mkdir, readFile,
|
|
3
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { assertTargetEnabled, targetAvailability } from "./adapters.js";
|
|
6
6
|
import { parseWxr } from "./core.js";
|
|
7
7
|
import { generateAstroProject } from "./generate.js";
|
|
8
8
|
import { writeReport } from "./report.js";
|
|
9
9
|
function usage() {
|
|
10
|
-
return `WP Migrate Core 0.1.
|
|
10
|
+
return `WP Migrate Core 0.1.1
|
|
11
11
|
|
|
12
12
|
Usage:
|
|
13
13
|
wp-migrate-core inspect <export.xml> [--out migration-plan] [--target astro]
|
|
@@ -16,9 +16,9 @@ Usage:
|
|
|
16
16
|
wp-migrate-core demo [--out wp-migrate-core-demo]
|
|
17
17
|
|
|
18
18
|
Targets:
|
|
19
|
-
astro implemented
|
|
20
|
-
next not implemented
|
|
21
|
-
nuxt not implemented
|
|
19
|
+
astro implemented
|
|
20
|
+
next planned, not implemented
|
|
21
|
+
nuxt planned, not implemented
|
|
22
22
|
|
|
23
23
|
This is a deliberately incomplete demonstration. It does not modify WordPress.`;
|
|
24
24
|
}
|
|
@@ -131,20 +131,11 @@ function isNodeErrorCode(error, code) {
|
|
|
131
131
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
132
132
|
}
|
|
133
133
|
function outputExistsError(outputPath) {
|
|
134
|
-
return new Error(`Refusing to overwrite existing output: ${outputPath}. Choose a different --out path or remove the existing
|
|
134
|
+
return new Error(`Refusing to overwrite existing output: ${outputPath}. Choose a different --out path or remove the existing path intentionally.`);
|
|
135
135
|
}
|
|
136
|
-
async function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!details.isDirectory()) {
|
|
140
|
-
throw new Error(`Inspect output path exists and is not a directory: ${directory}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
if (!isNodeErrorCode(error, "ENOENT"))
|
|
145
|
-
throw error;
|
|
146
|
-
await mkdir(directory, { recursive: true });
|
|
147
|
-
}
|
|
136
|
+
async function prepareInspectionOutput(directory) {
|
|
137
|
+
await mkdir(dirname(directory), { recursive: true });
|
|
138
|
+
await assertOutputDoesNotExist(directory);
|
|
148
139
|
}
|
|
149
140
|
async function assertOutputDoesNotExist(outputPath) {
|
|
150
141
|
try {
|
|
@@ -186,13 +177,23 @@ function printSummary(project) {
|
|
|
186
177
|
}
|
|
187
178
|
async function inspect(project, outputDirectory) {
|
|
188
179
|
const directory = resolve(outputDirectory);
|
|
180
|
+
await prepareInspectionOutput(directory);
|
|
181
|
+
// A sibling keeps the final rename on the same filesystem.
|
|
182
|
+
const stagingDirectory = await mkdtemp(resolve(dirname(directory), `.${basename(directory)}.staging-`));
|
|
183
|
+
try {
|
|
184
|
+
const stagingPlanPath = resolve(stagingDirectory, "migration-plan.json");
|
|
185
|
+
const stagingReportPath = resolve(stagingDirectory, "report.html");
|
|
186
|
+
await writeNewFile(stagingPlanPath, `${JSON.stringify(createMigrationPlan(project), null, 2)}\n`);
|
|
187
|
+
await writeReport(project, stagingReportPath, { noClobber: true });
|
|
188
|
+
await assertOutputDoesNotExist(directory);
|
|
189
|
+
await rename(stagingDirectory, directory);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
await rm(stagingDirectory, { recursive: true, force: true });
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
189
195
|
const planPath = resolve(directory, "migration-plan.json");
|
|
190
196
|
const reportPath = resolve(directory, "report.html");
|
|
191
|
-
await ensureInspectionDirectory(directory);
|
|
192
|
-
await assertOutputDoesNotExist(planPath);
|
|
193
|
-
await assertOutputDoesNotExist(reportPath);
|
|
194
|
-
await writeNewFile(planPath, `${JSON.stringify(createMigrationPlan(project), null, 2)}\n`);
|
|
195
|
-
await writeReport(project, reportPath, { noClobber: true });
|
|
196
197
|
printSummary(project);
|
|
197
198
|
console.log(`\nPlan: ${planPath}`);
|
|
198
199
|
console.log(`Report: ${reportPath}`);
|
package/dist/src/core.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { InspectOptions, MigrationProject } from "./types.js";
|
|
|
3
3
|
* Parse a bounded WXR export into a target-neutral migration model.
|
|
4
4
|
*
|
|
5
5
|
* This deliberately does not claim to be a complete XML or WordPress parser. It
|
|
6
|
-
* is dependency-free so
|
|
6
|
+
* is dependency-free so the conversion boundary stays visible.
|
|
7
7
|
*/
|
|
8
8
|
export declare function parseWxr(xml: string, options?: InspectOptions): MigrationProject;
|
|
9
9
|
export declare const inspectWxr: typeof parseWxr;
|
package/dist/src/core.js
CHANGED
|
@@ -82,11 +82,12 @@ const SAFE_ELEMENTOR_HREF_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
|
|
|
82
82
|
* Parse a bounded WXR export into a target-neutral migration model.
|
|
83
83
|
*
|
|
84
84
|
* This deliberately does not claim to be a complete XML or WordPress parser. It
|
|
85
|
-
* is dependency-free so
|
|
85
|
+
* is dependency-free so the conversion boundary stays visible.
|
|
86
86
|
*/
|
|
87
87
|
export function parseWxr(xml, options = {}) {
|
|
88
88
|
const records = [];
|
|
89
89
|
const projectIssues = [];
|
|
90
|
+
const seenWordPressIds = new Set();
|
|
90
91
|
const channelHeader = xml.slice(0, xml.search(/<item\b/i) === -1 ? xml.length : xml.search(/<item\b/i));
|
|
91
92
|
const itemPattern = /<item\b[^>]*>([\s\S]*?)<\/item>/gi;
|
|
92
93
|
let itemMatch;
|
|
@@ -101,19 +102,37 @@ export function parseWxr(xml, options = {}) {
|
|
|
101
102
|
continue;
|
|
102
103
|
}
|
|
103
104
|
const rawId = cleanField(readTag(itemXml, "wp:post_id"));
|
|
104
|
-
const wordpressId =
|
|
105
|
-
if (
|
|
105
|
+
const wordpressId = parseWordPressId(rawId);
|
|
106
|
+
if (wordpressId === undefined) {
|
|
107
|
+
const missingId = rawId === "";
|
|
106
108
|
projectIssues.push({
|
|
107
|
-
id: `project:
|
|
109
|
+
id: `project:${missingId ? "WXR_ITEM_MISSING_ID" : "WXR_ITEM_INVALID_ID"}:${projectIssues.length + 1}`,
|
|
108
110
|
severity: "warning",
|
|
109
|
-
code: "WXR_ITEM_MISSING_ID",
|
|
111
|
+
code: missingId ? "WXR_ITEM_MISSING_ID" : "WXR_ITEM_INVALID_ID",
|
|
110
112
|
sourceId: "wp:unknown",
|
|
111
|
-
title: "WordPress item is missing its ID",
|
|
112
|
-
message:
|
|
113
|
-
|
|
113
|
+
title: missingId ? "WordPress item is missing its ID" : "WordPress item has an invalid ID",
|
|
114
|
+
message: missingId
|
|
115
|
+
? `Skipped a ${postType} without a wp:post_id.`
|
|
116
|
+
: `Skipped a ${postType} because wp:post_id must be a positive integer.`,
|
|
117
|
+
requiredAction: missingId
|
|
118
|
+
? "Inspect the WXR export and restore the missing post identifier."
|
|
119
|
+
: "Inspect the WXR export and restore a positive integer post identifier."
|
|
114
120
|
});
|
|
115
121
|
continue;
|
|
116
122
|
}
|
|
123
|
+
if (seenWordPressIds.has(wordpressId)) {
|
|
124
|
+
projectIssues.push({
|
|
125
|
+
id: `project:WXR_ITEM_DUPLICATE_ID:${projectIssues.length + 1}`,
|
|
126
|
+
severity: "warning",
|
|
127
|
+
code: "WXR_ITEM_DUPLICATE_ID",
|
|
128
|
+
sourceId: `wp:${postType}:${wordpressId}`,
|
|
129
|
+
title: "WordPress item repeats an existing ID",
|
|
130
|
+
message: `Skipped a duplicate ${postType} with wp:post_id ${wordpressId}.`,
|
|
131
|
+
requiredAction: "Inspect the WXR export and resolve the duplicate post identifier before migration."
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
seenWordPressIds.add(wordpressId);
|
|
117
136
|
records.push(parseItem(itemXml, postType, status, wordpressId));
|
|
118
137
|
}
|
|
119
138
|
if (records.length === 0) {
|
|
@@ -330,7 +349,7 @@ function convertElementorElement(value, sourceId, collector, nextOrdinal) {
|
|
|
330
349
|
return [node];
|
|
331
350
|
}
|
|
332
351
|
const node = createMutableNode(nodeId, "elementor", widgetType, "unknown", "manual", settings, children);
|
|
333
|
-
collector.add("warning", "ELEMENTOR_WIDGET_UNKNOWN", `Elementor widget ${widgetType} has no
|
|
352
|
+
collector.add("warning", "ELEMENTOR_WIDGET_UNKNOWN", `Elementor widget ${widgetType} has no supported adapter in this release.`, "Replace it with an Astro component or preserve its rendered HTML.", { nodeId, evidence: widgetType });
|
|
334
353
|
return [node];
|
|
335
354
|
}
|
|
336
355
|
function scanShortcodes(content, sourceId, collector) {
|
|
@@ -376,7 +395,7 @@ function reportGutenbergCompatibility(node, collector) {
|
|
|
376
395
|
return;
|
|
377
396
|
}
|
|
378
397
|
if (!NATIVE_GUTENBERG_BLOCKS.has(node.sourceType)) {
|
|
379
|
-
collector.add("warning", "GUTENBERG_UNKNOWN_BLOCK", `Gutenberg block ${node.sourceType} has no
|
|
398
|
+
collector.add("warning", "GUTENBERG_UNKNOWN_BLOCK", `Gutenberg block ${node.sourceType} has no supported adapter in this release.`, "Add an adapter or preserve the block's rendered HTML.", { nodeId: node.id, evidence: node.sourceType });
|
|
380
399
|
}
|
|
381
400
|
}
|
|
382
401
|
function parseBlockAttributes(serialized, blockName, nodeId, collector) {
|
|
@@ -506,16 +525,36 @@ function cleanOptionalField(value) {
|
|
|
506
525
|
const cleaned = cleanField(value);
|
|
507
526
|
return cleaned === "" ? undefined : cleaned;
|
|
508
527
|
}
|
|
528
|
+
function parseWordPressId(value) {
|
|
529
|
+
if (!/^[0-9]+$/.test(value)) {
|
|
530
|
+
return undefined;
|
|
531
|
+
}
|
|
532
|
+
const wordpressId = Number(value);
|
|
533
|
+
return Number.isSafeInteger(wordpressId) && wordpressId > 0 ? wordpressId : undefined;
|
|
534
|
+
}
|
|
509
535
|
function decodeXmlEntities(value) {
|
|
510
536
|
return value
|
|
511
537
|
.replace(/</g, "<")
|
|
512
538
|
.replace(/>/g, ">")
|
|
513
539
|
.replace(/"/g, '"')
|
|
514
540
|
.replace(/'/g, "'")
|
|
515
|
-
.replace(/&#(\d+);/g, (
|
|
516
|
-
.replace(/&#x([0-9a-f]+);/gi, (
|
|
541
|
+
.replace(/&#(\d+);/g, (entity, decimal) => decodeNumericXmlEntity(entity, decimal, 10))
|
|
542
|
+
.replace(/&#x([0-9a-f]+);/gi, (entity, hexadecimal) => decodeNumericXmlEntity(entity, hexadecimal, 16))
|
|
517
543
|
.replace(/&/g, "&");
|
|
518
544
|
}
|
|
545
|
+
function decodeNumericXmlEntity(entity, value, radix) {
|
|
546
|
+
const codePoint = Number.parseInt(value, radix);
|
|
547
|
+
return isValidXmlCodePoint(codePoint) ? String.fromCodePoint(codePoint) : entity;
|
|
548
|
+
}
|
|
549
|
+
function isValidXmlCodePoint(value) {
|
|
550
|
+
return (Number.isSafeInteger(value) &&
|
|
551
|
+
(value === 0x9 ||
|
|
552
|
+
value === 0xa ||
|
|
553
|
+
value === 0xd ||
|
|
554
|
+
(value >= 0x20 && value <= 0xd7ff) ||
|
|
555
|
+
(value >= 0xe000 && value <= 0xfffd) ||
|
|
556
|
+
(value >= 0x10000 && value <= 0x10ffff)));
|
|
557
|
+
}
|
|
519
558
|
function htmlToText(value) {
|
|
520
559
|
return decodeXmlEntities(value.replace(/<[^>]*>/g, " ")).replace(/\s+/g, " ").trim();
|
|
521
560
|
}
|
package/dist/src/generate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, parse, resolve } from "node:path";
|
|
3
3
|
const GENERATOR_NAME = "wp-migrate-core";
|
|
4
|
-
const GENERATOR_VERSION = "0.1.
|
|
4
|
+
const GENERATOR_VERSION = "0.1.1";
|
|
5
5
|
const SAFE_ELEMENTOR_HREF_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
|
|
6
6
|
export async function generateAstroProject(project, outDir) {
|
|
7
7
|
const outputDirectory = await prepareOutputDirectory(outDir);
|
|
@@ -293,7 +293,7 @@ npm run dev
|
|
|
293
293
|
|
|
294
294
|
Generated content lives in \`src/content/pages\` and \`src/content/posts\`. Route mappings and source IDs live in \`migration/manifest.json\`.
|
|
295
295
|
|
|
296
|
-
Astro is the only enabled renderer in
|
|
296
|
+
Astro is the only enabled renderer in this handoff. Next.js and Nuxt appear in the migration manifest as planned, disabled targets; this output contains no fake compatibility layer for either framework.
|
|
297
297
|
`;
|
|
298
298
|
}
|
|
299
299
|
function renderContentRecord(generated) {
|
package/dist/src/report.js
CHANGED
|
@@ -86,7 +86,7 @@ function renderTargetCard(project, target) {
|
|
|
86
86
|
<span class="target-state target-state--${enabled ? "available" : "unavailable"}">${state}</span>
|
|
87
87
|
</div>
|
|
88
88
|
<p>${escapeHtml(targetDescriptions[target])}</p>
|
|
89
|
-
${enabled ? "" : '<span class="target-card__notice">Not implemented in
|
|
89
|
+
${enabled ? "" : '<span class="target-card__notice">Not implemented in this release</span>'}
|
|
90
90
|
</article>`;
|
|
91
91
|
}
|
|
92
92
|
function renderSourceBreakdown(project) {
|
|
@@ -646,7 +646,7 @@ export function renderReport(project) {
|
|
|
646
646
|
<header class="topbar">
|
|
647
647
|
<div class="topbar__inner">
|
|
648
648
|
<div class="product">WP Migrate Core</div>
|
|
649
|
-
<div class="report-meta">Local migration report · Version 0.1.
|
|
649
|
+
<div class="report-meta">Local migration report · Version 0.1.1</div>
|
|
650
650
|
</div>
|
|
651
651
|
</header>
|
|
652
652
|
|
package/dist/src/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface MigrationNode {
|
|
|
16
16
|
readonly rawHtml?: string;
|
|
17
17
|
}
|
|
18
18
|
export type MigrationIssueSeverity = "warning" | "blocker";
|
|
19
|
-
export type MigrationIssueCode = "WXR_NO_ITEMS" | "WXR_ITEM_MISSING_ID" | "GUTENBERG_UNCLOSED_BLOCK" | "GUTENBERG_UNMATCHED_CLOSE" | "GUTENBERG_INVALID_ATTRIBUTES" | "GUTENBERG_DYNAMIC_BLOCK" | "GUTENBERG_MEDIA_UNSUPPORTED" | "GUTENBERG_UNKNOWN_BLOCK" | "SHORTCODE_UNSUPPORTED" | "ELEMENTOR_INVALID_DATA" | "ELEMENTOR_FORM_UNSUPPORTED" | "ELEMENTOR_QUERY_UNSUPPORTED" | "ELEMENTOR_IMAGE_REMOTE_MEDIA" | "ELEMENTOR_BUTTON_UNSAFE_URL" | "ELEMENTOR_WIDGET_UNKNOWN";
|
|
19
|
+
export type MigrationIssueCode = "WXR_NO_ITEMS" | "WXR_ITEM_MISSING_ID" | "WXR_ITEM_INVALID_ID" | "WXR_ITEM_DUPLICATE_ID" | "GUTENBERG_UNCLOSED_BLOCK" | "GUTENBERG_UNMATCHED_CLOSE" | "GUTENBERG_INVALID_ATTRIBUTES" | "GUTENBERG_DYNAMIC_BLOCK" | "GUTENBERG_MEDIA_UNSUPPORTED" | "GUTENBERG_UNKNOWN_BLOCK" | "SHORTCODE_UNSUPPORTED" | "ELEMENTOR_INVALID_DATA" | "ELEMENTOR_FORM_UNSUPPORTED" | "ELEMENTOR_QUERY_UNSUPPORTED" | "ELEMENTOR_IMAGE_REMOTE_MEDIA" | "ELEMENTOR_BUTTON_UNSAFE_URL" | "ELEMENTOR_WIDGET_UNKNOWN";
|
|
20
20
|
export interface MigrationIssue {
|
|
21
21
|
readonly id: string;
|
|
22
22
|
readonly severity: MigrationIssueSeverity;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wp-migrate-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Turn a WordPress WXR export into an Astro handoff, with a repair queue for the parts that still need a human.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wordpress",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"wp-migrate-core": "dist/src/cli.js"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
+
"CHANGELOG.md",
|
|
28
29
|
"dist/src",
|
|
29
30
|
"fixtures/demo-wordpress.xml"
|
|
30
31
|
],
|