diff --git a/__tests__/commands/dev-env-sync-sql.ts b/__tests__/commands/dev-env-sync-sql.ts index a0392447d..72ad0813a 100644 --- a/__tests__/commands/dev-env-sync-sql.ts +++ b/__tests__/commands/dev-env-sync-sql.ts @@ -34,6 +34,10 @@ describe( 'commands/DevEnvSyncSQLCommand', () => { blogId: 2, homeUrl: 'https://subsite.com', }, + { + blogId: 3, + homeUrl: 'https://another.com/path', + }, ], }, }; @@ -56,7 +60,7 @@ describe( 'commands/DevEnvSyncSQLCommand', () => { it( 'should return a map of search-replace values', () => { const cmd = new DevEnvSyncSQLCommand( app, env, 'test-slug', lando ); cmd.slug = 'test-slug'; - cmd.siteUrls = [ 'test.go-vip.com' ]; + cmd.siteUrls = [ 'http://test.go-vip.com' ]; cmd.generateSearchReplaceMap(); expect( cmd.searchReplaceMap ).toEqual( { 'test.go-vip.com': 'test-slug.vipdev.lndo.site' } ); @@ -65,12 +69,13 @@ describe( 'commands/DevEnvSyncSQLCommand', () => { it( 'should return a map of search-replace values for multisite', () => { const cmd = new DevEnvSyncSQLCommand( app, msEnv, 'test-slug', lando ); cmd.slug = 'test-slug'; - cmd.siteUrls = [ 'test.go-vip.com', 'subsite.com' ]; + cmd.siteUrls = [ 'https://test.go-vip.com', 'http://subsite.com', 'http://another.com/path' ]; cmd.generateSearchReplaceMap(); expect( cmd.searchReplaceMap ).toEqual( { 'test.go-vip.com': 'test-slug.vipdev.lndo.site', - 'subsite.com': 'subsite-com-2.test-slug.vipdev.lndo.site', + 'subsite.com': 'subsite-com.test-slug.vipdev.lndo.site', + 'another.com/path': 'another-com.test-slug.vipdev.lndo.site/path', } ); } ); } ); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index f214ee89b..db90eba31 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -44,7 +44,7 @@ "socks-proxy-agent": "^5.0.1", "tar": "^7.4.0", "update-notifier": "7.3.1", - "uuid": "11.0.2", + "uuid": "11.0.3", "xdg-basedir": "^4.0.0", "xml2js": "^0.5.0" }, @@ -136,7 +136,7 @@ "dockerode": "^4.0.0", "eslint": "^8.35.0", "jest": "^29.7.0", - "nock": "13.5.5", + "nock": "13.5.6", "prettier": "npm:wp-prettier@2.8.5", "rimraf": "6.0.1", "typescript": "^5.2.2" @@ -10103,9 +10103,9 @@ "dev": true }, "node_modules/nock": { - "version": "13.5.5", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.5.tgz", - "integrity": "sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", "dev": true, "dependencies": { "debug": "^4.1.0", @@ -12634,9 +12634,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", - "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -20602,9 +20602,9 @@ "dev": true }, "nock": { - "version": "13.5.5", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.5.tgz", - "integrity": "sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA==", + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", "dev": true, "requires": { "debug": "^4.1.0", @@ -22396,9 +22396,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "uuid": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", - "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==" + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==" }, "v8-to-istanbul": { "version": "9.1.0", diff --git a/package.json b/package.json index a76415898..276b0a452 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "dockerode": "^4.0.0", "eslint": "^8.35.0", "jest": "^29.7.0", - "nock": "13.5.5", + "nock": "13.5.6", "prettier": "npm:wp-prettier@2.8.5", "rimraf": "6.0.1", "typescript": "^5.2.2" @@ -175,7 +175,7 @@ "socks-proxy-agent": "^5.0.1", "tar": "^7.4.0", "update-notifier": "7.3.1", - "uuid": "11.0.2", + "uuid": "11.0.3", "xdg-basedir": "^4.0.0", "xml2js": "^0.5.0" }, diff --git a/src/bin/vip-logs.js b/src/bin/vip-logs.js index 78c6cfde9..07ce6b6d1 100755 --- a/src/bin/vip-logs.js +++ b/src/bin/vip-logs.js @@ -11,6 +11,7 @@ import { trackEvent } from '../lib/tracker'; const LIMIT_MIN = 1; const LIMIT_MAX = 5000; +const LIMIT_DEFAULT = 500; const ALLOWED_TYPES = [ 'app', 'batch' ]; const ALLOWED_FORMATS = [ 'csv', 'json', 'table' ]; const DEFAULT_POLLING_DELAY_IN_SECONDS = 30; @@ -161,6 +162,10 @@ function printLogs( logs, format ) { * @param {string} format */ export function validateInputs( type, limit, format ) { + if ( limit === undefined ) { + limit = LIMIT_DEFAULT; + } + if ( ! ALLOWED_TYPES.includes( type ) ) { exit.withError( `Invalid type: ${ type }. The supported types are: ${ ALLOWED_TYPES.join( ', ' ) }.` @@ -202,7 +207,8 @@ command( { module: 'logs', } ) .option( 'type', 'The type of logs to be returned: "app" or "batch"', 'app' ) - .option( 'limit', 'The maximum number of log lines', 500 ) + // The default limit is set manually in the validateInputs function to address validation issues, avoiding incorrect replacement of the default value. + .option( 'limit', `The maximum number of log lines (defaults to ${ LIMIT_DEFAULT })` ) .option( 'follow', 'Keep fetching new logs as they are generated' ) .option( 'format', 'Output the log lines in CSV or JSON format', 'table' ) .examples( [ diff --git a/src/bin/vip-wp.js b/src/bin/vip-wp.js index 985ea156a..92cce82a3 100755 --- a/src/bin/vip-wp.js +++ b/src/bin/vip-wp.js @@ -53,11 +53,25 @@ const unpipeStreamsFromProcess = ( { stdin, stdout: outStream } ) => { }; const bindStreamEvents = ( { subShellRl, commonTrackingParams, isSubShell, stdoutStream } ) => { + const criticalErrors = [ + 'ECONNRESET', + 'ETIMEDOUT', + 'EHOSTUNREACH', + 'ENOSPC', + 'EACCES', + 'EMFILE', + 'ENOMEM', + ]; + stdoutStream.on( 'error', err => { commandRunning = false; - // TODO handle this better - console.error( 'Error: ' + err.message ); + if ( criticalErrors.includes( err.code ) ) { + console.error( `Error: ${ err.message }` ); + } else { + // TODO handle this better + debug( 'Error: ' + err.message ); + } } ); stdoutStream.on( 'end', async () => { @@ -231,7 +245,7 @@ const bindReconnectEvents = ( { } ); currentJob.socket.io.on( 'reconnect_attempt', attempt => { - console.error( 'There was an error connecting to the server. Retrying...' ); + debug( 'There was an error connecting to the server. Retrying...' ); if ( attempt > 1 ) { return; diff --git a/src/commands/dev-env-import-sql.ts b/src/commands/dev-env-import-sql.ts index c561d3953..3be537500 100644 --- a/src/commands/dev-env-import-sql.ts +++ b/src/commands/dev-env-import-sql.ts @@ -108,7 +108,7 @@ export class DevEnvImportSQLCommand { * Therefore, for the things to work, we have to pretend that stdin is not a TTY :-) */ process.stdin.isTTY = false; - await exec( lando, this.slug, importArg, { stdio: [ fd, 'pipe', 'pipe' ] } ); + await exec( lando, this.slug, importArg, { stdio: [ fd.fd, 'pipe', 'pipe' ] } ); if ( ! this.options.quiet ) { console.log( `${ chalk.green.bold( 'Success:' ) } Database imported.` ); diff --git a/src/commands/dev-env-sync-sql.ts b/src/commands/dev-env-sync-sql.ts index 4de7c557b..554ec731d 100644 --- a/src/commands/dev-env-sync-sql.ts +++ b/src/commands/dev-env-sync-sql.ts @@ -5,7 +5,6 @@ import chalk from 'chalk'; import fs from 'fs'; import Lando from 'lando'; import { pipeline } from 'node:stream/promises'; -import urlLib from 'url'; import { DevEnvImportSQLCommand, DevEnvImportSQLOptions } from './dev-env-import-sql'; import { ExportSQLCommand } from './export-sql'; @@ -18,6 +17,27 @@ import { fixMyDumperTransform, getSqlDumpDetails, SqlDumpType } from '../lib/dat import { makeTempDir } from '../lib/utils'; import { getReadInterface } from '../lib/validations/line-by-line'; +/** + * Replaces the domain in the given URL + * + * @param string str The URL to replace the domain in. + * @param string domain The new domain + * @return The URL with the new domain + */ +const replaceDomain = ( str: string, domain: string ): string => + str.replace( /^([^:]+:\/\/)([^:/]+)/, `$1${ domain }` ); + +/** + * Strips the protocol from the URL + * + * @param string url The URL to strip the protocol from + * @return The URL without the protocol + */ +function stripProtocol( url: string ): string { + const parts = url.split( '//', 2 ); + return parts.length > 1 ? parts[ 1 ] : parts[ 0 ]; +} + /** * Finds the site home url from the SQL line * @@ -27,8 +47,12 @@ import { getReadInterface } from '../lib/validations/line-by-line'; function findSiteHomeUrl( sql: string ): string | null { const regex = `['"](siteurl|home)['"],\\s?['"](.*?)['"]`; const url = sql.match( regex )?.[ 2 ] || ''; - - return urlLib.parse( url ).hostname || null; + try { + new URL( url ); + return url; + } catch { + return null; + } } /** @@ -42,17 +66,17 @@ async function extractSiteUrls( sqlFile: string ): Promise< string[] > { const readInterface = await getReadInterface( sqlFile ); return new Promise( ( resolve, reject ) => { - const domains: Set< string > = new Set(); + const urls: Set< string > = new Set(); readInterface.on( 'line', line => { - const domain = findSiteHomeUrl( line ); - if ( domain ) { - domains.add( domain ); + const url = findSiteHomeUrl( line ); + if ( url ) { + urls.add( url ); } } ); readInterface.on( 'close', () => { - // Soring by length so that longest domains are replaced first - resolve( Array.from( domains ).sort( ( dom1, dom2 ) => dom2.length - dom1.length ) ); + // Soring by length so that longest URLs are replaced first + resolve( Array.from( urls ).sort( ( url1, url2 ) => url2.length - url1.length ) ); } ); readInterface.on( 'error', reject ); @@ -168,7 +192,9 @@ export class DevEnvSyncSQLCommand { this.searchReplaceMap = {}; for ( const url of this.siteUrls ) { - this.searchReplaceMap[ url ] = this.landoDomain; + this.searchReplaceMap[ stripProtocol( url ) ] = stripProtocol( + replaceDomain( url, this.landoDomain ) + ); } const networkSites = this.env.wpSitesSDS?.nodes; @@ -177,12 +203,18 @@ export class DevEnvSyncSQLCommand { for ( const site of networkSites ) { if ( ! site?.blogId || site.blogId === 1 ) continue; - const url = site?.homeUrl?.replace( /https?:\/\//, '' ); - if ( ! url || ! this.searchReplaceMap[ url ] ) continue; + const url = site?.homeUrl; + if ( ! url ) continue; + + const strippedUrl = stripProtocol( url ); + if ( ! this.searchReplaceMap[ strippedUrl ] ) continue; + + const domain = new URL( url ).hostname; + const newDomain = `${ this.slugifyDomain( domain ) }.${ this.landoDomain }`; - this.searchReplaceMap[ url ] = `${ this.slugifyDomain( url ) }-${ site.blogId }.${ - this.landoDomain - }`; + this.searchReplaceMap[ stripProtocol( url ) ] = stripProtocol( + replaceDomain( url, newDomain ) + ); } } @@ -213,6 +245,34 @@ export class DevEnvSyncSQLCommand { await importCommand.run(); } + public async fixBlogsTable(): Promise< void > { + const networkSites = this.env.wpSitesSDS?.nodes; + if ( ! networkSites ) { + return; + } + + const queries: string[] = []; + for ( const site of networkSites ) { + if ( ! site?.blogId || ! site?.homeUrl ) { + continue; + } + + const oldDomain = new URL( site.homeUrl ).hostname; + const newDomain = + site.blogId !== 1 + ? `${ this.slugifyDomain( oldDomain ) }.${ this.landoDomain }` + : this.landoDomain; + + queries.push( + `UPDATE wp_blogs SET domain = '${ newDomain }' WHERE blog_id = ${ Number( site.blogId ) };` + ); + } + + if ( queries.length ) { + await fs.promises.appendFile( this.sqlFile, queries.join( '\n' ) ); + } + } + /** * Sequentially runs the commands to export, search-replace, and import the SQL file * to the local environment @@ -273,6 +333,7 @@ export class DevEnvSyncSQLCommand { } await this.runSearchReplace(); + await this.fixBlogsTable(); console.log( `${ chalk.green( '✓' ) } Search-replace operation is complete` ); } catch ( err ) { const error = err as Error; diff --git a/src/lib/dev-environment/dev-environment-lando.ts b/src/lib/dev-environment/dev-environment-lando.ts index 2f508c3c4..0d6c7090f 100644 --- a/src/lib/dev-environment/dev-environment-lando.ts +++ b/src/lib/dev-environment/dev-environment-lando.ts @@ -6,7 +6,7 @@ import Lando, { type LandoConfig } from 'lando/lib/lando'; import landoUtils, { type AppInfo } from 'lando/plugins/lando-core/lib/utils'; import landoBuildTask from 'lando/plugins/lando-tooling/lib/build'; import { lookup } from 'node:dns/promises'; -import { FileHandle, mkdir, rename } from 'node:fs/promises'; +import { mkdir, rename } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path, { dirname } from 'node:path'; import { satisfies } from 'semver'; @@ -24,9 +24,10 @@ import UserError from '../user-error'; import type { NetworkInspectInfo } from 'dockerode'; import type Landerode from 'lando/lib/docker'; +import type { StdioOptions } from 'node:child_process'; export interface LandoExecOptions { - stdio?: string | [ FileHandle, string, string ]; + stdio?: StdioOptions; } /**