diff --git a/README.md b/README.md index 97f05a854..14ff15085 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ Please visit [Twilio Docs](https://www.twilio.com/docs/flex/developer/plugins) f * [Plugins API](https://www.twilio.com/docs/flex/developer/plugins/api) * [Troubleshooting and FAQ](faq.md) +> **Note:** When using SSO v2 (OAuth flow), you may need to configure a custom domain for local development using the `--domain` flag to avoid authentication issues. See the [FAQ](faq.md) for setup instructions. + ## Changelog Major changelogs can be found in the [changelog directory](/changelog). diff --git a/faq.md b/faq.md index fbda9008d..3f94cbc8e 100644 --- a/faq.md +++ b/faq.md @@ -1,5 +1,60 @@ # Troubleshooting & FAQ +#### Configuring Custom Domain for Local Development + +You can configure a custom domain for local plugin development using the `--domain` flag. This is particularly important when using SSO v2 (OAuth flow), as `localhost` can cause authentication issues during login due to OAuth redirect URI restrictions. + +**Why use a custom domain?** +- **SSO v2 OAuth Flow**: If you're using Twilio Flex with SSO v2 (OAuth authentication flow), `localhost` URLs may not be accepted as valid redirect URIs, causing login authentication failures +- **CORS Issues**: Some authentication systems restrict `localhost` for security reasons +- **Development Consistency**: Using a custom domain provides a more production-like development environment + +**Usage:** +```bash +twilio flex:plugins:start --domain flex.local.com +``` + +**Setting up the custom domain:** +1. **macOS/Linux**: Add the domain to `/etc/hosts` + ```bash + sudo echo "127.0.0.1 flex.local.com" >> /etc/hosts + ``` + +2. **Windows**: Add the domain to `C:\Windows\System32\drivers\etc\hosts` + ``` + 127.0.0.1 flex.local.com + ``` + +**Available Options:** +- Without `--domain`: Uses `localhost` (default behavior, may cause issues with SSO v2) +- With `--domain flex.local.com`: Uses `flex.local.com` +- With `--domain my-custom.dev`: Uses any custom domain you prefer + +**When is this required?** +- **SSO v2 Users**: If your Twilio Flex instance uses SSO v2 with OAuth authentication, you will likely encounter login failures when using `localhost` +- **CORS-restricted environments**: Some authentication providers block `localhost` requests +- **Custom OAuth configurations**: If your OAuth provider has strict redirect URI policies + +After configuring your hosts file, your local plugin development server will be accessible at the specified domain (e.g., `http://flex.local.com:3000`). + +#### SSO v2 OAuth Authentication Issues with localhost + +If you're experiencing login failures during local development, especially with error messages related to OAuth redirects or authentication, this is likely due to SSO v2 OAuth flow restrictions with `localhost` URLs. + +**Common Error Symptoms:** +- Login redirects fail or loop infinitely +- OAuth callback errors mentioning invalid redirect URI +- Authentication timeouts during local development +- CORS errors related to authentication endpoints + +**Solution:** +Use the `--domain` flag with a custom domain: +```bash +twilio flex:plugins:start --domain flex.local.com +``` + +Make sure to add the domain to your hosts file as described above. This resolves the OAuth redirect URI validation issues that occur with `localhost`. + #### npm install fails on NPM 7 If you are using `npm 7` (you can find out the version by typing `npm -v` in your terminal), then you may get the following error when you run `npm install`: diff --git a/packages/create-flex-plugin/src/lib/commands.ts b/packages/create-flex-plugin/src/lib/commands.ts index 7b48f04e9..c0d80503a 100644 --- a/packages/create-flex-plugin/src/lib/commands.ts +++ b/packages/create-flex-plugin/src/lib/commands.ts @@ -43,7 +43,7 @@ export const setupConfiguration = async (config: FlexPluginArguments): Promise { beforeEach(() => { process.env = { ...OLD_ENV }; + delete process.env.DOMAIN; // Ensure clean state }); - it('should return http', () => { + it('should return http with localhost (default)', () => { const ip = jest.spyOn(address, 'ip').mockReturnValue('192.0.0.0'); const result = urls.getLocalAndNetworkUrls(1234); @@ -54,7 +55,23 @@ describe('urls', () => { ip.mockRestore(); }); - it('should return https', () => { + it('should return http with custom domain', () => { + process.env.DOMAIN = 'flex.local.com'; + const ip = jest.spyOn(address, 'ip').mockReturnValue('192.0.0.0'); + const result = urls.getLocalAndNetworkUrls(1234); + + expect(result.local.host).toEqual('0.0.0.0'); + expect(result.local.port).toEqual(1234); + expect(result.local.url).toEqual('http://flex.local.com:1234/'); + + expect(result.network.host).toEqual('192.0.0.0'); + expect(result.network.port).toEqual(1234); + expect(result.network.url).toEqual('http://192.0.0.0:1234/'); + + ip.mockRestore(); + }); + + it('should return https with localhost (default)', () => { process.env.HTTPS = 'true'; const ip = jest.spyOn(address, 'ip').mockReturnValue('192.0.0.0'); const result = urls.getLocalAndNetworkUrls(1234); @@ -69,5 +86,22 @@ describe('urls', () => { ip.mockRestore(); }); + + it('should return https with custom domain', () => { + process.env.HTTPS = 'true'; + process.env.DOMAIN = 'secure.local.dev'; + const ip = jest.spyOn(address, 'ip').mockReturnValue('192.0.0.0'); + const result = urls.getLocalAndNetworkUrls(1234); + + expect(result.local.host).toEqual('0.0.0.0'); + expect(result.local.port).toEqual(1234); + expect(result.local.url).toEqual('https://secure.local.dev:1234/'); + + expect(result.network.host).toEqual('192.0.0.0'); + expect(result.network.port).toEqual(1234); + expect(result.network.url).toEqual('https://192.0.0.0:1234/'); + + ip.mockRestore(); + }); }); }); diff --git a/packages/flex-dev-utils/src/urls.ts b/packages/flex-dev-utils/src/urls.ts index 807cfc683..8c04f9d44 100644 --- a/packages/flex-dev-utils/src/urls.ts +++ b/packages/flex-dev-utils/src/urls.ts @@ -77,14 +77,21 @@ export const findPort = async (startPort: number = 3000): Promise => { /** * Returns the local and network urls * @param port the port the server is running on + * @note Uses configurable domain instead of localhost to avoid SSO v2 OAuth authentication issues */ export const getLocalAndNetworkUrls = (port: number): InternalServiceUrls => { const protocol = env.isHTTPS() ? 'https' : 'http'; + const customDomain = env.getDomain(); + + let localHostname = 'localhost'; + if (customDomain) { + localHostname = customDomain; + } const localUrl = url.format({ protocol, port, - hostname: 'localhost', + hostname: localHostname, pathname: '/', }); const networkUrl = url.format({ @@ -98,7 +105,7 @@ export const getLocalAndNetworkUrls = (port: number): InternalServiceUrls => { local: { url: localUrl, port, - host: '0.0.0.0', + host: '0.0.0.0', // Keep binding host as 0.0.0.0 for proper network access }, network: { url: networkUrl, diff --git a/packages/flex-plugin-e2e-tests/README.md b/packages/flex-plugin-e2e-tests/README.md index 5ecbe0cfa..52f47a991 100644 --- a/packages/flex-plugin-e2e-tests/README.md +++ b/packages/flex-plugin-e2e-tests/README.md @@ -34,7 +34,7 @@ TWILIO_ACCOUNT_SID_drawin=ACxxx TWILIO_AUTH_TOKEN_darwin=123 CONSOLE_EMAIL="user You can also override certain defaults by setting these additional environment variables: - `FLEX_UI_VERSION` - the flexUIVersion to use. Defaults to `^1` otherwise -- `PLUGIN_BASE_URL` - the baseUrl. Defaults to `http://localhost:3000` otherwise +- `PLUGIN_BASE_URL` - the baseUrl. Defaults to `http://flex.local.com:3000` otherwise - `TWILIO_REGION` - the twilio region to use You can also run a specific step by using (don't forget the environment variables): diff --git a/packages/flex-plugin-e2e-tests/src/utils/browser.ts b/packages/flex-plugin-e2e-tests/src/utils/browser.ts index fc181b098..33003d946 100644 --- a/packages/flex-plugin-e2e-tests/src/utils/browser.ts +++ b/packages/flex-plugin-e2e-tests/src/utils/browser.ts @@ -11,7 +11,7 @@ export class Browser { private static _page: Page; - private static _domainsToInclude = ['twilio', 'localhost', 'unpkg']; + private static _domainsToInclude = ['twilio', 'localhost', 'flex.local.com', 'unpkg']; /** * Initializes browser object diff --git a/packages/flex-plugin-e2e-tests/src/utils/pages/view/plugins.ts b/packages/flex-plugin-e2e-tests/src/utils/pages/view/plugins.ts index cd9a90a28..9947f0339 100644 --- a/packages/flex-plugin-e2e-tests/src/utils/pages/view/plugins.ts +++ b/packages/flex-plugin-e2e-tests/src/utils/pages/view/plugins.ts @@ -28,7 +28,7 @@ export class Plugins extends Base { constructor(page: Page, baseUrl: string) { super(page); - this._baseUrl = baseUrl.includes('localhost') ? 'https://flex.twilio.com' : baseUrl; + this._baseUrl = baseUrl.includes('flex.local.com') ? 'https://flex.twilio.com' : baseUrl; } /** diff --git a/packages/flex-plugin-e2e-tests/src/utils/pages/view/twilio-console.ts b/packages/flex-plugin-e2e-tests/src/utils/pages/view/twilio-console.ts index 93506eadd..0b00cea03 100644 --- a/packages/flex-plugin-e2e-tests/src/utils/pages/view/twilio-console.ts +++ b/packages/flex-plugin-e2e-tests/src/utils/pages/view/twilio-console.ts @@ -25,10 +25,10 @@ export class TwilioConsole extends Base { } /** - * Creates a localhost url + * Creates a flex.local.com url * @param port */ - private static _createLocalhostUrl = (port: number) => `http://localhost:${port}&localPort=${port}`; + private static _createLocalhostUrl = (port: number) => `http://flex.local.com:${port}&localPort=${port}`; /** * Logs user in through service-login @@ -39,7 +39,7 @@ export class TwilioConsole extends Base { */ async login(flexPath: string, accountSid: string, localhostPort: number, firstLoad: boolean = true): Promise { logger.info('firstload', firstLoad); - const redirectUrl = this._flexBaseUrl.includes('localhost') + const redirectUrl = this._flexBaseUrl.includes('flex.local.com') ? TwilioConsole._createLocalhostUrl(localhostPort) : this._flexBaseUrl; const path = `console/flex/service-login/${accountSid}/?path=/${flexPath}&referer=${redirectUrl}`; diff --git a/packages/flex-plugin-webpack/src/devServer/pluginServer.ts b/packages/flex-plugin-webpack/src/devServer/pluginServer.ts index fcadabbcc..6e948096b 100644 --- a/packages/flex-plugin-webpack/src/devServer/pluginServer.ts +++ b/packages/flex-plugin-webpack/src/devServer/pluginServer.ts @@ -55,6 +55,7 @@ export const _getLocalPlugin = (name: string): FlexConfigurationPlugin | undefin // eslint-disable-next-line import/no-unused-modules export const _getLocalPlugins = (port: Port, names: string[]): Plugin[] => { const protocol = `http${env.isHTTPS() ? 's' : ''}://`; + const domain = env.getDomain() || 'localhost'; return names.map((name) => { const match = _getLocalPlugin(name); @@ -63,7 +64,7 @@ export const _getLocalPlugins = (port: Port, names: string[]): Plugin[] => { return { phase: 3, name, - src: `${protocol}localhost:${port}/plugins/${name}.js`, + src: `${protocol}${domain}:${port}/plugins/${name}.js`, }; } diff --git a/packages/flex-plugin-webpack/src/webpack/webpack.dev.ts b/packages/flex-plugin-webpack/src/webpack/webpack.dev.ts index 67cde43a9..47b3ca78a 100644 --- a/packages/flex-plugin-webpack/src/webpack/webpack.dev.ts +++ b/packages/flex-plugin-webpack/src/webpack/webpack.dev.ts @@ -24,7 +24,7 @@ export const _getBase = (): Configuration => { port: env.getPort(), }, }, - host: env.getHost(), + host: env.getHost() || '0.0.0.0', // Keep binding to 0.0.0.0 for network access port: env.getPort(), }; }; diff --git a/packages/flex-plugins-utils-env/src/index.ts b/packages/flex-plugins-utils-env/src/index.ts index 55f13a3cc..9b8005286 100644 --- a/packages/flex-plugins-utils-env/src/index.ts +++ b/packages/flex-plugins-utils-env/src/index.ts @@ -1,3 +1,3 @@ /* eslint-disable import/no-unused-modules */ -export { default, default as env, Environment, Lifecycle, Region } from './lib/env'; +export { default, default as env, Environment, Lifecycle, Region, setDomain, getDomain, hasDomain } from './lib/env'; diff --git a/packages/flex-plugins-utils-env/src/lib/__tests__/env.test.ts b/packages/flex-plugins-utils-env/src/lib/__tests__/env.test.ts index 9515cc1ca..9e856319f 100644 --- a/packages/flex-plugins-utils-env/src/lib/__tests__/env.test.ts +++ b/packages/flex-plugins-utils-env/src/lib/__tests__/env.test.ts @@ -325,6 +325,33 @@ describe('env', () => { }); }); + describe('domain', () => { + it('should return domain', () => { + process.env.DOMAIN = 'flex.local.com'; + expect(env.getDomain()).toEqual('flex.local.com'); + }); + + it('getDomain should return nothing', () => { + expect(env.getDomain()).toEqual(undefined); + }); + + it('should set domain', () => { + expect(env.getDomain()).toEqual(undefined); + env.setDomain('my-custom.domain'); + expect(env.getDomain()).toEqual('my-custom.domain'); + }); + + it('hasDomain should return true when domain is set', () => { + env.setDomain('test.domain'); + expect(env.hasDomain()).toBe(true); + }); + + it('hasDomain should return false when domain is not set', () => { + delete process.env.DOMAIN; + expect(env.hasDomain()).toBe(false); + }); + }); + describe('port', () => { it('should return port', () => { process.env.PORT = '1234'; diff --git a/packages/flex-plugins-utils-env/src/lib/env.ts b/packages/flex-plugins-utils-env/src/lib/env.ts index 530b579ac..efc467b84 100644 --- a/packages/flex-plugins-utils-env/src/lib/env.ts +++ b/packages/flex-plugins-utils-env/src/lib/env.ts @@ -86,6 +86,9 @@ export const getAccountSid = (): string | undefined => getProcessEnv('TWILIO_ACC export const getAuthToken = (): string | undefined => getProcessEnv('TWILIO_AUTH_TOKEN'); export const hasHost = (): boolean => isDefined(getProcessEnv('HOST')); export const getHost = (): string | undefined => getProcessEnv('HOST'); +export const hasDomain = (): boolean => isDefined(getProcessEnv('DOMAIN')); +export const getDomain = (): string | undefined => getProcessEnv('DOMAIN'); +export const setDomain = (domain: string): void => setProcessEnv('DOMAIN', domain); export const setHost = (host: string): void => setProcessEnv('HOST', host); export const hasPort = (): boolean => isDefined(getProcessEnv('PORT')); export const getPort = (): number => Number(getProcessEnv('PORT')); @@ -257,6 +260,9 @@ export default { hasHost, getHost, setHost, + hasDomain, + getDomain, + setDomain, hasPort, getPort, setPort, diff --git a/packages/plugin-flex/package.json b/packages/plugin-flex/package.json index ebb377678..723c8e75d 100644 --- a/packages/plugin-flex/package.json +++ b/packages/plugin-flex/package.json @@ -105,7 +105,8 @@ "flags": { "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.", "includeRemote": "Use this flag to include all remote plugins in your build.", - "port": "The port to start your local development server on." + "port": "The port to start your local development server on.", + "domain": "The domain to start your local development server on. Required when using SSO v2 OAuth flow to avoid authentication issues." } }, "flex:plugins:deploy": { diff --git a/packages/plugin-flex/src/commands/flex/plugins/start.ts b/packages/plugin-flex/src/commands/flex/plugins/start.ts index 39c313c41..bff762b23 100644 --- a/packages/plugin-flex/src/commands/flex/plugins/start.ts +++ b/packages/plugin-flex/src/commands/flex/plugins/start.ts @@ -14,6 +14,9 @@ const baseFlags = { ...FlexPlugin.flags }; delete baseFlags.json; const MULTI_PLUGINS_PILOT = FLAG_MULTI_PLUGINS.substring(2); +const NAME_FLAG = '--name'; +const PORT_FLAG = '--port'; +const INCLUDE_REMOTE_FLAG = 'include-remote'; /** * Starts the dev-server for building and iterating on a plugin bundle @@ -36,6 +39,9 @@ export default class FlexPluginsStart extends FlexPlugin { description: FlexPluginsStart.topic.flags.port, default: 3000, }), + domain: flags.string({ + description: FlexPluginsStart.topic.flags.domain, + }), 'flex-ui-source': flags.string({ hidden: true, }), @@ -50,7 +56,7 @@ export default class FlexPluginsStart extends FlexPlugin { async init(): Promise { this._flags = (await this.parseCommand(FlexPluginsStart)).flags; - if (this._flags['include-remote'] || this._flags.name) { + if (this._flags[INCLUDE_REMOTE_FLAG] || this._flags.name) { this.opts.runInDirectory = false; } } @@ -59,90 +65,17 @@ export default class FlexPluginsStart extends FlexPlugin { * @override */ async doRun(): Promise { - const flexArgs: string[] = []; - const localPluginNames: string[] = []; - - if (this._flags.name) { - for (const name of this._flags.name) { - flexArgs.push('--name', name); - - const groups = name.match(PLUGIN_INPUT_PARSER_REGEX); - if (!groups) { - throw new TwilioCliError('Unexpected plugin format was provided.'); - } - - const pluginName = groups[1]; - const version = groups[2]; - - // local plugin - if (!version) { - localPluginNames.push(name); - continue; - } - - // remote plugin - if (version === 'remote') { - continue; - } - - if (!semver.valid(version)) { - throw new TwilioCliError(`Version ${version} is not a valid semver string.`); - } - await this.checkPluginVersionExists(pluginName, version); - } - } + const { flexArgs, localPluginNames } = await this.processPluginNames(); - if (this._flags['include-remote']) { - flexArgs.push('--include-remote'); - } - - if (this._flags['flex-ui-source']) { - env.setFlexUISrc(this._flags['flex-ui-source']); - } - - // If running in a plugin directory, append it to the names - if (this.isPluginFolder() && !flexArgs.includes(this.pkg.name)) { - flexArgs.push('--name', this.pkg.name); - localPluginNames.push(this.pkg.name); - } - - if (!localPluginNames.length) { - throw new TwilioCliError( - 'You must run at least one local plugin. To view all remote plugins, go to flex.twilio.com.', - ); - } + this.setupEnvironmentFlags(); + this.addCurrentPluginIfNeeded(flexArgs, localPluginNames); + this.validateLocalPlugins(localPluginNames); const flexPort = await this.getPort(); - flexArgs.push('--port', flexPort.toString()); + flexArgs.push(PORT_FLAG, flexPort.toString()); if (flexArgs.length && localPluginNames.length) { - // Verify the users environment is ready to run plugins locally - await this.checkLocalEnvironment(localPluginNames); - - // Verify all plugins are correct - for (let i = 0; localPluginNames && i < localPluginNames.length; i++) { - await this.checkPlugin(localPluginNames[i]); - } - - // Now spawn each plugin as a separate process - const pluginsConfig: PluginsConfig = {}; - for (let i = 0; localPluginNames && i < localPluginNames.length; i++) { - const port = await findPortAvailablePort('--port', (flexPort + (i + 1) * 100).toString()); - pluginsConfig[localPluginNames[i]] = { port }; - } - - await this.runScript('start', ['flex', ...flexArgs, '--plugin-config', JSON.stringify(pluginsConfig)]); - - for (let i = 0; localPluginNames && i < localPluginNames.length; i++) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.spawnScript('start', [ - 'plugin', - '--name', - localPluginNames[i], - '--port', - pluginsConfig[localPluginNames[i]].port.toString(), - ]); - } + await this.runPlugins(flexArgs, localPluginNames, flexPort); } } @@ -151,7 +84,7 @@ export default class FlexPluginsStart extends FlexPlugin { * @param pluginName the plugin name */ async checkPlugin(pluginName: string): Promise { - const preScriptArgs = ['--name', pluginName]; + const preScriptArgs = [NAME_FLAG, pluginName]; if (this.isMultiPlugin()) { preScriptArgs.push(`--${MULTI_PLUGINS_PILOT}`); } @@ -200,11 +133,11 @@ export default class FlexPluginsStart extends FlexPlugin { * @returns */ async getPort(): Promise { - const port = await findPortAvailablePort('--port', this._flags.port); + const port = await findPortAvailablePort(PORT_FLAG, this._flags.port); // If port provided, check it is available - if (this._flags.port !== port && this.argv.includes('--port')) { - throw new TwilioCliError(`Port ${this._flags.port} already in use. Use --port to choose another port.`); + if (this._flags.port !== port && this.argv.includes(PORT_FLAG)) { + throw new TwilioCliError(`Port ${this._flags.port} already in use. Use ${PORT_FLAG} to choose another port.`); } return port; @@ -221,7 +154,7 @@ export default class FlexPluginsStart extends FlexPlugin { * @override */ get pluginFolderErrorMessage(): string { - return `${this.cwd} directory is not a flex plugin directory. You must either run a plugin inside a directory or use the --name flag`; + return `${this.cwd} directory is not a flex plugin directory. You must either run a plugin inside a directory or use the ${NAME_FLAG} flag`; } /** @@ -231,12 +164,124 @@ export default class FlexPluginsStart extends FlexPlugin { return FlexPluginsStart.topicName; } + /** + * Processes plugin names from flags and returns flex args and local plugin names + */ + private async processPluginNames(): Promise<{ flexArgs: string[]; localPluginNames: string[] }> { + const flexArgs: string[] = []; + const localPluginNames: string[] = []; + + if (this._flags.name) { + for (const name of this._flags.name) { + flexArgs.push(NAME_FLAG, name); + + const groups = name.match(PLUGIN_INPUT_PARSER_REGEX); + if (!groups) { + throw new TwilioCliError('Unexpected plugin format was provided.'); + } + + const pluginName = groups[1]; + const version = groups[2]; + + // local plugin + if (!version) { + localPluginNames.push(name); + continue; + } + + // remote plugin + if (version === 'remote') { + continue; + } + + if (!semver.valid(version)) { + throw new TwilioCliError(`Version ${version} is not a valid semver string.`); + } + await this.checkPluginVersionExists(pluginName, version); + } + } + + return { flexArgs, localPluginNames }; + } + + /** + * Sets up environment flags + */ + private setupEnvironmentFlags(): void { + if (this._flags['flex-ui-source']) { + env.setFlexUISrc(this._flags['flex-ui-source']); + } + + if (this._flags.domain) { + const customDomain = this._flags.domain; + env.setDomain(customDomain); + } + } + + /** + * Adds current plugin to the list if running in a plugin directory + */ + private addCurrentPluginIfNeeded(flexArgs: string[], localPluginNames: string[]): void { + if (this.isPluginFolder() && !flexArgs.includes(this.pkg.name)) { + flexArgs.push(NAME_FLAG, this.pkg.name); + localPluginNames.push(this.pkg.name); + } + + if (this._flags[INCLUDE_REMOTE_FLAG]) { + flexArgs.push('--include-remote'); + } + } + + /** + * Validates that we have at least one local plugin + */ + private validateLocalPlugins(localPluginNames: string[]): void { + if (!localPluginNames.length) { + throw new TwilioCliError( + 'You must run at least one local plugin. To view all remote plugins, go to flex.twilio.com.', + ); + } + } + + /** + * Runs the plugins with the given configuration + */ + private async runPlugins(flexArgs: string[], localPluginNames: string[], flexPort: number): Promise { + // Verify the users environment is ready to run plugins locally + await this.checkLocalEnvironment(localPluginNames); + + // Verify all plugins are correct + for (let i = 0; localPluginNames && i < localPluginNames.length; i++) { + await this.checkPlugin(localPluginNames[i]); + } + + // Now spawn each plugin as a separate process + const pluginsConfig: PluginsConfig = {}; + for (let i = 0; localPluginNames && i < localPluginNames.length; i++) { + const port = await findPortAvailablePort(PORT_FLAG, (flexPort + (i + 1) * 100).toString()); + pluginsConfig[localPluginNames[i]] = { port }; + } + + await this.runScript('start', ['flex', ...flexArgs, '--plugin-config', JSON.stringify(pluginsConfig)]); + + for (let i = 0; localPluginNames && i < localPluginNames.length; i++) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.spawnScript('start', [ + 'plugin', + NAME_FLAG, + localPluginNames[i], + PORT_FLAG, + pluginsConfig[localPluginNames[i]].port.toString(), + ]); + } + } + /** * Returns true if we are running multiple plugins * @private */ private isMultiPlugin(): boolean { - if (this._flags['include-remote']) { + if (this._flags[INCLUDE_REMOTE_FLAG]) { return true; } const { name } = this._flags;