@composius/payload-plugin-import-wordpress 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1832 @@
1
+ // src/defaults.ts
2
+ var authenticated = ({ req: { user } }) => Boolean(user);
3
+ var defaultArticleUrl = (slug) => `${process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3000"}/articles/${slug ?? ""}`;
4
+ var JOBS_SLUG = "wp-import-jobs";
5
+ var RECORDS_SLUG = "wp-import-records";
6
+ var TASK_SLUG = "importWordpress";
7
+ var resolveOptions = (pluginOptions) => ({
8
+ access: {
9
+ create: pluginOptions.access?.create ?? authenticated,
10
+ delete: pluginOptions.access?.delete ?? authenticated,
11
+ read: pluginOptions.access?.read ?? authenticated,
12
+ update: pluginOptions.access?.update ?? authenticated
13
+ },
14
+ articleUrl: pluginOptions.articleUrl ?? defaultArticleUrl,
15
+ authorMapping: {
16
+ strategy: pluginOptions.authorMapping?.strategy ?? "users",
17
+ defaultUserId: pluginOptions.authorMapping?.defaultUserId,
18
+ syntheticEmailDomain: pluginOptions.authorMapping?.syntheticEmailDomain ?? "imported.invalid"
19
+ },
20
+ collections: {
21
+ articles: pluginOptions.collections?.articles ?? "articles",
22
+ categories: pluginOptions.collections?.categories ?? "categories",
23
+ media: pluginOptions.collections?.media ?? "media",
24
+ authors: pluginOptions.collections?.authors ?? "authors",
25
+ users: pluginOptions.collections?.users ?? "users"
26
+ },
27
+ dryRunPageLimit: pluginOptions.dryRunPageLimit ?? 1,
28
+ excerptToSeoDescription: pluginOptions.excerptToSeoDescription ?? true,
29
+ firstImageAsCover: pluginOptions.firstImageAsCover ?? true,
30
+ fieldMap: {
31
+ title: pluginOptions.fieldMap?.title ?? "title",
32
+ slug: pluginOptions.fieldMap?.slug ?? "slug",
33
+ content: pluginOptions.fieldMap?.content ?? "content",
34
+ coverImage: pluginOptions.fieldMap?.coverImage ?? "coverImage",
35
+ category: pluginOptions.fieldMap?.category ?? "category",
36
+ publishedAt: pluginOptions.fieldMap?.publishedAt ?? "publishedAt"
37
+ },
38
+ redirects: pluginOptions.redirects ?? true,
39
+ request: {
40
+ concurrency: pluginOptions.request?.concurrency ?? 5,
41
+ timeoutMs: pluginOptions.request?.timeoutMs ?? 3e4,
42
+ userAgent: pluginOptions.request?.userAgent
43
+ }
44
+ });
45
+
46
+ // src/translations/en.ts
47
+ var en = {
48
+ jobs: {
49
+ singular: "WordPress import",
50
+ plural: "WordPress imports",
51
+ tabs: {
52
+ configuration: "Configuration",
53
+ authors: "Authors",
54
+ categories: "Categories",
55
+ media: "Media",
56
+ posts: "Posts",
57
+ links: "Links & redirects",
58
+ report: "Report"
59
+ },
60
+ fields: {
61
+ sourceUrl: "WordPress site URL",
62
+ sourceUrlDescription: "Base URL of the WordPress site, e.g. https://example.com \u2014 its REST API (/wp-json) is read.",
63
+ credentials: "Credentials (optional)",
64
+ credentialsDescription: "Optional WordPress user + application password. When set, requests are authenticated so non-public data (e.g. author emails) can be imported.",
65
+ username: "Username",
66
+ applicationPassword: "Application password",
67
+ applicationPasswordDescription: "Create one in WordPress under Users \u2192 Profile \u2192 Application Passwords. Stored on this document \u2014 delete the job (or clear this field) after the import.",
68
+ dateFrom: "From date",
69
+ dateTo: "To date",
70
+ limit: "Max posts",
71
+ limitDescription: "Optional cap on the number of posts imported in this run.",
72
+ dryRun: "Dry run",
73
+ dryRunDescription: "Preview only: report what would be imported without writing anything. Limited to the first page(s).",
74
+ resume: "Resume / retry",
75
+ resumeDescription: "Re-queue this job to continue where it stopped (already-imported items are skipped).",
76
+ status: "Status",
77
+ progress: "Progress",
78
+ runs: "Run history",
79
+ authorsReport: "Imported authors",
80
+ categoriesReport: "Imported categories",
81
+ mediaReport: "Imported media",
82
+ postsReport: "Imported posts",
83
+ linksReport: "Link mapping",
84
+ errorsReport: "Errors",
85
+ startedAt: "Started at",
86
+ finishedAt: "Finished at"
87
+ },
88
+ status: {
89
+ queued: "Queued",
90
+ running: "Running",
91
+ paused: "Paused",
92
+ completed: "Completed",
93
+ failed: "Failed"
94
+ }
95
+ },
96
+ records: {
97
+ singular: "WordPress import record",
98
+ plural: "WordPress import records",
99
+ fields: {
100
+ job: "Import job",
101
+ site: "Site",
102
+ sourceType: "Source type",
103
+ sourceId: "Source ID",
104
+ sourceKey: "Source key",
105
+ targetCollection: "Target collection",
106
+ targetId: "Target ID",
107
+ status: "Status",
108
+ error: "Error"
109
+ }
110
+ }
111
+ };
112
+
113
+ // src/translations/fr.ts
114
+ var fr = {
115
+ jobs: {
116
+ singular: "Import WordPress",
117
+ plural: "Imports WordPress",
118
+ tabs: {
119
+ configuration: "Configuration",
120
+ authors: "Auteurs",
121
+ categories: "Cat\xE9gories",
122
+ media: "M\xE9dias",
123
+ posts: "Articles",
124
+ links: "Liens et redirections",
125
+ report: "Rapport"
126
+ },
127
+ fields: {
128
+ sourceUrl: "URL du site WordPress",
129
+ sourceUrlDescription: "URL de base du site WordPress, ex. https://example.com \u2014 son API REST (/wp-json) est lue.",
130
+ credentials: "Identifiants (optionnel)",
131
+ credentialsDescription: "Utilisateur WordPress + mot de passe d\u2019application optionnels. Une fois d\xE9finis, les requ\xEAtes sont authentifi\xE9es pour importer des donn\xE9es non publiques (ex. e-mails des auteurs).",
132
+ username: "Nom d\u2019utilisateur",
133
+ applicationPassword: "Mot de passe d\u2019application",
134
+ applicationPasswordDescription: "\xC0 cr\xE9er dans WordPress sous Utilisateurs \u2192 Profil \u2192 Mots de passe d\u2019application. Stock\xE9 sur ce document \u2014 supprimez le travail (ou videz ce champ) apr\xE8s l\u2019import.",
135
+ dateFrom: "\xC0 partir du",
136
+ dateTo: "Jusqu'au",
137
+ limit: "Nombre max. d\u2019articles",
138
+ limitDescription: "Limite optionnelle du nombre d\u2019articles import\xE9s lors de cette ex\xE9cution.",
139
+ dryRun: "Simulation",
140
+ dryRunDescription: "Aper\xE7u uniquement : indique ce qui serait import\xE9 sans rien \xE9crire. Limit\xE9 \xE0 la ou les premi\xE8res pages.",
141
+ resume: "Reprendre / r\xE9essayer",
142
+ resumeDescription: "Remettre ce travail en file pour continuer l\xE0 o\xF9 il s\u2019est arr\xEAt\xE9 (les \xE9l\xE9ments d\xE9j\xE0 import\xE9s sont ignor\xE9s).",
143
+ status: "Statut",
144
+ progress: "Progression",
145
+ runs: "Historique des ex\xE9cutions",
146
+ authorsReport: "Auteurs import\xE9s",
147
+ categoriesReport: "Cat\xE9gories import\xE9es",
148
+ mediaReport: "M\xE9dias import\xE9s",
149
+ postsReport: "Articles import\xE9s",
150
+ linksReport: "Correspondance des liens",
151
+ errorsReport: "Erreurs",
152
+ startedAt: "D\xE9marr\xE9 \xE0",
153
+ finishedAt: "Termin\xE9 \xE0"
154
+ },
155
+ status: {
156
+ queued: "En file",
157
+ running: "En cours",
158
+ paused: "En pause",
159
+ completed: "Termin\xE9",
160
+ failed: "\xC9chou\xE9"
161
+ }
162
+ },
163
+ records: {
164
+ singular: "Enregistrement d\u2019import WordPress",
165
+ plural: "Enregistrements d\u2019import WordPress",
166
+ fields: {
167
+ job: "Travail d\u2019import",
168
+ site: "Site",
169
+ sourceType: "Type de source",
170
+ sourceId: "ID source",
171
+ sourceKey: "Cl\xE9 source",
172
+ targetCollection: "Collection cible",
173
+ targetId: "ID cible",
174
+ status: "Statut",
175
+ error: "Erreur"
176
+ }
177
+ }
178
+ };
179
+
180
+ // src/translations/index.ts
181
+ var label = (pick) => ({
182
+ en: pick(en),
183
+ fr: pick(fr)
184
+ });
185
+
186
+ // src/collections/ImportJobs.ts
187
+ var queueImport = async (req, jobId) => {
188
+ const queue = req.payload.jobs.queue;
189
+ await queue({ input: { jobId }, req, task: TASK_SLUG });
190
+ };
191
+ var ImportJobs = ({ access }) => ({
192
+ slug: "wp-import-jobs",
193
+ labels: {
194
+ singular: label((t) => t.jobs.singular),
195
+ plural: label((t) => t.jobs.plural)
196
+ },
197
+ admin: {
198
+ useAsTitle: "sourceUrl",
199
+ defaultColumns: ["sourceUrl", "status", "dryRun", "updatedAt"],
200
+ group: "WordPress import"
201
+ },
202
+ access: {
203
+ read: access.read,
204
+ create: access.create,
205
+ update: access.update,
206
+ delete: access.delete
207
+ },
208
+ hooks: {
209
+ afterChange: [
210
+ async ({ context, doc, operation, req }) => {
211
+ if (context?.wpImport) {
212
+ return doc;
213
+ }
214
+ if (operation === "create") {
215
+ await queueImport(req, doc.id);
216
+ return doc;
217
+ }
218
+ if (operation === "update" && doc.resume) {
219
+ await req.payload.update({
220
+ collection: "wp-import-jobs",
221
+ id: doc.id,
222
+ data: { resume: false, status: "queued" },
223
+ context: { wpImport: true },
224
+ req
225
+ });
226
+ await queueImport(req, doc.id);
227
+ }
228
+ return doc;
229
+ }
230
+ ]
231
+ },
232
+ fields: [
233
+ // Unnamed tabs keep their fields at the top level of the document data —
234
+ // one tab per import step, so each step's outcome reads in its own tab.
235
+ {
236
+ type: "tabs",
237
+ tabs: [
238
+ {
239
+ label: label((t) => t.jobs.tabs.configuration),
240
+ fields: [
241
+ {
242
+ name: "sourceUrl",
243
+ type: "text",
244
+ label: label((t) => t.jobs.fields.sourceUrl),
245
+ required: true,
246
+ admin: {
247
+ description: label((t) => t.jobs.fields.sourceUrlDescription)
248
+ }
249
+ },
250
+ {
251
+ name: "credentials",
252
+ type: "group",
253
+ label: label((t) => t.jobs.fields.credentials),
254
+ admin: {
255
+ description: label((t) => t.jobs.fields.credentialsDescription)
256
+ },
257
+ fields: [
258
+ {
259
+ type: "row",
260
+ fields: [
261
+ {
262
+ name: "username",
263
+ type: "text",
264
+ label: label((t) => t.jobs.fields.username),
265
+ admin: { width: "50%" }
266
+ },
267
+ {
268
+ name: "applicationPassword",
269
+ type: "text",
270
+ label: label((t) => t.jobs.fields.applicationPassword),
271
+ admin: {
272
+ width: "50%",
273
+ description: label((t) => t.jobs.fields.applicationPasswordDescription),
274
+ components: {
275
+ // Masked (•••) input instead of plain text.
276
+ Field: "@composius/payload-plugin-import-wordpress/client#ApplicationPasswordFieldClient"
277
+ }
278
+ }
279
+ }
280
+ ]
281
+ }
282
+ ]
283
+ },
284
+ {
285
+ type: "row",
286
+ fields: [
287
+ {
288
+ name: "dateFrom",
289
+ type: "date",
290
+ label: label((t) => t.jobs.fields.dateFrom),
291
+ admin: { width: "50%", date: { pickerAppearance: "dayOnly" } }
292
+ },
293
+ {
294
+ name: "dateTo",
295
+ type: "date",
296
+ label: label((t) => t.jobs.fields.dateTo),
297
+ admin: { width: "50%", date: { pickerAppearance: "dayOnly" } }
298
+ }
299
+ ]
300
+ },
301
+ {
302
+ name: "limit",
303
+ type: "number",
304
+ label: label((t) => t.jobs.fields.limit),
305
+ min: 1,
306
+ admin: {
307
+ description: label((t) => t.jobs.fields.limitDescription)
308
+ }
309
+ },
310
+ {
311
+ name: "dryRun",
312
+ type: "checkbox",
313
+ label: label((t) => t.jobs.fields.dryRun),
314
+ defaultValue: false,
315
+ admin: {
316
+ description: label((t) => t.jobs.fields.dryRunDescription)
317
+ }
318
+ },
319
+ {
320
+ name: "resume",
321
+ type: "checkbox",
322
+ label: label((t) => t.jobs.fields.resume),
323
+ defaultValue: false,
324
+ admin: {
325
+ description: label((t) => t.jobs.fields.resumeDescription)
326
+ }
327
+ }
328
+ ]
329
+ },
330
+ {
331
+ label: label((t) => t.jobs.tabs.authors),
332
+ fields: [
333
+ {
334
+ name: "authorsReport",
335
+ type: "json",
336
+ label: label((t) => t.jobs.fields.authorsReport),
337
+ admin: { readOnly: true }
338
+ }
339
+ ]
340
+ },
341
+ {
342
+ label: label((t) => t.jobs.tabs.categories),
343
+ fields: [
344
+ {
345
+ name: "categoriesReport",
346
+ type: "json",
347
+ label: label((t) => t.jobs.fields.categoriesReport),
348
+ admin: { readOnly: true }
349
+ }
350
+ ]
351
+ },
352
+ {
353
+ label: label((t) => t.jobs.tabs.media),
354
+ fields: [
355
+ {
356
+ name: "mediaReport",
357
+ type: "json",
358
+ label: label((t) => t.jobs.fields.mediaReport),
359
+ admin: { readOnly: true }
360
+ }
361
+ ]
362
+ },
363
+ {
364
+ label: label((t) => t.jobs.tabs.posts),
365
+ fields: [
366
+ {
367
+ name: "postsReport",
368
+ type: "json",
369
+ label: label((t) => t.jobs.fields.postsReport),
370
+ admin: { readOnly: true }
371
+ }
372
+ ]
373
+ },
374
+ {
375
+ label: label((t) => t.jobs.tabs.links),
376
+ fields: [
377
+ {
378
+ name: "linksReport",
379
+ type: "json",
380
+ label: label((t) => t.jobs.fields.linksReport),
381
+ admin: { readOnly: true }
382
+ }
383
+ ]
384
+ },
385
+ {
386
+ label: label((t) => t.jobs.tabs.report),
387
+ fields: [
388
+ {
389
+ name: "runs",
390
+ type: "json",
391
+ label: label((t) => t.jobs.fields.runs),
392
+ admin: { readOnly: true }
393
+ },
394
+ {
395
+ name: "progress",
396
+ type: "json",
397
+ label: label((t) => t.jobs.fields.progress),
398
+ admin: { readOnly: true }
399
+ },
400
+ {
401
+ name: "errorsReport",
402
+ type: "json",
403
+ label: label((t) => t.jobs.fields.errorsReport),
404
+ admin: { readOnly: true }
405
+ }
406
+ ]
407
+ }
408
+ ]
409
+ },
410
+ {
411
+ name: "status",
412
+ type: "select",
413
+ label: label((t) => t.jobs.fields.status),
414
+ defaultValue: "queued",
415
+ options: [
416
+ { label: label((t) => t.jobs.status.queued), value: "queued" },
417
+ { label: label((t) => t.jobs.status.running), value: "running" },
418
+ { label: label((t) => t.jobs.status.paused), value: "paused" },
419
+ { label: label((t) => t.jobs.status.completed), value: "completed" },
420
+ { label: label((t) => t.jobs.status.failed), value: "failed" }
421
+ ],
422
+ admin: {
423
+ position: "sidebar",
424
+ readOnly: true
425
+ }
426
+ },
427
+ {
428
+ name: "startedAt",
429
+ type: "date",
430
+ label: label((t) => t.jobs.fields.startedAt),
431
+ admin: { position: "sidebar", readOnly: true, date: { pickerAppearance: "dayAndTime" } }
432
+ },
433
+ {
434
+ name: "finishedAt",
435
+ type: "date",
436
+ label: label((t) => t.jobs.fields.finishedAt),
437
+ admin: { position: "sidebar", readOnly: true, date: { pickerAppearance: "dayAndTime" } }
438
+ }
439
+ ]
440
+ });
441
+
442
+ // src/collections/ImportRecords.ts
443
+ var ImportRecords = ({ access }) => ({
444
+ slug: "wp-import-records",
445
+ labels: {
446
+ singular: label((t) => t.records.singular),
447
+ plural: label((t) => t.records.plural)
448
+ },
449
+ admin: {
450
+ useAsTitle: "sourceKey",
451
+ defaultColumns: ["site", "sourceType", "sourceId", "targetCollection", "targetId", "status"],
452
+ group: "WordPress import",
453
+ hidden: true
454
+ },
455
+ access: {
456
+ read: access.read,
457
+ create: access.create,
458
+ update: access.update,
459
+ delete: access.delete
460
+ },
461
+ indexes: [
462
+ { fields: ["site", "sourceType", "sourceId"] },
463
+ { fields: ["site", "sourceType", "sourceKey"] }
464
+ ],
465
+ fields: [
466
+ {
467
+ name: "job",
468
+ type: "relationship",
469
+ label: label((t) => t.records.fields.job),
470
+ relationTo: "wp-import-jobs"
471
+ },
472
+ {
473
+ name: "site",
474
+ type: "text",
475
+ label: label((t) => t.records.fields.site),
476
+ index: true,
477
+ required: true
478
+ },
479
+ {
480
+ name: "sourceType",
481
+ type: "select",
482
+ label: label((t) => t.records.fields.sourceType),
483
+ options: ["post", "category", "author", "media"],
484
+ index: true,
485
+ required: true
486
+ },
487
+ {
488
+ name: "sourceId",
489
+ type: "number",
490
+ label: label((t) => t.records.fields.sourceId),
491
+ index: true
492
+ },
493
+ {
494
+ name: "sourceKey",
495
+ type: "text",
496
+ label: label((t) => t.records.fields.sourceKey),
497
+ index: true
498
+ },
499
+ {
500
+ name: "targetCollection",
501
+ type: "text",
502
+ label: label((t) => t.records.fields.targetCollection)
503
+ },
504
+ {
505
+ name: "targetId",
506
+ type: "text",
507
+ label: label((t) => t.records.fields.targetId)
508
+ },
509
+ {
510
+ name: "status",
511
+ type: "select",
512
+ label: label((t) => t.records.fields.status),
513
+ defaultValue: "done",
514
+ options: ["done", "failed"]
515
+ },
516
+ {
517
+ name: "error",
518
+ type: "text",
519
+ label: label((t) => t.records.fields.error)
520
+ }
521
+ ]
522
+ });
523
+
524
+ // src/endpoints/index.ts
525
+ import { APIError } from "payload";
526
+ var assertAccess = async (access, operation, req) => {
527
+ const result = await access[operation]({ req });
528
+ if (!result) {
529
+ throw new APIError("Forbidden", 403);
530
+ }
531
+ };
532
+ var startEndpoint = (access) => ({
533
+ path: "/wp-import/start",
534
+ method: "post",
535
+ handler: async (req) => {
536
+ if (!req.user) {
537
+ throw new APIError("Unauthorized", 401);
538
+ }
539
+ await assertAccess(access, "create", req);
540
+ const body = await req.json?.() ?? {};
541
+ if (!body.sourceUrl) {
542
+ return Response.json({ error: "sourceUrl is required" }, { status: 400 });
543
+ }
544
+ const job = await req.payload.create({
545
+ collection: JOBS_SLUG,
546
+ data: {
547
+ dateFrom: body.dateFrom,
548
+ dateTo: body.dateTo,
549
+ dryRun: body.dryRun ?? false,
550
+ limit: body.limit,
551
+ sourceUrl: body.sourceUrl
552
+ },
553
+ overrideAccess: false,
554
+ req,
555
+ user: req.user
556
+ });
557
+ return Response.json({ jobId: job.id }, { status: 202 });
558
+ }
559
+ });
560
+ var statusEndpoint = (access) => ({
561
+ path: "/wp-import/status/:id",
562
+ method: "get",
563
+ handler: async (req) => {
564
+ if (!req.user) {
565
+ throw new APIError("Unauthorized", 401);
566
+ }
567
+ await assertAccess(access, "read", req);
568
+ const id = req.routeParams?.id;
569
+ const job = await req.payload.findByID({
570
+ collection: JOBS_SLUG,
571
+ id,
572
+ overrideAccess: false,
573
+ req,
574
+ user: req.user
575
+ });
576
+ return Response.json({
577
+ progress: job.progress,
578
+ report: {
579
+ authors: job.authorsReport,
580
+ categories: job.categoriesReport,
581
+ errors: job.errorsReport,
582
+ links: job.linksReport,
583
+ media: job.mediaReport,
584
+ posts: job.postsReport
585
+ },
586
+ runs: job.runs,
587
+ status: job.status
588
+ });
589
+ }
590
+ });
591
+
592
+ // src/lib/authors.ts
593
+ import crypto from "crypto";
594
+
595
+ // src/lib/id.ts
596
+ var coerceId = (id) => typeof id === "number" ? id : /^\d+$/.test(id) ? Number(id) : id;
597
+
598
+ // src/lib/url.ts
599
+ var hostOf = (url) => {
600
+ try {
601
+ return new URL(url).host.toLowerCase();
602
+ } catch {
603
+ return null;
604
+ }
605
+ };
606
+ var pathOf = (url) => {
607
+ try {
608
+ return new URL(url).pathname || "/";
609
+ } catch {
610
+ const [pathAndQuery] = url.split("#");
611
+ const [path] = pathAndQuery.split("?");
612
+ return path.startsWith("/") ? path : `/${path}`;
613
+ }
614
+ };
615
+ var isInternalUrl = (url, siteHost) => {
616
+ if (!url) {
617
+ return false;
618
+ }
619
+ if (url.startsWith("#") || url.startsWith("mailto:") || url.startsWith("tel:")) {
620
+ return false;
621
+ }
622
+ const host = hostOf(url);
623
+ if (host === null) {
624
+ return true;
625
+ }
626
+ return host === siteHost.toLowerCase();
627
+ };
628
+ var permalinkToSlug = (url) => {
629
+ const path = pathOf(url).replace(/\/+$/, "");
630
+ if (!path || path === "") {
631
+ return null;
632
+ }
633
+ const segments = path.split("/").filter(Boolean);
634
+ const last = segments[segments.length - 1];
635
+ return last ? decodeURIComponent(last) : null;
636
+ };
637
+ var deriveOriginalImageUrl = (url) => {
638
+ const [beforeHash] = url.split("#");
639
+ const [path, query] = beforeHash.split("?");
640
+ const original = path.replace(/-\d+x\d+(\.[a-zA-Z0-9]+)$/, "$1");
641
+ return query ? `${original}?${query}` : original;
642
+ };
643
+ var filenameOf = (url) => {
644
+ const path = pathOf(url);
645
+ const base = path.split("/").filter(Boolean).pop() ?? "image";
646
+ return decodeURIComponent(base) || "image";
647
+ };
648
+ var NAMED_ENTITIES = {
649
+ agrave: "\xE0",
650
+ amp: "&",
651
+ apos: "'",
652
+ bdquo: "\u201E",
653
+ bull: "\u2022",
654
+ ccedil: "\xE7",
655
+ copy: "\xA9",
656
+ deg: "\xB0",
657
+ eacute: "\xE9",
658
+ ecirc: "\xEA",
659
+ egrave: "\xE8",
660
+ gt: ">",
661
+ hellip: "\u2026",
662
+ laquo: "\xAB",
663
+ ldquo: "\u201C",
664
+ lsaquo: "\u2039",
665
+ lsquo: "\u2018",
666
+ lt: "<",
667
+ mdash: "\u2014",
668
+ middot: "\xB7",
669
+ nbsp: " ",
670
+ ndash: "\u2013",
671
+ quot: '"',
672
+ raquo: "\xBB",
673
+ rdquo: "\u201D",
674
+ reg: "\xAE",
675
+ rsaquo: "\u203A",
676
+ rsquo: "\u2019",
677
+ sbquo: "\u201A",
678
+ trade: "\u2122"
679
+ };
680
+ var decodeEntities = (value) => value.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&([a-zA-Z]+);/g, (match, name) => NAMED_ENTITIES[name.toLowerCase()] ?? match).trim();
681
+ var stripHtml = (html) => decodeEntities(html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ")).trim();
682
+
683
+ // src/lib/payloadOps.ts
684
+ var createDoc = (payload, args) => payload.create({ overrideAccess: true, ...args });
685
+ var findDocs = (payload, args) => payload.find({
686
+ overrideAccess: true,
687
+ ...args
688
+ });
689
+ var countDocs = (payload, args) => payload.count({
690
+ overrideAccess: true,
691
+ ...args
692
+ });
693
+ var findDoc = (payload, args) => payload.findByID({ overrideAccess: true, ...args });
694
+ var updateDoc = (payload, args) => payload.update({ overrideAccess: true, ...args });
695
+
696
+ // src/lib/records.ts
697
+ var findDoneRecord = async (payload, lookup) => {
698
+ const where = lookup.sourceType === "media" && lookup.sourceKey ? {
699
+ and: [
700
+ { site: { equals: lookup.site } },
701
+ { sourceType: { equals: "media" } },
702
+ { sourceKey: { equals: lookup.sourceKey } },
703
+ { status: { equals: "done" } }
704
+ ]
705
+ } : {
706
+ and: [
707
+ { site: { equals: lookup.site } },
708
+ { sourceType: { equals: lookup.sourceType } },
709
+ { sourceId: { equals: lookup.sourceId } },
710
+ { status: { equals: "done" } }
711
+ ]
712
+ };
713
+ const { docs } = await findDocs(payload, {
714
+ collection: RECORDS_SLUG,
715
+ where,
716
+ limit: 1,
717
+ depth: 0
718
+ });
719
+ const doc = docs[0];
720
+ return doc?.targetId != null ? String(doc.targetId) : null;
721
+ };
722
+ var saveRecord = async (payload, args) => {
723
+ await createDoc(payload, {
724
+ collection: RECORDS_SLUG,
725
+ data: {
726
+ job: args.jobId,
727
+ site: args.site,
728
+ sourceType: args.sourceType,
729
+ sourceId: args.sourceId ?? void 0,
730
+ sourceKey: args.sourceKey ?? void 0,
731
+ targetCollection: args.targetCollection,
732
+ targetId: args.targetId != null ? String(args.targetId) : void 0,
733
+ status: args.status ?? "done",
734
+ error: args.error
735
+ }
736
+ });
737
+ };
738
+
739
+ // src/lib/media.ts
740
+ var download = async (url, timeoutMs, fetchImpl) => {
741
+ const controller = new AbortController();
742
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
743
+ try {
744
+ const res = await fetchImpl(url, { signal: controller.signal });
745
+ if (!res.ok) {
746
+ throw new Error(`image download failed: ${res.status}`);
747
+ }
748
+ const arrayBuffer = await res.arrayBuffer();
749
+ const mimeType = res.headers.get("content-type")?.split(";")[0] || "image/jpeg";
750
+ return { buffer: Buffer.from(arrayBuffer), mimeType };
751
+ } finally {
752
+ clearTimeout(timer);
753
+ }
754
+ };
755
+ var importImage = async (payload, args) => {
756
+ const fetchImpl = args.fetchImpl ?? fetch;
757
+ const canonical = deriveOriginalImageUrl(args.url);
758
+ const cached = args.cache.get(canonical);
759
+ if (cached) {
760
+ return { ...cached, reused: true, uploaded: false };
761
+ }
762
+ const existing = await findDoneRecord(payload, {
763
+ site: args.site,
764
+ sourceKey: canonical,
765
+ sourceType: "media"
766
+ });
767
+ if (existing) {
768
+ const result = { mediaId: existing, reused: true, uploaded: false };
769
+ args.cache.set(canonical, result);
770
+ return result;
771
+ }
772
+ if (args.dryRun) {
773
+ const result = { mediaId: null, reused: false, uploaded: true };
774
+ args.cache.set(canonical, result);
775
+ return result;
776
+ }
777
+ try {
778
+ let downloaded;
779
+ try {
780
+ downloaded = await download(canonical, args.timeoutMs, fetchImpl);
781
+ } catch {
782
+ downloaded = await download(args.url, args.timeoutMs, fetchImpl);
783
+ }
784
+ const created = await createDoc(payload, {
785
+ collection: args.mediaSlug,
786
+ data: { alt: args.alt || filenameOf(canonical) },
787
+ file: {
788
+ data: downloaded.buffer,
789
+ mimetype: downloaded.mimeType,
790
+ name: filenameOf(canonical),
791
+ size: downloaded.buffer.length
792
+ }
793
+ });
794
+ const mediaId = String(created.id);
795
+ await saveRecord(payload, {
796
+ jobId: args.jobId,
797
+ site: args.site,
798
+ sourceId: args.sourceId ?? null,
799
+ sourceKey: canonical,
800
+ sourceType: "media",
801
+ targetCollection: args.mediaSlug,
802
+ targetId: mediaId
803
+ });
804
+ const result = { mediaId, reused: false, uploaded: true };
805
+ args.cache.set(canonical, result);
806
+ return result;
807
+ } catch (error) {
808
+ return {
809
+ error: error instanceof Error ? error.message : String(error),
810
+ mediaId: null,
811
+ reused: false,
812
+ uploaded: false
813
+ };
814
+ }
815
+ };
816
+
817
+ // src/lib/authors.ts
818
+ var avatarUrl = (wpUser) => {
819
+ const urls = wpUser.avatar_urls;
820
+ if (!urls) {
821
+ return void 0;
822
+ }
823
+ const sizes = Object.keys(urls).map(Number).filter((n) => !Number.isNaN(n)).sort((a, b) => b - a);
824
+ return sizes.length ? urls[String(sizes[0])] : void 0;
825
+ };
826
+ var resolveAuthor = async (payload, args) => {
827
+ if (args.strategy === "fixed") {
828
+ return {
829
+ author: args.defaultUserId != null ? { field: "editor", value: args.defaultUserId } : null
830
+ };
831
+ }
832
+ const wpUser = args.wpUser;
833
+ if (!wpUser) {
834
+ return { author: args.defaultUserId != null ? { field: "editor", value: args.defaultUserId } : null };
835
+ }
836
+ const name = decodeEntities(wpUser.name ?? wpUser.slug ?? `author-${wpUser.id}`);
837
+ const existing = await findDoneRecord(payload, {
838
+ site: args.site,
839
+ sourceId: wpUser.id,
840
+ sourceType: "author"
841
+ });
842
+ if (args.strategy === "users") {
843
+ const field2 = "editor";
844
+ if (existing) {
845
+ return { author: { field: field2, value: existing } };
846
+ }
847
+ const email = wpUser.email || (args.syntheticEmailDomain ? `${wpUser.slug || `author-${wpUser.id}`}@${args.syntheticEmailDomain}` : null);
848
+ if (!email) {
849
+ return {
850
+ author: args.defaultUserId != null ? { field: field2, value: args.defaultUserId } : null,
851
+ skippedNoEmail: { name, sourceId: wpUser.id }
852
+ };
853
+ }
854
+ if (args.dryRun) {
855
+ return { author: null, imported: { sourceId: wpUser.id, targetId: "dry-run", title: name } };
856
+ }
857
+ const { docs } = await findDocs(payload, {
858
+ collection: args.usersSlug,
859
+ where: { email: { equals: email } },
860
+ limit: 1,
861
+ depth: 0
862
+ });
863
+ let userId = docs[0]?.id;
864
+ if (!userId) {
865
+ const created2 = await createDoc(payload, {
866
+ collection: args.usersSlug,
867
+ data: {
868
+ email,
869
+ name,
870
+ password: crypto.randomBytes(18).toString("hex")
871
+ }
872
+ });
873
+ userId = created2.id;
874
+ }
875
+ await saveRecord(payload, {
876
+ jobId: args.jobId,
877
+ site: args.site,
878
+ sourceId: wpUser.id,
879
+ sourceType: "author",
880
+ targetCollection: args.usersSlug,
881
+ targetId: userId
882
+ });
883
+ return {
884
+ author: { field: field2, value: userId },
885
+ imported: { sourceId: wpUser.id, targetId: userId, title: name }
886
+ };
887
+ }
888
+ const field = "author";
889
+ if (existing) {
890
+ return { author: { field, value: existing } };
891
+ }
892
+ if (args.dryRun) {
893
+ return { author: null, imported: { sourceId: wpUser.id, targetId: "dry-run", title: name } };
894
+ }
895
+ const avatar = avatarUrl(wpUser);
896
+ let pictureId = null;
897
+ if (avatar) {
898
+ const result = await importImage(payload, {
899
+ cache: args.imageCache,
900
+ dryRun: args.dryRun,
901
+ fetchImpl: args.fetchImpl,
902
+ jobId: args.jobId,
903
+ mediaSlug: args.mediaSlug,
904
+ site: args.site,
905
+ timeoutMs: args.timeoutMs,
906
+ url: avatar
907
+ });
908
+ pictureId = result.mediaId;
909
+ }
910
+ const created = await createDoc(payload, {
911
+ collection: args.authorsSlug,
912
+ data: {
913
+ name,
914
+ ...pictureId ? { picture: coerceId(pictureId) } : {},
915
+ ...wpUser.description ? { biography: decodeEntities(wpUser.description) } : {}
916
+ }
917
+ });
918
+ const authorId = created.id;
919
+ await saveRecord(payload, {
920
+ jobId: args.jobId,
921
+ site: args.site,
922
+ sourceId: wpUser.id,
923
+ sourceType: "author",
924
+ targetCollection: args.authorsSlug,
925
+ targetId: authorId
926
+ });
927
+ return {
928
+ author: { field, value: authorId },
929
+ imported: { sourceId: wpUser.id, targetId: authorId, title: name }
930
+ };
931
+ };
932
+
933
+ // src/lib/categories.ts
934
+ var sortCategoriesParentsFirst = (categories) => {
935
+ const byId = new Map(categories.map((c) => [c.id, c]));
936
+ const sorted = [];
937
+ const placed = /* @__PURE__ */ new Set();
938
+ const place = (cat, stack) => {
939
+ if (placed.has(cat.id) || stack.has(cat.id)) {
940
+ return;
941
+ }
942
+ stack.add(cat.id);
943
+ const parent = cat.parent && cat.parent !== 0 ? byId.get(cat.parent) : void 0;
944
+ if (parent) {
945
+ place(parent, stack);
946
+ }
947
+ stack.delete(cat.id);
948
+ if (!placed.has(cat.id)) {
949
+ placed.add(cat.id);
950
+ sorted.push(cat);
951
+ }
952
+ };
953
+ for (const cat of categories) {
954
+ place(cat, /* @__PURE__ */ new Set());
955
+ }
956
+ return sorted;
957
+ };
958
+ var importCategories = async (payload, args) => {
959
+ const idMap = /* @__PURE__ */ new Map();
960
+ const imported = [];
961
+ const errors = [];
962
+ const ordered = sortCategoriesParentsFirst(args.categories);
963
+ for (const cat of ordered) {
964
+ try {
965
+ const existing = await findDoneRecord(payload, {
966
+ site: args.site,
967
+ sourceId: cat.id,
968
+ sourceType: "category"
969
+ });
970
+ if (existing) {
971
+ idMap.set(cat.id, coerceId(existing));
972
+ continue;
973
+ }
974
+ const name = decodeEntities(cat.name ?? cat.slug ?? `category-${cat.id}`);
975
+ if (args.dryRun) {
976
+ imported.push({ sourceId: cat.id, targetId: "dry-run", title: name });
977
+ continue;
978
+ }
979
+ const parentId = cat.parent && cat.parent !== 0 ? idMap.get(cat.parent) : void 0;
980
+ const created = await createDoc(payload, {
981
+ collection: args.categoriesSlug,
982
+ data: {
983
+ name,
984
+ ...cat.slug ? { slug: cat.slug } : {},
985
+ ...parentId != null ? { parent: coerceId(parentId) } : {},
986
+ ...cat.description ? { description: decodeEntities(cat.description) } : {}
987
+ }
988
+ });
989
+ const rawId = created.id;
990
+ idMap.set(cat.id, rawId);
991
+ imported.push({ sourceId: cat.id, targetId: rawId, title: name });
992
+ await saveRecord(payload, {
993
+ jobId: args.jobId,
994
+ site: args.site,
995
+ sourceId: cat.id,
996
+ sourceType: "category",
997
+ targetCollection: args.categoriesSlug,
998
+ targetId: rawId
999
+ });
1000
+ } catch (error) {
1001
+ errors.push({
1002
+ message: error instanceof Error ? error.message : String(error),
1003
+ scope: "category",
1004
+ sourceId: cat.id
1005
+ });
1006
+ }
1007
+ }
1008
+ return { errors, idMap, imported };
1009
+ };
1010
+
1011
+ // src/lib/content.ts
1012
+ import { convertHTMLToLexical } from "@payloadcms/richtext-lexical";
1013
+ import { JSDOM } from "jsdom";
1014
+
1015
+ // src/lib/lexical.ts
1016
+ var imageToken = (index) => `\u2063WPIMG:${index}\u2063`;
1017
+ var IMAGE_TOKEN_RE = /^⁣WPIMG:(\d+)⁣$/;
1018
+ var buildUploadNode = (value, relationTo) => ({
1019
+ type: "upload",
1020
+ fields: null,
1021
+ format: "",
1022
+ relationTo,
1023
+ value: coerceId(value),
1024
+ version: 3
1025
+ });
1026
+ var walk = (node, visit) => {
1027
+ visit(node);
1028
+ if (Array.isArray(node.children)) {
1029
+ for (const child of node.children) {
1030
+ walk(child, visit);
1031
+ }
1032
+ }
1033
+ };
1034
+ var soleTextTokenIndex = (node) => {
1035
+ if (node.type !== "paragraph" || !Array.isArray(node.children) || node.children.length !== 1) {
1036
+ return null;
1037
+ }
1038
+ const child = node.children[0];
1039
+ if (child.type !== "text" || typeof child.text !== "string") {
1040
+ return null;
1041
+ }
1042
+ const m = IMAGE_TOKEN_RE.exec(child.text.trim());
1043
+ return m ? Number(m[1]) : null;
1044
+ };
1045
+ var replaceImageTokens = (root, resolve, mediaSlug) => {
1046
+ const replaceIn = (parent) => {
1047
+ if (!Array.isArray(parent.children)) {
1048
+ return;
1049
+ }
1050
+ const next = [];
1051
+ for (const child of parent.children) {
1052
+ const index = soleTextTokenIndex(child);
1053
+ if (index !== null) {
1054
+ const mediaId = resolve(index);
1055
+ if (mediaId !== null && mediaId !== void 0) {
1056
+ next.push(buildUploadNode(mediaId, mediaSlug));
1057
+ }
1058
+ continue;
1059
+ }
1060
+ replaceIn(child);
1061
+ next.push(child);
1062
+ }
1063
+ parent.children = next;
1064
+ };
1065
+ replaceIn(root);
1066
+ return root;
1067
+ };
1068
+ var takeFirstUploadNode = (root) => {
1069
+ const search = (parent) => {
1070
+ if (!Array.isArray(parent.children)) {
1071
+ return null;
1072
+ }
1073
+ for (let i = 0; i < parent.children.length; i += 1) {
1074
+ const child = parent.children[i];
1075
+ if (child.type === "upload") {
1076
+ parent.children.splice(i, 1);
1077
+ return child.value ?? null;
1078
+ }
1079
+ const nested = search(child);
1080
+ if (nested !== null) {
1081
+ return nested;
1082
+ }
1083
+ }
1084
+ return null;
1085
+ };
1086
+ return search(root);
1087
+ };
1088
+ var isEmptyParagraph = (node) => node.type === "paragraph" && (!Array.isArray(node.children) || node.children.every((child) => child.type === "text" && !(child.text ?? "").trim()));
1089
+ var removeLeadingUploadNode = (root, value) => {
1090
+ const children = root.children;
1091
+ if (!Array.isArray(children)) {
1092
+ return false;
1093
+ }
1094
+ for (let i = 0; i < children.length; i += 1) {
1095
+ const child = children[i];
1096
+ if (isEmptyParagraph(child)) {
1097
+ continue;
1098
+ }
1099
+ if (child.type === "upload" && String(child.value) === String(value)) {
1100
+ children.splice(i, 1);
1101
+ return true;
1102
+ }
1103
+ return false;
1104
+ }
1105
+ return false;
1106
+ };
1107
+ var rewriteLinkNodes = (root, options) => {
1108
+ const { articleUrl, resolveInternal, siteHost } = options;
1109
+ const links = [];
1110
+ walk(root, (node) => {
1111
+ if (node.type !== "link" || !node.fields) {
1112
+ return;
1113
+ }
1114
+ const url = typeof node.fields.url === "string" ? node.fields.url : "";
1115
+ if (!url || !isInternalUrl(url, siteHost)) {
1116
+ return;
1117
+ }
1118
+ const slug = permalinkToSlug(url);
1119
+ const targetSlug = resolveInternal(url, slug);
1120
+ if (targetSlug) {
1121
+ const to = articleUrl(targetSlug);
1122
+ node.fields.url = to;
1123
+ node.fields.linkType = "custom";
1124
+ links.push({ action: "rewritten", from: url, to });
1125
+ } else {
1126
+ links.push({ action: "unresolved", from: url });
1127
+ }
1128
+ });
1129
+ return links;
1130
+ };
1131
+
1132
+ // src/lib/content.ts
1133
+ var buildContent = async (args) => {
1134
+ const dom = new JSDOM(`<!DOCTYPE html><body>${args.html}</body>`);
1135
+ const { document } = dom.window;
1136
+ const images = [];
1137
+ const tokenToMedia = /* @__PURE__ */ new Map();
1138
+ const imgs = Array.from(document.querySelectorAll("img"));
1139
+ let index = 0;
1140
+ for (const img of imgs) {
1141
+ const src = img.getAttribute("src") || "";
1142
+ if (!src) {
1143
+ img.remove();
1144
+ continue;
1145
+ }
1146
+ const result = await args.importContentImage(src);
1147
+ images.push({ result, src });
1148
+ const token = index;
1149
+ tokenToMedia.set(token, result.mediaId);
1150
+ index += 1;
1151
+ let target = img;
1152
+ const anchor = img.parentElement;
1153
+ if (anchor && anchor.tagName === "A" && anchor.childNodes.length === 1) {
1154
+ target = anchor;
1155
+ }
1156
+ const figure = target.closest("figure");
1157
+ if (figure) {
1158
+ target = figure;
1159
+ }
1160
+ const placeholder = document.createElement("p");
1161
+ placeholder.textContent = imageToken(token);
1162
+ target.replaceWith(placeholder);
1163
+ }
1164
+ const modifiedHtml = document.body.innerHTML;
1165
+ const state = convertHTMLToLexical({
1166
+ editorConfig: args.editorConfig,
1167
+ html: modifiedHtml,
1168
+ JSDOM
1169
+ });
1170
+ replaceImageTokens(state.root, (i) => tokenToMedia.get(i) ?? null, args.mediaSlug);
1171
+ const links = rewriteLinkNodes(state.root, {
1172
+ articleUrl: args.articleUrl,
1173
+ resolveInternal: args.resolveInternal,
1174
+ siteHost: args.siteHost
1175
+ });
1176
+ return { content: state, images, links };
1177
+ };
1178
+
1179
+ // src/lib/editorConfig.ts
1180
+ import { editorConfigFactory } from "@payloadcms/richtext-lexical";
1181
+ var findRichTextField = (fields, name) => {
1182
+ for (const field of fields) {
1183
+ if (field.type === "richText" && "name" in field && field.name === name) {
1184
+ return field;
1185
+ }
1186
+ if ("fields" in field && Array.isArray(field.fields)) {
1187
+ const nested = findRichTextField(field.fields, name);
1188
+ if (nested) {
1189
+ return nested;
1190
+ }
1191
+ }
1192
+ if (field.type === "tabs" && Array.isArray(field.tabs)) {
1193
+ for (const tab of field.tabs) {
1194
+ const nested = findRichTextField(tab.fields, name);
1195
+ if (nested) {
1196
+ return nested;
1197
+ }
1198
+ }
1199
+ }
1200
+ }
1201
+ return void 0;
1202
+ };
1203
+ var resolveEditorConfig = async (payload, articlesSlug, contentFieldName) => {
1204
+ const collections = payload.collections;
1205
+ const collection = collections[articlesSlug];
1206
+ const field = collection ? findRichTextField(collection.config.fields, contentFieldName) : void 0;
1207
+ if (field) {
1208
+ return editorConfigFactory.fromField({ field });
1209
+ }
1210
+ return editorConfigFactory.default({ config: payload.config });
1211
+ };
1212
+
1213
+ // src/lib/post.ts
1214
+ var selectPrimaryCategoryId = (post) => Array.isArray(post.categories) && post.categories.length > 0 ? post.categories[0] : null;
1215
+ var publishDate = (post) => {
1216
+ if (post.date_gmt) {
1217
+ const value = post.date_gmt.endsWith("Z") ? post.date_gmt : `${post.date_gmt}Z`;
1218
+ const date = new Date(value);
1219
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
1220
+ }
1221
+ if (post.date) {
1222
+ const date = new Date(post.date);
1223
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
1224
+ }
1225
+ return void 0;
1226
+ };
1227
+
1228
+ // src/lib/redirects.ts
1229
+ var createRedirect = async (payload, args) => {
1230
+ if (!payload.collections.redirects) {
1231
+ return false;
1232
+ }
1233
+ const { totalDocs } = await countDocs(payload, {
1234
+ collection: "redirects",
1235
+ where: { from: { equals: args.from } }
1236
+ });
1237
+ if (totalDocs > 0) {
1238
+ return false;
1239
+ }
1240
+ await createDoc(payload, {
1241
+ collection: "redirects",
1242
+ data: {
1243
+ from: args.from,
1244
+ to: {
1245
+ type: "reference",
1246
+ reference: { relationTo: args.articlesSlug, value: args.articleId }
1247
+ }
1248
+ }
1249
+ });
1250
+ return true;
1251
+ };
1252
+
1253
+ // src/lib/report.ts
1254
+ var emptyProgress = () => ({
1255
+ currentPhase: "queued",
1256
+ cursorPage: 0,
1257
+ importedAuthors: 0,
1258
+ importedCategories: 0,
1259
+ importedMedia: 0,
1260
+ importedPosts: 0,
1261
+ linksRewritten: 0,
1262
+ linksUnresolved: 0,
1263
+ redirectsCreated: 0,
1264
+ reusedMedia: 0,
1265
+ skippedPosts: 0,
1266
+ failedPosts: 0,
1267
+ totalPosts: 0
1268
+ });
1269
+ var emptyReport = (dryRun) => ({
1270
+ dryRun,
1271
+ errors: [],
1272
+ imported: { authors: [], categories: [], media: [], posts: [] },
1273
+ links: [],
1274
+ remaining: { posts: 0 }
1275
+ });
1276
+ var rehydrateReport = (job, dryRun) => {
1277
+ const previousRuns = Array.isArray(job.runs) ? job.runs : [];
1278
+ const dryRunNumbers = new Set(previousRuns.filter((r) => r.dryRun).map((r) => r.run));
1279
+ const keep = (items) => (items ?? []).filter((item) => item.run === void 0 || !dryRunNumbers.has(item.run));
1280
+ const report = emptyReport(dryRun);
1281
+ report.imported.authors = keep(job.authorsReport?.imported);
1282
+ report.imported.categories = keep(job.categoriesReport?.imported);
1283
+ report.imported.media = keep(job.mediaReport?.imported);
1284
+ report.imported.posts = keep(job.postsReport?.imported);
1285
+ report.links = keep(job.linksReport?.links);
1286
+ report.errors = keep(job.errorsReport?.errors);
1287
+ const runNumber = previousRuns.reduce((max, r) => Math.max(max, r.run), 0) + 1;
1288
+ return { previousRuns, report, runNumber };
1289
+ };
1290
+
1291
+ // src/lib/wpClient.ts
1292
+ var restBase = (siteUrl) => {
1293
+ const trimmed = siteUrl.trim().replace(/\/+$/, "");
1294
+ return `${trimmed}/wp-json/wp/v2`;
1295
+ };
1296
+ var WPRequestError = class extends Error {
1297
+ status;
1298
+ constructor(message, status) {
1299
+ super(message);
1300
+ this.name = "WPRequestError";
1301
+ this.status = status;
1302
+ }
1303
+ };
1304
+ var createWPClient = (siteUrl, request, fetchImpl = fetch) => {
1305
+ const base = restBase(siteUrl);
1306
+ const headers = { Accept: "application/json" };
1307
+ if (request.userAgent) {
1308
+ headers["User-Agent"] = request.userAgent;
1309
+ }
1310
+ const { applicationPassword, username } = request.credentials ?? {};
1311
+ const authenticated2 = Boolean(username && applicationPassword);
1312
+ if (authenticated2) {
1313
+ headers.Authorization = `Basic ${Buffer.from(`${username}:${applicationPassword}`).toString("base64")}`;
1314
+ }
1315
+ const get = async (path) => {
1316
+ const controller = new AbortController();
1317
+ const timer = setTimeout(() => controller.abort(), request.timeoutMs);
1318
+ try {
1319
+ const res = await fetchImpl(`${base}${path}`, { headers, signal: controller.signal });
1320
+ if (!res.ok) {
1321
+ throw new WPRequestError(`WordPress request failed: ${res.status} ${path}`, res.status);
1322
+ }
1323
+ const totalPages = Number(res.headers.get("X-WP-TotalPages") ?? "1") || 1;
1324
+ const body = await res.json();
1325
+ return { body, totalPages };
1326
+ } finally {
1327
+ clearTimeout(timer);
1328
+ }
1329
+ };
1330
+ return {
1331
+ authenticated: authenticated2,
1332
+ fetchPostsPage: async ({ after, before, page, perPage }) => {
1333
+ const params = new URLSearchParams({
1334
+ _embed: "1",
1335
+ page: String(page),
1336
+ per_page: String(perPage),
1337
+ status: "publish"
1338
+ });
1339
+ if (after) {
1340
+ params.set("after", after);
1341
+ }
1342
+ if (before) {
1343
+ params.set("before", before);
1344
+ }
1345
+ const { body, totalPages } = await get(`/posts?${params.toString()}`);
1346
+ return { posts: body ?? [], totalPages };
1347
+ },
1348
+ fetchCategories: async () => {
1349
+ const all = [];
1350
+ let page = 1;
1351
+ let totalPages = 1;
1352
+ do {
1353
+ const { body, totalPages: tp } = await get(`/categories?per_page=100&page=${page}`);
1354
+ all.push(...body ?? []);
1355
+ totalPages = tp;
1356
+ page += 1;
1357
+ } while (page <= totalPages);
1358
+ return all;
1359
+ },
1360
+ fetchMedia: async (id) => {
1361
+ try {
1362
+ const { body } = await get(`/media/${id}`);
1363
+ return body;
1364
+ } catch {
1365
+ return null;
1366
+ }
1367
+ },
1368
+ fetchUser: async (id) => {
1369
+ if (authenticated2) {
1370
+ try {
1371
+ const { body } = await get(`/users/${id}?context=edit`);
1372
+ return body;
1373
+ } catch {
1374
+ }
1375
+ }
1376
+ try {
1377
+ const { body } = await get(`/users/${id}`);
1378
+ return body;
1379
+ } catch {
1380
+ return null;
1381
+ }
1382
+ }
1383
+ };
1384
+ };
1385
+
1386
+ // src/jobs/runImport.ts
1387
+ var PER_PAGE = 100;
1388
+ var SEO_DESCRIPTION_MAX = 150;
1389
+ var runImport = async (payload, args) => {
1390
+ const { options } = args;
1391
+ const { collections: slugs } = options;
1392
+ const progress = emptyProgress();
1393
+ const job = await findDoc(payload, {
1394
+ collection: JOBS_SLUG,
1395
+ id: args.jobId,
1396
+ depth: 0
1397
+ });
1398
+ const dryRun = Boolean(job.dryRun);
1399
+ const { previousRuns, report, runNumber } = rehydrateReport(job, dryRun);
1400
+ const runSummary = {
1401
+ dryRun,
1402
+ run: runNumber,
1403
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1404
+ status: "running"
1405
+ };
1406
+ const runs = [...previousRuns, runSummary];
1407
+ const site = hostOf(job.sourceUrl) ?? job.sourceUrl;
1408
+ const imageCache = /* @__PURE__ */ new Map();
1409
+ const stepReports = () => ({
1410
+ authorsReport: { imported: report.imported.authors },
1411
+ categoriesReport: { imported: report.imported.categories },
1412
+ errorsReport: { dryRun: report.dryRun, errors: report.errors },
1413
+ linksReport: { links: report.links },
1414
+ mediaReport: { imported: report.imported.media, reused: progress.reusedMedia },
1415
+ postsReport: {
1416
+ dryRun: report.dryRun,
1417
+ imported: report.imported.posts,
1418
+ remaining: report.remaining.posts
1419
+ },
1420
+ runs
1421
+ });
1422
+ const updateJob = async (data) => {
1423
+ await updateDoc(payload, {
1424
+ collection: JOBS_SLUG,
1425
+ id: args.jobId,
1426
+ data,
1427
+ context: { wpImport: true }
1428
+ });
1429
+ };
1430
+ const accountImage = (result, sourceId) => {
1431
+ if (result.error) {
1432
+ report.errors.push({ message: result.error, run: runNumber, scope: "media", sourceId });
1433
+ } else if (result.uploaded) {
1434
+ progress.importedMedia += 1;
1435
+ report.imported.media.push({
1436
+ run: runNumber,
1437
+ sourceId,
1438
+ targetId: result.mediaId ?? "dry-run"
1439
+ });
1440
+ } else if (result.reused) {
1441
+ progress.reusedMedia += 1;
1442
+ }
1443
+ };
1444
+ try {
1445
+ progress.currentPhase = "running";
1446
+ await updateJob({
1447
+ status: "running",
1448
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1449
+ progress,
1450
+ ...stepReports()
1451
+ });
1452
+ const editorConfig = await resolveEditorConfig(payload, slugs.articles, options.fieldMap.content);
1453
+ const client = createWPClient(
1454
+ job.sourceUrl,
1455
+ {
1456
+ credentials: job.credentials,
1457
+ timeoutMs: options.request.timeoutMs,
1458
+ userAgent: options.request.userAgent
1459
+ },
1460
+ args.fetchImpl
1461
+ );
1462
+ progress.currentPhase = "categories";
1463
+ const wpCategories = await client.fetchCategories();
1464
+ const categoriesResult = await importCategories(payload, {
1465
+ categories: wpCategories,
1466
+ categoriesSlug: slugs.categories,
1467
+ dryRun,
1468
+ jobId: args.jobId,
1469
+ site
1470
+ });
1471
+ const categoryIdMap = categoriesResult.idMap;
1472
+ report.imported.categories.push(
1473
+ ...categoriesResult.imported.map((item) => ({ ...item, run: runNumber }))
1474
+ );
1475
+ report.errors.push(...categoriesResult.errors.map((error) => ({ ...error, run: runNumber })));
1476
+ progress.importedCategories = categoriesResult.imported.length;
1477
+ await updateJob({ progress, ...stepReports() });
1478
+ progress.currentPhase = "posts";
1479
+ const after = job.dateFrom ? new Date(job.dateFrom).toISOString() : void 0;
1480
+ const before = job.dateTo ? new Date(job.dateTo).toISOString() : void 0;
1481
+ const maxPages = dryRun ? options.dryRunPageLimit : Number.POSITIVE_INFINITY;
1482
+ const limit = job.limit ?? void 0;
1483
+ const gathered = [];
1484
+ let page = 1;
1485
+ let totalPages = 1;
1486
+ do {
1487
+ const { posts, totalPages: tp } = await client.fetchPostsPage({ after, before, page, perPage: PER_PAGE });
1488
+ totalPages = tp;
1489
+ gathered.push(...posts);
1490
+ progress.cursorPage = page;
1491
+ page += 1;
1492
+ } while (page <= totalPages && page <= maxPages && (!limit || gathered.length < limit));
1493
+ const postsToImport = limit ? gathered.slice(0, limit) : gathered;
1494
+ progress.totalPosts = postsToImport.length;
1495
+ const existingArticles = await findDocs(payload, {
1496
+ collection: slugs.articles,
1497
+ depth: 0,
1498
+ limit: 1e5,
1499
+ select: { slug: true }
1500
+ });
1501
+ const knownSlugs = new Set(
1502
+ existingArticles.docs.map((d) => d.slug).filter(Boolean)
1503
+ );
1504
+ const slugByPath = /* @__PURE__ */ new Map();
1505
+ for (const p of postsToImport) {
1506
+ if (p.slug) {
1507
+ knownSlugs.add(p.slug);
1508
+ if (p.link) {
1509
+ slugByPath.set(pathOf(p.link), p.slug);
1510
+ }
1511
+ }
1512
+ }
1513
+ const resolveInternal = (url, slug) => {
1514
+ const byPath = slugByPath.get(pathOf(url));
1515
+ if (byPath) {
1516
+ return byPath;
1517
+ }
1518
+ const candidate = slug ?? permalinkToSlug(url);
1519
+ return candidate && knownSlugs.has(candidate) ? candidate : null;
1520
+ };
1521
+ const authorDetailCache = /* @__PURE__ */ new Map();
1522
+ const enrichAuthor = async (embedded) => {
1523
+ if (!embedded || embedded.email || !client.authenticated) {
1524
+ return embedded;
1525
+ }
1526
+ if (!authorDetailCache.has(embedded.id)) {
1527
+ authorDetailCache.set(embedded.id, await client.fetchUser(embedded.id));
1528
+ }
1529
+ const detailed = authorDetailCache.get(embedded.id);
1530
+ return detailed?.email ? { ...embedded, email: detailed.email } : embedded;
1531
+ };
1532
+ for (const post of postsToImport) {
1533
+ const permalinkPath = post.link ? pathOf(post.link) : `/?p=${post.id}`;
1534
+ try {
1535
+ const already = await findDoneRecord(payload, { site, sourceId: post.id, sourceType: "post" });
1536
+ if (already) {
1537
+ progress.skippedPosts += 1;
1538
+ continue;
1539
+ }
1540
+ let coverId = null;
1541
+ let featured = post._embedded?.["wp:featuredmedia"]?.[0];
1542
+ if (!featured?.source_url && post.featured_media) {
1543
+ featured = await client.fetchMedia(post.featured_media) ?? void 0;
1544
+ }
1545
+ if (featured?.source_url) {
1546
+ const result = await importImage(payload, {
1547
+ alt: featured.alt_text,
1548
+ cache: imageCache,
1549
+ dryRun,
1550
+ fetchImpl: args.fetchImpl,
1551
+ jobId: args.jobId,
1552
+ mediaSlug: slugs.media,
1553
+ site,
1554
+ sourceId: featured.id,
1555
+ timeoutMs: options.request.timeoutMs,
1556
+ url: featured.source_url
1557
+ });
1558
+ coverId = result.mediaId;
1559
+ accountImage(result, featured.id);
1560
+ }
1561
+ const built = await buildContent({
1562
+ articleUrl: options.articleUrl,
1563
+ editorConfig,
1564
+ html: post.content?.rendered ?? "",
1565
+ importContentImage: (src) => importImage(payload, {
1566
+ cache: imageCache,
1567
+ dryRun,
1568
+ fetchImpl: args.fetchImpl,
1569
+ jobId: args.jobId,
1570
+ mediaSlug: slugs.media,
1571
+ site,
1572
+ timeoutMs: options.request.timeoutMs,
1573
+ url: src
1574
+ }),
1575
+ mediaSlug: slugs.media,
1576
+ resolveInternal,
1577
+ siteHost: site
1578
+ });
1579
+ for (const img of built.images) {
1580
+ accountImage(img.result, post.id);
1581
+ }
1582
+ report.links.push(...built.links.map((link) => ({ ...link, run: runNumber })));
1583
+ for (const link of built.links) {
1584
+ if (link.action === "rewritten") {
1585
+ progress.linksRewritten += 1;
1586
+ } else if (link.action === "unresolved") {
1587
+ progress.linksUnresolved += 1;
1588
+ }
1589
+ }
1590
+ if (!coverId && options.firstImageAsCover) {
1591
+ const promoted = takeFirstUploadNode(built.content.root);
1592
+ if (promoted != null) {
1593
+ coverId = String(promoted);
1594
+ }
1595
+ } else if (coverId) {
1596
+ removeLeadingUploadNode(built.content.root, coverId);
1597
+ }
1598
+ const {
1599
+ author,
1600
+ imported: importedAuthor,
1601
+ skippedNoEmail
1602
+ } = await resolveAuthor(payload, {
1603
+ authorsSlug: slugs.authors,
1604
+ defaultUserId: options.authorMapping.defaultUserId,
1605
+ dryRun,
1606
+ fetchImpl: args.fetchImpl,
1607
+ imageCache,
1608
+ jobId: args.jobId,
1609
+ mediaSlug: slugs.media,
1610
+ site,
1611
+ strategy: options.authorMapping.strategy,
1612
+ syntheticEmailDomain: options.authorMapping.syntheticEmailDomain,
1613
+ timeoutMs: options.request.timeoutMs,
1614
+ usersSlug: slugs.users,
1615
+ wpUser: await enrichAuthor(post._embedded?.author?.[0])
1616
+ });
1617
+ if (importedAuthor) {
1618
+ progress.importedAuthors += 1;
1619
+ report.imported.authors.push({ ...importedAuthor, run: runNumber });
1620
+ }
1621
+ if (skippedNoEmail && !report.errors.some(
1622
+ (e) => e.scope === "author" && e.sourceId === skippedNoEmail.sourceId
1623
+ )) {
1624
+ report.errors.push({
1625
+ message: `author "${skippedNoEmail.name}" has no email exposed by WordPress; user creation skipped (set authorMapping.syntheticEmailDomain or defaultUserId)`,
1626
+ run: runNumber,
1627
+ scope: "author",
1628
+ sourceId: skippedNoEmail.sourceId
1629
+ });
1630
+ }
1631
+ const title = decodeEntities(post.title?.rendered ?? post.slug ?? `post-${post.id}`);
1632
+ const primaryCategory = selectPrimaryCategoryId(post);
1633
+ const categoryId = primaryCategory != null ? categoryIdMap.get(primaryCategory) : void 0;
1634
+ const data = {
1635
+ [options.fieldMap.title]: title,
1636
+ [options.fieldMap.content]: built.content,
1637
+ _status: "published"
1638
+ };
1639
+ if (post.slug) {
1640
+ data[options.fieldMap.slug] = post.slug;
1641
+ }
1642
+ if (coverId) {
1643
+ data[options.fieldMap.coverImage] = coerceId(coverId);
1644
+ }
1645
+ if (categoryId != null) {
1646
+ data[options.fieldMap.category] = coerceId(categoryId);
1647
+ }
1648
+ const date = publishDate(post);
1649
+ if (date) {
1650
+ data[options.fieldMap.publishedAt] = date;
1651
+ }
1652
+ if (author) {
1653
+ data[author.field] = coerceId(author.value);
1654
+ }
1655
+ const meta = { title };
1656
+ if (coverId) {
1657
+ meta.image = coerceId(coverId);
1658
+ }
1659
+ if (options.excerptToSeoDescription && post.excerpt?.rendered) {
1660
+ meta.description = stripHtml(post.excerpt.rendered).slice(0, SEO_DESCRIPTION_MAX);
1661
+ }
1662
+ data.meta = meta;
1663
+ if (dryRun) {
1664
+ report.imported.posts.push({
1665
+ run: runNumber,
1666
+ slug: post.slug,
1667
+ sourceId: post.id,
1668
+ targetId: "dry-run",
1669
+ title
1670
+ });
1671
+ progress.importedPosts += 1;
1672
+ continue;
1673
+ }
1674
+ const created = await createDoc(payload, {
1675
+ collection: slugs.articles,
1676
+ data
1677
+ });
1678
+ const articleId = created.id;
1679
+ await saveRecord(payload, {
1680
+ jobId: args.jobId,
1681
+ site,
1682
+ sourceId: post.id,
1683
+ sourceKey: permalinkPath,
1684
+ sourceType: "post",
1685
+ targetCollection: slugs.articles,
1686
+ targetId: articleId
1687
+ });
1688
+ report.imported.posts.push({
1689
+ run: runNumber,
1690
+ slug: post.slug,
1691
+ sourceId: post.id,
1692
+ targetId: articleId,
1693
+ title
1694
+ });
1695
+ progress.importedPosts += 1;
1696
+ if (options.redirects) {
1697
+ const made = await createRedirect(payload, {
1698
+ articleId: coerceId(articleId),
1699
+ articlesSlug: slugs.articles,
1700
+ from: permalinkPath
1701
+ });
1702
+ if (made) {
1703
+ progress.redirectsCreated += 1;
1704
+ report.links.push({
1705
+ action: "redirect",
1706
+ from: permalinkPath,
1707
+ run: runNumber,
1708
+ to: options.articleUrl(post.slug)
1709
+ });
1710
+ }
1711
+ }
1712
+ if (progress.importedPosts % 5 === 0) {
1713
+ await updateJob({ progress, ...stepReports() });
1714
+ }
1715
+ } catch (error) {
1716
+ progress.failedPosts += 1;
1717
+ const message = error instanceof Error ? error.message : String(error);
1718
+ report.errors.push({ message, run: runNumber, scope: "post", sourceId: post.id });
1719
+ if (!dryRun) {
1720
+ await saveRecord(payload, {
1721
+ error: message,
1722
+ jobId: args.jobId,
1723
+ site,
1724
+ sourceId: post.id,
1725
+ sourceType: "post",
1726
+ status: "failed"
1727
+ }).catch(() => void 0);
1728
+ }
1729
+ }
1730
+ }
1731
+ report.remaining.posts = Math.max(0, gathered.length - postsToImport.length);
1732
+ progress.currentPhase = "done";
1733
+ runSummary.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1734
+ runSummary.progress = { ...progress };
1735
+ runSummary.status = "completed";
1736
+ await updateJob({
1737
+ finishedAt: runSummary.finishedAt,
1738
+ progress,
1739
+ status: "completed",
1740
+ ...stepReports()
1741
+ });
1742
+ return report;
1743
+ } catch (error) {
1744
+ const message = error instanceof Error ? error.message : String(error);
1745
+ report.errors.push({ message, run: runNumber, scope: "run" });
1746
+ progress.currentPhase = "failed";
1747
+ runSummary.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1748
+ runSummary.progress = { ...progress };
1749
+ runSummary.status = "failed";
1750
+ await updateJob({
1751
+ finishedAt: runSummary.finishedAt,
1752
+ progress,
1753
+ status: "failed",
1754
+ ...stepReports()
1755
+ });
1756
+ return report;
1757
+ }
1758
+ };
1759
+
1760
+ // src/jobs/task.ts
1761
+ var importWordpressTask = (options) => ({
1762
+ slug: TASK_SLUG,
1763
+ handler: async ({ input, req }) => {
1764
+ await runImport(req.payload, { jobId: input.jobId, options });
1765
+ return { output: {} };
1766
+ },
1767
+ inputSchema: [{ name: "jobId", type: "text", required: true }]
1768
+ });
1769
+
1770
+ // src/index.ts
1771
+ var LOG_PREFIX = "@composius/payload-plugin-import-wordpress";
1772
+ var ComposiusPayloadPluginImportWordpress = (pluginOptions = {}) => async (config) => {
1773
+ if (!config.collections) {
1774
+ config.collections = [];
1775
+ }
1776
+ const options = resolveOptions(pluginOptions);
1777
+ const warnings = [];
1778
+ config.collections.push(ImportJobs({ access: options.access }));
1779
+ config.collections.push(ImportRecords({ access: options.access }));
1780
+ config.jobs = {
1781
+ ...config.jobs,
1782
+ tasks: [...config.jobs?.tasks ?? [], importWordpressTask(options)]
1783
+ };
1784
+ if (pluginOptions.disabled) {
1785
+ return config;
1786
+ }
1787
+ config.endpoints = [
1788
+ ...config.endpoints ?? [],
1789
+ startEndpoint(options.access),
1790
+ statusEndpoint(options.access)
1791
+ ];
1792
+ if (pluginOptions.autoRun) {
1793
+ const schedule = typeof pluginOptions.autoRun === "object" ? pluginOptions.autoRun : {};
1794
+ const entry = { cron: schedule.cron ?? "* * * * *", queue: schedule.queue ?? "default" };
1795
+ const existing = config.jobs.autoRun;
1796
+ if (existing === void 0) {
1797
+ config.jobs.autoRun = [entry];
1798
+ } else if (Array.isArray(existing)) {
1799
+ config.jobs.autoRun = [...existing, entry];
1800
+ } else {
1801
+ warnings.push("`jobs.autoRun` is a function; skipping the plugin auto-run schedule.");
1802
+ }
1803
+ }
1804
+ if (options.redirects) {
1805
+ try {
1806
+ const { redirectsPlugin } = await import("@payloadcms/plugin-redirects");
1807
+ config = redirectsPlugin({
1808
+ collections: [options.collections.articles]
1809
+ })(config);
1810
+ } catch {
1811
+ warnings.push(
1812
+ "`redirects` is enabled but `@payloadcms/plugin-redirects` is not installed; redirects will be skipped."
1813
+ );
1814
+ }
1815
+ }
1816
+ if (warnings.length > 0) {
1817
+ const incomingOnInit = config.onInit;
1818
+ config.onInit = async (payload) => {
1819
+ if (incomingOnInit) {
1820
+ await incomingOnInit(payload);
1821
+ }
1822
+ for (const warning of warnings) {
1823
+ payload.logger.warn(`[${LOG_PREFIX}] ${warning}`);
1824
+ }
1825
+ };
1826
+ }
1827
+ return config;
1828
+ };
1829
+ export {
1830
+ ComposiusPayloadPluginImportWordpress
1831
+ };
1832
+ //# sourceMappingURL=index.js.map