@swell/cli
Version:
Swell's command line interface/utility
231 lines (230 loc) • 10.9 kB
JavaScript
import { Flags } from '@oclif/core';
import { CUSTOM_FRAMEWORK_SLUG, getManualDevHint, getProjectCommands, } from '../../../lib/apps/index.js';
import { PushAppCommand } from '../../../push-app-command.js';
import AppDev from '../dev.js';
export default class AppFrontendDev extends PushAppCommand {
static examples = [
'swell app frontend dev',
'swell app frontend dev --no-push',
'swell app frontend dev --proxy-port 3000',
'swell app frontend dev --storefront-select',
'swell app frontend dev --storefront-id <id>',
];
static flags = {
...AppDev.flags,
'proxy-port': Flags.integer({
description: 'specify the port for the frontend proxy',
}),
'storefront-id': Flags.string({
description: 'identify a storefront to preview with and push theme files to',
}),
'storefront-select': Flags.boolean({
default: false,
description: 'prompt to select a storefront to preview with',
}),
'app-dev': Flags.boolean({
description: 'indicates frontend app is running in app dev mode',
default: false,
}),
yes: Flags.boolean({
char: 'y',
description: 'skip prompts (non-interactive mode)',
}),
};
static orientation = {
env: 'test',
};
static summary = `Run a frontend app in dev mode from your local machine.`;
async run() {
const { flags } = await this.parse(AppFrontendDev);
const { port, 'proxy-port': proxyPort, 'app-dev': isAppDev } = flags;
const noPush = flags['no-push'];
if (!(await this.ensureAppExists(undefined, false))) {
return;
}
if (!noPush && !isAppDev) {
await this.pushAppConfigs();
}
if (this.app.type === 'storefront') {
await this.getStorefrontToPush(flags);
this.logStorefrontConnected();
}
this.saveCurrentStorefront();
if (!isAppDev) {
this.log(`Starting app dev server...\n`);
}
// When running as child of `swell app dev`, don't create a new tunnel.
// The parent command already created the tunnel and handles routing.
const serverPort = isAppDev && proxyPort
? proxyPort
: await this.startProxyServer(proxyPort || port);
await this.execFrontendProject(serverPort, isAppDev).catch((error) => {
// SIGTERM/SIGINT during dev server shutdown is expected
switch (error?.signal) {
case 'SIGTERM':
case 'SIGINT': {
return;
}
default: {
throw error;
}
}
});
}
async execFrontendProject(serverPort, isAppDev) {
const projectType = await this.getFrontendProjectType(!isAppDev);
if (!projectType) {
return;
}
// Manual mode for unrecognized frameworks
if (projectType.slug === CUSTOM_FRAMEWORK_SLUG) {
const hintCommand = await getManualDevHint(this.appPath, serverPort);
this.log(`Detected a frontend project but could not identify the framework.`);
this.log(`Start your dev server manually on port ${serverPort}:\n`);
this.log(` cd frontend && ${hintCommand}\n`);
this.log(`The Swell tunnel is ready and will route traffic once your server is running.\n`);
this.logLocalPreviewUrl();
if (!isAppDev) {
this.watchForChanges();
}
// Keep the process alive so the tunnel remains open
await new Promise(() => { });
return;
}
// Get commands transformed for the detected package manager
const { devCommand } = await getProjectCommands(this.appPath, projectType);
let serverReadyDetected = false;
await this.execFrontend(devCommand.replace('${PORT}', String(serverPort)), (string) => {
// Shared: Hide Vite noise output common across frameworks
if (/press h \+ enter to show help/i.test(string)) {
return false;
}
// Framework-specific output handling and ready detection
switch (projectType.slug) {
case 'nextjs': {
// 1. NOISE FILTER: Hide unwanted output
if (string.includes('Using vars defined in .dev.vars')) {
return false;
}
// 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
if (/- local:\s+http:\/\//i.test(string) ||
/- network:\s+http:\/\//i.test(string) ||
/ready on http:\/\/localhost/i.test(string)) {
return false;
}
// 3. READY DETECTION: Detect server ready, show our message
if (!serverReadyDetected &&
(/✓ ready in \d+/i.test(string) || /✓ starting/i.test(string))) {
serverReadyDetected = true;
this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
setTimeout(() => this.logLocalPreviewUrl(), 100);
if (!isAppDev) {
this.watchForChanges();
}
}
break;
}
case 'astro': {
// 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
if (string.includes('┃ Local') || string.includes('┃ Network')) {
return false;
}
// 3. READY DETECTION: Detect server ready, show our message
if (!serverReadyDetected &&
/watching for file changes/i.test(string)) {
serverReadyDetected = true;
this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
setTimeout(() => this.logLocalPreviewUrl(), 100);
if (!isAppDev) {
this.watchForChanges();
}
}
break;
}
case 'angular': {
// 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
if (/➜\s+local:\s+http:\/\//i.test(string)) {
return false;
}
// 3. READY DETECTION: Detect server ready, show our message
if (!serverReadyDetected &&
(/application bundle generation complete/i.test(string) ||
/watch mode enabled/i.test(string))) {
serverReadyDetected = true;
this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
setTimeout(() => this.logLocalPreviewUrl(), 100);
if (!isAppDev) {
this.watchForChanges();
}
}
break;
}
case 'hono': {
// 2. URL FILTER: Hide localhost/network/debug URLs (confusing - users should use Swell tunnel)
// Vite-based Hono apps output "➜ Local: http://localhost:PORT/"
if (/➜\s+local:\s+http:\/\//i.test(string) ||
/➜\s+network:/i.test(string) ||
/➜\s+debug:\s+http:\/\//i.test(string)) {
if (!serverReadyDetected) {
serverReadyDetected = true;
this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
setTimeout(() => this.logLocalPreviewUrl(), 100);
if (!isAppDev) {
this.watchForChanges();
}
}
return false;
}
// 3. READY DETECTION: Native Hono dev server outputs "ready on http://localhost:PORT"
if (!serverReadyDetected &&
/ready on http:\/\/localhost:\d+/i.test(string)) {
serverReadyDetected = true;
this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
setTimeout(() => this.logLocalPreviewUrl(), 100);
if (!isAppDev) {
this.watchForChanges();
}
return false;
}
break;
}
case 'react':
case 'react-storefront': {
// Vite emits the Local URL as its ready signal — no separate
// detector needed.
if (/➜\s+local:\s+http:\/\//i.test(string) ||
/➜\s+network:/i.test(string)) {
if (!serverReadyDetected) {
serverReadyDetected = true;
this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
setTimeout(() => this.logLocalPreviewUrl(), 100);
if (!isAppDev) {
this.watchForChanges();
}
}
return false;
}
break;
}
case 'nuxt': {
// 2. URL FILTER: Hide localhost/network URLs (confusing - users should use Swell tunnel)
if (/➜ local:\s+http:\/\//i.test(string) ||
/➜ network:/i.test(string)) {
return false;
}
// 3. READY DETECTION: Detect server ready, show our message
if (!serverReadyDetected && /nuxt \d+\.\d+\.\d+/i.test(string)) {
serverReadyDetected = true;
this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
setTimeout(() => this.logLocalPreviewUrl(), 100);
if (!isAppDev) {
this.watchForChanges();
}
}
break;
}
// No default
}
});
}
}