Skip to content

Commit f9eda80

Browse files
committed
feat: enhance command rewriter with smart server detection
- Auto-detect server-like Python scripts (serve.py, server.py, http_server.py, web.py, webserver.py, httpserver.py) - Extract port from --port, -p, and bare port arguments - Server scripts rewrite to npx serve instead of showing error - Add generic unsupported runtime catch-all (ruby, perl, php) - Expand test suite from 16 to 26 tests (409 total passing) - Live tested: python3 serve.py now serves via npx and preview works
1 parent 9f1f8ff commit f9eda80

2 files changed

Lines changed: 116 additions & 5 deletions

File tree

app/utils/command-rewriter.spec.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,87 @@ describe('rewriteUnsupportedCommand', () => {
6565
});
6666
});
6767

68-
describe('Python script execution', () => {
69-
it('replaces python script with echo warning', () => {
68+
describe('Python script execution — server detection', () => {
69+
it('auto-serves serve.py with npx serve', () => {
70+
const result = rewriteUnsupportedCommand('python3 serve.py');
71+
expect(result.wasRewritten).toBe(true);
72+
expect(result.command).toBe('npx --yes serve -l 8000');
73+
expect(result.reason).toContain('serve.py');
74+
});
75+
76+
it('auto-serves server.py with npx serve', () => {
77+
const result = rewriteUnsupportedCommand('python server.py');
78+
expect(result.wasRewritten).toBe(true);
79+
expect(result.command).toBe('npx --yes serve -l 8000');
80+
});
81+
82+
it('auto-serves http_server.py with npx serve', () => {
83+
const result = rewriteUnsupportedCommand('python3 http_server.py');
84+
expect(result.wasRewritten).toBe(true);
85+
expect(result.command).toBe('npx --yes serve -l 8000');
86+
});
87+
88+
it('auto-serves web.py with npx serve', () => {
89+
const result = rewriteUnsupportedCommand('python3 web.py');
90+
expect(result.wasRewritten).toBe(true);
91+
expect(result.command).toBe('npx --yes serve -l 8000');
92+
});
93+
94+
it('extracts port from --port argument', () => {
95+
const result = rewriteUnsupportedCommand('python3 serve.py --port 3000');
96+
expect(result.wasRewritten).toBe(true);
97+
expect(result.command).toBe('npx --yes serve -l 3000');
98+
});
99+
100+
it('extracts port from -p argument', () => {
101+
const result = rewriteUnsupportedCommand('python3 server.py -p 9090');
102+
expect(result.wasRewritten).toBe(true);
103+
expect(result.command).toBe('npx --yes serve -l 9090');
104+
});
105+
106+
it('extracts bare port number as argument', () => {
107+
const result = rewriteUnsupportedCommand('python3 serve.py 4000');
108+
expect(result.wasRewritten).toBe(true);
109+
expect(result.command).toBe('npx --yes serve -l 4000');
110+
});
111+
112+
it('shows error for non-server Python scripts', () => {
70113
const result = rewriteUnsupportedCommand('python3 app.py');
71114
expect(result.wasRewritten).toBe(true);
72115
expect(result.command).toContain('echo');
73116
expect(result.command).toContain('WebContainer only supports Node.js');
74117
});
75118

76-
it('handles python script with path', () => {
119+
it('shows error for Python script with path', () => {
77120
const result = rewriteUnsupportedCommand('python ./scripts/start.py');
78121
expect(result.wasRewritten).toBe(true);
79122
expect(result.command).toContain('echo');
80123
});
81124
});
82125

126+
describe('Generic unsupported runtimes', () => {
127+
it('catches ruby commands', () => {
128+
const result = rewriteUnsupportedCommand('ruby app.rb');
129+
expect(result.wasRewritten).toBe(true);
130+
expect(result.command).toContain('echo');
131+
expect(result.command).toContain('ruby');
132+
});
133+
134+
it('catches perl commands', () => {
135+
const result = rewriteUnsupportedCommand('perl script.pl');
136+
expect(result.wasRewritten).toBe(true);
137+
expect(result.command).toContain('echo');
138+
expect(result.command).toContain('perl');
139+
});
140+
141+
it('catches php commands', () => {
142+
const result = rewriteUnsupportedCommand('php index.php');
143+
expect(result.wasRewritten).toBe(true);
144+
expect(result.command).toContain('echo');
145+
expect(result.command).toContain('php');
146+
});
147+
});
148+
83149
describe('Passthrough (no rewrite needed)', () => {
84150
it('does not rewrite npm commands', () => {
85151
const result = rewriteUnsupportedCommand('npm run dev');

app/utils/command-rewriter.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,40 @@ export function rewriteUnsupportedCommand(command: string): RewriteResult {
8787
};
8888
}
8989

90-
// Generic python/python3 script execution → echo warning
91-
const pythonScriptMatch = trimmed.match(/^python3?\s+[\w./-]+\.py/);
90+
/*
91+
* Python script execution — smart detection:
92+
* If the script name looks like a server (serve.py, server.py, app.py, etc.)
93+
* we auto-rewrite to `npx --yes serve -l PORT`.
94+
* Otherwise, show a clear error message.
95+
*/
96+
const pythonScriptMatch = trimmed.match(/^python3?\s+([\w./-]+\.py)(?:\s+(.*))?$/);
9297

9398
if (pythonScriptMatch) {
99+
const scriptName = pythonScriptMatch[1].toLowerCase();
100+
const args = pythonScriptMatch[2] || '';
101+
102+
// Check if script name suggests an HTTP server
103+
const serverPatterns = ['serve.py', 'server.py', 'http_server.py', 'httpserver.py', 'web.py', 'webserver.py'];
104+
const isLikelyServer = serverPatterns.some(
105+
(pattern) => scriptName === pattern || scriptName.endsWith(`/${pattern}`),
106+
);
107+
108+
if (isLikelyServer) {
109+
// Try to extract port from args: --port PORT, -p PORT, or bare PORT
110+
const portMatch = args.match(/(?:--port\s+|-p\s+)(\d+)/) || args.match(/^(\d+)$/);
111+
const port = portMatch ? portMatch[1] : '8000';
112+
const rewritten = `npx --yes serve -l ${port}`;
113+
logger.info(`Rewrote server script: "${trimmed}" → "${rewritten}"`);
114+
115+
return {
116+
command: rewritten,
117+
wasRewritten: true,
118+
originalCommand: trimmed,
119+
reason: `WebContainer has no Python runtime. Detected "${scriptName}" as HTTP server — replaced with Node.js serve on port ${port}.`,
120+
};
121+
}
122+
123+
// Non-server Python script — show error
94124
logger.warn(`Cannot run Python script in WebContainer: ${trimmed}`);
95125

96126
return {
@@ -101,5 +131,20 @@ export function rewriteUnsupportedCommand(command: string): RewriteResult {
101131
};
102132
}
103133

134+
// Generic unsupported runtime commands
135+
const genericUnsupported = trimmed.match(/^(python3?|ruby|perl|php)\s/);
136+
137+
if (genericUnsupported) {
138+
const runtime = genericUnsupported[1];
139+
logger.warn(`Unsupported runtime "${runtime}" in WebContainer: ${trimmed}`);
140+
141+
return {
142+
command: `echo "Error: WebContainer only supports Node.js. Cannot run ${runtime} commands: ${trimmed.replace(/"/g, '\\"')}"`,
143+
wasRewritten: true,
144+
originalCommand: trimmed,
145+
reason: `WebContainer only supports Node.js. ${runtime} is not available.`,
146+
};
147+
}
148+
104149
return { command, wasRewritten: false };
105150
}

0 commit comments

Comments
 (0)