@caweb/cli 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,6 +11,7 @@ The following WordPress packages are used:
11
11
  - <strong>For Debian-Based Linux distributions:</strong> <code>docker-compose</code> may need to be installed with: <code>sudo apt-get install docker-compose</code>.
12
12
  - <strong>For Windows users:</strong> [WSL should be set to version 2 for Windows Docker Desktop compatibility](https://docs.docker.com/desktop/windows/wsl/).
13
13
  - git is installed.
14
+ - php is installed.
14
15
 
15
16
  ## Additional Features
16
17
  - Downloads and configures the [CAWeb Theme](https://github.com/CAWebPublishing/CAWeb)
@@ -20,13 +21,17 @@ The following WordPress packages are used:
20
21
  - phpMyAdmin development site starts at http://localhost:8080
21
22
  - phpMyAdmin test site started at http://localhost:9090
22
23
  - Uses CAWebPublishing [External Project Template Configuration](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/packages-create-block-external-template/) when creating Gutenberg Blocks, see configurations [here](https://github.com/CAWebPublishing/cli/lib/template)
23
- - Adds config.yml to both cli containers
24
+ - Allows for syncing of WordPress instance via Rest API and CLI, requires ssh.
25
+ - Adds config.yml to both cli containers, alias are added for each ssh connection.
26
+ <b>Example config.yml file</b>
24
27
  <pre>
25
- path: /var/www/html
26
- apache_modules:
27
- - mod_rewrite
28
+ path: /var/www/html
29
+ apache_modules:
30
+ - mod_rewrite
31
+ @staging:
32
+ ssh: wpcli@staging.wp-cli.org
28
33
  </pre>
29
- .
34
+
30
35
 
31
36
  ## Command Reference
32
37
  ### `caweb start`
@@ -20,15 +20,17 @@ const CONFIG_CACHE_KEY = 'config_checksum';
20
20
  /**
21
21
  * Internal dependencies
22
22
  */
23
- import { appPath } from '../../lib/helpers.js';
24
- import wpEnvConfig from '../../configs/wp-env.js';
25
- import dockerConfig from '../../configs/docker-compose.js';
26
-
27
23
  import {
24
+ appPath,
25
+ projectPath,
28
26
  configureCAWeb,
29
27
  downloadSources,
30
- configureWordPress
31
- } from '../../lib/wordpress/index.js';
28
+ configureWordPress,
29
+ runCmd
30
+ } from '../../lib/index.js';
31
+ import wpEnvConfig from '../../configs/wp-env.js';
32
+ import dockerConfig from '../../configs/docker-compose.js';
33
+
32
34
 
33
35
  /**
34
36
  * Starts the development server.
@@ -145,6 +147,19 @@ export default async function start({
145
147
  } ),
146
148
  ] );
147
149
 
150
+ // Create an Application Password for the user.
151
+ /*
152
+ const devAppPwd = await runCmd(
153
+ 'php',
154
+ [
155
+ path.join(projectPath, 'bin', 'wp-cli.phar'),
156
+ `--ssh=docker:${path.basename(workDirectoryPath)}-cli-1`,
157
+ `user application-password create 1 caweb`,
158
+ '--porcelain'
159
+ ]
160
+ );
161
+ */
162
+
148
163
  }
149
164
 
150
165
  // Start phpMyAdmin Service.
package/commands/sync.js CHANGED
@@ -3,338 +3,20 @@
3
3
  */
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
- import axios from 'axios';
7
- import terminalLink from 'terminal-link';
8
6
  import loadConfig from '@wordpress/env/lib/config/load-config.js';
7
+ import axios from 'axios';
9
8
 
10
9
  /**
11
10
  * Internal dependencies
12
11
  */
13
12
  import {
14
13
  appPath,
15
- projectPath,
16
- runCmd
14
+ getTaxonomies,
15
+ createTaxonomies
17
16
  } from '../lib/index.js';
18
17
 
19
- const endpoint = '/wp-json/wp/v2';
20
- const requestDelay = 1000; // we wait 1 second between requests
21
-
22
- const sleep = (waitTimeInMs) => new Promise(resolve => setTimeout(resolve, waitTimeInMs));
23
-
24
- function processData( data ){
25
- /**
26
- * If wpautop is enabled and data contains Divi shortcodes,
27
- * WordPress will automatically add an opening/closing p tag that needs to be removed.
28
- * @link https://developer.wordpress.org/reference/functions/wpautop/
29
- */
30
- if( data.includes('et_pb_section') &&
31
- data.startsWith('<p>') &&
32
- data.endsWith('</p>\n')
33
- ){
34
- data = data.substring(3, data.length - 5)
35
- }
36
- return data
37
- .replace(/(&#8221;)|(&#8243;)|(&#8220;)/g, '') // replace double encoding
38
- .replace(/(\s{2,})/g, ' ') // replace misc spacing // &#8220;
39
-
40
- }
41
-
42
- async function getTaxonomies( request, tax = 'pages', limit = 5000 ){
43
- const fields = request.fields ? request.fields : [
44
- 'id',
45
- 'type',
46
- 'alt_text',
47
- 'caption',
48
- 'slug',
49
- 'title',
50
- 'source_url',
51
- 'mime_type',
52
- 'content',
53
- 'date',
54
- 'date_gmt',
55
- 'status',
56
- 'featured_media',
57
- 'comment_status',
58
- 'ping_status',
59
- 'template',
60
- 'format',
61
- 'categories',
62
- 'tags',
63
- 'meta'
64
- ];
65
-
66
- let urlParams = [
67
- 'per_page=100',
68
- `_fields=${ fields.join(',')}`,
69
- 'order=asc'
70
- ]
71
-
72
- // if no request is added default to GET
73
- if( ! request.method ){
74
- request.method = 'GET'
75
- }
76
-
77
- // if parent argument
78
- if( request.parent ){
79
- urlParams.push( `parent=` + ( Array === typeof request.parent ? request.parent.join(',') : request.parent ))
80
- }
81
-
82
- // if include argument
83
- if( request.include ){
84
- urlParams.push( `include=` + ( Array === typeof request.include ? request.include.join(',') : request.include ))
85
- }
86
-
87
- let collection = [];
88
-
89
- for( let i = 1; i < limit / 100; i++ ){
90
- // make requests for each page
91
- urlParams.push(`page=${i}`)
92
-
93
- let results = await axios.request(
94
- {
95
- ...request,
96
- url: `${request.url}${endpoint}/${tax}?` + urlParams.join('&')
97
- }
98
- ).then(
99
- (res) => {
100
- return res.data
101
- }).catch(
102
- (error) => {return false}
103
- );
104
-
105
-
106
- // if no more results stop making requests
107
- if( ! results ){
108
- break;
109
- }
110
-
111
- // add results to collection
112
- collection = collection.concat( results );
113
-
114
- // lets wait requestDelay between requests.
115
- await sleep( requestDelay );
116
- }
117
-
118
- return collection;
119
- }
120
-
121
- async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
122
- // if no request is added default to POST
123
- if( ! request.method ){
124
- request.method = 'POST'
125
- }
126
-
127
- let collection = [];
128
-
129
-
130
- for( let obj of taxData ){
131
- // endpoint url.
132
- let url = `${request.url}${endpoint}/${tax}`;
133
- let existingID = false;
134
-
135
- // in order to maintain ID's, we have to use the cli, the REST API doesn't allow passing the id.
136
- if( obj.id ){
137
- // first we check if the ID exist
138
- let idExists = await axios.request(
139
- {
140
- ...request,
141
- method: 'GET',
142
- url: `${url}/${ obj.id }`
143
- }
144
- )
145
- .then((res) => { return res.data.id })
146
- .catch(error => {return false;})
147
-
148
- // for menus if ID doesn't exist we also check for matching slug
149
- // if a menu with a matching slug exists we return that id instead.
150
- if( 'menus' === tax && ! idExists ){
151
- idExists = await axios.request(
152
- {
153
- ...request,
154
- method: 'GET',
155
- url: `${url}/?slug=${ obj.slug }`
156
- }
157
- )
158
- .then((res) => { return res.data[0].id })
159
- .catch(error => {return false;})
160
-
161
- }
162
-
163
- /**
164
- * if the ID doesn't exist we save it so we can update via CLI later on.
165
- * if it does exist update endpoint url so we update the existing item.
166
- */
167
- if( ! idExists ){
168
- existingID = obj.id;
169
- }else{
170
- url = `${url}/${ obj.id }`;
171
- }
172
-
173
- // id has to be deleted.
174
- delete obj.id;
175
- }
176
-
177
- // process properties
178
- if( obj.content ){
179
- obj.content = processData(obj.content.rendered);
180
- }
181
-
182
- if( obj.title ){
183
- obj.title = processData(obj.title.rendered);
184
- }
185
-
186
- if( obj.caption ){
187
- obj.caption = processData(obj.caption.rendered);
188
- }
189
-
190
- // make WordPress REST API request.
191
- let results = await axios.request({
192
- ...request,
193
- data: 'media' === tax ? createMediaItem(obj) : obj,
194
- url
195
- })
196
- .then( async (res) => { return res.data; }, axiosErrorHandler )
197
-
198
-
199
- /**
200
- * if the obj had an existing ID that didn't exist
201
- */
202
- if( existingID ){
203
-
204
- let post_tbl = `wp_posts`;
205
- let post_meta_tbl = `wp_postmeta`;
206
- let term_taxonomy_tbl = `wp_term_taxonomy`;
207
-
208
- const cmd = [
209
- path.join(projectPath, 'bin', 'wp-cli.phar'),
210
- `--ssh=${request.ssh}`,
211
- '--skip-themes',
212
- '--skip-column-names',
213
- `--url=${new URL(request.url).origin}`
214
- ];
215
-
216
- // if taxonomy is page/post/media.
217
- if( ['pages', 'posts', 'media'].includes(tax) ){
218
-
219
-
220
- /**
221
- * Since the REST API doesn't detected trashed posts/pages.
222
- * We have to do a drop before we can do an update.
223
- * Since trashed posts/pages stay in the database
224
- */
225
- // drop post
226
- await runCmd(
227
- 'php',
228
- [
229
- ...cmd,
230
- `db query 'DELETE FROM ${post_tbl} WHERE ID=${existingID}'`,
231
- ]
232
- )
233
-
234
- // drop post meta.
235
- await runCmd(
236
- 'php',
237
- [
238
- ...cmd,
239
- `db query 'DELETE FROM ${post_meta_tbl} WHERE post_id=${existingID}'`,
240
- ]
241
- )
242
-
243
- // now we can update the new ID with the existing ID.
244
- await runCmd(
245
- 'php',
246
- [
247
- ...cmd,
248
- `db query 'UPDATE ${post_tbl} SET ID=${existingID} WHERE ID=${results.id}'`,
249
- ]
250
- )
251
- // update post meta.
252
- await runCmd(
253
- 'php',
254
- [
255
- ...cmd,
256
- `db query 'UPDATE ${post_meta_tbl} SET post_id=${existingID} WHERE post_id=${results.id}'`,
257
- ]
258
- )
18
+ const errors = [];
259
19
 
260
- // if taxonomy is menu.
261
- }else if( 'menus' === tax ){
262
- // update the term taxonomy table.
263
- await runCmd(
264
- 'php',
265
- [
266
- ...cmd,
267
- `db query 'UPDATE ${term_taxonomy_tbl} SET term_taxonomy_id=${existingID},term_id=${existingID} WHERE term_id=${results.id}'`,
268
- ]
269
- )
270
-
271
- }
272
-
273
- // update the API results ID, back to the existing ID from earlier.
274
- results.id = existingID;
275
- }
276
-
277
- // add results to collection
278
- collection = collection.concat( results );
279
-
280
- // We wait requestDelay seconds between requests.
281
- await sleep( requestDelay );
282
-
283
- }
284
-
285
-
286
- }
287
-
288
- function createMediaItem( media ){
289
-
290
- let fd = new FormData();
291
- let featureMediaFile = new File(
292
- [
293
- media.data
294
- ],
295
- media.source_url.split('/').pop(),
296
- {
297
- type: media.mime_type
298
- }
299
- );
300
- fd.append('file', featureMediaFile );
301
- fd.append('title', media.title);
302
- fd.append('caption', media.caption);
303
- fd.append('alt_text', media.alt_text);
304
- fd.append('date', media.date);
305
-
306
- return fd;
307
- }
308
-
309
- function axiosErrorHandler( error ){
310
-
311
- if (error.response) {
312
- // The request was made and the server responded with a status code
313
- // that falls out of the range of 2xx
314
- let { data, status } = error.response;
315
-
316
- switch( status ){
317
- case 401: // Invalid Credentials
318
- data.message += `\nPlease check your ${terminalLink('Application Password', 'https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/')}`;
319
- case 404: // Not Found
320
- //spinner.fail( `${data.message}` )
321
- //console.log('Error', `${data.message}`);
322
- //console.log('Error', `${data}`);
323
- //process.exit(1)
324
- default:
325
- console.log('Error', error.response);
326
- break;
327
- }
328
- } else if (error.request) {
329
- // The request was made but no response was received
330
- // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
331
- // http.ClientRequest in node.js
332
- console.log(error.request);
333
- } else {
334
- // Something happened in setting up the request that triggered an Error
335
- console.log('Error', error.message);
336
- }
337
- }
338
20
 
339
21
  /**
340
22
  * Sync Environments.
@@ -408,22 +90,23 @@ export default async function sync({
408
90
  'accept': '*/*'
409
91
  }
410
92
  }
411
-
412
-
93
+
94
+ // get all settings
95
+ spinner.text = `Getting Site Settings from ${from.url}`;
96
+ let settings = await getTaxonomies({
97
+ ...fromOptions,
98
+ fields: [
99
+ 'show_on_front',
100
+ 'page_on_front'
101
+ ],
102
+ }, 'settings')
103
+
413
104
  // get all pages/posts
414
- spinner.text = `Gathering all pages from ${from.url}`;
105
+ spinner.text = `Gathering all pages/posts from ${from.url}`;
415
106
  let pages = await getTaxonomies(fromOptions);
416
-
417
- // data display
418
- //spinner.info();
419
-
420
107
  let posts = await getTaxonomies(fromOptions, 'posts');
421
108
 
422
- // create all pages/posts
423
- spinner.text = `Creating all pages to ${to.url}`;
424
- await createTaxonomies( pages, toOptions, 'pages', spinner );
425
- await createTaxonomies( posts, toOptions, 'posts', spinner );
426
-
109
+ spinner.text = `Collecting all attached/featured images from ${from.url}`;
427
110
  /**
428
111
  * Media Library Handling
429
112
  * 1) attached media items by default are only possible on pages.
@@ -438,7 +121,6 @@ export default async function sync({
438
121
  'date',
439
122
  'mime_type'
440
123
  ];
441
-
442
124
  let attachedMedia = await getTaxonomies({
443
125
  ...fromOptions,
444
126
  fields: mediaFields,
@@ -454,7 +136,7 @@ export default async function sync({
454
136
  },
455
137
  'media'
456
138
  );
457
-
139
+
458
140
  // before we can upload media files.
459
141
  for( let mediaObj of [].concat( attachedMedia, featuredMedia ) ){
460
142
  // generate a blob from the media object source_url.
@@ -464,16 +146,20 @@ export default async function sync({
464
146
  url: mediaObj.source_url,
465
147
  responseType: 'arraybuffer'
466
148
  }
467
- ).then( (img) => { return new Blob([img.data]) });
149
+ )
150
+ .then( (img) => { return new Blob([img.data]) })
151
+ .catch( (error) => {
152
+ errors.push(`${mediaObj.source_url} could not be downloaded.`)
153
+ return false;
154
+ });
468
155
 
469
- mediaObj.data = mediaBlob;
156
+ if( mediaBlob ){
157
+ mediaObj.data = mediaBlob;
158
+ }
470
159
  }
471
-
472
- // create media attachments
473
- spinner.text = `Uploading media files to ${to.url}`;
474
- //await createTaxonomies([].concat( attachedMedia, featuredMedia ), toOptions, 'media', spinner)
475
-
476
- // get all menus
160
+
161
+ spinner.text = `Collecting assigned navigation menus from ${from.url}`;
162
+ // get all menus and navigation links
477
163
  let menus = await getTaxonomies({
478
164
  ...fromOptions,
479
165
  fields: [
@@ -484,15 +170,57 @@ export default async function sync({
484
170
  'meta',
485
171
  'locations'
486
172
  ]
487
- }, 'menus').then((navs) =>{
488
- // filter out any menus not assigned to a location.
489
- return navs.filter((menu) => {
490
- return menu.locations.length
491
- });
492
- })
173
+ }, 'menus');
493
174
 
494
- // create menus
175
+ let menuNavItems = await getTaxonomies(
176
+ {
177
+ ...fromOptions,
178
+ fields: [
179
+ 'id',
180
+ 'title',
181
+ 'url',
182
+ 'status',
183
+ 'attr_title',
184
+ 'description',
185
+ 'type',
186
+ 'type_label',
187
+ 'object',
188
+ 'object_id',
189
+ 'parent',
190
+ 'menu_order',
191
+ 'target',
192
+ 'classes',
193
+ 'xfn',
194
+ 'meta',
195
+ 'menus'
196
+ ],
197
+ menus: menus.map((menu) => { return menu.id; })
198
+ },
199
+ 'menu-items'
200
+ )
201
+
202
+ // create all pages/posts
203
+ spinner.text = `Creating all pages/posts to ${to.url}`;
204
+ await createTaxonomies( pages, toOptions, 'pages', spinner );
205
+ await createTaxonomies( posts, toOptions, 'posts', spinner );
206
+
207
+ // create media attachments
208
+ spinner.text = `Uploading media files to ${to.url}`;
209
+ await createTaxonomies(
210
+ [].concat( attachedMedia, featuredMedia ).filter((img) => { return img.data }),
211
+ toOptions,
212
+ 'media',
213
+ spinner
214
+ )
215
+
216
+ // create menus and navigation links
217
+ spinner.text = `Reconstructing navigation menus to ${to.url}`;
495
218
  await createTaxonomies(menus, toOptions, 'menus', spinner);
219
+ await createTaxonomies(menuNavItems, toOptions, 'menu-items', spinner);
220
+
221
+ // update settings
222
+ await createTaxonomies(settings, toOptions, 'settings', spinner);
496
223
 
224
+ spinner.text = `Sync from ${from.url} to ${to.url} completed successfully.`
497
225
 
498
226
  };
package/lib/cli.js CHANGED
@@ -264,12 +264,12 @@ export default function cli() {
264
264
 
265
265
  // Test Command.
266
266
  // Ensure this is commented out.
267
- program.command('test')
267
+ /*program.command('test')
268
268
  .description('Test commands on a WordPress environment')
269
269
  //.addArgument(envArg)
270
270
  .allowUnknownOption(true)
271
271
  .action(withSpinner(env.test))
272
-
272
+ */
273
273
 
274
274
  addWPEnvCommands();
275
275
 
package/lib/helpers.js CHANGED
@@ -30,7 +30,7 @@ const appPath = path.resolve( projectPath, '../../../' );
30
30
  *
31
31
  * @returns {Promise}
32
32
  */
33
- async function runCmd(cmd, args,opts = { stdio: 'inherit' }){
33
+ async function runCmd(cmd, args,opts = { stdio: ['inherit', 'pipe'] }){
34
34
  // fix various commands.
35
35
  switch (cmd) {
36
36
  case 'npm':
package/lib/index.js CHANGED
@@ -14,6 +14,20 @@ import {
14
14
  appPath
15
15
  } from './helpers.js';
16
16
 
17
+ import {
18
+ activateCAWeb,
19
+ configureCAWeb,
20
+ downloadSources,
21
+ configureDivi,
22
+ isDiviThemeActive,
23
+ configureWordPress,
24
+ isMultisite,
25
+ convertToMultisite,
26
+ generateHTAccess,
27
+ getTaxonomies,
28
+ createTaxonomies
29
+ } from './wordpress/index.js';
30
+
17
31
  export {
18
32
  currentPath,
19
33
  projectPath,
@@ -24,5 +38,16 @@ export {
24
38
  wpYellow,
25
39
  withSpinner,
26
40
  runCmd,
27
- runCLICmds
41
+ runCLICmds,
42
+ activateCAWeb,
43
+ configureCAWeb,
44
+ downloadSources,
45
+ configureDivi,
46
+ isDiviThemeActive,
47
+ configureWordPress,
48
+ isMultisite,
49
+ convertToMultisite,
50
+ generateHTAccess,
51
+ getTaxonomies,
52
+ createTaxonomies
28
53
  }
package/lib/spinner.js CHANGED
@@ -1,5 +1,9 @@
1
+ /**
2
+ * External dependencies
3
+ */
1
4
  import chalk from 'chalk';
2
5
  import ora from 'ora';
6
+ import terminalLink from 'terminal-link';
3
7
 
4
8
  // Colors.
5
9
  const boldWhite = chalk.bold.white;
@@ -48,7 +52,25 @@ const withSpinner =
48
52
  process.exit( 0 );
49
53
  },
50
54
  ( error ) => {
51
- if( error ){
55
+
56
+ // Axios Error
57
+ if( error &&
58
+ 'object' === typeof error &&
59
+ 'response' in error &&
60
+ 'request' in error
61
+ ){
62
+
63
+ let { data, status } = error.response;
64
+
65
+ if( 401 === status ){
66
+ data.message += `\nPlease check your ${terminalLink('Application Password', 'https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/')}`;
67
+ }
68
+
69
+ spinner.fail( data.message )
70
+ process.exit( 1 );
71
+
72
+
73
+ }else if( error ){
52
74
  // Error is an unknown error. That means there was a bug in our code.
53
75
  spinner.fail(
54
76
  typeof error === 'string' ? error : error.message
@@ -56,10 +78,13 @@ const withSpinner =
56
78
  // Disable reason: Using console.error() means we get a stack trace.
57
79
  console.error( error );
58
80
  process.exit( 1 );
81
+
59
82
  }else{
60
83
  spinner.fail( 'An unknown error occurred.' );
61
84
  process.exit( 1 );
85
+
62
86
  }
87
+
63
88
  }
64
89
  );
65
90
  };
@@ -0,0 +1,392 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import path from 'path';
5
+ import axios from 'axios';
6
+ import terminalLink from 'terminal-link';
7
+
8
+
9
+ /**
10
+ * Internal dependencies
11
+ */
12
+ import {
13
+ projectPath,
14
+ runCmd
15
+ } from '../index.js';
16
+
17
+ const endpoint = '/wp-json/wp/v2';
18
+ const requestDelay = 1000; // we wait 1 second between requests
19
+
20
+
21
+ const sleep = (waitTimeInMs) => new Promise(resolve => setTimeout(resolve, waitTimeInMs));
22
+
23
+ function processData( data ){
24
+ /**
25
+ * If wpautop is enabled and data contains Divi shortcodes,
26
+ * WordPress will automatically add an opening/closing p tag that needs to be removed.
27
+ * @link https://developer.wordpress.org/reference/functions/wpautop/
28
+ */
29
+ if( data.includes('et_pb_section') &&
30
+ data.startsWith('<p>') &&
31
+ data.endsWith('</p>\n')
32
+ ){
33
+ data = data.substring(3, data.length - 5)
34
+ }
35
+ return data
36
+ .replace(/(&#8221;)|(&#8243;)|(&#8220;)/g, '') // replace double encoding
37
+ .replace(/(\s{2,})/g, ' ') // replace misc spacing // &#8220;
38
+
39
+ }
40
+
41
+ function processUrlParam( param ){
42
+ return ( Array === typeof param ? param.join(',') : param )
43
+ }
44
+
45
+ /**
46
+ * Make Requests to WordPress Rest API and return different taxonomy results.
47
+ *
48
+ *
49
+ * @async
50
+ * @param {Object} request Data to pass to Axios request.
51
+ * @param {string} [tax='pages'] Taxonomy of the request made. Default pages, available choices: pages,posts,menus,menu-items,settings,media
52
+ * @returns {unknown}
53
+ */
54
+ async function getTaxonomies( request, tax = 'pages' ){
55
+ const fields = request.fields ? request.fields : [
56
+ 'id',
57
+ 'type',
58
+ 'alt_text',
59
+ 'caption',
60
+ 'slug',
61
+ 'title',
62
+ 'source_url',
63
+ 'mime_type',
64
+ 'content',
65
+ 'date',
66
+ 'date_gmt',
67
+ 'status',
68
+ 'featured_media',
69
+ 'comment_status',
70
+ 'ping_status',
71
+ 'template',
72
+ 'format',
73
+ 'categories',
74
+ 'tags',
75
+ 'meta',
76
+ ];
77
+
78
+ let urlParams = [
79
+ 'order=asc'
80
+ ]
81
+
82
+
83
+ // if fields are passed
84
+ if( fields.length && 'all' !== fields ){
85
+ urlParams.push( `_fields=${ fields.join(',')}` );
86
+ }
87
+
88
+
89
+ // if no request is added default to GET
90
+ if( ! request.method ){
91
+ request.method = 'GET'
92
+ }
93
+
94
+ // if parent argument
95
+ if( request.parent ){
96
+ urlParams.push( `parent=` + processUrlParam( request.parent ))
97
+ }
98
+
99
+ // if include argument
100
+ if( request.include ){
101
+ urlParams.push( `include=` + processUrlParam( request.include ))
102
+ }
103
+
104
+ // if menus argument
105
+ if( request.menus ){
106
+ urlParams.push( 'menus=' + processUrlParam( request.menus ) );
107
+ }
108
+
109
+ // if per page argument.
110
+ urlParams.push( 'per_page=' + request.per_page ? request.per_page : '100' )
111
+
112
+
113
+ // if posts_per_page argument.
114
+ urlParams.push( 'posts_per_page=' + request.posts_per_page ? request.posts_per_page : '10' )
115
+
116
+
117
+ let collection = [];
118
+ let results = false;
119
+
120
+ /**
121
+ * Results are capped to 100 max per page
122
+ * in order to return all results, we make the same requests and pass the page parameter.
123
+ * @see https://developer.wordpress.org/rest-api/using-the-rest-api/pagination/
124
+ * @type {number}
125
+ */
126
+ let page = 1;
127
+
128
+ do{
129
+
130
+ results = await axios.request(
131
+ {
132
+ ...request,
133
+ url: `${request.url}${endpoint}/${tax}?` + urlParams.join('&') + `&page=${page}`
134
+ }
135
+ ).then(
136
+ (res) => {
137
+ // if no data or empty data return false.
138
+ if( ! res.data || ( 'object' === typeof res.data && ! Object.entries(res.data).length ) ){
139
+ return false;
140
+ }
141
+
142
+ // add results to collection
143
+ collection = collection.concat( res.data );
144
+ return true
145
+ }).catch(
146
+ (error) => {
147
+ return false
148
+ }
149
+ );
150
+
151
+ // settings taxonomies will always return results regardless of the page parameter.
152
+ // so we return false after the first request
153
+ if( 'settings' === tax ){
154
+ break;
155
+ }
156
+
157
+ page++;
158
+ }while( results )
159
+
160
+
161
+ return collection;
162
+ }
163
+
164
+ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
165
+ // if no request is added default to POST
166
+ if( ! request.method ){
167
+ request.method = 'POST'
168
+ }
169
+
170
+ let collection = [];
171
+
172
+ for( let obj of taxData ){
173
+ // endpoint url.
174
+ let url = `${request.url}${endpoint}/${tax}`;
175
+ let existingID = false;
176
+
177
+ // in order to maintain ID's, we have to use the cli, the REST API doesn't allow passing the id.
178
+ if( obj.id ){
179
+ // first we check if the ID exist
180
+ let idExists = await axios.request(
181
+ {
182
+ ...request,
183
+ method: 'GET',
184
+ url: `${url}/${ obj.id }`
185
+ }
186
+ )
187
+ .then((res) => { return res.data.id })
188
+ .catch(error => {return false;})
189
+
190
+ // for menus if ID doesn't exist we also check for matching slug
191
+ // if a menu with a matching slug exists we return that id instead.
192
+ if( 'menus' === tax && ! idExists ){
193
+ idExists = await axios.request(
194
+ {
195
+ ...request,
196
+ method: 'GET',
197
+ url: `${url}/?slug=${ obj.slug }`
198
+ }
199
+ )
200
+ .then((res) => { return res.data[0].id })
201
+ .catch(error => {return false;})
202
+
203
+ }
204
+
205
+ /**
206
+ * if the ID doesn't exist we save it so we can update via CLI later on.
207
+ * if it does exist update endpoint url so we update the existing item.
208
+ */
209
+ if( ! idExists ){
210
+ existingID = obj.id;
211
+ }else{
212
+ url = `${url}/${ obj.id }`;
213
+ }
214
+
215
+ // id has to be deleted.
216
+ delete obj.id;
217
+ }
218
+
219
+ // process properties
220
+ if( obj.content && obj.content.rendered ){
221
+ obj.content = processData(obj.content.rendered);
222
+ }
223
+
224
+ if( obj.title && obj.title.rendered ){
225
+ obj.title = processData(obj.title.rendered);
226
+ }
227
+
228
+ if( obj.caption && obj.caption.rendered ){
229
+ obj.caption = processData(obj.caption.rendered);
230
+ }
231
+
232
+ // make WordPress REST API request.
233
+ let results = await axios.request({
234
+ ...request,
235
+ data: 'media' === tax ? createMediaItem(obj) : obj,
236
+ url
237
+ })
238
+ .then( async (res) => { return res.data; } )
239
+
240
+
241
+ /**
242
+ * if the obj had an existing ID that didn't exist
243
+ */
244
+ if( existingID ){
245
+ let post_tbl = 'wp_posts';
246
+ let post_meta_tbl = 'wp_postmeta';
247
+ let term_taxonomy_tbl = 'wp_term_taxonomy';
248
+ let terms_tbl = 'wp_terms';
249
+
250
+ const cmd = [
251
+ path.join(projectPath, 'bin', 'wp-cli.phar'),
252
+ `--ssh=${request.ssh}`,
253
+ '--skip-themes',
254
+ `--url=${new URL(request.url).origin}`
255
+ ];
256
+
257
+ // if taxonomy is page/post/media.
258
+ if( ['pages', 'posts', 'media'].includes(tax) ){
259
+
260
+
261
+ /**
262
+ * Since the REST API doesn't detected trashed posts/pages.
263
+ * We have to do a drop before we can do an update.
264
+ * Since trashed posts/pages stay in the database
265
+ */
266
+ // drop post
267
+ /*await runCmd(
268
+ 'php',
269
+ [
270
+ ...cmd,
271
+ '--skip-column-names',
272
+ `db query 'DELETE FROM ${post_tbl} WHERE ID=${existingID}'`,
273
+ ]
274
+ )
275
+
276
+ // drop post meta.
277
+ await runCmd(
278
+ 'php',
279
+ [
280
+ ...cmd,
281
+ '--skip-column-names',
282
+ `db query 'DELETE FROM ${post_meta_tbl} WHERE post_id=${existingID}'`,
283
+ ]
284
+ )*/
285
+
286
+ // now we can update the new ID with the existing ID.
287
+ await runCmd(
288
+ 'php',
289
+ [
290
+ ...cmd,
291
+ '--skip-column-names',
292
+ `db query 'UPDATE ${post_tbl} SET ID=${existingID} WHERE ID=${results.id}'`,
293
+ ]
294
+ )
295
+ // update post meta.
296
+ await runCmd(
297
+ 'php',
298
+ [
299
+ ...cmd,
300
+ '--skip-column-names',
301
+ `db query 'UPDATE ${post_meta_tbl} SET post_id=${existingID} WHERE post_id=${results.id}'`,
302
+ ]
303
+ )
304
+
305
+ // if taxonomy is menu.
306
+ }else if( 'menus' === tax ){
307
+ // update the terms table.
308
+ await runCmd(
309
+ 'php',
310
+ [
311
+ ...cmd,
312
+ '--skip-column-names',
313
+ `db query 'UPDATE ${terms_tbl} SET term_id=${existingID} WHERE term_id=${results.id}'`,
314
+ ]
315
+ )
316
+
317
+ // update the term taxonomy table.
318
+ await runCmd(
319
+ 'php',
320
+ [
321
+ ...cmd,
322
+ '--skip-column-names',
323
+ `db query 'UPDATE ${term_taxonomy_tbl} SET term_taxonomy_id=${existingID},term_id=${existingID} WHERE term_id=${results.id}'`,
324
+ ]
325
+ )
326
+
327
+ /**
328
+ * Menus are also stored under nav_menu_locations in the the option theme_mods_<theme_name>
329
+ * for each location we patch that nav_menu_locations key with the correct ID
330
+ *
331
+ * @todo remove hardcoding of theme name
332
+ */
333
+ results.locations.map( async (location) => {
334
+ await runCmd(
335
+ 'php',
336
+ [
337
+ ...cmd,
338
+ 'option patch update',
339
+ `theme_mods_CAWeb`,
340
+ 'nav_menu_locations',
341
+ location,
342
+ existingID
343
+ ]
344
+ )
345
+ })
346
+
347
+ }
348
+
349
+ // update the API results ID, back to the existing ID from earlier.
350
+ results.id = existingID;
351
+ }
352
+
353
+ // add results to collection
354
+ collection = collection.concat( results );
355
+
356
+ let p = tax.charAt(0).toUpperCase() + tax.substring(1, tax.length - ( tax.endsWith('s') ? 1 : 0 ) );
357
+
358
+ if( undefined !== results && results.id ){
359
+ spinner.info(`${p}: ${results.id} was created.`);
360
+ }
361
+
362
+ }
363
+
364
+
365
+ }
366
+
367
+ function createMediaItem( media ){
368
+
369
+ let fd = new FormData();
370
+ let featureMediaFile = new File(
371
+ [
372
+ media.data
373
+ ],
374
+ media.source_url.split('/').pop(),
375
+ {
376
+ type: media.mime_type
377
+ }
378
+ );
379
+ fd.append('file', featureMediaFile );
380
+ fd.append('title', media.title);
381
+ fd.append('caption', media.caption);
382
+ fd.append('alt_text', media.alt_text);
383
+ fd.append('date', media.date);
384
+
385
+ return fd;
386
+ }
387
+
388
+
389
+ export {
390
+ getTaxonomies,
391
+ createTaxonomies
392
+ }
@@ -2,6 +2,7 @@ import { activateCAWeb, configureCAWeb } from "./caweb.js";
2
2
  import { downloadSources } from "./download-sources.js";
3
3
  import { configureDivi, isDiviThemeActive } from "./divi.js";
4
4
  import { configureWordPress, isMultisite, convertToMultisite, generateHTAccess } from "./wordpress.js";
5
+ import { getTaxonomies, createTaxonomies } from "./api.js";
5
6
 
6
7
  export {
7
8
  activateCAWeb,
@@ -12,5 +13,7 @@ export {
12
13
  configureWordPress,
13
14
  isMultisite,
14
15
  convertToMultisite,
15
- generateHTAccess
16
+ generateHTAccess,
17
+ getTaxonomies,
18
+ createTaxonomies
16
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caweb/cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "CAWebPublishing Command Line Interface.",
5
5
  "exports": "./lib/env.js",
6
6
  "type": "module",
@@ -57,8 +57,8 @@
57
57
  }
58
58
  },
59
59
  "dependencies": {
60
- "@wordpress/env": "^9.2.0",
61
- "@wordpress/scripts": "^27.1.0",
60
+ "@wordpress/env": "^9.4.0",
61
+ "@wordpress/scripts": "^27.3.0",
62
62
  "accessibility-checker": "^3.1.67",
63
63
  "autoprefixer": "^10.4.17",
64
64
  "axios": "^1.6.7",
@@ -66,17 +66,17 @@
66
66
  "commander": "^12.0.0",
67
67
  "cross-spawn": "^7.0.3",
68
68
  "css-loader": "^6.10.0",
69
- "docker-compose": "^0.24.3",
69
+ "docker-compose": "^0.24.6",
70
70
  "handlebars-loader": "^1.7.3",
71
- "html-to-json-parser": "^2.0.0",
71
+ "html-to-json-parser": "^2.0.1",
72
72
  "html-webpack-plugin": "^5.6.0",
73
73
  "mini-css-extract-plugin": "^2.8.0",
74
74
  "ora": "^8.0.1",
75
75
  "postcss-loader": "^8.1.0",
76
76
  "resolve-bin": "^1.0.1",
77
- "sass-loader": "^14.1.0",
77
+ "sass-loader": "^14.1.1",
78
78
  "terminal-link": "^3.0.0",
79
79
  "url": "^0.11.3",
80
- "webpack": "^5.90.1"
80
+ "webpack": "^5.90.3"
81
81
  }
82
82
  }