Skip to content

Commit 841ec0a

Browse files
committed
FLEX-6022: feat - added custom domain
1 parent 7600927 commit 841ec0a

File tree

9 files changed

+244
-8
lines changed

9 files changed

+244
-8
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import { spawn } from 'child_process';
4+
5+
export interface HostsEntry {
6+
ip: string;
7+
domain: string;
8+
comment?: string;
9+
}
10+
11+
/**
12+
* Simple logger interface to avoid circular dependency
13+
*/
14+
interface Logger {
15+
info: (message: string) => void;
16+
error: (message: string) => void;
17+
}
18+
19+
// Simple console logger to avoid circular dependency
20+
const logger: Logger = {
21+
info: (message: string) => console.log(`[INFO] ${message}`),
22+
error: (message: string) => console.error(`[ERROR] ${message}`)
23+
};
24+
25+
/**
26+
* Manages /etc/hosts file entries for custom domains
27+
*/
28+
export class HostsManager {
29+
private readonly hostsPath: string;
30+
private readonly marker = '# Flex Plugin Builder';
31+
32+
constructor() {
33+
this.hostsPath = os.platform() === 'win32' ?
34+
'C:\\Windows\\System32\\drivers\\etc\\hosts' :
35+
'/etc/hosts';
36+
}
37+
38+
/**
39+
* Adds a domain mapping to /etc/hosts file
40+
* @param domain - The domain to map (e.g., 'flex.local.com')
41+
* @param ip - The IP address to map to (default: '127.0.0.1')
42+
*/
43+
async addEntry(domain: string, ip: string = '127.0.0.1'): Promise<void> {
44+
try {
45+
const content = await this.readHostsFile();
46+
const lines = content.split('\n');
47+
48+
// Remove any existing entry for this domain
49+
const filteredLines = lines.filter(line => {
50+
const trimmed = line.trim();
51+
if (trimmed.includes(domain) && trimmed.includes(this.marker)) {
52+
return false;
53+
}
54+
return true;
55+
});
56+
57+
// Add new entry
58+
const newEntry = `${ip}\t${domain}\t${this.marker}`;
59+
filteredLines.push(newEntry);
60+
61+
const newContent = filteredLines.join('\n');
62+
await this.writeHostsFile(newContent);
63+
64+
logger.info(`✓ Added ${domain}${ip} to hosts file`);
65+
} catch (error) {
66+
logger.error(`Failed to update hosts file: ${error instanceof Error ? error.message : 'Unknown error'}`);
67+
throw new Error(`Could not update hosts file. Please ensure you have necessary permissions.`);
68+
}
69+
}
70+
71+
/**
72+
* Removes a domain mapping from /etc/hosts file
73+
* @param domain - The domain to remove
74+
*/
75+
async removeEntry(domain: string): Promise<void> {
76+
try {
77+
const content = await this.readHostsFile();
78+
const lines = content.split('\n');
79+
80+
const filteredLines = lines.filter(line => {
81+
const trimmed = line.trim();
82+
if (trimmed.includes(domain) && trimmed.includes(this.marker)) {
83+
return false;
84+
}
85+
return true;
86+
});
87+
88+
const newContent = filteredLines.join('\n');
89+
await this.writeHostsFile(newContent);
90+
91+
logger.info(`✓ Removed ${domain} from hosts file`);
92+
} catch (error) {
93+
logger.error(`Failed to remove from hosts file: ${error instanceof Error ? error.message : 'Unknown error'}`);
94+
}
95+
}
96+
97+
/**
98+
* Checks if a domain is already mapped in /etc/hosts
99+
* @param domain - The domain to check
100+
*/
101+
async hasEntry(domain: string): Promise<boolean> {
102+
try {
103+
const content = await this.readHostsFile();
104+
return content.includes(domain);
105+
} catch (error) {
106+
return false;
107+
}
108+
}
109+
110+
/**
111+
* Removes all Flex Plugin Builder entries from hosts file
112+
*/
113+
async cleanup(): Promise<void> {
114+
try {
115+
const content = await this.readHostsFile();
116+
const lines = content.split('\n');
117+
118+
const filteredLines = lines.filter(line => {
119+
return !line.includes(this.marker);
120+
});
121+
122+
const newContent = filteredLines.join('\n');
123+
await this.writeHostsFile(newContent);
124+
125+
logger.info('✓ Cleaned up all Flex Plugin Builder hosts entries');
126+
} catch (error) {
127+
logger.error(`Failed to cleanup hosts file: ${error instanceof Error ? error.message : 'Unknown error'}`);
128+
}
129+
}
130+
131+
private async readHostsFile(): Promise<string> {
132+
return new Promise((resolve, reject) => {
133+
fs.readFile(this.hostsPath, 'utf8', (err, data) => {
134+
if (err) {
135+
reject(err);
136+
} else {
137+
resolve(data);
138+
}
139+
});
140+
});
141+
}
142+
143+
private async writeHostsFile(content: string): Promise<void> {
144+
return new Promise((resolve, reject) => {
145+
const isWindows = os.platform() === 'win32';
146+
147+
if (isWindows) {
148+
// On Windows, try to run with elevated privileges
149+
const child = spawn('powershell', [
150+
'-Command',
151+
`Start-Process powershell -ArgumentList "-Command \\"Set-Content -Path '${this.hostsPath}' -Value @'${content}'@\\"" -Verb RunAs -WindowStyle Hidden -Wait`
152+
], { stdio: 'inherit' });
153+
154+
child.on('close', (code) => {
155+
if (code === 0) {
156+
resolve();
157+
} else {
158+
reject(new Error(`PowerShell exited with code ${code}`));
159+
}
160+
});
161+
162+
child.on('error', reject);
163+
} else {
164+
// On Unix-like systems, use sudo
165+
const child = spawn('sudo', ['tee', this.hostsPath], {
166+
stdio: ['pipe', 'inherit', 'inherit']
167+
});
168+
169+
child.stdin.write(content);
170+
child.stdin.end();
171+
172+
child.on('close', (code) => {
173+
if (code === 0) {
174+
resolve();
175+
} else {
176+
reject(new Error(`sudo tee exited with code ${code}`));
177+
}
178+
});
179+
180+
child.on('error', reject);
181+
}
182+
});
183+
}
184+
}
185+
186+
export default HostsManager;

packages/flex-dev-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export {
2828
export { default as runner } from './runner';
2929
export { default as urls } from './urls';
3030
export { default as env } from './env';
31+
export { HostsManager } from './hosts';
3132
export { getPaths as paths } from './fs';
3233
export { default as semver, ReleaseType, SemVer, versionSatisfiesRange } from './semver';
3334
export { default as exit } from './exit';

packages/flex-dev-utils/src/urls.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,17 @@ export const findPort = async (startPort: number = 3000): Promise<number> => {
8080
*/
8181
export const getLocalAndNetworkUrls = (port: number): InternalServiceUrls => {
8282
const protocol = env.isHTTPS() ? 'https' : 'http';
83+
const customDomain = process.env.DOMAIN;
84+
85+
let localHostname = 'localhost';
86+
if (customDomain) {
87+
localHostname = customDomain;
88+
}
8389

8490
const localUrl = url.format({
8591
protocol,
8692
port,
87-
hostname: 'localhost',
93+
hostname: localHostname,
8894
pathname: '/',
8995
});
9096
const networkUrl = url.format({
@@ -98,7 +104,7 @@ export const getLocalAndNetworkUrls = (port: number): InternalServiceUrls => {
98104
local: {
99105
url: localUrl,
100106
port,
101-
host: '0.0.0.0',
107+
host: customDomain || '0.0.0.0',
102108
},
103109
network: {
104110
url: networkUrl,

packages/flex-plugin-webpack/src/devServer/pluginServer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const _getLocalPlugin = (name: string): FlexConfigurationPlugin | undefin
5555
// eslint-disable-next-line import/no-unused-modules
5656
export const _getLocalPlugins = (port: Port, names: string[]): Plugin[] => {
5757
const protocol = `http${env.isHTTPS() ? 's' : ''}://`;
58+
const domain = process.env.DOMAIN || 'localhost';
5859

5960
return names.map((name) => {
6061
const match = _getLocalPlugin(name);
@@ -63,7 +64,7 @@ export const _getLocalPlugins = (port: Port, names: string[]): Plugin[] => {
6364
return {
6465
phase: 3,
6566
name,
66-
src: `${protocol}localhost:${port}/plugins/${name}.js`,
67+
src: `${protocol}${domain}:${port}/plugins/${name}.js`,
6768
};
6869
}
6970

packages/flex-plugin-webpack/src/webpack/webpack.dev.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@ import { WebpackType } from '..';
1212
// eslint-disable-next-line import/no-unused-modules
1313
export const _getBase = (): Configuration => {
1414
const { local } = getLocalAndNetworkUrls(env.getPort());
15+
const customDomain = process.env.DOMAIN;
1516

1617
return {
1718
compress: true,
1819
static: {},
1920
client: {
2021
logging: 'none',
2122
webSocketURL: {
22-
hostname: local.host,
23+
hostname: customDomain || local.host,
2324
pathname: local.url,
2425
port: env.getPort(),
2526
},
2627
},
27-
host: env.getHost(),
28+
host: customDomain || env.getHost(),
2829
port: env.getPort(),
2930
};
3031
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
/* eslint-disable import/no-unused-modules */
22

3-
export { default, default as env, Environment, Lifecycle, Region } from './lib/env';
3+
export { default, default as env, Environment, Lifecycle, Region, setDomain, getDomain, hasDomain, setFlexUISrc } from './lib/env';

packages/flex-plugins-utils-env/src/lib/env.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ export const getAuthToken = (): string | undefined => getProcessEnv('TWILIO_AUTH
8787
export const hasHost = (): boolean => isDefined(getProcessEnv('HOST'));
8888
export const getHost = (): string | undefined => getProcessEnv('HOST');
8989
export const setHost = (host: string): void => setProcessEnv('HOST', host);
90+
export const hasDomain = (): boolean => isDefined(getProcessEnv('DOMAIN'));
91+
export const getDomain = (): string | undefined => getProcessEnv('DOMAIN');
92+
export const setDomain = (domain: string): void => setProcessEnv('DOMAIN', domain);
9093
export const hasPort = (): boolean => isDefined(getProcessEnv('PORT'));
9194
export const getPort = (): number => Number(getProcessEnv('PORT'));
9295
export const setPort = (port: number): void => setProcessEnv('PORT', String(port));
@@ -257,6 +260,9 @@ export default {
257260
hasHost,
258261
getHost,
259262
setHost,
263+
hasDomain,
264+
getDomain,
265+
setDomain,
260266
hasPort,
261267
getPort,
262268
setPort,

packages/plugin-flex/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@
105105
"flags": {
106106
"name": "The name of the plugin you would like to run. You can provide multiple to run them all concurrently. You can include specific active remote plugins using \"--name 'plugin-name@remote'\" or \"--name 'plugin-name@0.0.0'\" for a specific remote version.",
107107
"includeRemote": "Use this flag to include all remote plugins in your build.",
108-
"port": "The port to start your local development server on."
108+
"port": "The port to start your local development server on.",
109+
"domain": "The domain to start your local development server on."
109110
}
110111
},
111112
"flex:plugins:deploy": {

packages/plugin-flex/src/commands/flex/plugins/start.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { flags } from '@oclif/command';
22
import { PluginsConfig, PLUGIN_INPUT_PARSER_REGEX } from '@twilio/flex-plugin-scripts';
33
import { findPortAvailablePort } from '@twilio/flex-plugin-scripts/dist/scripts/start';
44
import { FLAG_MULTI_PLUGINS } from '@twilio/flex-plugin-scripts/dist/scripts/pre-script-check';
5-
import { TwilioCliError, semver, env, TwilioApiError } from '@twilio/flex-dev-utils';
5+
import { TwilioCliError, semver, env, TwilioApiError, HostsManager } from '@twilio/flex-dev-utils';
66
import { readJsonFile } from '@twilio/flex-dev-utils/dist/fs';
77
import { OutputFlags } from '@oclif/parser/lib/parse';
88

@@ -36,6 +36,9 @@ export default class FlexPluginsStart extends FlexPlugin {
3636
description: FlexPluginsStart.topic.flags.port,
3737
default: 3000,
3838
}),
39+
domain: flags.string({
40+
description: FlexPluginsStart.topic.flags.domain,
41+
}),
3942
'flex-ui-source': flags.string({
4043
hidden: true,
4144
}),
@@ -115,6 +118,37 @@ export default class FlexPluginsStart extends FlexPlugin {
115118
const flexPort = await this.getPort();
116119
flexArgs.push('--port', flexPort.toString());
117120

121+
// Handle custom domain and hosts file
122+
if (this._flags.domain) {
123+
const customDomain = this._flags.domain;
124+
env.setDomain(customDomain);
125+
126+
// Update /etc/hosts to map custom domain to localhost
127+
const hostsManager = new HostsManager();
128+
try {
129+
if (!(await hostsManager.hasEntry(customDomain))) {
130+
await hostsManager.addEntry(customDomain, '127.0.0.1');
131+
}
132+
133+
// Setup cleanup on process exit
134+
const cleanup = async () => {
135+
try {
136+
await hostsManager.removeEntry(customDomain);
137+
} catch (error) {
138+
// Ignore cleanup errors
139+
}
140+
};
141+
142+
process.on('SIGINT', cleanup);
143+
process.on('SIGTERM', cleanup);
144+
process.on('exit', cleanup);
145+
146+
} catch (error) {
147+
// Continue without hosts file update but warn user
148+
this.warn(`Could not update hosts file automatically. Please add this line to your /etc/hosts file manually:\n127.0.0.1\t${customDomain}`);
149+
}
150+
}
151+
118152
if (flexArgs.length && localPluginNames.length) {
119153
// Verify the users environment is ready to run plugins locally
120154
await this.checkLocalEnvironment(localPluginNames);

0 commit comments

Comments
 (0)