@caweb/cli 1.5.1 → 1.5.3
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/commands/blocks/create-block.js +8 -9
- package/commands/index.js +1 -1
- package/commands/sync/index.js +553 -0
- package/commands/sync/prompts.js +107 -0
- package/lib/cli.js +27 -7
- package/package.json +4 -2
- package/commands/sync.js +0 -402
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import fs from 'fs';
|
|
6
|
-
|
|
6
|
+
// since we are in a spinner,
|
|
7
|
+
// we have to silence all the cancellation errors when using prompts
|
|
8
|
+
// .catch(() => {process.exit(1);})
|
|
9
|
+
import { confirm } from '@inquirer/prompts';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Internal dependencies
|
|
@@ -49,14 +52,10 @@ export default async function createBlock({
|
|
|
49
52
|
if( fs.existsSync(path.resolve(process.cwd(), slug)) ){
|
|
50
53
|
spinner.info(`${slug} already exists.`)
|
|
51
54
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
message: 'Would you like to update it?',
|
|
57
|
-
default: false,
|
|
58
|
-
},
|
|
59
|
-
] );
|
|
55
|
+
const yesUpdate = await confirm({
|
|
56
|
+
message: 'Would you like to update it?',
|
|
57
|
+
default: false,
|
|
58
|
+
} ).catch(() => {process.exit(1);});
|
|
60
59
|
|
|
61
60
|
if( yesUpdate ){
|
|
62
61
|
spinner.text = `Updating ${slug}...`;
|
package/commands/index.js
CHANGED
|
@@ -23,7 +23,7 @@ const a11y = new A11yPlugin().a11yCheck;
|
|
|
23
23
|
|
|
24
24
|
import shell from './tasks/shell.js';
|
|
25
25
|
|
|
26
|
-
import sync from './sync.js';
|
|
26
|
+
import sync from './sync/index.js';
|
|
27
27
|
|
|
28
28
|
import updatePlugins from './tasks/update-plugins.js'
|
|
29
29
|
import createBlock from './blocks/create-block.js'
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import loadConfig from '@wordpress/env/lib/config/load-config.js';
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
|
|
9
|
+
// since we are in a spinner,
|
|
10
|
+
// we have to silence all the cancellation errors when using prompts
|
|
11
|
+
// .catch(() => {process.exit(1);})
|
|
12
|
+
import { confirm, input, password } from '@inquirer/prompts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Internal dependencies
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
appPath,
|
|
19
|
+
getTaxonomies,
|
|
20
|
+
createTaxonomies
|
|
21
|
+
} from '../../lib/index.js';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
promptGetInstanceInfo,
|
|
25
|
+
promptSaveInstanceInfo,
|
|
26
|
+
promptForSync,
|
|
27
|
+
promptForId
|
|
28
|
+
} from './prompts.js';
|
|
29
|
+
|
|
30
|
+
const errors = [];
|
|
31
|
+
const configFile = path.join(appPath, 'caweb.json');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return all parent items for a given set of ids.
|
|
35
|
+
*
|
|
36
|
+
* @async
|
|
37
|
+
* @param {Array} ids
|
|
38
|
+
* @param {*} request
|
|
39
|
+
* @param {string} [tax='pages']
|
|
40
|
+
* @returns {unknown}
|
|
41
|
+
*/
|
|
42
|
+
async function getParentItems( objects, request, tax = 'pages' ){
|
|
43
|
+
let parentItemsObjects = [];
|
|
44
|
+
let objectParentIds = objects.map((obj) => { return obj.parent; }).filter(n => n);
|
|
45
|
+
|
|
46
|
+
while( objectParentIds.length > 0 ){
|
|
47
|
+
|
|
48
|
+
// if we have parent ids, we have to collect any parent items.
|
|
49
|
+
let parentItems = await getTaxonomies({
|
|
50
|
+
...request,
|
|
51
|
+
orderby: 'parent',
|
|
52
|
+
embed: true,
|
|
53
|
+
include: objectParentIds
|
|
54
|
+
}, tax);
|
|
55
|
+
|
|
56
|
+
// if we have parent items, we have to add to the items array.
|
|
57
|
+
if( parentItems ){
|
|
58
|
+
parentItemsObjects = parentItemsObjects.concat( parentItems );
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// if the parent items have parent ids, we have to save those for the next run.
|
|
62
|
+
objectParentIds = parentItems.map((obj) => { return obj.parent; }).filter((id) => { return id !== 0; })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return objects.concat( parentItemsObjects ).sort( (a,b) => a.parent - b.parent );
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get information for an instance.
|
|
71
|
+
*
|
|
72
|
+
* @async
|
|
73
|
+
* @param {string} [instance='target']
|
|
74
|
+
* @returns {Object}
|
|
75
|
+
*/
|
|
76
|
+
async function getInstanceInfo( config, instance = 'target'){
|
|
77
|
+
// ask for target information to be entered.
|
|
78
|
+
const inputTarget = await confirm(
|
|
79
|
+
{
|
|
80
|
+
message: `A ${instance} instance was not specified, would you like to enter the information?`,
|
|
81
|
+
default: true
|
|
82
|
+
}
|
|
83
|
+
).catch(() => {process.exit(1);});
|
|
84
|
+
|
|
85
|
+
// if user said yes
|
|
86
|
+
if( inputTarget ){
|
|
87
|
+
let target = await promptGetInstanceInfo();
|
|
88
|
+
|
|
89
|
+
// ask to save information
|
|
90
|
+
let nickname = await promptSaveInstanceInfo()
|
|
91
|
+
|
|
92
|
+
if( nickname ){
|
|
93
|
+
config = config ?? {};
|
|
94
|
+
|
|
95
|
+
// add the target to sync list
|
|
96
|
+
config.sync[nickname] = target;
|
|
97
|
+
|
|
98
|
+
fs.writeFileSync(
|
|
99
|
+
configFile,
|
|
100
|
+
JSON.stringify(config, null, 4)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return target;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sync Environments.
|
|
110
|
+
*
|
|
111
|
+
* @see https://developer.wordpress.org/rest-api/reference/
|
|
112
|
+
*
|
|
113
|
+
* @param {Object} options
|
|
114
|
+
* @param {Object} options.spinner A CLI spinner which indicates progress.
|
|
115
|
+
* @param {boolean} options.target Remote Site URL with current changes.
|
|
116
|
+
* @param {boolean} options.dest Destination Site URL that should be synced.
|
|
117
|
+
* @param {boolean} options.debug True if debug mode is enabled.
|
|
118
|
+
* @param {Array} options.tax Taxonomy that should be synced.
|
|
119
|
+
* @param {Array} options.include Include specific IDs only.
|
|
120
|
+
*/
|
|
121
|
+
export default async function sync({
|
|
122
|
+
spinner,
|
|
123
|
+
debug,
|
|
124
|
+
target,
|
|
125
|
+
dest,
|
|
126
|
+
interactive,
|
|
127
|
+
tax,
|
|
128
|
+
mediaIds,
|
|
129
|
+
menuIds,
|
|
130
|
+
pageIds,
|
|
131
|
+
postIds
|
|
132
|
+
} ) {
|
|
133
|
+
|
|
134
|
+
// this gets the working directory path after wp-env validation
|
|
135
|
+
const {workDirectoryPath} = await loadConfig(path.resolve('.'));
|
|
136
|
+
|
|
137
|
+
// read caweb configuration file.
|
|
138
|
+
let serviceConfig = fs.existsSync(configFile) ? JSON.parse( fs.readFileSync(configFile) ) : {};
|
|
139
|
+
|
|
140
|
+
process.env.WP_CLI_CONFIG_PATH = path.join(workDirectoryPath, 'config.yml');
|
|
141
|
+
|
|
142
|
+
// get target and dest instance data
|
|
143
|
+
target = serviceConfig.sync[target];
|
|
144
|
+
dest = serviceConfig.sync[dest];
|
|
145
|
+
|
|
146
|
+
// if no target was specified or no target saved.
|
|
147
|
+
if( ! target ){
|
|
148
|
+
spinner.stop()
|
|
149
|
+
|
|
150
|
+
target = await getInstanceInfo(serviceConfig, 'target')
|
|
151
|
+
|
|
152
|
+
// if still no target then exit
|
|
153
|
+
if( ! target ){
|
|
154
|
+
process.exit(1)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
// if no dest was specified or no dest saved.
|
|
160
|
+
if( ! dest ){
|
|
161
|
+
spinner.stop()
|
|
162
|
+
|
|
163
|
+
dest = await getInstanceInfo(serviceConfig, 'destination');
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
// if still no target then exit
|
|
167
|
+
if( ! dest ){
|
|
168
|
+
process.exit(1)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* each instance has to have a url, user, pwd property
|
|
174
|
+
*/
|
|
175
|
+
if(
|
|
176
|
+
! target || ! target.url || ! target.user || ! target.pwd ||
|
|
177
|
+
! dest || ! dest.url || ! dest.user || ! dest.pwd
|
|
178
|
+
){
|
|
179
|
+
spinner.fail(`caweb.json is not configured properly for ${target} and ${dest}.`);
|
|
180
|
+
process.exit(1)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let targetOptions = {
|
|
184
|
+
url: target.url,
|
|
185
|
+
headers: {
|
|
186
|
+
Authorization: 'Basic ' + Buffer.from(`${target.user}:${target.pwd}`).toString('base64')
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
let destOptions = {
|
|
190
|
+
url: dest.url,
|
|
191
|
+
headers: {
|
|
192
|
+
Authorization: 'Basic ' + Buffer.from(`${dest.user}:${dest.pwd}`).toString('base64'),
|
|
193
|
+
'content-type': 'multipart/form-data',
|
|
194
|
+
'accept': '*/*'
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Sync Process
|
|
200
|
+
* If taxonomy is undefined then we don't sync that section.
|
|
201
|
+
*
|
|
202
|
+
* 1) We collect media if the taxonomy is undefined or includes media, pages, posts.
|
|
203
|
+
* 2) We only collect settings if the taxonomy is undefined or it's set to settings.
|
|
204
|
+
* 3) We only collect pages if the taxonomy is undefined or it's set to pages.
|
|
205
|
+
* 4) We only collect posts if the taxonomy is undefined or it's set to posts.
|
|
206
|
+
* 5) We only collect menus if the taxonomy is undefined or it's set to menus.
|
|
207
|
+
* - We also collect menu items if menus are collected.
|
|
208
|
+
*/
|
|
209
|
+
let settings = [];
|
|
210
|
+
let mediaLibrary = [];
|
|
211
|
+
let media = [];
|
|
212
|
+
let pages = [];
|
|
213
|
+
let posts = [];
|
|
214
|
+
let menus = [];
|
|
215
|
+
let menuNavItems = [];
|
|
216
|
+
|
|
217
|
+
// Run prompts if interactive
|
|
218
|
+
if( interactive ){
|
|
219
|
+
spinner.stop();
|
|
220
|
+
tax = await promptForSync(tax);
|
|
221
|
+
|
|
222
|
+
let inputtedMediaIds = tax.includes('media') ? await promptForId('Media') : '';
|
|
223
|
+
let inputtedMenuIds = tax.includes('menus') ? await promptForId('Menus') : '';
|
|
224
|
+
let inputtedPageIds = tax.includes('pages') ? await promptForId('Pages') : '';
|
|
225
|
+
let inputtedPostIds = tax.includes('posts') ? await promptForId('Posts') : '';
|
|
226
|
+
|
|
227
|
+
// if inputted ids is all then we dont need ids,
|
|
228
|
+
inputtedMediaIds = 'all' === inputtedMediaIds ? [] : inputtedMediaIds.split(',');
|
|
229
|
+
inputtedMenuIds = 'all' === inputtedMenuIds ? [] : inputtedMenuIds.split(',');
|
|
230
|
+
inputtedPageIds = 'all' === inputtedPageIds ? [] : inputtedPageIds.split(',');
|
|
231
|
+
inputtedPostIds = 'all' === inputtedPostIds ? [] : inputtedPostIds.split(',');
|
|
232
|
+
|
|
233
|
+
mediaIds = mediaIds ? mediaIds.concat(inputtedMediaIds) : inputtedMediaIds;
|
|
234
|
+
menuIds = menuIds ? menuIds.concat(inputtedMenuIds) : inputtedMenuIds;
|
|
235
|
+
pageIds = pageIds ? pageIds.concat(inputtedPageIds) : inputtedPageIds;
|
|
236
|
+
postIds = postIds ? postIds.concat(inputtedPostIds) : inputtedPostIds;
|
|
237
|
+
|
|
238
|
+
spinner.start()
|
|
239
|
+
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Media Library.
|
|
243
|
+
if( tax.includes('media', 'pages', 'posts') ){
|
|
244
|
+
spinner.text = `Collecting Media Library ${target.url}`;
|
|
245
|
+
mediaLibrary = await getTaxonomies({
|
|
246
|
+
...targetOptions,
|
|
247
|
+
fields: [
|
|
248
|
+
'id',
|
|
249
|
+
'source_url',
|
|
250
|
+
'title',
|
|
251
|
+
'caption',
|
|
252
|
+
'alt_text',
|
|
253
|
+
'date',
|
|
254
|
+
'mime_type',
|
|
255
|
+
'post',
|
|
256
|
+
'media_details'
|
|
257
|
+
],
|
|
258
|
+
include: mediaIds && mediaIds.length ? mediaIds.join(',') : null
|
|
259
|
+
},
|
|
260
|
+
'media'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Site Settings.
|
|
265
|
+
if( tax.includes('settings') ){
|
|
266
|
+
spinner.text = `Collecting Site Settings from ${target.url}`;
|
|
267
|
+
settings = await getTaxonomies({
|
|
268
|
+
...targetOptions,
|
|
269
|
+
fields: [
|
|
270
|
+
'title',
|
|
271
|
+
'description',
|
|
272
|
+
'show_on_front',
|
|
273
|
+
'page_on_front',
|
|
274
|
+
'posts_per_page'
|
|
275
|
+
],
|
|
276
|
+
}, 'settings')
|
|
277
|
+
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Pages.
|
|
281
|
+
if( tax.includes('pages') ){
|
|
282
|
+
// get all pages/posts
|
|
283
|
+
spinner.text = `Collecting pages from ${target.url}`;
|
|
284
|
+
pages = await getTaxonomies({
|
|
285
|
+
...targetOptions,
|
|
286
|
+
orderby: 'parent',
|
|
287
|
+
embed: true,
|
|
288
|
+
include: pageIds && pageIds.length ? pageIds.join(',') : null
|
|
289
|
+
},
|
|
290
|
+
'pages'
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// pages can be nested so we have to collect any parent items.
|
|
294
|
+
pages = await getParentItems(
|
|
295
|
+
pages,
|
|
296
|
+
targetOptions,
|
|
297
|
+
'pages'
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Posts.
|
|
302
|
+
if( tax.includes('posts') ){
|
|
303
|
+
// get all pages/posts
|
|
304
|
+
spinner.text = `Collecting all posts from ${target.url}`;
|
|
305
|
+
posts = await getTaxonomies({
|
|
306
|
+
...targetOptions,
|
|
307
|
+
orderby: 'parent',
|
|
308
|
+
include: postIds && postIds.length ? postIds.join(',') : null
|
|
309
|
+
},
|
|
310
|
+
'posts'
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// posts can be nested so we have to collect any parent items.
|
|
314
|
+
posts = await getParentItems(
|
|
315
|
+
posts,
|
|
316
|
+
targetOptions,
|
|
317
|
+
'posts'
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Media Library Handling
|
|
323
|
+
*
|
|
324
|
+
* We iterate thru the media library to identify which media should be synced.
|
|
325
|
+
* 1) attached media items by default are only possible on pages.
|
|
326
|
+
* 2) featured media by default are only possible on posts.
|
|
327
|
+
* 3) save any linked media in the content of pages and posts.
|
|
328
|
+
*/
|
|
329
|
+
for( let m of mediaLibrary ){
|
|
330
|
+
// remove any new line characters.
|
|
331
|
+
m.source_url = m.source_url.replace('\n','');
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* We collect the media if:
|
|
335
|
+
* - media is attached
|
|
336
|
+
* - id was explicitly requested
|
|
337
|
+
* - tax includes media and media ids is blank
|
|
338
|
+
*/
|
|
339
|
+
if(
|
|
340
|
+
m.post ||
|
|
341
|
+
( mediaIds && mediaIds.includes( m.id.toString() ) ) ||
|
|
342
|
+
( tax.includes('media') && undefined === mediaIds )
|
|
343
|
+
){
|
|
344
|
+
media.push( m );
|
|
345
|
+
|
|
346
|
+
// we don't have to check any further.
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for( let p of [].concat( pages, posts ) ){
|
|
351
|
+
/**
|
|
352
|
+
* We collect the media if:
|
|
353
|
+
* - media is featured image
|
|
354
|
+
* - media is the src attribute in the content
|
|
355
|
+
*/
|
|
356
|
+
if( p.featured_media === m.id ||
|
|
357
|
+
p.content.rendered.match( new RegExp(`src=”(${m.source_url}.*)”`, 'g') )){
|
|
358
|
+
media.push( m );
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// filter any duplicate media.
|
|
364
|
+
media = media.filter((m, index, self) => { return index === self.findIndex((t) => { return t.id === m.id; })} );
|
|
365
|
+
let i = 0;
|
|
366
|
+
|
|
367
|
+
// before we can upload media files we have to generate the media blob data.
|
|
368
|
+
for( let m of media ){
|
|
369
|
+
if( debug ){
|
|
370
|
+
i++;
|
|
371
|
+
spinner.info(`Media ID ${m.id} Collected: ${i}/${media.length}`)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const mediaBlob = await axios.request(
|
|
375
|
+
{
|
|
376
|
+
...targetOptions,
|
|
377
|
+
url: m.source_url,
|
|
378
|
+
responseType: 'arraybuffer'
|
|
379
|
+
}
|
|
380
|
+
)
|
|
381
|
+
.then( (img) => { return new Blob([img.data]) })
|
|
382
|
+
.catch( (error) => {
|
|
383
|
+
errors.push(`${m.source_url} could not be downloaded.`)
|
|
384
|
+
return false;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
if( mediaBlob ){
|
|
388
|
+
m.data = mediaBlob;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// this has to be done after we have the media data.
|
|
393
|
+
// Lets replace the url references in the content of the pages and posts.
|
|
394
|
+
for( let p of [].concat( pages, posts ) ){
|
|
395
|
+
p.content.rendered = p.content.rendered.replace( new RegExp(target.url, 'g'), dest.url );
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
// Menu and Nav Items.
|
|
400
|
+
if( tax.includes('menus')){
|
|
401
|
+
spinner.text = `Collecting assigned navigation menus from ${target.url}`;
|
|
402
|
+
// get all menus and navigation links
|
|
403
|
+
menus = await getTaxonomies({
|
|
404
|
+
...targetOptions,
|
|
405
|
+
fields: [
|
|
406
|
+
'id',
|
|
407
|
+
'description',
|
|
408
|
+
'name',
|
|
409
|
+
'slug',
|
|
410
|
+
'meta',
|
|
411
|
+
'locations'
|
|
412
|
+
],
|
|
413
|
+
include: menuIds && menuIds.length ? menuIds.join(',') : null
|
|
414
|
+
}, 'menus');
|
|
415
|
+
|
|
416
|
+
menuNavItems = await getTaxonomies(
|
|
417
|
+
{
|
|
418
|
+
...targetOptions,
|
|
419
|
+
fields: [
|
|
420
|
+
'id',
|
|
421
|
+
'title',
|
|
422
|
+
'url',
|
|
423
|
+
'status',
|
|
424
|
+
'attr_title',
|
|
425
|
+
'description',
|
|
426
|
+
'type',
|
|
427
|
+
'type_label',
|
|
428
|
+
'object',
|
|
429
|
+
'object_id',
|
|
430
|
+
'parent',
|
|
431
|
+
'menu_order',
|
|
432
|
+
'target',
|
|
433
|
+
'classes',
|
|
434
|
+
'xfn',
|
|
435
|
+
'meta',
|
|
436
|
+
'menus'
|
|
437
|
+
],
|
|
438
|
+
menus: menus.map((menu) => { return menu.id; })
|
|
439
|
+
},
|
|
440
|
+
'menu-items'
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
let missingPages = [];
|
|
444
|
+
let missingPosts = [];
|
|
445
|
+
// we iterate over menu items
|
|
446
|
+
menuNavItems.forEach(item => {
|
|
447
|
+
// if the item is a page and it wasn't previously collected
|
|
448
|
+
if( 'page' === item.object && ! pages.map(p => p.id ).includes(item.object_id) ){
|
|
449
|
+
missingPages.push(item.object_id)
|
|
450
|
+
// if the item is a post and it wasn't previously collected
|
|
451
|
+
}else if( 'post' === item.object && ! posts.map(p => p.id ).includes(item.object_id) ){
|
|
452
|
+
missingPosts.push(item.object_id)
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
// if navigation pages weren't previously collected they must be collected.
|
|
457
|
+
if( missingPages.length ){
|
|
458
|
+
missingPages = await getTaxonomies({
|
|
459
|
+
...targetOptions,
|
|
460
|
+
orderby: 'parent',
|
|
461
|
+
embed: true,
|
|
462
|
+
include: missingPages.join(',')
|
|
463
|
+
},
|
|
464
|
+
'pages'
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
// pages can be nested so we have to collect any parent items.
|
|
468
|
+
missingPages = await getParentItems(
|
|
469
|
+
missingPages,
|
|
470
|
+
targetOptions,
|
|
471
|
+
'pages'
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// add the missing pages to the pages array
|
|
475
|
+
pages = pages.concat(missingPages)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// if navigation posts weren't previously collected they must be collected.
|
|
479
|
+
if( missingPosts.length ){
|
|
480
|
+
// get all pages/posts
|
|
481
|
+
spinner.text = `Collecting all posts from ${target.url}`;
|
|
482
|
+
missingPosts = await getTaxonomies({
|
|
483
|
+
...targetOptions,
|
|
484
|
+
orderby: 'parent',
|
|
485
|
+
include: missingPosts.join(',')
|
|
486
|
+
},
|
|
487
|
+
'posts'
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// posts can be nested so we have to collect any parent items.
|
|
491
|
+
missingPosts = await getParentItems(
|
|
492
|
+
missingPosts,
|
|
493
|
+
targetOptions,
|
|
494
|
+
'posts'
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
// add the missing posts to the posts array
|
|
498
|
+
posts = posts.concat(missingPages);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Now we have all the data we can begin to create the taxonomies on the target.
|
|
504
|
+
*
|
|
505
|
+
* Import Order is important otherwise missing dependencies will cause errors to trigger
|
|
506
|
+
* 1) Media
|
|
507
|
+
* 2) Pages
|
|
508
|
+
* 3) Posts
|
|
509
|
+
* 4) Menus & Navigation Links
|
|
510
|
+
* 5) Settings
|
|
511
|
+
*/
|
|
512
|
+
|
|
513
|
+
// Media.
|
|
514
|
+
if( media ){
|
|
515
|
+
spinner.text = `Uploading media files to ${dest.url}`;
|
|
516
|
+
await createTaxonomies(
|
|
517
|
+
media.filter((img) => { return img.data }),
|
|
518
|
+
destOptions,
|
|
519
|
+
'media',
|
|
520
|
+
spinner
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Pages.
|
|
525
|
+
if( pages ){
|
|
526
|
+
spinner.text = `Creating all pages to ${dest.url}`;
|
|
527
|
+
await createTaxonomies( pages, destOptions, 'pages', spinner );
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Posts.
|
|
531
|
+
if( posts ){
|
|
532
|
+
spinner.text = `Creating all posts to ${dest.url}`;
|
|
533
|
+
await createTaxonomies( posts, destOptions, 'posts', spinner );
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Menus and Navigation Links.
|
|
537
|
+
if( menus ){
|
|
538
|
+
spinner.text = `Reconstructing navigation menus to ${dest.url}`;
|
|
539
|
+
await createTaxonomies(menus, destOptions, 'menus', spinner);
|
|
540
|
+
await createTaxonomies(menuNavItems, destOptions, 'menu-items', spinner);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
// Settings.
|
|
545
|
+
if( settings ){
|
|
546
|
+
spinner.text = `Updating site settings to ${dest.url}`;
|
|
547
|
+
await createTaxonomies(settings, destOptions, 'settings', spinner);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
spinner.text = `Sync from ${target.url} to ${dest.url} completed successfully.`
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { select } from 'inquirer-select-pro';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
// since we are in a spinner,
|
|
8
|
+
// we have to silence all the cancellation errors when using prompts
|
|
9
|
+
// .catch(() => {process.exit(1);})
|
|
10
|
+
import { confirm, input, password } from '@inquirer/prompts';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const boldWhite = chalk.bold.white;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Prompt for instance information
|
|
17
|
+
* { user, pwd, url }
|
|
18
|
+
*
|
|
19
|
+
* @asyncc
|
|
20
|
+
* @returns {Prompt}
|
|
21
|
+
*/
|
|
22
|
+
async function promptGetInstanceInfo(){
|
|
23
|
+
return {
|
|
24
|
+
user: await input({ message: "User Name:" }).catch(() => {process.exit(1);}),
|
|
25
|
+
pwd: await password({ message: 'Application Password:' }).catch(() => {process.exit(1);}),
|
|
26
|
+
url: await input({ message: 'URL:' }).catch(() => {process.exit(1);}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prompt for saving information
|
|
33
|
+
*
|
|
34
|
+
* @async
|
|
35
|
+
* @returns {Prompt}
|
|
36
|
+
*/
|
|
37
|
+
async function promptSaveInstanceInfo(){
|
|
38
|
+
let answer = await confirm(
|
|
39
|
+
{
|
|
40
|
+
message: 'Would you like to save this information?',
|
|
41
|
+
default: true
|
|
42
|
+
}
|
|
43
|
+
).catch(() => {process.exit(1);});
|
|
44
|
+
|
|
45
|
+
if( answer ){
|
|
46
|
+
// ask instance nickname
|
|
47
|
+
return await input({ message: "Instance Nickname:" }).catch(() => {process.exit(1);})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function promptForSync(tax){
|
|
52
|
+
let bullet = chalk.yellow('-');
|
|
53
|
+
let info = chalk.cyan('i');
|
|
54
|
+
console.log(`Sync WordPress Instances\n${chalk.green('#'.repeat(25))}\n`);
|
|
55
|
+
|
|
56
|
+
console.log(chalk.red('Requirements:'));
|
|
57
|
+
console.log(bullet, 'Both instances must have the CAWebPublishing Development Toolbox WordPless plugin installed and activated');
|
|
58
|
+
console.log(bullet.repeat(2), 'Plugin can be found https://github.com/CAWebPublishing/caweb-dev/releases');
|
|
59
|
+
console.log(bullet, 'An application password must be setup for the username');
|
|
60
|
+
console.log(bullet.repeat(2), 'For more information regarding application password https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/');
|
|
61
|
+
|
|
62
|
+
console.log(chalk.cyan('\nNotes:'));
|
|
63
|
+
// syncing menus note.
|
|
64
|
+
console.info(info, 'If Menus is selected, the following will also be synced:');
|
|
65
|
+
console.info(bullet, 'Any pages/posts (including parents) in the menu items.');
|
|
66
|
+
|
|
67
|
+
// syncing pages/posts note.
|
|
68
|
+
console.info(info, 'If Pages/Posts is selected, the following will also be synced:');
|
|
69
|
+
console.info(bullet, 'Any media that is attached.');
|
|
70
|
+
console.info(bullet, 'Any parent pages.');
|
|
71
|
+
|
|
72
|
+
console.log('');
|
|
73
|
+
|
|
74
|
+
let taxonomies = ['Media', 'Menus', 'Pages', 'Posts', 'Settings'];
|
|
75
|
+
let options = [];
|
|
76
|
+
|
|
77
|
+
taxonomies.forEach(t => {
|
|
78
|
+
options.push({
|
|
79
|
+
name: t,
|
|
80
|
+
value: t.toLowerCase(),
|
|
81
|
+
checked: tax.includes(t.toLowerCase())
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
return await select({
|
|
85
|
+
message: 'Select from the following taxonomies...',
|
|
86
|
+
multiple: true,
|
|
87
|
+
canToggleAll: true,
|
|
88
|
+
options,
|
|
89
|
+
defaultValue: options.map(o => o.checked ? o.value : false ).filter( e => e)
|
|
90
|
+
}).catch(() => {process.exit(1);});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function promptForId(title){
|
|
94
|
+
console.log(chalk.cyan('i'), `Enter comma separated list of IDs for ${title}`)
|
|
95
|
+
return await input({
|
|
96
|
+
message: 'Which IDs would you like to sync?',
|
|
97
|
+
default: 'all',
|
|
98
|
+
value: 'test'
|
|
99
|
+
}).catch(() => {process.exit(1);});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export {
|
|
103
|
+
promptGetInstanceInfo,
|
|
104
|
+
promptSaveInstanceInfo,
|
|
105
|
+
promptForSync,
|
|
106
|
+
promptForId
|
|
107
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -306,15 +306,35 @@ export default function cli() {
|
|
|
306
306
|
// Update a Design System Block Command.
|
|
307
307
|
program.command('sync')
|
|
308
308
|
.description('Sync changes from one WordPress instance to another.')
|
|
309
|
-
.argument('
|
|
310
|
-
.argument('
|
|
309
|
+
.argument('[target]', 'Target Site URL.')
|
|
310
|
+
.argument('[dest]', 'Destination Site URL.')
|
|
311
|
+
.addOption(new Option(
|
|
312
|
+
'--interactive',
|
|
313
|
+
'Runs the sync process with prompts'
|
|
314
|
+
))
|
|
315
|
+
.addOption(
|
|
316
|
+
new Option(
|
|
317
|
+
'-t,--tax <tax...>',
|
|
318
|
+
'Taxonomy that should be synced. Default is full site sync.'
|
|
319
|
+
)
|
|
320
|
+
.choices(['media', 'menus', 'pages', 'posts', 'settings'])
|
|
321
|
+
.default(['media', 'menus', 'pages', 'posts', 'settings'])
|
|
322
|
+
)
|
|
311
323
|
.addOption(new Option(
|
|
312
|
-
'-
|
|
313
|
-
'
|
|
314
|
-
)
|
|
324
|
+
'--media-ids <ids...>',
|
|
325
|
+
'Sync specific Media IDs.'
|
|
326
|
+
))
|
|
327
|
+
.addOption(new Option(
|
|
328
|
+
'--menu-ids <ids...>',
|
|
329
|
+
'Sync specific Menu IDs.'
|
|
330
|
+
))
|
|
331
|
+
.addOption(new Option(
|
|
332
|
+
'--page-ids <ids...>',
|
|
333
|
+
'Sync specific Page IDs.'
|
|
334
|
+
))
|
|
315
335
|
.addOption(new Option(
|
|
316
|
-
'-
|
|
317
|
-
'
|
|
336
|
+
'--post-ids <ids...>',
|
|
337
|
+
'Sync specific Post IDs.'
|
|
318
338
|
))
|
|
319
339
|
.allowUnknownOption(true)
|
|
320
340
|
.action( withSpinner(env.sync) )
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caweb/cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "CAWebPublishing Command Line Interface.",
|
|
5
5
|
"exports": "./lib/env.js",
|
|
6
6
|
"type": "module",
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@caweb/webpack": "^1.0.
|
|
59
|
+
"@caweb/webpack": "^1.0.3",
|
|
60
|
+
"@inquirer/prompts": "^5.2.0",
|
|
60
61
|
"@wordpress/create-block": "^4.46.0",
|
|
61
62
|
"@wordpress/env": "^10.3.0",
|
|
62
63
|
"axios": "^1.7.2",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"commander": "^12.1.0",
|
|
66
67
|
"cross-spawn": "^7.0.3",
|
|
67
68
|
"docker-compose": "^0.24.8",
|
|
69
|
+
"inquirer-select-pro": "^1.0.0-alpha.6",
|
|
68
70
|
"ora": "^8.0.1",
|
|
69
71
|
"resolve-bin": "^1.0.1",
|
|
70
72
|
"terminal-link": "^3.0.0"
|
package/commands/sync.js
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* External dependencies
|
|
3
|
-
*/
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import loadConfig from '@wordpress/env/lib/config/load-config.js';
|
|
7
|
-
import axios from 'axios';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Internal dependencies
|
|
11
|
-
*/
|
|
12
|
-
import {
|
|
13
|
-
appPath,
|
|
14
|
-
getTaxonomies,
|
|
15
|
-
createTaxonomies
|
|
16
|
-
} from '../lib/index.js';
|
|
17
|
-
|
|
18
|
-
const errors = [];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Return all parent items for a given set of ids.
|
|
22
|
-
*
|
|
23
|
-
* @async
|
|
24
|
-
* @param {Array} ids
|
|
25
|
-
* @param {*} request
|
|
26
|
-
* @param {string} [tax='pages']
|
|
27
|
-
* @returns {unknown}
|
|
28
|
-
*/
|
|
29
|
-
async function getParentItems( objects, request, tax = 'pages' ){
|
|
30
|
-
let parentItemsObjects = [];
|
|
31
|
-
let objectParentIds = objects.map((obj) => { return obj.parent; }).filter(n => n);
|
|
32
|
-
|
|
33
|
-
while( objectParentIds.length > 0 ){
|
|
34
|
-
|
|
35
|
-
// if we have parent ids, we have to collect any parent items.
|
|
36
|
-
let parentItems = await getTaxonomies({
|
|
37
|
-
...request,
|
|
38
|
-
orderby: 'parent',
|
|
39
|
-
embed: true,
|
|
40
|
-
include: objectParentIds
|
|
41
|
-
}, tax);
|
|
42
|
-
|
|
43
|
-
// if we have parent items, we have to add to the items array.
|
|
44
|
-
if( parentItems ){
|
|
45
|
-
parentItemsObjects = parentItemsObjects.concat( parentItems );
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// if the parent items have parent ids, we have to save those for the next run.
|
|
49
|
-
objectParentIds = parentItems.map((obj) => { return obj.parent; }).filter((id) => { return id !== 0; })
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return objects.concat( parentItemsObjects ).sort( (a,b) => a.parent - b.parent );
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Sync Environments.
|
|
57
|
-
*
|
|
58
|
-
* @see https://developer.wordpress.org/rest-api/reference/
|
|
59
|
-
*
|
|
60
|
-
* @param {Object} options
|
|
61
|
-
* @param {Object} options.spinner A CLI spinner which indicates progress.
|
|
62
|
-
* @param {boolean} options.from Remote Site URL with current changes.
|
|
63
|
-
* @param {boolean} options.to Destination Site URL that should be synced.
|
|
64
|
-
* @param {boolean} options.debug True if debug mode is enabled.
|
|
65
|
-
* @param {Array} options.tax Taxonomy that should be synced.
|
|
66
|
-
* @param {Array} options.include Include specific IDs only.
|
|
67
|
-
*/
|
|
68
|
-
export default async function sync({
|
|
69
|
-
spinner,
|
|
70
|
-
debug,
|
|
71
|
-
from,
|
|
72
|
-
to,
|
|
73
|
-
tax,
|
|
74
|
-
include
|
|
75
|
-
} ) {
|
|
76
|
-
|
|
77
|
-
const localFile = path.join(appPath, 'caweb.json');
|
|
78
|
-
const {workDirectoryPath} = await loadConfig(path.resolve('.'));
|
|
79
|
-
|
|
80
|
-
process.env.WP_CLI_CONFIG_PATH = path.join(workDirectoryPath, 'config.yml');
|
|
81
|
-
|
|
82
|
-
// if caweb.json file doesn't exist we don't do anything
|
|
83
|
-
if( ! fs.existsSync(localFile) ){
|
|
84
|
-
spinner.fail('caweb.json file not found.')
|
|
85
|
-
process.exit(1)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// read configuration file
|
|
89
|
-
const serviceConfig = JSON.parse( fs.readFileSync(localFile) );
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* make sure instances are in the file.
|
|
93
|
-
* must have sync property
|
|
94
|
-
* each instance has to have a url, user, pwd property
|
|
95
|
-
*/
|
|
96
|
-
if(
|
|
97
|
-
! serviceConfig.sync ||
|
|
98
|
-
! serviceConfig.sync[from] ||
|
|
99
|
-
! serviceConfig.sync[from].url ||
|
|
100
|
-
! serviceConfig.sync[from].user ||
|
|
101
|
-
! serviceConfig.sync[from].pwd ||
|
|
102
|
-
! serviceConfig.sync[to] ||
|
|
103
|
-
! serviceConfig.sync[to].url ||
|
|
104
|
-
! serviceConfig.sync[to].user ||
|
|
105
|
-
! serviceConfig.sync[to].pwd
|
|
106
|
-
){
|
|
107
|
-
spinner.fail(`caweb.json is not configured properly for ${from} and ${to}.`);
|
|
108
|
-
process.exit(1)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// get instance data
|
|
112
|
-
from = serviceConfig.sync[from];
|
|
113
|
-
let fromOptions = {
|
|
114
|
-
url: from.url,
|
|
115
|
-
headers: {
|
|
116
|
-
Authorization: 'Basic ' + Buffer.from(`${from.user}:${from.pwd}`).toString('base64')
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
to = serviceConfig.sync[to];
|
|
122
|
-
|
|
123
|
-
let toOptions = {
|
|
124
|
-
url: to.url,
|
|
125
|
-
headers: {
|
|
126
|
-
Authorization: 'Basic ' + Buffer.from(`${to.user}:${to.pwd}`).toString('base64'),
|
|
127
|
-
'content-type': 'multipart/form-data',
|
|
128
|
-
'accept': '*/*'
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Sync Process
|
|
134
|
-
* If taxonomy is undefined then we don't sync that section.
|
|
135
|
-
*
|
|
136
|
-
* 1) We collect media if the taxonomy is undefined or includes media, pages, posts.
|
|
137
|
-
* 2) We only collect settings if the taxonomy is undefined or it's set to settings.
|
|
138
|
-
* 3) We only collect pages if the taxonomy is undefined or it's set to pages.
|
|
139
|
-
* 4) We only collect posts if the taxonomy is undefined or it's set to posts.
|
|
140
|
-
* 5) We only collect menus if the taxonomy is undefined or it's set to menus.
|
|
141
|
-
* - We also collect menu items if menus are collected.
|
|
142
|
-
*/
|
|
143
|
-
let settings = [];
|
|
144
|
-
let mediaLibrary = [];
|
|
145
|
-
let media = [];
|
|
146
|
-
let pages = [];
|
|
147
|
-
let posts = [];
|
|
148
|
-
let menus = [];
|
|
149
|
-
let menuNavItems = [];
|
|
150
|
-
|
|
151
|
-
// Media Library.
|
|
152
|
-
if( undefined === tax || tax.includes('media', 'pages', 'posts') ){
|
|
153
|
-
spinner.text = `Collecting Media Library ${from.url}`;
|
|
154
|
-
mediaLibrary = await getTaxonomies({
|
|
155
|
-
...fromOptions,
|
|
156
|
-
fields: [
|
|
157
|
-
'id',
|
|
158
|
-
'source_url',
|
|
159
|
-
'title',
|
|
160
|
-
'caption',
|
|
161
|
-
'alt_text',
|
|
162
|
-
'date',
|
|
163
|
-
'mime_type',
|
|
164
|
-
'post',
|
|
165
|
-
'media_details'
|
|
166
|
-
],
|
|
167
|
-
include: tax && tax.includes('media') && include ? include.join(',') : null
|
|
168
|
-
},
|
|
169
|
-
'media'
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Site Settings.
|
|
174
|
-
if( undefined === tax || tax.includes('settings') ){
|
|
175
|
-
spinner.text = `Collecting Site Settings from ${from.url}`;
|
|
176
|
-
settings = await getTaxonomies({
|
|
177
|
-
...fromOptions,
|
|
178
|
-
fields: [
|
|
179
|
-
'title',
|
|
180
|
-
'description',
|
|
181
|
-
'show_on_front',
|
|
182
|
-
'page_on_front',
|
|
183
|
-
'posts_per_page'
|
|
184
|
-
],
|
|
185
|
-
}, 'settings')
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Pages.
|
|
190
|
-
if( undefined === tax || tax.includes('pages') ){
|
|
191
|
-
// get all pages/posts
|
|
192
|
-
spinner.text = `Collecting pages from ${from.url}`;
|
|
193
|
-
pages = await getTaxonomies({
|
|
194
|
-
...fromOptions,
|
|
195
|
-
orderby: 'parent',
|
|
196
|
-
embed: true,
|
|
197
|
-
include: tax && include ? include.join(',') : null
|
|
198
|
-
},
|
|
199
|
-
'pages'
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
// we only do this if specific ids were requested.
|
|
203
|
-
if( include ){
|
|
204
|
-
// if we have parent ids, we have to collect any parent items.
|
|
205
|
-
pages = await getParentItems(
|
|
206
|
-
pages,
|
|
207
|
-
fromOptions,
|
|
208
|
-
'pages'
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Posts.
|
|
214
|
-
if( undefined === tax || tax.includes('posts') ){
|
|
215
|
-
// get all pages/posts
|
|
216
|
-
spinner.text = `Collecting all posts from ${from.url}`;
|
|
217
|
-
posts = await getTaxonomies({
|
|
218
|
-
...fromOptions,
|
|
219
|
-
orderby: 'parent',
|
|
220
|
-
include: tax && include ? include.join(',') : null
|
|
221
|
-
},
|
|
222
|
-
'posts'
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
// we only do this if specific ids were requested.
|
|
226
|
-
if( include ){
|
|
227
|
-
// if we have parent ids, we have to collect any parent items.
|
|
228
|
-
posts = await getParentItems(
|
|
229
|
-
posts,
|
|
230
|
-
fromOptions,
|
|
231
|
-
'posts'
|
|
232
|
-
)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Media Library Handling
|
|
238
|
-
*
|
|
239
|
-
* We iterate thru the media library to identify which media should be synced.
|
|
240
|
-
* 1) attached media items by default are only possible on pages.
|
|
241
|
-
* 2) featured media by default are only possible on posts.
|
|
242
|
-
* 3) save any linked media in the content of pages and posts.
|
|
243
|
-
*/
|
|
244
|
-
for( let m of mediaLibrary ){
|
|
245
|
-
// remove any new line characters.
|
|
246
|
-
m.source_url = m.source_url.replace('\n','');
|
|
247
|
-
|
|
248
|
-
// check if the media is attached to a page.
|
|
249
|
-
// if tax is undefined, we collect all media attached to posts and pages.
|
|
250
|
-
// if tax is defined, if include is undefined or media is matches post/page, we collect the media.
|
|
251
|
-
if( ( ! tax && m.post ) ||
|
|
252
|
-
( tax &&
|
|
253
|
-
( undefined === include || include.includes( m.id.toString() ) )
|
|
254
|
-
) ){
|
|
255
|
-
media.push( m );
|
|
256
|
-
|
|
257
|
-
// we don't have to check any further.
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
for( let p of [].concat( pages, posts ) ){
|
|
262
|
-
// if the media is featured on a post or linked in the content.
|
|
263
|
-
if( p.featured_media === m.id ||
|
|
264
|
-
p.content.rendered.match( new RegExp(`src=”(${m.source_url}.*)”`, 'g') )){
|
|
265
|
-
media.push( m );
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// filter any duplicate media.
|
|
271
|
-
media = media.filter((m, index, self) => { return index === self.findIndex((t) => { return t.id === m.id; })} );
|
|
272
|
-
let i = 0;
|
|
273
|
-
|
|
274
|
-
// before we can upload media files we have to generate the media blob data.
|
|
275
|
-
for( let m of media ){
|
|
276
|
-
if( debug ){
|
|
277
|
-
i++;
|
|
278
|
-
spinner.info(`Media ID ${m.id} Collected: ${i}/${media.length}`)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const mediaBlob = await axios.request(
|
|
282
|
-
{
|
|
283
|
-
...fromOptions,
|
|
284
|
-
url: m.source_url,
|
|
285
|
-
responseType: 'arraybuffer'
|
|
286
|
-
}
|
|
287
|
-
)
|
|
288
|
-
.then( (img) => { return new Blob([img.data]) })
|
|
289
|
-
.catch( (error) => {
|
|
290
|
-
errors.push(`${m.source_url} could not be downloaded.`)
|
|
291
|
-
return false;
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
if( mediaBlob ){
|
|
295
|
-
m.data = mediaBlob;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// this has to be done after we have the media data.
|
|
300
|
-
// Lets replace the url references in the content of the pages and posts.
|
|
301
|
-
for( let p of [].concat( pages, posts ) ){
|
|
302
|
-
p.content.rendered = p.content.rendered.replace( new RegExp(from.url, 'g'), to.url );
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// Menu and Nav Items.
|
|
307
|
-
if( undefined === tax || tax.includes('menus')){
|
|
308
|
-
spinner.text = `Collecting assigned navigation menus from ${from.url}`;
|
|
309
|
-
// get all menus and navigation links
|
|
310
|
-
menus = await getTaxonomies({
|
|
311
|
-
...fromOptions,
|
|
312
|
-
fields: [
|
|
313
|
-
'id',
|
|
314
|
-
'description',
|
|
315
|
-
'name',
|
|
316
|
-
'slug',
|
|
317
|
-
'meta',
|
|
318
|
-
'locations'
|
|
319
|
-
],
|
|
320
|
-
include: tax && include ? include.join(',') : null
|
|
321
|
-
}, 'menus');
|
|
322
|
-
|
|
323
|
-
menuNavItems = await getTaxonomies(
|
|
324
|
-
{
|
|
325
|
-
...fromOptions,
|
|
326
|
-
fields: [
|
|
327
|
-
'id',
|
|
328
|
-
'title',
|
|
329
|
-
'url',
|
|
330
|
-
'status',
|
|
331
|
-
'attr_title',
|
|
332
|
-
'description',
|
|
333
|
-
'type',
|
|
334
|
-
'type_label',
|
|
335
|
-
'object',
|
|
336
|
-
'object_id',
|
|
337
|
-
'parent',
|
|
338
|
-
'menu_order',
|
|
339
|
-
'target',
|
|
340
|
-
'classes',
|
|
341
|
-
'xfn',
|
|
342
|
-
'meta',
|
|
343
|
-
'menus'
|
|
344
|
-
],
|
|
345
|
-
menus: menus.map((menu) => { return menu.id; })
|
|
346
|
-
},
|
|
347
|
-
'menu-items'
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Now we have all the data we can begin to create the taxonomies on the target.
|
|
354
|
-
*
|
|
355
|
-
* Import Order is important otherwise missing dependencies will cause errors to trigger
|
|
356
|
-
* 1) Media
|
|
357
|
-
* 2) Pages
|
|
358
|
-
* 3) Posts
|
|
359
|
-
* 4) Menus & Navigation Links
|
|
360
|
-
* 5) Settings
|
|
361
|
-
*/
|
|
362
|
-
|
|
363
|
-
// Media.
|
|
364
|
-
if( media ){
|
|
365
|
-
spinner.text = `Uploading media files to ${to.url}`;
|
|
366
|
-
await createTaxonomies(
|
|
367
|
-
media.filter((img) => { return img.data }),
|
|
368
|
-
toOptions,
|
|
369
|
-
'media',
|
|
370
|
-
spinner
|
|
371
|
-
)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Pages.
|
|
375
|
-
if( pages ){
|
|
376
|
-
spinner.text = `Creating all pages to ${to.url}`;
|
|
377
|
-
await createTaxonomies( pages, toOptions, 'pages', spinner );
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Posts.
|
|
381
|
-
if( posts ){
|
|
382
|
-
spinner.text = `Creating all posts to ${to.url}`;
|
|
383
|
-
await createTaxonomies( posts, toOptions, 'posts', spinner );
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Menus and Navigation Links.
|
|
387
|
-
if( menus ){
|
|
388
|
-
spinner.text = `Reconstructing navigation menus to ${to.url}`;
|
|
389
|
-
await createTaxonomies(menus, toOptions, 'menus', spinner);
|
|
390
|
-
await createTaxonomies(menuNavItems, toOptions, 'menu-items', spinner);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
// Settings.
|
|
395
|
-
if( settings ){
|
|
396
|
-
spinner.text = `Updating site settings to ${to.url}`;
|
|
397
|
-
await createTaxonomies(settings, toOptions, 'settings', spinner);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
spinner.text = `Sync from ${from.url} to ${to.url} completed successfully.`
|
|
401
|
-
|
|
402
|
-
};
|