Skip to content

Commit

Permalink
feat: add livereload to run command (#6831)
Browse files Browse the repository at this point in the history
  • Loading branch information
IT-MikeS committed Aug 31, 2023
1 parent 599aae4 commit 54a63ae
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 4 deletions.
18 changes: 17 additions & 1 deletion cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,26 @@ export function runProgram(config: Config): void {
'--forwardPorts <port:port>',
'Automatically run "adb reverse" for better live-reloading support',
)
.option('-l, --live-reload', 'Enable Live Reload')
.option('--host <host>', 'Host used for live reload')
.option('--port <port>', 'Port used for live reload')
.action(
wrapAction(
telemetryAction(
config,
async (
platform,
{ scheme, flavor, list, target, sync, forwardPorts },
{
scheme,
flavor,
list,
target,
sync,
forwardPorts,
liveReload,
host,
port,
},
) => {
const { runCommand } = await import('./tasks/run');
await runCommand(config, platform, {
Expand All @@ -226,6 +239,9 @@ export function runProgram(config: Config): void {
target,
sync,
forwardPorts,
liveReload,
host,
port,
});
},
),
Expand Down
51 changes: 48 additions & 3 deletions cli/src/tasks/run.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sleepForever } from '@ionic/utils-process';
import { columnar } from '@ionic/utils-terminal';

import { runAndroid } from '../android/run';
Expand All @@ -10,10 +11,11 @@ import {
promptForPlatform,
getPlatformTargetName,
} from '../common';
import type { Config } from '../definitions';
import type { AppConfig, Config } from '../definitions';
import { fatal, isFatal } from '../errors';
import { runIOS } from '../ios/run';
import { logger, output } from '../log';
import { CapLiveReloadHelper } from '../util/livereload';
import { getPlatformTargets } from '../util/native-run';

import { sync } from './sync';
Expand All @@ -25,13 +27,19 @@ export interface RunCommandOptions {
target?: string;
sync?: boolean;
forwardPorts?: string;
liveReload?: boolean;
host?: string;
port?: string;
}

export async function runCommand(
config: Config,
selectedPlatformName: string,
options: RunCommandOptions,
): Promise<void> {
options.host =
options.host ?? CapLiveReloadHelper.getIpAddress() ?? 'localhost';
options.port = options.port ?? '3000';
if (selectedPlatformName && !(await isValidPlatform(selectedPlatformName))) {
const platformDir = resolvePlatform(config, selectedPlatformName);
if (platformDir) {
Expand Down Expand Up @@ -83,10 +91,47 @@ export async function runCommand(

try {
if (options.sync) {
await sync(config, platformName, false, true);
if (options.liveReload) {
const newExtConfig =
await CapLiveReloadHelper.editExtConfigForLiveReload(
config,
platformName,
options,
);
const cfg: {
-readonly [K in keyof Config]: Config[K];
} = config;
const cfgapp: {
-readonly [K in keyof AppConfig]: AppConfig[K];
} = config.app;
cfgapp.extConfig = newExtConfig;
cfg.app = cfgapp;
await sync(cfg, platformName, false, true);
} else {
await sync(config, platformName, false, true);
}
} else {
if (options.liveReload) {
await CapLiveReloadHelper.editCapConfigForLiveReload(
config,
platformName,
options,
);
}
}

await run(config, platformName, options);
if (options.liveReload) {
process.on('SIGINT', async () => {
if (options.liveReload) {
await CapLiveReloadHelper.revertCapConfigForLiveReload();
}
process.exit();
});
console.log(
`\nApp running with live reload listing for: http://${options.host}:${options.port}. Press Ctrl+C to quit.`,
);
await sleepForever();
}
} catch (e: any) {
if (!isFatal(e)) {
fatal(e.stack ?? e);
Expand Down
191 changes: 191 additions & 0 deletions cli/src/util/livereload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { readJSONSync, writeJSONSync } from '@ionic/utils-fs';
import { networkInterfaces } from 'os';
import { join } from 'path';

import type { Config } from '../definitions';
import type { RunCommandOptions } from '../tasks/run';

class CapLiveReload {
configJsonToRevertTo: {
json: string | null;
platformPath: string | null;
} = {
json: null,
platformPath: null,
};

constructor() {
// nothing to do
}

getIpAddress(name?: string, family?: any) {
const interfaces: any = networkInterfaces() ?? {};

const _normalizeFamily = (family?: any) => {
if (family === 4) {
return 'ipv4';
}
if (family === 6) {
return 'ipv6';
}
return family ? family.toLowerCase() : 'ipv4';
};
const isLoopback = (addr: string) => {
return (
/^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/.test(addr) ||
/^fe80::1$/.test(addr) ||
/^::1$/.test(addr) ||
/^::$/.test(addr)
);
};
const isPrivate = (addr: string) => {
return (
/^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(
addr,
) ||
/^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) ||
/^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(
addr,
) ||
/^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(
addr,
) ||
/^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) ||
/^f[cd][0-9a-f]{2}:/i.test(addr) ||
/^fe80:/i.test(addr) ||
/^::1$/.test(addr) ||
/^::$/.test(addr)
);
};
const isPublic = (addr: string) => {
return !isPrivate(addr);
};
const loopback = (family?: any) => {
//
// Default to `ipv4`
//
family = _normalizeFamily(family);

if (family !== 'ipv4' && family !== 'ipv6') {
throw new Error('family must be ipv4 or ipv6');
}

return family === 'ipv4' ? '127.0.0.1' : 'fe80::1';
};

//
// Default to `ipv4`
//
family = _normalizeFamily(family);

//
// If a specific network interface has been named,
// return the address.
//
if (name && name !== 'private' && name !== 'public') {
const res = interfaces[name].filter((details: any) => {
const itemFamily = _normalizeFamily(details.family);
return itemFamily === family;
});
if (res.length === 0) {
return undefined;
}
return res[0].address;
}

const all = Object.keys(interfaces)
.map(nic => {
//
// Note: name will only be `public` or `private`
// when this is called.
//
const addresses = interfaces[nic].filter((details: any) => {
details.family = _normalizeFamily(details.family);
if (details.family !== family || isLoopback(details.address)) {
return false;
}
if (!name) {
return true;
}

return name === 'public'
? isPrivate(details.address)
: isPublic(details.address);
});

return addresses.length ? addresses[0].address : undefined;
})
.filter(Boolean);

return !all.length ? loopback(family) : all[0];
}

async editExtConfigForLiveReload(
config: Config,
platformName: string,
options: RunCommandOptions,
rootConfigChange = false,
): Promise<any> {
const platformAbsPath =
platformName == config.ios.name
? config.ios.nativeTargetDirAbs
: platformName == config.android.name
? config.android.assetsDirAbs
: null;
if (platformAbsPath == null) throw new Error('Platform not found.');
const capConfigPath = rootConfigChange
? config.app.extConfigFilePath
: join(platformAbsPath, 'capacitor.config.json');

const configJson = { ...config.app.extConfig };
this.configJsonToRevertTo.json = JSON.stringify(configJson, null, 2);
this.configJsonToRevertTo.platformPath = capConfigPath;
const url = `http://${options.host}:${options.port}`;
configJson.server = {
url,
};
return configJson;
}

async editCapConfigForLiveReload(
config: Config,
platformName: string,
options: RunCommandOptions,
rootConfigChange = false,
): Promise<void> {
const platformAbsPath =
platformName == config.ios.name
? config.ios.nativeTargetDirAbs
: platformName == config.android.name
? config.android.assetsDirAbs
: null;
if (platformAbsPath == null) throw new Error('Platform not found.');
const capConfigPath = rootConfigChange
? config.app.extConfigFilePath
: join(platformAbsPath, 'capacitor.config.json');

const configJson = readJSONSync(capConfigPath);
this.configJsonToRevertTo.json = JSON.stringify(configJson, null, 2);
this.configJsonToRevertTo.platformPath = capConfigPath;
const url = `http://${options.host}:${options.port}`;
configJson.server = {
url,
};
writeJSONSync(capConfigPath, configJson, { spaces: '\t' });
}

async revertCapConfigForLiveReload(): Promise<void> {
if (
this.configJsonToRevertTo.json == null ||
this.configJsonToRevertTo.platformPath == null
)
return;
const capConfigPath = this.configJsonToRevertTo.platformPath;
const configJson = this.configJsonToRevertTo.json;
writeJSONSync(capConfigPath, JSON.parse(configJson), { spaces: '\t' });
this.configJsonToRevertTo.json = null;
this.configJsonToRevertTo.platformPath = null;
}
}

export const CapLiveReloadHelper = new CapLiveReload();

0 comments on commit 54a63ae

Please sign in to comment.