Skip to content

Commit 2c7503d

Browse files
committed
Custom Domain: Allow updating domains for existing sites.
1 parent 7201752 commit 2c7503d

10 files changed

Lines changed: 261 additions & 43 deletions

File tree

‎src/components/site-form.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { ACCEPTED_IMPORT_FILE_TYPES } from 'src/constants';
1313
import { useDocsLink } from 'src/hooks/use-docs-link';
1414
import { useOffline } from 'src/hooks/use-offline';
1515
import { cx } from 'src/lib/cx';
16-
import { generateCustomDomainFromSiteName } from 'src/lib/generate-custom-domain';
16+
import { generateCustomDomainFromSiteName } from 'src/lib/domains';
1717
import { getIpcApi } from 'src/lib/get-ipc-api';
1818
import { useAppDispatch, useRootSelector } from 'src/stores';
1919
import {

‎src/hooks/use-add-site.ts‎

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useI18n } from '@wordpress/react-i18n';
33
import { useCallback, useMemo, useState } from 'react';
44
import { useImportExport } from 'src/hooks/use-import-export';
55
import { useSiteDetails } from 'src/hooks/use-site-details';
6-
import { generateCustomDomainFromSiteName } from 'src/lib/generate-custom-domain';
6+
import { generateCustomDomainFromSiteName, validateDomainName } from 'src/lib/domains';
77
import { getIpcApi } from 'src/lib/get-ipc-api';
88
import {
99
DEFAULT_PHP_VERSION,
@@ -39,20 +39,9 @@ export function useAddSite() {
3939
const handleCustomDomainChange = useCallback(
4040
( value: string | null ) => {
4141
setCustomDomain( value );
42-
// Validate custom domain if enabled
43-
const domainPattern =
44-
/^(?!-)[\p{L}\p{N}][\p{L}\p{N}-]{0,61}[\p{L}\p{N}](?<!-)(?:\.(?!-)[\p{L}\p{N}-]{1,61}[\p{L}\p{N}](?<!-))+$/u;
45-
if ( useCustomDomain && value && ! domainPattern.test( value ) ) {
46-
setCustomDomainError( __( 'Please enter a valid domain name' ) );
47-
} else if ( useCustomDomain && value && value.length > 253 ) {
48-
setCustomDomainError( __( 'The domain name is too long' ) );
49-
} else if ( useCustomDomain && value === '' ) {
50-
setCustomDomainError( __( 'The domain name is required' ) );
51-
} else {
52-
setCustomDomainError( '' );
53-
}
42+
setCustomDomainError( validateDomainName( useCustomDomain, value ) );
5443
},
55-
[ __, useCustomDomain, setCustomDomain, setCustomDomainError ]
44+
[ useCustomDomain, setCustomDomain, setCustomDomainError ]
5645
);
5746

5847
const handlePathSelectorClick = useCallback( async () => {

‎src/ipc-handlers.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { download } from 'src/lib/download';
2727
import { isEmptyDir, pathExists, isWordPressDirectory, sanitizeFolderName } from 'src/lib/fs-utils';
2828
import { getImageData } from 'src/lib/get-image-data';
2929
import { getSyncBackupTempPath } from 'src/lib/get-sync-backup-temp-path';
30-
import { addDomainToHosts } from 'src/lib/hosts-file';
30+
import { addDomainToHosts, replaceDomainInHosts } from 'src/lib/hosts-file';
3131
import { exportBackup } from 'src/lib/import-export/export/export-manager';
3232
import { ExportOptions } from 'src/lib/import-export/export/types';
3333
import { ImportExportEventData } from 'src/lib/import-export/handle-events';
@@ -220,6 +220,10 @@ export async function updateSite(
220220
updatedSite: SiteDetails
221221
): Promise< SiteDetails[] > {
222222
const userData = await loadUserData();
223+
224+
const existingSite = userData.sites.find( ( site ) => site.id === updatedSite.id );
225+
const oldDomain = existingSite?.customDomain;
226+
const newDomain = updatedSite.customDomain;
223227
const updatedSites = userData.sites.map( ( site ) =>
224228
site.id === updatedSite.id ? updatedSite : site
225229
);
@@ -228,6 +232,12 @@ export async function updateSite(
228232
const server = SiteServer.get( updatedSite.id );
229233
if ( server ) {
230234
server.updateSiteDetails( updatedSite );
235+
236+
// Handle domain changes if the site is running (updates hosts and database)
237+
if ( oldDomain !== newDomain ) {
238+
replaceDomainInHosts( oldDomain, newDomain, server.details.port );
239+
await updateSiteUrlToLocal( updatedSite.id );
240+
}
231241
}
232242
await saveUserData( userData );
233243
return mergeSiteDetailsWithRunningDetails( userData.sites );

‎src/lib/domains.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { __ } from '@wordpress/i18n';
2+
import { sanitizeFolderName } from './generate-site-name';
3+
4+
/**
5+
* Generates a suitable domain name from site name
6+
*/
7+
export const generateCustomDomainFromSiteName = ( siteName: string ): string => {
8+
const domainBase = sanitizeFolderName( siteName );
9+
10+
return `${ domainBase }.wp.local`;
11+
};
12+
13+
export const validateDomainName = (
14+
useCustomDomain: boolean,
15+
domainName: string | null
16+
): string => {
17+
// Validate custom domain if enabled
18+
const domainPattern =
19+
/^(?!-)[\p{L}\p{N}][\p{L}\p{N}-]{0,61}[\p{L}\p{N}](?<!-)(?:\.(?!-)[\p{L}\p{N}-]{1,61}[\p{L}\p{N}](?<!-))+$/u;
20+
if ( useCustomDomain && domainName && ! domainPattern.test( domainName ) ) {
21+
return __( 'Please enter a valid domain name' );
22+
}
23+
24+
if ( useCustomDomain && domainName && domainName.length > 253 ) {
25+
return __( 'The domain name is too long' );
26+
}
27+
28+
if ( useCustomDomain && domainName === '' ) {
29+
return __( 'The domain name is required' );
30+
}
31+
32+
return '';
33+
};

‎src/lib/generate-custom-domain.ts‎

Lines changed: 0 additions & 10 deletions
This file was deleted.

‎src/lib/hosts-file.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,48 @@ export const removeDomainFromHosts = async ( domain: string ): Promise< void > =
127127
}
128128
};
129129

130+
export const replaceDomainInHosts = async (
131+
oldDomain: string | undefined,
132+
newDomain: string | undefined,
133+
port: number
134+
): Promise< void > => {
135+
if ( oldDomain === newDomain ) {
136+
return;
137+
}
138+
139+
if ( ! oldDomain && newDomain ) {
140+
await addDomainToHosts( newDomain, port );
141+
return;
142+
}
143+
144+
if ( oldDomain && ! newDomain ) {
145+
await removeDomainFromHosts( oldDomain );
146+
return;
147+
}
148+
149+
try {
150+
const hostsContent = await readHostsFile();
151+
const encodedOldDomain = domainToASCII( oldDomain as string );
152+
const encodedNewDomain = domainToASCII( newDomain as string );
153+
const oldPattern = createHostsEntryPattern( encodedOldDomain );
154+
const newContent = updateStudioBlock( hostsContent, ( entries ) => {
155+
const filtered = entries.filter( ( entry ) => ! entry.match( oldPattern ) );
156+
return [ ...filtered, `127.0.0.1 ${ encodedNewDomain } # Port ${ port }` ];
157+
} );
158+
159+
if ( newContent !== hostsContent ) {
160+
await writeHostsFile( newContent );
161+
}
162+
} catch ( error ) {
163+
Sentry.captureException( error );
164+
console.error(
165+
`Error replacing domain ${ oldDomain } with ${ newDomain } in hosts file:`,
166+
error
167+
);
168+
throw error;
169+
}
170+
};
171+
130172
/**
131173
* Helper function for manipulating the "block" of entries in the hosts file
132174
* pertaining to WordPres Studio.

‎src/lib/tests/hosts-file.test.ts‎

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readFile, writeFile } from 'fs';
2-
import { addDomainToHosts, removeDomainFromHosts } from '../hosts-file';
2+
import { addDomainToHosts, removeDomainFromHosts, replaceDomainInHosts } from '../hosts-file';
33

44
const readFileCallbackMock = jest.fn();
55

@@ -191,4 +191,94 @@ describe( 'hosts-file', () => {
191191
await expect( removeDomainFromHosts( 'test.wp.cloud' ) ).rejects.toThrow( 'Read error' );
192192
} );
193193
} );
194+
195+
// Generate a few unit tests for the replaceDomainInHosts function
196+
describe( 'replaceDomainInHosts', () => {
197+
it( 'should replace an existing domain with a new domain', async () => {
198+
readFileCallbackMock.mockResolvedValueOnce( sampleHostsContent );
199+
200+
await replaceDomainInHosts( 'foo.wp.cloud', 'new-domain.wp.cloud', 8002 );
201+
202+
expect( readFile ).toHaveBeenCalled();
203+
expect( writeFile ).toHaveBeenCalled();
204+
205+
const newContent = ( writeFile as unknown as jest.Mock ).mock.calls[ 0 ][ 1 ];
206+
207+
expect( newContent ).toEqual(
208+
`127.0.0.1 localhost
209+
::1 localhost
210+
211+
# Some comment
212+
213+
# BEGIN WordPress Studio
214+
127.0.0.1 bar.wp.cloud # Port 8001
215+
127.0.0.1 new-domain.wp.cloud # Port 8002
216+
# END WordPress Studio
217+
218+
# Other entries
219+
192.168.1.1 router`
220+
);
221+
} );
222+
223+
it( 'should add a new domain if old domain is undefined', async () => {
224+
readFileCallbackMock.mockResolvedValueOnce( sampleHostsContent );
225+
226+
await replaceDomainInHosts( undefined, 'new-domain.wp.cloud', 8002 );
227+
228+
expect( readFile ).toHaveBeenCalled();
229+
expect( writeFile ).toHaveBeenCalled();
230+
231+
const newContent = ( writeFile as unknown as jest.Mock ).mock.calls[ 0 ][ 1 ];
232+
233+
expect( newContent ).toEqual(
234+
`127.0.0.1 localhost
235+
::1 localhost
236+
237+
# Some comment
238+
239+
# BEGIN WordPress Studio
240+
127.0.0.1 foo.wp.cloud # Port 8000
241+
127.0.0.1 bar.wp.cloud # Port 8001
242+
127.0.0.1 new-domain.wp.cloud # Port 8002
243+
# END WordPress Studio
244+
245+
# Other entries
246+
192.168.1.1 router`
247+
);
248+
} );
249+
250+
it( 'should remove the old domain if new domain is undefined', async () => {
251+
readFileCallbackMock.mockResolvedValueOnce( sampleHostsContent );
252+
253+
await replaceDomainInHosts( 'foo.wp.cloud', undefined, 8000 );
254+
255+
expect( readFile ).toHaveBeenCalled();
256+
expect( writeFile ).toHaveBeenCalled();
257+
258+
const newContent = ( writeFile as unknown as jest.Mock ).mock.calls[ 0 ][ 1 ];
259+
260+
expect( newContent ).toEqual(
261+
`127.0.0.1 localhost
262+
::1 localhost
263+
264+
# Some comment
265+
266+
# BEGIN WordPress Studio
267+
127.0.0.1 bar.wp.cloud # Port 8001
268+
# END WordPress Studio
269+
270+
# Other entries
271+
192.168.1.1 router`
272+
);
273+
} );
274+
275+
it( 'should not modify the hosts file if old and new domains are the same', async () => {
276+
readFileCallbackMock.mockResolvedValueOnce( sampleHostsContent );
277+
278+
await replaceDomainInHosts( 'foo.wp.cloud', 'foo.wp.cloud', 8000 );
279+
280+
expect( readFile ).not.toHaveBeenCalled();
281+
expect( writeFile ).not.toHaveBeenCalled();
282+
} );
283+
} );
194284
} );

‎src/lib/updateSiteUrlToLocal.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ export const updateSiteUrlToLocal = async ( siteId: string ) => {
1212
} );
1313

1414
if ( ! currentSiteUrl ) {
15-
console.error( 'Failed to fetch site URL after import' );
15+
console.error( 'Failed to fetch site URL' );
1616
return;
1717
}
1818

1919
const studioUrl = getSiteUrl( server.details );
2020
const oldUrl = currentSiteUrl.trim();
21+
if ( studioUrl === oldUrl ) {
22+
return;
23+
}
2124
const urlWithoutProtocol = oldUrl.replace( /^https?:\/\//, '' );
2225

2326
const oldUrlVariants = [

0 commit comments

Comments
 (0)