From 6fbad2569ed927da4dcf95cd88fba918c7f6dfdb Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Sat, 2 Nov 2024 01:14:33 +0200 Subject: [PATCH 1/5] fix(dev-env): detection of URLs to replace for multisites --- src/commands/dev-env-sync-sql.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/commands/dev-env-sync-sql.ts b/src/commands/dev-env-sync-sql.ts index 4de7c557b..52c66772d 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,19 @@ import { fixMyDumperTransform, getSqlDumpDetails, SqlDumpType } from '../lib/dat import { makeTempDir } from '../lib/utils'; import { getReadInterface } from '../lib/validations/line-by-line'; +function extractDomain( str: string | null | undefined ): string | null { + if ( ! str ) { + return null; + } + + try { + const url = new URL( str ); + return url.hostname; + } catch { + return null; + } +} + /** * Finds the site home url from the SQL line * @@ -27,8 +39,7 @@ 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; + return extractDomain( url ); } /** @@ -177,7 +188,7 @@ export class DevEnvSyncSQLCommand { for ( const site of networkSites ) { if ( ! site?.blogId || site.blogId === 1 ) continue; - const url = site?.homeUrl?.replace( /https?:\/\//, '' ); + const url = extractDomain( site?.homeUrl ); if ( ! url || ! this.searchReplaceMap[ url ] ) continue; this.searchReplaceMap[ url ] = `${ this.slugifyDomain( url ) }-${ site.blogId }.${ From 66bd4d6170a86c56f6d6bc9d67e085ab9098c2c7 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Wed, 6 Nov 2024 12:24:11 +0200 Subject: [PATCH 2/5] fix: replacement takes paths into consideration --- __tests__/commands/dev-env-sync-sql.ts | 9 +++- src/commands/dev-env-sync-sql.ts | 75 ++++++++++++++++++-------- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/__tests__/commands/dev-env-sync-sql.ts b/__tests__/commands/dev-env-sync-sql.ts index a0392447d..7fdc47b53 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', + 'another.com/path': 'another-com-3.test-slug.vipdev.lndo.site/path', } ); } ); } ); diff --git a/src/commands/dev-env-sync-sql.ts b/src/commands/dev-env-sync-sql.ts index 52c66772d..9935a1a59 100644 --- a/src/commands/dev-env-sync-sql.ts +++ b/src/commands/dev-env-sync-sql.ts @@ -17,17 +17,33 @@ import { fixMyDumperTransform, getSqlDumpDetails, SqlDumpType } from '../lib/dat import { makeTempDir } from '../lib/utils'; import { getReadInterface } from '../lib/validations/line-by-line'; -function extractDomain( str: string | null | undefined ): string | null { - if ( ! str ) { - return null; - } +/** + * Replaces the domain in the given URL + * + * @param string str The URL to replace the domain in. Must be a valid URL. + * @param string domain The new domain + * @return The URL with the new domain + */ +function replaceDomain( str: string, domain: string ): string { + const url = new URL( str ); + url.hostname = domain; + + // `URL.href()` always adds `/` for an empty path; we don't want that + const retval = url.href; + return retval.endsWith( '/' ) && ! str.endsWith( '/' ) + ? retval.substring( 0, retval.length - 1 ) + : retval; +} - try { - const url = new URL( str ); - return url.hostname; - } catch { - return null; - } +/** + * 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 ]; } /** @@ -39,7 +55,12 @@ function extractDomain( str: string | null | undefined ): string | null { function findSiteHomeUrl( sql: string ): string | null { const regex = `['"](siteurl|home)['"],\\s?['"](.*?)['"]`; const url = sql.match( regex )?.[ 2 ] || ''; - return extractDomain( url ); + try { + new URL( url ); + return url; + } catch { + return null; + } } /** @@ -53,17 +74,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 ); @@ -179,7 +200,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; @@ -188,12 +211,18 @@ export class DevEnvSyncSQLCommand { for ( const site of networkSites ) { if ( ! site?.blogId || site.blogId === 1 ) continue; - const url = extractDomain( site?.homeUrl ); - 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 ) }-${ site.blogId }.${ this.landoDomain }`; - this.searchReplaceMap[ url ] = `${ this.slugifyDomain( url ) }-${ site.blogId }.${ - this.landoDomain - }`; + this.searchReplaceMap[ stripProtocol( url ) ] = stripProtocol( + replaceDomain( url, newDomain ) + ); } } From e8bfc16928525e1d1433161ac57c658fa8470464 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Wed, 6 Nov 2024 12:30:47 +0200 Subject: [PATCH 3/5] refactor: simplify `replaceDomain()` --- src/commands/dev-env-sync-sql.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/commands/dev-env-sync-sql.ts b/src/commands/dev-env-sync-sql.ts index 9935a1a59..f6fb092b4 100644 --- a/src/commands/dev-env-sync-sql.ts +++ b/src/commands/dev-env-sync-sql.ts @@ -20,20 +20,12 @@ 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. Must be a valid URL. + * @param string str The URL to replace the domain in. * @param string domain The new domain * @return The URL with the new domain */ -function replaceDomain( str: string, domain: string ): string { - const url = new URL( str ); - url.hostname = domain; - - // `URL.href()` always adds `/` for an empty path; we don't want that - const retval = url.href; - return retval.endsWith( '/' ) && ! str.endsWith( '/' ) - ? retval.substring( 0, retval.length - 1 ) - : retval; -} +const replaceDomain = ( str: string, domain: string ): string => + str.replace( /^([^:]+:\/\/)([^:/]+)/, `$1${ domain }` ); /** * Strips the protocol from the URL From e23552041727eed32b50fa56d9fdad9677f152af Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Thu, 7 Nov 2024 01:15:29 +0200 Subject: [PATCH 4/5] fix: update `wp_blogs` --- src/commands/dev-env-import-sql.ts | 2 +- src/commands/dev-env-sync-sql.ts | 31 ++++++++++++++++++- .../dev-environment/dev-environment-lando.ts | 5 +-- 3 files changed, 34 insertions(+), 4 deletions(-) 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 f6fb092b4..554ec731d 100644 --- a/src/commands/dev-env-sync-sql.ts +++ b/src/commands/dev-env-sync-sql.ts @@ -210,7 +210,7 @@ export class DevEnvSyncSQLCommand { if ( ! this.searchReplaceMap[ strippedUrl ] ) continue; const domain = new URL( url ).hostname; - const newDomain = `${ this.slugifyDomain( domain ) }-${ site.blogId }.${ this.landoDomain }`; + const newDomain = `${ this.slugifyDomain( domain ) }.${ this.landoDomain }`; this.searchReplaceMap[ stripProtocol( url ) ] = stripProtocol( replaceDomain( url, newDomain ) @@ -245,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 @@ -305,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; } /** From aa5112cc2a5d01582252b00b55ed07d5ce07fe98 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Thu, 7 Nov 2024 15:55:57 -0600 Subject: [PATCH 5/5] Fix test cases - we don't have id's anymore --- __tests__/commands/dev-env-sync-sql.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/commands/dev-env-sync-sql.ts b/__tests__/commands/dev-env-sync-sql.ts index 7fdc47b53..72ad0813a 100644 --- a/__tests__/commands/dev-env-sync-sql.ts +++ b/__tests__/commands/dev-env-sync-sql.ts @@ -74,8 +74,8 @@ describe( 'commands/DevEnvSyncSQLCommand', () => { expect( cmd.searchReplaceMap ).toEqual( { 'test.go-vip.com': 'test-slug.vipdev.lndo.site', - 'subsite.com': 'subsite-com-2.test-slug.vipdev.lndo.site', - 'another.com/path': 'another-com-3.test-slug.vipdev.lndo.site/path', + 'subsite.com': 'subsite-com.test-slug.vipdev.lndo.site', + 'another.com/path': 'another-com.test-slug.vipdev.lndo.site/path', } ); } ); } );