From 8f1b6fac87ca3383fc8ee4de66f4675efa20ac66 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 19 Feb 2022 22:59:51 +0000 Subject: [PATCH 01/14] Add convert command --- lib/commands/convert/index.js | 501 ++++++++++++++++++++++++++++++++++ 1 file changed, 501 insertions(+) create mode 100644 lib/commands/convert/index.js diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js new file mode 100644 index 0000000..58becc7 --- /dev/null +++ b/lib/commands/convert/index.js @@ -0,0 +1,501 @@ +const chalk = require( 'chalk' ); +const child_process = require( 'child_process' ); +const fs = require( 'fs' ); +const inquirer = require( 'inquirer' ); +const os = require( 'os' ); +const path = require( 'path' ); +const rimraf = require( 'rimraf' ); +const util = require( 'util' ); + +const promiseRename = util.promisify( fs.rename ); +const promiseRimraf = util.promisify( rimraf ); + +const TYPE_TRADITIONAL = 'traditional'; +const TYPE_SKELETON = 'skeleton'; +const TYPE_UNKNOWN = 'unknown'; + +const WP_FILES = [ + 'wp-admin', + 'wp-includes', + 'index.php', + 'license.txt', + 'readme.html', + 'wp-activate.php', + 'wp-blog-header.php', + 'wp-comments-post.php', + 'wp-config-sample.php', + 'wp-cron.php', + 'wp-links-opml.php', + 'wp-load.php', + 'wp-login.php', + 'wp-mail.php', + 'wp-settings.php', + 'wp-signup.php', + 'wp-trackback.php', + 'xmlrpc.php', +]; +const SKELETON_FILES = [ + 'wp', + 'wordpress', + 'index.php', +]; +const BUNDLED_DROPINS = [ + 'advanced-cache.php', + 'db.php', + 'object-cache.php', + 'hm-platform', +]; +const BUNDLED_PLUGINS = [ + 'altis-reusable-blocks', + 'asset-loader', + 'aws-analytics', + 'aws-rekognition', + 'aws-ses-wp-mail', + 'aws-xray', + 'authorship', + 'batcache', + 'browser-security', + 'cavalcade', + 'consent', + 'consent-api', + 'clean-html', + 'debug-bar-elasticpress', + 'delegated-oauth', + 'elasticpress', + 'extended-cpts', + 'gaussholder', + 'hm-gtm', + 'hm-redirects', + 'ludicrousdb', + 'meta-tags', + 'publication-checklist', + 'query-monitor', + 'require-login', + 'safe-svg', + 'simple-local-avatars', + 'smart-media', + 'stream', + 's3-uploads', + 'tachyon-plugin', + 'two-factor', + 'wordpress-seo', + 'workflows', + 'wp-redis', + 'wp-simple-saml', + 'wp-user-signups', +]; +const STATUS_FILE = '.altis-convert'; + +class Converter { + constructor( root, options ) { + this.root = root; + this.options = { + confirmDelete: true, + ...options + }; + this.status = null; + + this.steps = [ + { + name: 'Remove existing WP', + callback: () => this.removeExistingWP(), + }, + { + name: 'Back up config', + callback: () => this.backUpConfig(), + }, + { + name: 'Remove plugins', + callback: () => this.removePlugins(), + }, + { + name: 'Set up Composer', + callback: () => this.setUpComposer(), + }, + { + name: 'Add Altis', + callback: () => this.addAltis(), + }, + ]; + + } + + async removeFiles( dir, filenames ) { + // Remove WordPress files. + const files = fs.readdirSync( dir ); + for ( let i = 0; i < files.length; i++ ) { + const file = files[ i ]; + if ( file === STATUS_FILE ) { + continue; + } + + const fullPath = path.join( dir, file ); + const fullPathDisplay = chalk.grey( dir.replace( os.homedir(), '~' ) + path.sep ) + file; + + if ( filenames.indexOf( file ) === -1 ) { + console.log( chalk.grey( `Skipping ${ fullPathDisplay }` ) ); + continue; + } + + if ( this.options.confirmDelete ) { + const answers = await inquirer.prompt( { + type: 'confirm', + name: 'confirmed', + message: `Delete ${ fullPathDisplay }?`, + default: true, + } ); + if ( ! answers.confirmed ) { + console.log( 'Stopping.' ); + throw new Error( `Failed to delete ${ fullPathDisplay }` ); + } + } else { + console.log( chalk.red( `Deleting ${ fullPathDisplay }` ) ); + } + + await promiseRimraf( fullPath, { + glob: false, + } ); + continue; + } + } + + async init() { + this.status = this.loadStatus(); + let currentStep = this.status.step !== undefined ? this.status.step : null; + if ( currentStep !== null ) { + const answers = await inquirer.prompt( { + type: 'list', + name: 'resume', + message: 'Existing conversion detected.', + choices: [ + { + name: `Resume conversion from step ${ currentStep + 1 } ("${ this.steps[ currentStep ].name }")`, + value: 'resume', + short: 'Resume conversion', + }, + { + name: 'Restart from step 1', + value: 'restart', + short: 'Restart conversion', + }, + ] + } ); + if ( answers.resume === 'restart' ) { + currentStep = null; + } + } + if ( currentStep === null ) { + currentStep = 0; + console.log( `Beginning conversion for ${ chalk.bold( this.root ) }\n` ); + + const type = await this.checkWordPress(); + + switch ( type ) { + case TYPE_TRADITIONAL: + console.log( `${ chalk.green( '✓' ) } Identified as traditional WordPress (found wp-admin and wp-includes).` ); + break; + + case TYPE_SKELETON: + console.log( `${ chalk.green( '✓' ) } Identified as WordPress Skeleton (found wp or wordpress submodule).` ); + break; + } + + // Save the status for later. + this.updateStatus( { + ...this.status, + type, + } ); + + console.log( `\nYou'll need to confirm each step as it proceeds.` ); + console.log( `\nAt any point, you can hit Ctrl-C to cancel the conversion. Run this command again\nto resume the process from where you left off.\n` ); + console.log( 'You can also pass --confirm to require manual confirmation of every file deletion.\n' ); + } + + return currentStep; + } + + async processSteps() { + const startFrom = await this.init(); + for ( let i = startFrom; i < this.steps.length; i++ ) { + const step = this.steps[ i ]; + this.updateStatus( { + ...this.status, + step: i, + } ); + + const answers = await inquirer.prompt( { + type: 'confirm', + name: 'confirmed', + message: `Start step "${ step.name }"?`, + } ); + if ( ! answers.confirmed ) { + console.log( 'Stopping.' ); + break; + } + + try { + await step.callback(); + } catch ( err ) { + console.warn( `${ chalk.bold.red( 'Error:' ) } ${ err.message }` ); + console.log( `Failed step "${ step.name }".` ); + console.log( 'Correct the problem, then run the command again to resume from where you left off.' ); + return; + } + + console.log( `${ chalk.green( '✓' ) } Completed step "${ step.name }" (${ i + 1 }/${ this.steps.length } complete).\n` ); + } + + console.log( chalk.bold.green( 'Conversion completed!' ) ); + console.log( `\nAltis has been installed into your repository. Don't forget to check wp-config-backup.php and copy over any custom config.` ); + console.log( `\nAltis Local Server has also been installed. To try out your project, run:\n` ); + console.log( chalk.blue( ` composer serve\n` ) ); + console.log( `Welcome to Altis. We're glad you're here. 🚀\n` ); + + // Clean up the status file. + try { + fs.unlinkSync( path.join( this.root, STATUS_FILE ) ); + } catch ( err ) { + console.log( `(Unable to delete the conversion status file. Manually delete ${ STATUS_FILE })` ); + } + } + + loadStatus() { + const filename = path.join( this.root, STATUS_FILE ); + if ( ! fs.existsSync( filename ) ) { + return {}; + } + + const data = fs.readFileSync( filename ); + return JSON.parse( data ); + } + + updateStatus( status ) { + this.status = status; + const filename = path.join( this.root, STATUS_FILE ); + const encoded = JSON.stringify( status ); + fs.writeFileSync( filename, encoded ); + } + + /** + * Check whether this is WordPress, and what type. + * + * @return {TYPE_TRADITIONAL|TYPE_SKELETON} + */ + async checkWordPress() { + // First, check this is WordPress at all. + const configFile = path.join( this.root, 'wp-config.php' ); + if ( ! fs.existsSync( configFile ) ){ + throw new Error( 'No wp-config.php found. Are you sure this is a WordPress install?' ); + } + + // Check for wp-admin and wp-includes. + const wpAdmin = path.join( this.root, 'wp-admin' ); + const wpIncludes = path.join( this.root, 'wp-includes' ); + if ( fs.existsSync( wpAdmin ) && fs.existsSync( wpIncludes ) ) { + return TYPE_TRADITIONAL; + } + + const wpSubmodule = path.join( this.root, 'wp' ); + if ( fs.existsSync( wpSubmodule ) && fs.existsSync( path.join( wpSubmodule, '.git' ) ) ) { + return TYPE_SKELETON; + } + + const wordpressSubmodule = path.join( this.root, 'wordpress' ); + if ( fs.existsSync( wordpressSubmodule ) && fs.existsSync( path.join( wordpressSubmodule, '.git' ) ) ) { + return TYPE_SKELETON; + } + + throw new Error( 'Unable to identify the type of project. Are you sure this is a WordPress install?' ); + } + + /** + * Remove any existing WordPress files. + */ + async removeExistingWP() { + switch ( this.status.type ) { + case TYPE_TRADITIONAL: + // Remove WordPress files. + await this.removeFiles( this.root, WP_FILES ); + + // Move wp-content to content + const wpContentPath = path.join( this.root, 'wp-content' ); + if ( fs.existsSync( wpContentPath ) ) { + console.log( 'Moving wp-content to content' ); + const contentPath = path.join( this.root, 'content' ); + await promiseRename( wpContentPath, contentPath ); + } else { + console.log( 'No wp-content found.' ); + } + break; + + case TYPE_SKELETON: + // Remove git submodule if we have to. + // ... + + // Remove skeleton files + await this.removeFiles( this.root, SKELETON_FILES ); + break; + + case TYPE_UNKNOWN: + default: + throw new Error( 'Unable to identify the type of project. Are you sure this is a WordPress install?' ); + } + } + + /** + * Move wp-config.php to wp-config-backup.php + */ + async backUpConfig() { + // Rename `wp-config.php` to `wp-config-backup.php`. + const configPath = path.join( this.root, 'wp-config.php' ); + if ( ! fs.existsSync( configPath ) ) { + throw new Error( 'No wp-config.php file found.' ); + } + + console.log( 'Moving wp-config.php to wp-config-backup.php' ); + const bakPath = path.join( this.root, 'wp-config-backup.php' ); + await promiseRename( configPath, bakPath ); + + const cliConfigPath = path.join( this.root, 'wp-cli.yml' ); + if ( fs.existsSync( cliConfigPath ) ) { + const cliBakPath = path.join( this.root, 'wp-cli.yml.bak' ); + console.log( 'Moving wp-cli.yml to wp-cli.yml.bak' ); + console.log( `(Custom configuration in your wp-cli.yml can break installation, and isn't usually needed on Altis.)` ); + await promiseRename( cliConfigPath, cliBakPath ); + } + } + + /** + * Remove bundled plugins and drop-ins + */ + async removePlugins() { + const pluginsDir = path.join( this.root, 'content', 'plugins' ); + if ( ! fs.existsSync( pluginsDir ) ) { + console.log( 'No plugins directory found.' ); + return; + } + + await this.removeFiles( pluginsDir, BUNDLED_PLUGINS ); + + // Clean up any drop-ins if they exist. + console.log( 'Cleaning up drop-ins...' ); + const contentDir = path.join( this.root, 'content' ); + await this.removeFiles( contentDir, BUNDLED_DROPINS ); + } + + async setUpComposer() { + // Next, we're going to add configuration for Composer. Composer is a dependency manager for PHP, and is how you'll manage installing WordPress and its dependencies. + const composerJsonPath = path.join( this.root, 'composer.json' ); + if ( fs.existsSync( composerJsonPath ) ) { + console.log( 'Existing composer.json found, skipping creation.' ); + console.log( '\nEnsure you remove any conflicting packages, such as johnpbloch/wordpress.\n' ); + return; + } + + // If you don't already have Composer configuration in place, run `composer init` and follow the prompts. When asked if you would like to define your dependencies or dev dependencies, enter "n". When asked if you would like to add PSR-4 autoloading, enter "n". + const answers = await inquirer.prompt( [ + { + name: 'name', + message: 'Package name (/):', + default: () => { + const username = os.userInfo().username; + const name = path.basename( this.root ); + return `${ username }/${ name }`; + }, + }, + ] ); + + const config = { + name: answers.name, + require: {}, + extra: { + "installer-paths": { + "content/mu-plugins/{$name}/": [ + "type:wordpress-muplugin" + ], + "content/plugins/{$name}/": [ + "type:wordpress-plugin" + ], + "content/themes/{$name}/": [ + "type:wordpress-theme" + ] + } + }, + config: { + platform: { + php: "7.4.13", + "ext-mbstring": "7.4.13" + }, + "allow-plugins": { + "composer/installers": true, + "johnpbloch/wordpress-core-installer": true, + "altis/cms-installer": true, + "altis/dev-tools-command": true, + "altis/core": true, + "altis/local-chassis": true, + "altis/local-server": true + } + } + }; + const configString = JSON.stringify( config, null, '\t' ); + + console.log( `Creating ${ composerJsonPath }` ); + fs.writeFileSync( composerJsonPath, configString ); + } + + async addAltis() { + console.log( `Running ${ chalk.blue( 'composer require altis/altis' ) }` ); + console.log( ' --' ); + const res = child_process.spawnSync( 'composer', [ 'require', 'altis/altis:~9' ], { + cwd: this.root, + stdio: 'inherit', + // shell: true, + } ); + console.log( ' --' ); + if ( res.status != 0 ) { + throw new Error( 'Non-zero exit code from Composer; an error occurred.' ); + } + + console.log( `Running ${ chalk.blue( 'composer require --dev altis/local-chassis altis/local-server' ) }` ); + console.log( ' --' ); + const res2 = child_process.spawnSync( 'composer', [ 'require', '--dev', 'altis/local-chassis:~9', 'altis/local-server:~9' ], { + cwd: this.root, + stdio: 'inherit', + // shell: true, + } ); + console.log( ' --' ); + if ( res2.status != 0 ) { + throw new Error( 'Non-zero exit code from Composer; an error occurred.' ); + } + } +} + +module.exports = { + command: 'convert [path]', + description: 'Convert your existing WordPress codebase to Altis.', + builder: yargs => { + yargs.positional( 'path', { + description: 'Path to your WordPress codebase', + type: 'string', + normalize: true, + default: '.', + coerce: path => fs.realpathSync( path ), + } ); + yargs.option( 'confirm', { + description: 'Require confirmation of each operation', + type: 'boolean', + default: false, + } ); + }, + handler: async argv => { + const converter = new Converter( argv.path, { + confirmDelete: argv.confirm, + } ); + try { + await converter.processSteps(); + } catch ( err ) { + console.warn( `${ chalk.bold.red( 'Error:' ) } ${ err.message }` ); + process.exit( 1 ); + } + } +}; From 4e946e6db2ed7e4da0e98ee650983eba033808e1 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 20 Feb 2022 15:02:44 +0000 Subject: [PATCH 02/14] Improve some formatting --- lib/commands/convert/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index 58becc7..3943f67 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -133,7 +133,7 @@ class Converter { const fullPathDisplay = chalk.grey( dir.replace( os.homedir(), '~' ) + path.sep ) + file; if ( filenames.indexOf( file ) === -1 ) { - console.log( chalk.grey( `Skipping ${ fullPathDisplay }` ) ); + console.log( chalk.grey( ` - Skipping ${ fullPathDisplay }` ) ); continue; } @@ -141,15 +141,16 @@ class Converter { const answers = await inquirer.prompt( { type: 'confirm', name: 'confirmed', - message: `Delete ${ fullPathDisplay }?`, + message: chalk.red( `Delete ${ fullPathDisplay }?` ), default: true, + prefix: ' -', } ); if ( ! answers.confirmed ) { console.log( 'Stopping.' ); throw new Error( `Failed to delete ${ fullPathDisplay }` ); } } else { - console.log( chalk.red( `Deleting ${ fullPathDisplay }` ) ); + console.log( ' - ' + chalk.red( `Deleting ${ fullPathDisplay }` ) ); } await promiseRimraf( fullPath, { @@ -375,10 +376,11 @@ class Converter { return; } + console.log( 'Cleaning up plugins...' ); await this.removeFiles( pluginsDir, BUNDLED_PLUGINS ); // Clean up any drop-ins if they exist. - console.log( 'Cleaning up drop-ins...' ); + console.log( '\nCleaning up drop-ins...' ); const contentDir = path.join( this.root, 'content' ); await this.removeFiles( contentDir, BUNDLED_DROPINS ); } From 59e365a8611cdd192e97bad56155671a33568600 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sun, 20 Feb 2022 15:11:17 +0000 Subject: [PATCH 03/14] Abstract away file deletion --- lib/commands/convert/index.js | 41 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index 3943f67..436a770 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -120,6 +120,28 @@ class Converter { } + async deleteFile( fullPath, fullPathDisplay ) { + if ( this.options.confirmDelete ) { + const answers = await inquirer.prompt( { + type: 'confirm', + name: 'confirmed', + message: chalk.red( `Delete ${ fullPathDisplay }?` ), + default: true, + prefix: ' -', + } ); + if ( ! answers.confirmed ) { + console.log( 'Stopping.' ); + throw new Error( `Failed to delete ${ fullPathDisplay }` ); + } + } else { + console.log( ' - ' + chalk.red( `Deleting ${ fullPathDisplay }` ) ); + } + + await promiseRimraf( fullPath, { + glob: false, + } ); + } + async removeFiles( dir, filenames ) { // Remove WordPress files. const files = fs.readdirSync( dir ); @@ -137,25 +159,8 @@ class Converter { continue; } - if ( this.options.confirmDelete ) { - const answers = await inquirer.prompt( { - type: 'confirm', - name: 'confirmed', - message: chalk.red( `Delete ${ fullPathDisplay }?` ), - default: true, - prefix: ' -', - } ); - if ( ! answers.confirmed ) { - console.log( 'Stopping.' ); - throw new Error( `Failed to delete ${ fullPathDisplay }` ); - } - } else { - console.log( ' - ' + chalk.red( `Deleting ${ fullPathDisplay }` ) ); - } + await this.deleteFile( fullPath, fullPathDisplay ); - await promiseRimraf( fullPath, { - glob: false, - } ); continue; } } From 62aa65cf2d66768f9a29fb2291f1987ecea578e0 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 1 Sep 2023 14:12:39 +0100 Subject: [PATCH 04/14] Add VIP type --- lib/commands/convert/index.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index 436a770..9cfbb35 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -13,6 +13,7 @@ const promiseRimraf = util.promisify( rimraf ); const TYPE_TRADITIONAL = 'traditional'; const TYPE_SKELETON = 'skeleton'; const TYPE_UNKNOWN = 'unknown'; +const TYPE_VIP = 'vip'; const WP_FILES = [ 'wp-admin', @@ -204,6 +205,10 @@ class Converter { case TYPE_SKELETON: console.log( `${ chalk.green( '✓' ) } Identified as WordPress Skeleton (found wp or wordpress submodule).` ); break; + + case TYPE_VIP: + console.log( `${ chalk.green( '✓' ) } Identified as WPVIP (found vip-config).` ); + break; } // Save the status for later. @@ -285,15 +290,21 @@ class Converter { /** * Check whether this is WordPress, and what type. * - * @return {TYPE_TRADITIONAL|TYPE_SKELETON} + * @return {TYPE_TRADITIONAL|TYPE_SKELETON|TYPE_VIP} */ async checkWordPress() { // First, check this is WordPress at all. const configFile = path.join( this.root, 'wp-config.php' ); - if ( ! fs.existsSync( configFile ) ){ + const vipConfigDir = path.join( this.root, 'vip-config' ); + const hasVipConfig = fs.existsSync( vipConfigDir ); + if ( ! fs.existsSync( configFile ) && ! hasVipConfig ) { throw new Error( 'No wp-config.php found. Are you sure this is a WordPress install?' ); } + if ( hasVipConfig ) { + return TYPE_VIP; + } + // Check for wp-admin and wp-includes. const wpAdmin = path.join( this.root, 'wp-admin' ); const wpIncludes = path.join( this.root, 'wp-includes' ); From 6c379e57242d1da506230379c4446a1825590bfd Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 1 Sep 2023 14:12:55 +0100 Subject: [PATCH 05/14] Move dependencies for VIP --- lib/commands/convert/index.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index 9cfbb35..e7fd205 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -7,6 +7,7 @@ const path = require( 'path' ); const rimraf = require( 'rimraf' ); const util = require( 'util' ); +const promiseMkdir = util.promisify( fs.mkdir ); const promiseRename = util.promisify( fs.rename ); const promiseRimraf = util.promisify( rimraf ); @@ -353,6 +354,36 @@ class Converter { await this.removeFiles( this.root, SKELETON_FILES ); break; + case TYPE_VIP: + // Move files into a content directory. + const contentPath = path.join( this.root, 'content' ); + if ( ! fs.existsSync( contentPath ) ) { + console.log( 'Creating content directory' ); + await promiseMkdir( contentPath ); + } + + const vipPluginsDir = path.join( this.root, 'plugins' ); + if ( fs.existsSync( vipPluginsDir ) ) { + console.log( 'Moving plugins to content/plugins' ); + const pluginsDir = path.join( contentPath, 'plugins' ); + await promiseRename( vipPluginsDir, pluginsDir ); + } + + const vipThemesDir = path.join( this.root, 'themes' ); + if ( fs.existsSync( vipThemesDir ) ) { + console.log( 'Moving themes to content/themes' ); + const themesDir = path.join( contentPath, 'themes' ); + await promiseRename( vipThemesDir, themesDir ); + } + + const vipMuPluginsDir = path.join( this.root, 'client-mu-plugins' ); + if ( fs.existsSync( vipPluginsDir ) ) { + console.log( 'Moving client-mu-plugins to content/mu-plugins' ); + const muPluginsDir = path.join( contentPath, 'mu-plugins' ); + await promiseRename( vipMuPluginsDir, muPluginsDir ); + } + break; + case TYPE_UNKNOWN: default: throw new Error( 'Unable to identify the type of project. Are you sure this is a WordPress install?' ); From 3560da39e44b5e0d4252313f99e8cf653e017916 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 1 Sep 2023 14:13:26 +0100 Subject: [PATCH 06/14] Handle vip-config --- lib/commands/convert/index.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index e7fd205..77c570f 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -394,15 +394,21 @@ class Converter { * Move wp-config.php to wp-config-backup.php */ async backUpConfig() { - // Rename `wp-config.php` to `wp-config-backup.php`. - const configPath = path.join( this.root, 'wp-config.php' ); - if ( ! fs.existsSync( configPath ) ) { - throw new Error( 'No wp-config.php file found.' ); - } + if ( this.status.type === TYPE_VIP ) { + console.log( 'Leaving your vip-config in place.' ); + console.log( '(You can clean this up later if you want.)' ); + return; + } else { + // Rename `wp-config.php` to `wp-config-backup.php`. + const configPath = path.join( this.root, 'wp-config.php' ); + if ( ! fs.existsSync( configPath ) ) { + throw new Error( 'No wp-config.php file found.' ); + } - console.log( 'Moving wp-config.php to wp-config-backup.php' ); - const bakPath = path.join( this.root, 'wp-config-backup.php' ); - await promiseRename( configPath, bakPath ); + console.log( 'Moving wp-config.php to wp-config-backup.php' ); + const bakPath = path.join( this.root, 'wp-config-backup.php' ); + await promiseRename( configPath, bakPath ); + } const cliConfigPath = path.join( this.root, 'wp-cli.yml' ); if ( fs.existsSync( cliConfigPath ) ) { From dc286b404b8736fccd36d96919471dff7b228b10 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 1 Sep 2023 14:13:33 +0100 Subject: [PATCH 07/14] Update to Altis v16 --- lib/commands/convert/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index 77c570f..5c7aa15 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -501,7 +501,7 @@ class Converter { async addAltis() { console.log( `Running ${ chalk.blue( 'composer require altis/altis' ) }` ); console.log( ' --' ); - const res = child_process.spawnSync( 'composer', [ 'require', 'altis/altis:~9' ], { + const res = child_process.spawnSync( 'composer', [ 'require', '--with-all-dependencies', 'altis/altis:~16' ], { cwd: this.root, stdio: 'inherit', // shell: true, @@ -513,7 +513,7 @@ class Converter { console.log( `Running ${ chalk.blue( 'composer require --dev altis/local-chassis altis/local-server' ) }` ); console.log( ' --' ); - const res2 = child_process.spawnSync( 'composer', [ 'require', '--dev', 'altis/local-chassis:~9', 'altis/local-server:~9' ], { + const res2 = child_process.spawnSync( 'composer', [ 'require', '--dev', '--with-all-dependencies', 'altis/local-server:~16' ], { cwd: this.root, stdio: 'inherit', // shell: true, From 2528f4155d86db259efd736f46f0dd7801789a12 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 1 Sep 2023 15:55:52 +0100 Subject: [PATCH 08/14] Prompt for moves --- lib/commands/convert/index.js | 41 +++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index 5c7aa15..d742d03 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -122,6 +122,28 @@ class Converter { } + async renamePath( oldPath, newPath ) { + const relOld = path.relative( this.root, oldPath ); + const relNew = path.relative( this.root, newPath ); + if ( this.options.confirmDelete ) { + const answers = await inquirer.prompt( { + type: 'confirm', + name: 'confirmed', + message: chalk.yellow( `Move ${ relOld } to ${ relNew }?` ), + default: true, + prefix: ' -', + } ); + if ( ! answers.confirmed ) { + console.log( 'Stopping.' ); + throw new Error( `Failed to move ${ relOld } to ${ relNew }` ); + } + } else { + console.log( ' → ' + chalk.yellow( `Moving ${ relOld } to ${ relNew }` ) ); + } + + await promiseRename( oldPath, newPath ); + } + async deleteFile( fullPath, fullPathDisplay ) { if ( this.options.confirmDelete ) { const answers = await inquirer.prompt( { @@ -220,7 +242,7 @@ class Converter { console.log( `\nYou'll need to confirm each step as it proceeds.` ); console.log( `\nAt any point, you can hit Ctrl-C to cancel the conversion. Run this command again\nto resume the process from where you left off.\n` ); - console.log( 'You can also pass --confirm to require manual confirmation of every file deletion.\n' ); + console.log( 'You can also pass --confirm to require manual confirmation of every file deletion and move.\n' ); } return currentStep; @@ -259,6 +281,9 @@ class Converter { console.log( chalk.bold.green( 'Conversion completed!' ) ); console.log( `\nAltis has been installed into your repository. Don't forget to check wp-config-backup.php and copy over any custom config.` ); + if ( this.status.type === TYPE_TRADITIONAL || this.status.type === TYPE_VIP ) { + console.log( 'You may also need to adjust paths for any files we moved.' ); + } console.log( `\nAltis Local Server has also been installed. To try out your project, run:\n` ); console.log( chalk.blue( ` composer serve\n` ) ); console.log( `Welcome to Altis. We're glad you're here. 🚀\n` ); @@ -362,25 +387,23 @@ class Converter { await promiseMkdir( contentPath ); } + console.log( 'Moving content files into content directory' ); const vipPluginsDir = path.join( this.root, 'plugins' ); if ( fs.existsSync( vipPluginsDir ) ) { - console.log( 'Moving plugins to content/plugins' ); const pluginsDir = path.join( contentPath, 'plugins' ); - await promiseRename( vipPluginsDir, pluginsDir ); + await this.renamePath( vipPluginsDir, pluginsDir ); } const vipThemesDir = path.join( this.root, 'themes' ); if ( fs.existsSync( vipThemesDir ) ) { - console.log( 'Moving themes to content/themes' ); const themesDir = path.join( contentPath, 'themes' ); - await promiseRename( vipThemesDir, themesDir ); + await this.renamePath( vipThemesDir, themesDir ); } const vipMuPluginsDir = path.join( this.root, 'client-mu-plugins' ); if ( fs.existsSync( vipPluginsDir ) ) { - console.log( 'Moving client-mu-plugins to content/mu-plugins' ); const muPluginsDir = path.join( contentPath, 'mu-plugins' ); - await promiseRename( vipMuPluginsDir, muPluginsDir ); + await this.renamePath( vipMuPluginsDir, muPluginsDir ); } break; @@ -407,7 +430,7 @@ class Converter { console.log( 'Moving wp-config.php to wp-config-backup.php' ); const bakPath = path.join( this.root, 'wp-config-backup.php' ); - await promiseRename( configPath, bakPath ); + await this.renamePath( configPath, bakPath ); } const cliConfigPath = path.join( this.root, 'wp-cli.yml' ); @@ -415,7 +438,7 @@ class Converter { const cliBakPath = path.join( this.root, 'wp-cli.yml.bak' ); console.log( 'Moving wp-cli.yml to wp-cli.yml.bak' ); console.log( `(Custom configuration in your wp-cli.yml can break installation, and isn't usually needed on Altis.)` ); - await promiseRename( cliConfigPath, cliBakPath ); + await this.renamePath( cliConfigPath, cliBakPath ); } } From 9ed91d9e6058e77025fd04c14c7422596fece121 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 1 Sep 2023 15:56:12 +0100 Subject: [PATCH 09/14] Remove older packages --- lib/commands/convert/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index d742d03..a75b2ee 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -54,7 +54,6 @@ const BUNDLED_PLUGINS = [ 'aws-rekognition', 'aws-ses-wp-mail', 'aws-xray', - 'authorship', 'batcache', 'browser-security', 'cavalcade', @@ -70,7 +69,6 @@ const BUNDLED_PLUGINS = [ 'hm-redirects', 'ludicrousdb', 'meta-tags', - 'publication-checklist', 'query-monitor', 'require-login', 'safe-svg', @@ -80,7 +78,6 @@ const BUNDLED_PLUGINS = [ 's3-uploads', 'tachyon-plugin', 'two-factor', - 'wordpress-seo', 'workflows', 'wp-redis', 'wp-simple-saml', @@ -510,7 +507,6 @@ class Converter { "altis/cms-installer": true, "altis/dev-tools-command": true, "altis/core": true, - "altis/local-chassis": true, "altis/local-server": true } } From ea4b005ff42247cac8cbd60e898978c5622fef0f Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 1 Sep 2023 15:56:32 +0100 Subject: [PATCH 10/14] Delete plugins from composer.json too --- lib/commands/convert/index.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index a75b2ee..d1c3739 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -83,6 +83,11 @@ const BUNDLED_PLUGINS = [ 'wp-simple-saml', 'wp-user-signups', ]; +const BUNDLED_PACKAGES = [ + 'johnpbloch/wordpress', + + ...BUNDLED_PLUGINS.map( name => `humanmade/${ name }` ), +]; const STATUS_FILE = '.altis-convert'; class Converter { @@ -456,6 +461,36 @@ class Converter { console.log( '\nCleaning up drop-ins...' ); const contentDir = path.join( this.root, 'content' ); await this.removeFiles( contentDir, BUNDLED_DROPINS ); + + const composerJsonPath = path.join( this.root, 'composer.json' ); + if ( fs.existsSync( composerJsonPath ) ) { + console.log( '\nCleaning up composer.json dependencies...' ); + const originalComposerData = require( composerJsonPath ); + + // Clone data before editing. + const composerData = { ...originalComposerData }; + const removeDep = ( dep ) => { + if ( composerData.require[ dep ] ) { + console.log( ` - ${ chalk.red( `Removing ${ dep } from require` ) }` ); + composerData.require = { + ...composerData.require, + [ dep ]: undefined, + }; + } + if ( composerData['require-dev'][ dep ] ) { + console.log( ` - ${ chalk.red( `Removing ${ dep } from require-dev` ) }` ); + composerData['require-dev'] = { + ...composerData['require-dev'], + [ dep ]: undefined, + }; + } + } + BUNDLED_PACKAGES.forEach( removeDep ); + + // Serialize and save. + const nextData = JSON.stringify( composerData, undefined, '\t' ); + fs.writeFileSync( composerJsonPath, nextData ); + } } async setUpComposer() { From 967dc43bd5e7456b15f99f4d936ca4c859b190e5 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 1 Sep 2023 15:56:54 +0100 Subject: [PATCH 11/14] Adjust existing composer.json where needed --- lib/commands/convert/index.js | 62 +++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index d1c3739..cf7ce73 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -497,8 +497,66 @@ class Converter { // Next, we're going to add configuration for Composer. Composer is a dependency manager for PHP, and is how you'll manage installing WordPress and its dependencies. const composerJsonPath = path.join( this.root, 'composer.json' ); if ( fs.existsSync( composerJsonPath ) ) { - console.log( 'Existing composer.json found, skipping creation.' ); - console.log( '\nEnsure you remove any conflicting packages, such as johnpbloch/wordpress.\n' ); + console.log( 'Existing composer.json found.' ); + const originalComposerData = require( composerJsonPath ); + + // Clone data before editing. + const composerData = { ...originalComposerData }; + console.log( '\nChecking configuration for incompatibilities' ); + if ( composerData.config['vendor-dir'] ) { + console.log( ` - ${ chalk.red( 'Removing config.vendor-dir' ) }` ); + composerData.config = { + ...composerData.config, + 'vendor-dir': undefined, + }; + } + + console.log( 'Setting other configuration' ); + console.log( ' - Setting extra.installer-paths' ); + composerData.extra = { + ...( composerData.extra || {} ), + "installer-paths": { + "content/mu-plugins/{$name}/": [ + "type:wordpress-muplugin" + ], + "content/plugins/{$name}/": [ + "type:wordpress-plugin" + ], + "content/themes/{$name}/": [ + "type:wordpress-theme" + ] + } + }; + + console.log( ' - Setting config.platform' ); + console.log( ' - Adding Altis modules to config.allow-plugins' ); + console.log( ' - Adding default Altis configuration' ); + composerData.config = { + ...( composerData.config || {} ), + "platform": { + php: "8.0", + "ext-mbstring": "8.0" + }, + 'allow-plugins': { + ...( ( composerData.config || {} )['allow-plugins'] || {} ), + 'composer/installers': true, + 'johnpbloch/wordpress-core-installer': true, + 'altis/*': true, + }, + + // Altis configuration. + "altis": { + "modules": { + "analytics": { + "enabled": false + } + } + } + }; + + // Serialize and save. + const nextData = JSON.stringify( composerData, undefined, '\t' ); + fs.writeFileSync( composerJsonPath, nextData ); return; } From 3c49d2ce3c365b7641a672ead350b1068781f08e Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Fri, 1 Sep 2023 15:57:28 +0100 Subject: [PATCH 12/14] Run composer require separately to update This ensures we don't accidentally add incompatible packages halfway through. --- lib/commands/convert/index.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index cf7ce73..5784ad9 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -16,6 +16,8 @@ const TYPE_SKELETON = 'skeleton'; const TYPE_UNKNOWN = 'unknown'; const TYPE_VIP = 'vip'; +const ALTIS_VERSION = 16; + const WP_FILES = [ 'wp-admin', 'wp-includes', @@ -611,28 +613,45 @@ class Converter { } async addAltis() { - console.log( `Running ${ chalk.blue( 'composer require altis/altis' ) }` ); + console.log( `Running ${ chalk.blue( `composer require altis/altis:~${ ALTIS_VERSION }` ) }` ); console.log( ' --' ); - const res = child_process.spawnSync( 'composer', [ 'require', '--with-all-dependencies', 'altis/altis:~16' ], { + const res = child_process.spawnSync( 'composer', [ 'require', '--no-update', `altis/altis:~${ ALTIS_VERSION }` ], { cwd: this.root, stdio: 'inherit', // shell: true, } ); console.log( ' --' ); if ( res.status != 0 ) { - throw new Error( 'Non-zero exit code from Composer; an error occurred.' ); + throw new Error( 'Non-zero exit code from Composer; an error occurred. (Try composer update before resuming.)' ); } - console.log( `Running ${ chalk.blue( 'composer require --dev altis/local-chassis altis/local-server' ) }` ); + console.log( `Running ${ chalk.blue( `composer require --dev altis/local-server:~${ ALTIS_VERSION }` ) }` ); console.log( ' --' ); - const res2 = child_process.spawnSync( 'composer', [ 'require', '--dev', '--with-all-dependencies', 'altis/local-server:~16' ], { + const res2 = child_process.spawnSync( 'composer', [ 'require', '--dev', '--no-update', `altis/local-server:~${ ALTIS_VERSION }` ], { cwd: this.root, stdio: 'inherit', // shell: true, } ); console.log( ' --' ); if ( res2.status != 0 ) { - throw new Error( 'Non-zero exit code from Composer; an error occurred.' ); + throw new Error( 'Non-zero exit code from Composer; an error occurred. (Try composer update before resuming.)' ); + } + + console.log( `Running ${ chalk.blue( 'composer update --with-all-dependencies altis/altis altis/local-server' ) }` ); + console.log( ' --' ); + const res3 = child_process.spawnSync( 'composer', [ + 'update', + '--with-all-dependencies', + 'altis/altis', + 'altis/local-server', + ], { + cwd: this.root, + stdio: 'inherit', + // shell: true, + } ); + console.log( ' --' ); + if ( res3.status != 0 ) { + throw new Error( 'Non-zero exit code from Composer; an error occurred. (Try composer update before resuming.)' ); } } } From aea92bfbd975e8bb7bc0047027853525e508c589 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 2 Sep 2023 16:39:26 +0100 Subject: [PATCH 13/14] Allow setting PHP version --- lib/commands/convert/index.js | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index 5784ad9..693c0b2 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -496,6 +496,31 @@ class Converter { } async setUpComposer() { + const verAnswer = await inquirer.prompt( { + type: 'list', + name: 'version', + message: 'Select PHP version to use.', + choices: [ + { + value: '8.0', + }, + { + value: '8.1', + }, + { + name: '8.2 (experimental)', + value: '8.2', + short: '8.2', + }, + { + name: '7.4 (deprecated)', + value: '7.4', + short: '7.4', + }, + ] + } ); + const phpVersion = verAnswer.version; + // Next, we're going to add configuration for Composer. Composer is a dependency manager for PHP, and is how you'll manage installing WordPress and its dependencies. const composerJsonPath = path.join( this.root, 'composer.json' ); if ( fs.existsSync( composerJsonPath ) ) { @@ -530,14 +555,14 @@ class Converter { } }; - console.log( ' - Setting config.platform' ); + console.log( ` - Setting config.platform (PHP ${ phpVersion })` ); console.log( ' - Adding Altis modules to config.allow-plugins' ); console.log( ' - Adding default Altis configuration' ); composerData.config = { ...( composerData.config || {} ), "platform": { - php: "8.0", - "ext-mbstring": "8.0" + php: phpVersion, + "ext-mbstring": phpVersion, }, 'allow-plugins': { ...( ( composerData.config || {} )['allow-plugins'] || {} ), @@ -593,8 +618,8 @@ class Converter { }, config: { platform: { - php: "7.4.13", - "ext-mbstring": "7.4.13" + php: phpVersion, + "ext-mbstring": phpVersion, }, "allow-plugins": { "composer/installers": true, From cc4dc2574b48d8fea41504d37669e6ed0dd86c14 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Sat, 2 Sep 2023 16:39:35 +0100 Subject: [PATCH 14/14] Simplify renames --- lib/commands/convert/index.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/commands/convert/index.js b/lib/commands/convert/index.js index 693c0b2..8efba5f 100644 --- a/lib/commands/convert/index.js +++ b/lib/commands/convert/index.js @@ -392,22 +392,18 @@ class Converter { } console.log( 'Moving content files into content directory' ); - const vipPluginsDir = path.join( this.root, 'plugins' ); - if ( fs.existsSync( vipPluginsDir ) ) { - const pluginsDir = path.join( contentPath, 'plugins' ); - await this.renamePath( vipPluginsDir, pluginsDir ); - } - - const vipThemesDir = path.join( this.root, 'themes' ); - if ( fs.existsSync( vipThemesDir ) ) { - const themesDir = path.join( contentPath, 'themes' ); - await this.renamePath( vipThemesDir, themesDir ); - } - const vipMuPluginsDir = path.join( this.root, 'client-mu-plugins' ); - if ( fs.existsSync( vipPluginsDir ) ) { - const muPluginsDir = path.join( contentPath, 'mu-plugins' ); - await this.renamePath( vipMuPluginsDir, muPluginsDir ); + const dirMap = { + "plugins": path.join( contentPath, 'plugins' ), + "themes": path.join( contentPath, 'themes' ), + "client-mu-plugins": path.join( contentPath, 'client-mu-plugins' ), + "languages": path.join( contentPath, 'languages' ), + }; + const dirKeys = Object.keys( dirMap ); + for ( let i = 0; i < dirKeys.length; i++ ) { + const dirName = dirKeys[0]; + const fullPath = path.join( this.root, dirName ); + await this.renamePath( fullPath, dirMap[ dirName ] ); } break;