Proxy server to inject shim (#178)
things to test: - [x] allow real URL to open in new window - [x] packaging in electron? - [ ] does it work on windows? - [x] make sure it works with older apps - [x] what about cache / reuse? - maybe use a bigger range of ports??
This commit is contained in:
@@ -22,6 +22,9 @@ const ignore = (file: string) => {
|
|||||||
if (file.startsWith("/scaffold")) {
|
if (file.startsWith("/scaffold")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (file.startsWith("/worker")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (file.startsWith("/node_modules/better-sqlite3")) {
|
if (file.startsWith("/node_modules/better-sqlite3")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
59
package-lock.json
generated
59
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "dyad",
|
"name": "dyad",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "dyad",
|
"name": "dyad",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^1.2.8",
|
"@ai-sdk/anthropic": "^1.2.8",
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
"shell-env": "^4.0.1",
|
"shell-env": "^4.0.1",
|
||||||
"shiki": "^3.2.1",
|
"shiki": "^3.2.1",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
|
"stacktrace-js": "^2.0.2",
|
||||||
"tailwind-merge": "^3.1.0",
|
"tailwind-merge": "^3.1.0",
|
||||||
"tailwindcss": "^4.1.3",
|
"tailwindcss": "^4.1.3",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
@@ -9657,6 +9658,15 @@
|
|||||||
"is-arrayish": "^0.2.1"
|
"is-arrayish": "^0.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/error-stack-parser": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"stackframe": "^1.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-abstract": {
|
"node_modules/es-abstract": {
|
||||||
"version": "1.23.9",
|
"version": "1.23.9",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
|
||||||
@@ -17564,6 +17574,15 @@
|
|||||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stack-generator": {
|
||||||
|
"version": "2.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
|
||||||
|
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"stackframe": "^1.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stack-utils": {
|
"node_modules/stack-utils": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||||
@@ -17592,6 +17611,42 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/stackframe": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/stacktrace-gps": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"source-map": "0.5.6",
|
||||||
|
"stackframe": "^1.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stacktrace-gps/node_modules/source-map": {
|
||||||
|
"version": "0.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
|
||||||
|
"integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stacktrace-js": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"error-stack-parser": "^2.0.6",
|
||||||
|
"stack-generator": "^2.0.5",
|
||||||
|
"stacktrace-gps": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/state-local": {
|
"node_modules/state-local": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
"shell-env": "^4.0.1",
|
"shell-env": "^4.0.1",
|
||||||
"shiki": "^3.2.1",
|
"shiki": "^3.2.1",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
|
"stacktrace-js": "^2.0.2",
|
||||||
"tailwind-merge": "^3.1.0",
|
"tailwind-merge": "^3.1.0",
|
||||||
"tailwindcss": "^4.1.3",
|
"tailwindcss": "^4.1.3",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^1.5.0",
|
||||||
"stacktrace-js": "^2.0.2",
|
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.9.3",
|
"vaul": "^0.9.3",
|
||||||
|
|||||||
45
scaffold/pnpm-lock.yaml
generated
45
scaffold/pnpm-lock.yaml
generated
@@ -143,9 +143,6 @@ importers:
|
|||||||
sonner:
|
sonner:
|
||||||
specifier: ^1.5.0
|
specifier: ^1.5.0
|
||||||
version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
stacktrace-js:
|
|
||||||
specifier: ^2.0.2
|
|
||||||
version: 2.0.2
|
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^2.5.2
|
specifier: ^2.5.2
|
||||||
version: 2.6.0
|
version: 2.6.0
|
||||||
@@ -1618,9 +1615,6 @@ packages:
|
|||||||
emoji-regex@9.2.2:
|
emoji-regex@9.2.2:
|
||||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
|
|
||||||
error-stack-parser@2.1.4:
|
|
||||||
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
|
|
||||||
|
|
||||||
esbuild@0.25.3:
|
esbuild@0.25.3:
|
||||||
resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
|
resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2230,22 +2224,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
source-map@0.5.6:
|
|
||||||
resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
stack-generator@2.0.10:
|
|
||||||
resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==}
|
|
||||||
|
|
||||||
stackframe@1.3.4:
|
|
||||||
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
|
|
||||||
|
|
||||||
stacktrace-gps@3.1.2:
|
|
||||||
resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==}
|
|
||||||
|
|
||||||
stacktrace-js@2.0.2:
|
|
||||||
resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==}
|
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -3782,10 +3760,6 @@ snapshots:
|
|||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
|
|
||||||
error-stack-parser@2.1.4:
|
|
||||||
dependencies:
|
|
||||||
stackframe: 1.3.4
|
|
||||||
|
|
||||||
esbuild@0.25.3:
|
esbuild@0.25.3:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.25.3
|
'@esbuild/aix-ppc64': 0.25.3
|
||||||
@@ -4389,25 +4363,6 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
source-map@0.5.6: {}
|
|
||||||
|
|
||||||
stack-generator@2.0.10:
|
|
||||||
dependencies:
|
|
||||||
stackframe: 1.3.4
|
|
||||||
|
|
||||||
stackframe@1.3.4: {}
|
|
||||||
|
|
||||||
stacktrace-gps@3.1.2:
|
|
||||||
dependencies:
|
|
||||||
source-map: 0.5.6
|
|
||||||
stackframe: 1.3.4
|
|
||||||
|
|
||||||
stacktrace-js@2.0.2:
|
|
||||||
dependencies:
|
|
||||||
error-stack-parser: 2.1.4
|
|
||||||
stack-generator: 2.0.10
|
|
||||||
stacktrace-gps: 3.1.2
|
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
emoji-regex: 8.0.0
|
emoji-regex: 8.0.0
|
||||||
|
|||||||
@@ -1,98 +1,13 @@
|
|||||||
import { defineConfig, Plugin, HtmlTagDescriptor } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
|
||||||
|
|
||||||
export function devErrorAndNavigationPlugin(): Plugin {
|
|
||||||
let stacktraceJsContent: string | null = null;
|
|
||||||
let dyadShimContent: string | null = null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "dev-error-and-navigation-handler",
|
|
||||||
apply: "serve",
|
|
||||||
|
|
||||||
configResolved() {
|
|
||||||
const stackTraceLibPath = path.join(
|
|
||||||
"node_modules",
|
|
||||||
"stacktrace-js",
|
|
||||||
"dist",
|
|
||||||
"stacktrace.min.js",
|
|
||||||
);
|
|
||||||
if (stackTraceLibPath) {
|
|
||||||
try {
|
|
||||||
stacktraceJsContent = fs.readFileSync(stackTraceLibPath, "utf-8");
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[dyad-shim] Failed to read stacktrace.js from ${stackTraceLibPath}:`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
stacktraceJsContent = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(`[dyad-shim] stacktrace.js not found.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dyadShimPath = path.join("dyad-shim.js");
|
|
||||||
if (dyadShimPath) {
|
|
||||||
try {
|
|
||||||
dyadShimContent = fs.readFileSync(dyadShimPath, "utf-8");
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[dyad-shim] Failed to read dyad-shim from ${dyadShimPath}:`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
dyadShimContent = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(`[dyad-shim] stacktrace.js not found.`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
transformIndexHtml(html) {
|
|
||||||
const tags: HtmlTagDescriptor[] = [];
|
|
||||||
|
|
||||||
// 1. Inject stacktrace.js
|
|
||||||
if (stacktraceJsContent) {
|
|
||||||
tags.push({
|
|
||||||
tag: "script",
|
|
||||||
injectTo: "head-prepend",
|
|
||||||
children: stacktraceJsContent,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tags.push({
|
|
||||||
tag: "script",
|
|
||||||
injectTo: "head-prepend",
|
|
||||||
children:
|
|
||||||
"console.warn('[dyad-shim] stacktrace.js library was not injected.');",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Inject dyad shim
|
|
||||||
if (dyadShimContent) {
|
|
||||||
tags.push({
|
|
||||||
tag: "script",
|
|
||||||
injectTo: "head-prepend",
|
|
||||||
children: dyadShimContent,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tags.push({
|
|
||||||
tag: "script",
|
|
||||||
injectTo: "head-prepend",
|
|
||||||
children: "console.warn('[dyad-shim] dyad shim was not injected.');",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { html, tags };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig(() => ({
|
export default defineConfig(() => ({
|
||||||
server: {
|
server: {
|
||||||
host: "::",
|
host: "::",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
},
|
},
|
||||||
plugins: [react(), devErrorAndNavigationPlugin()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ export const previewModeAtom = atom<"preview" | "code">("preview");
|
|||||||
export const selectedVersionIdAtom = atom<string | null>(null);
|
export const selectedVersionIdAtom = atom<string | null>(null);
|
||||||
export const appOutputAtom = atom<AppOutput[]>([]);
|
export const appOutputAtom = atom<AppOutput[]>([]);
|
||||||
export const appUrlAtom = atom<
|
export const appUrlAtom = atom<
|
||||||
{ appUrl: string; appId: number } | { appUrl: null; appId: null }
|
| { appUrl: string; appId: number; originalUrl: string }
|
||||||
>({ appUrl: null, appId: null });
|
| { appUrl: null; appId: null; originalUrl: null }
|
||||||
|
>({ appUrl: null, appId: null, originalUrl: null });
|
||||||
export const userSettingsAtom = atom<UserSettings | null>(null);
|
export const userSettingsAtom = atom<UserSettings | null>(null);
|
||||||
|
|
||||||
// Atom for storing allow-listed environment variables
|
// Atom for storing allow-listed environment variables
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
|
|||||||
// Preview iframe component
|
// Preview iframe component
|
||||||
export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||||
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
||||||
const { appUrl } = useAtomValue(appUrlAtom);
|
const { appUrl, originalUrl } = useAtomValue(appUrlAtom);
|
||||||
const setAppOutput = useSetAtom(appOutputAtom);
|
const setAppOutput = useSetAtom(appOutputAtom);
|
||||||
// State to trigger iframe reload
|
// State to trigger iframe reload
|
||||||
const [reloadKey, setReloadKey] = useState(0);
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
@@ -429,8 +429,8 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (appUrl) {
|
if (originalUrl) {
|
||||||
IpcClient.getInstance().openExternalUrl(appUrl);
|
IpcClient.getInstance().openExternalUrl(originalUrl);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed dark:text-gray-300"
|
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed dark:text-gray-300"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
selectedAppIdAtom,
|
selectedAppIdAtom,
|
||||||
} from "@/atoms/appAtoms";
|
} from "@/atoms/appAtoms";
|
||||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
|
import { AppOutput } from "@/ipc/ipc_types";
|
||||||
|
|
||||||
export function useRunApp() {
|
export function useRunApp() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -18,6 +19,29 @@ export function useRunApp() {
|
|||||||
const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom);
|
const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom);
|
||||||
const appId = useAtomValue(selectedAppIdAtom);
|
const appId = useAtomValue(selectedAppIdAtom);
|
||||||
const setPreviewErrorMessage = useSetAtom(previewErrorMessageAtom);
|
const setPreviewErrorMessage = useSetAtom(previewErrorMessageAtom);
|
||||||
|
|
||||||
|
const processProxyServerOutput = (output: AppOutput) => {
|
||||||
|
const matchesProxyServerStart = output.message.includes(
|
||||||
|
"[dyad-proxy-server]started=[",
|
||||||
|
);
|
||||||
|
if (matchesProxyServerStart) {
|
||||||
|
// Extract both proxy URL and original URL using regex
|
||||||
|
const proxyUrlMatch = output.message.match(
|
||||||
|
/\[dyad-proxy-server\]started=\[(.*?)\]/,
|
||||||
|
);
|
||||||
|
const originalUrlMatch = output.message.match(/original=\[(.*?)\]/);
|
||||||
|
|
||||||
|
if (proxyUrlMatch && proxyUrlMatch[1]) {
|
||||||
|
const proxyUrl = proxyUrlMatch[1];
|
||||||
|
const originalUrl = originalUrlMatch && originalUrlMatch[1];
|
||||||
|
setAppUrlObj({
|
||||||
|
appUrl: proxyUrl,
|
||||||
|
appId: appId!,
|
||||||
|
originalUrl: originalUrl!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
const runApp = useCallback(async (appId: number) => {
|
const runApp = useCallback(async (appId: number) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -26,7 +50,7 @@ export function useRunApp() {
|
|||||||
|
|
||||||
// Clear the URL and add restart message
|
// Clear the URL and add restart message
|
||||||
if (appUrlObj?.appId !== appId) {
|
if (appUrlObj?.appId !== appId) {
|
||||||
setAppUrlObj({ appUrl: null, appId: null });
|
setAppUrlObj({ appUrl: null, appId: null, originalUrl: null });
|
||||||
}
|
}
|
||||||
setAppOutput((prev) => [
|
setAppOutput((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -41,11 +65,7 @@ export function useRunApp() {
|
|||||||
setApp(app);
|
setApp(app);
|
||||||
await ipcClient.runApp(appId, (output) => {
|
await ipcClient.runApp(appId, (output) => {
|
||||||
setAppOutput((prev) => [...prev, output]);
|
setAppOutput((prev) => [...prev, output]);
|
||||||
// Check if the output contains a localhost URL
|
processProxyServerOutput(output);
|
||||||
const urlMatch = output.message.match(/(https?:\/\/localhost:\d+\/?)/);
|
|
||||||
if (urlMatch) {
|
|
||||||
setAppUrlObj({ appUrl: urlMatch[1], appId });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
setPreviewErrorMessage(undefined);
|
setPreviewErrorMessage(undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -100,7 +120,7 @@ export function useRunApp() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Clear the URL and add restart message
|
// Clear the URL and add restart message
|
||||||
setAppUrlObj({ appUrl: null, appId: null });
|
setAppUrlObj({ appUrl: null, appId: null, originalUrl: null });
|
||||||
setAppOutput((prev) => [
|
setAppOutput((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -124,13 +144,7 @@ export function useRunApp() {
|
|||||||
onHotModuleReload();
|
onHotModuleReload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check if the output contains a localhost URL
|
processProxyServerOutput(output);
|
||||||
const urlMatch = output.message.match(
|
|
||||||
/(https?:\/\/localhost:\d+\/?)/,
|
|
||||||
);
|
|
||||||
if (urlMatch) {
|
|
||||||
setAppUrlObj({ appUrl: urlMatch[1], appId });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
removeNodeModules,
|
removeNodeModules,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,10 +33,14 @@ import log from "electron-log";
|
|||||||
import { getSupabaseProjectName } from "../../supabase_admin/supabase_management_client";
|
import { getSupabaseProjectName } from "../../supabase_admin/supabase_management_client";
|
||||||
import { createLoggedHandler } from "./safe_handle";
|
import { createLoggedHandler } from "./safe_handle";
|
||||||
import { getLanguageModelProviders } from "../shared/language_model_helpers";
|
import { getLanguageModelProviders } from "../shared/language_model_helpers";
|
||||||
|
import { startProxy } from "../utils/start_proxy_server";
|
||||||
|
import { Worker } from "worker_threads";
|
||||||
|
|
||||||
const logger = log.scope("app_handlers");
|
const logger = log.scope("app_handlers");
|
||||||
const handle = createLoggedHandler(logger);
|
const handle = createLoggedHandler(logger);
|
||||||
|
|
||||||
|
let proxyWorker: Worker | null = null;
|
||||||
|
|
||||||
// Needed, otherwise electron in MacOS/Linux will not be able
|
// Needed, otherwise electron in MacOS/Linux will not be able
|
||||||
// to find node/pnpm.
|
// to find node/pnpm.
|
||||||
fixPath();
|
fixPath();
|
||||||
@@ -50,8 +54,13 @@ async function executeApp({
|
|||||||
appId: number;
|
appId: number;
|
||||||
event: Electron.IpcMainInvokeEvent;
|
event: Electron.IpcMainInvokeEvent;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
|
if (proxyWorker) {
|
||||||
|
proxyWorker.terminate();
|
||||||
|
proxyWorker = null;
|
||||||
|
}
|
||||||
await executeAppLocalNode({ appPath, appId, event });
|
await executeAppLocalNode({ appPath, appId, event });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeAppLocalNode({
|
async function executeAppLocalNode({
|
||||||
appPath,
|
appPath,
|
||||||
appId,
|
appId,
|
||||||
@@ -90,14 +99,27 @@ async function executeAppLocalNode({
|
|||||||
runningApps.set(appId, { process, processId: currentProcessId });
|
runningApps.set(appId, { process, processId: currentProcessId });
|
||||||
|
|
||||||
// Log output
|
// Log output
|
||||||
process.stdout?.on("data", (data) => {
|
process.stdout?.on("data", async (data) => {
|
||||||
const message = util.stripVTControlCharacters(data.toString());
|
const message = util.stripVTControlCharacters(data.toString());
|
||||||
logger.debug(`App ${appId} (PID: ${process.pid}) stdout: ${message}`);
|
logger.debug(`App ${appId} (PID: ${process.pid}) stdout: ${message}`);
|
||||||
|
|
||||||
event.sender.send("app:output", {
|
event.sender.send("app:output", {
|
||||||
type: "stdout",
|
type: "stdout",
|
||||||
message,
|
message,
|
||||||
appId,
|
appId,
|
||||||
});
|
});
|
||||||
|
const urlMatch = message.match(/(https?:\/\/localhost:\d+\/?)/);
|
||||||
|
if (urlMatch) {
|
||||||
|
proxyWorker = await startProxy(urlMatch[1], {
|
||||||
|
onStarted: (proxyUrl) => {
|
||||||
|
event.sender.send("app:output", {
|
||||||
|
type: "stdout",
|
||||||
|
message: `[dyad-proxy-server]started=[${proxyUrl}] original=[${urlMatch[1]}]`,
|
||||||
|
appId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stderr?.on("data", (data) => {
|
process.stderr?.on("data", (data) => {
|
||||||
|
|||||||
48
src/ipc/utils/port_utils.ts
Normal file
48
src/ipc/utils/port_utils.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import net from "net";
|
||||||
|
|
||||||
|
export function findAvailablePort(
|
||||||
|
minPort: number,
|
||||||
|
maxPort: number,
|
||||||
|
): Promise<number> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 3;
|
||||||
|
|
||||||
|
function tryPort() {
|
||||||
|
if (attempts >= maxAttempts) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Failed to find an available port after ${maxAttempts} attempts.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
const port =
|
||||||
|
Math.floor(Math.random() * (maxPort - minPort + 1)) + minPort;
|
||||||
|
const server = net.createServer();
|
||||||
|
|
||||||
|
server.once("error", (err: any) => {
|
||||||
|
if (err.code === "EADDRINUSE") {
|
||||||
|
// Port is in use, try another one
|
||||||
|
console.log(`Port ${port} is in use, trying another...`);
|
||||||
|
server.close(() => tryPort());
|
||||||
|
} else {
|
||||||
|
// Other error
|
||||||
|
server.close(() => reject(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.once("listening", () => {
|
||||||
|
server.close(() => {
|
||||||
|
resolve(port);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, "localhost");
|
||||||
|
}
|
||||||
|
|
||||||
|
tryPort();
|
||||||
|
});
|
||||||
|
}
|
||||||
55
src/ipc/utils/start_proxy_server.ts
Normal file
55
src/ipc/utils/start_proxy_server.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// startProxy.js – helper to launch proxy.js as a worker
|
||||||
|
|
||||||
|
import { Worker } from "worker_threads";
|
||||||
|
import path from "path";
|
||||||
|
import { findAvailablePort } from "./port_utils";
|
||||||
|
import log from "electron-log";
|
||||||
|
|
||||||
|
const logger = log.scope("start_proxy_server");
|
||||||
|
|
||||||
|
export async function startProxy(
|
||||||
|
targetOrigin: string,
|
||||||
|
opts: {
|
||||||
|
// host?: string;
|
||||||
|
// port?: number;
|
||||||
|
// env?: Record<string, string>;
|
||||||
|
onStarted?: (proxyUrl: string) => void;
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
if (!/^https?:\/\//.test(targetOrigin))
|
||||||
|
throw new Error("startProxy: targetOrigin must be absolute http/https URL");
|
||||||
|
const port = await findAvailablePort(50_000, 60_000);
|
||||||
|
logger.info("Found available port", port);
|
||||||
|
const {
|
||||||
|
// host = "localhost",
|
||||||
|
// env = {}, // additional env vars to pass to the worker
|
||||||
|
onStarted,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
const worker = new Worker(
|
||||||
|
path.resolve(__dirname, "..", "..", "worker", "proxy_server.js"),
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
...process.env, // inherit parent env
|
||||||
|
|
||||||
|
TARGET_URL: targetOrigin,
|
||||||
|
},
|
||||||
|
workerData: {
|
||||||
|
targetOrigin,
|
||||||
|
port,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
worker.on("message", (m) => {
|
||||||
|
logger.info("[proxy]", m);
|
||||||
|
if (typeof m === "string" && m.startsWith("proxy-server-start url=")) {
|
||||||
|
const url = m.substring("proxy-server-start url=".length);
|
||||||
|
onStarted?.(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
worker.on("error", (e) => logger.error("[proxy] error:", e));
|
||||||
|
worker.on("exit", (c) => logger.info("[proxy] exit", c));
|
||||||
|
|
||||||
|
return worker; // let the caller keep a handle if desired
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a cute app name like "blue-fox" or "jumping-zebra"
|
* Generates a cute app name.
|
||||||
*/
|
*/
|
||||||
export function generateCuteAppName(): string {
|
export function generateCuteAppName(): string {
|
||||||
const adjectives = [
|
const adjectives = [
|
||||||
@@ -144,9 +144,64 @@ export function generateCuteAppName(): string {
|
|||||||
"kraken",
|
"kraken",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const verbs = [
|
||||||
|
"run",
|
||||||
|
"hop",
|
||||||
|
"dash",
|
||||||
|
"zoom",
|
||||||
|
"skip",
|
||||||
|
"jump",
|
||||||
|
"glow",
|
||||||
|
"play",
|
||||||
|
"chirp",
|
||||||
|
"buzz",
|
||||||
|
"flip",
|
||||||
|
"flit",
|
||||||
|
"soar",
|
||||||
|
"dive",
|
||||||
|
"swim",
|
||||||
|
"climb",
|
||||||
|
"sprint",
|
||||||
|
"wiggle",
|
||||||
|
"twirl",
|
||||||
|
"pounce",
|
||||||
|
"bop",
|
||||||
|
"spin",
|
||||||
|
"hum",
|
||||||
|
"roll",
|
||||||
|
"blink",
|
||||||
|
"skid",
|
||||||
|
"kick",
|
||||||
|
"drift",
|
||||||
|
"bloom",
|
||||||
|
"burst",
|
||||||
|
"slide",
|
||||||
|
"bounce",
|
||||||
|
"crawl",
|
||||||
|
"sniff",
|
||||||
|
"peek",
|
||||||
|
"scurry",
|
||||||
|
"nudge",
|
||||||
|
"snap",
|
||||||
|
"swoop",
|
||||||
|
"roam",
|
||||||
|
"trot",
|
||||||
|
"dart",
|
||||||
|
"yawn",
|
||||||
|
"snore",
|
||||||
|
"hug",
|
||||||
|
"nap",
|
||||||
|
"chase",
|
||||||
|
"rest",
|
||||||
|
"wag",
|
||||||
|
"bob",
|
||||||
|
"beam",
|
||||||
|
"cheer",
|
||||||
|
];
|
||||||
|
|
||||||
const randomAdjective =
|
const randomAdjective =
|
||||||
adjectives[Math.floor(Math.random() * adjectives.length)];
|
adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||||
const randomAnimal = animals[Math.floor(Math.random() * animals.length)];
|
const randomAnimal = animals[Math.floor(Math.random() * animals.length)];
|
||||||
|
const randomVerb = verbs[Math.floor(Math.random() * verbs.length)];
|
||||||
return `${randomAdjective}-${randomAnimal}`;
|
return `${randomAdjective}-${randomAnimal}-${randomVerb}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
(function () {
|
(function () {
|
||||||
|
console.debug("dyad-shim.js loaded via proxy v0.6.0");
|
||||||
const isInsideIframe = window.parent !== window;
|
const isInsideIframe = window.parent !== window;
|
||||||
if (!isInsideIframe) return;
|
if (!isInsideIframe) return;
|
||||||
|
|
||||||
281
worker/proxy_server.js
Normal file
281
worker/proxy_server.js
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
/**
|
||||||
|
* proxy.js – zero-dependency worker-based HTTP/WS forwarder
|
||||||
|
*/
|
||||||
|
|
||||||
|
const {
|
||||||
|
Worker,
|
||||||
|
isMainThread,
|
||||||
|
parentPort,
|
||||||
|
workerData,
|
||||||
|
} = require("worker_threads");
|
||||||
|
|
||||||
|
const http = require("http");
|
||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
const { URL } = require("url");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
/* ─────────────────── configuration (main thread only) ─────────────────── */
|
||||||
|
|
||||||
|
const LISTEN_HOST = "localhost";
|
||||||
|
|
||||||
|
if (isMainThread) {
|
||||||
|
// Stand-alone mode: fork the worker and pass through the env as-is
|
||||||
|
const w = new Worker(__filename, {
|
||||||
|
workerData: {
|
||||||
|
targetOrigin: process.env.TARGET_URL, // may be undefined
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
w.on("message", (m) => console.log("[proxy-worker]", m));
|
||||||
|
w.on("error", (e) => console.error("[proxy-worker] error:", e));
|
||||||
|
w.on("exit", (c) => console.log("[proxy-worker] exited", c));
|
||||||
|
console.log("proxy worker launching …");
|
||||||
|
return; // do not execute the rest of the file in the main thread
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────── worker code ─────────────────────────────── */
|
||||||
|
|
||||||
|
const LISTEN_PORT = process.env.LISTEN_PORT || workerData.port;
|
||||||
|
let rememberedOrigin = null; // e.g. "http://localhost:5173"
|
||||||
|
|
||||||
|
/* ---------- pre-configure rememberedOrigin from env or workerData ------- */
|
||||||
|
{
|
||||||
|
const fixed = process.env.TARGET_URL || workerData?.targetOrigin;
|
||||||
|
if (fixed) {
|
||||||
|
try {
|
||||||
|
rememberedOrigin = new URL(fixed).origin;
|
||||||
|
parentPort?.postMessage(
|
||||||
|
`[proxy-worker] fixed upstream: ${rememberedOrigin}`,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid TARGET_URL "${fixed}". Must be absolute http/https URL.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- optional resources for HTML injection ---------------------- */
|
||||||
|
|
||||||
|
let stacktraceJsContent = null;
|
||||||
|
let dyadShimContent = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stackTraceLibPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"node_modules",
|
||||||
|
"stacktrace-js",
|
||||||
|
"dist",
|
||||||
|
"stacktrace.min.js",
|
||||||
|
);
|
||||||
|
stacktraceJsContent = fs.readFileSync(stackTraceLibPath, "utf-8");
|
||||||
|
parentPort?.postMessage("[proxy-worker] stacktrace.js loaded.");
|
||||||
|
} catch (error) {
|
||||||
|
parentPort?.postMessage(
|
||||||
|
`[proxy-worker] Failed to read stacktrace.js: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dyadShimPath = path.join(__dirname, "dyad-shim.js");
|
||||||
|
dyadShimContent = fs.readFileSync(dyadShimPath, "utf-8");
|
||||||
|
parentPort?.postMessage("[proxy-worker] dyad-shim.js loaded.");
|
||||||
|
} catch (error) {
|
||||||
|
parentPort?.postMessage(
|
||||||
|
`[proxy-worker] Failed to read dyad-shim.js: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------- helper: need to inject? ------------------------ */
|
||||||
|
function needsInjection(pathname) {
|
||||||
|
return pathname.endsWith("index.html") || pathname === "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectHTML(buf) {
|
||||||
|
let txt = buf.toString("utf8");
|
||||||
|
// These are strings that were used since the first version of the dyad shim.
|
||||||
|
// If the dyad shim is used from legacy apps which came pre-baked with the shim
|
||||||
|
// as a vite plugin, then do not inject the shim twice to avoid weird behaviors.
|
||||||
|
if (txt.includes("window-error") && txt.includes("unhandled-rejection")) {
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scripts = [];
|
||||||
|
|
||||||
|
if (stacktraceJsContent)
|
||||||
|
scripts.push(`<script>${stacktraceJsContent}</script>`);
|
||||||
|
else
|
||||||
|
scripts.push(
|
||||||
|
'<script>console.warn("[proxy-worker] stacktrace.js was not injected.");</script>',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dyadShimContent) scripts.push(`<script>${dyadShimContent}</script>`);
|
||||||
|
else
|
||||||
|
scripts.push(
|
||||||
|
'<script>console.warn("[proxy-worker] dyad shim was not injected.");</script>',
|
||||||
|
);
|
||||||
|
|
||||||
|
const allScripts = scripts.join("\n");
|
||||||
|
|
||||||
|
const headRegex = /<head[^>]*>/i;
|
||||||
|
if (headRegex.test(txt)) {
|
||||||
|
txt = txt.replace(headRegex, `$&\n${allScripts}`);
|
||||||
|
} else {
|
||||||
|
txt = allScripts + "\n" + txt;
|
||||||
|
parentPort?.postMessage(
|
||||||
|
"[proxy-worker] Warning: <head> tag not found – scripts prepended.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Buffer.from(txt, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- helper: build upstream URL from request -------------- */
|
||||||
|
function buildTargetURL(clientReq) {
|
||||||
|
// Support the old "?url=" mechanism
|
||||||
|
const parsedLocal = new URL(clientReq.url, `http://${LISTEN_HOST}`);
|
||||||
|
const urlParam = parsedLocal.searchParams.get("url");
|
||||||
|
if (urlParam) {
|
||||||
|
const abs = new URL(urlParam);
|
||||||
|
if (!/^https?:$/.test(abs.protocol))
|
||||||
|
throw new Error("only http/https targets allowed");
|
||||||
|
rememberedOrigin = abs.origin; // remember for later
|
||||||
|
return abs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rememberedOrigin)
|
||||||
|
throw new Error(
|
||||||
|
"No upstream configured. Use ?url=… once or set TARGET_URL env var.",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Forward to the remembered origin keeping path & query
|
||||||
|
return new URL(clientReq.url, rememberedOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------- */
|
||||||
|
/* 1. Plain HTTP request / response */
|
||||||
|
/* ----------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const server = http.createServer((clientReq, clientRes) => {
|
||||||
|
let target;
|
||||||
|
try {
|
||||||
|
target = buildTargetURL(clientReq);
|
||||||
|
} catch (err) {
|
||||||
|
clientRes.writeHead(400, { "content-type": "text/plain" });
|
||||||
|
return void clientRes.end("Bad request: " + err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTLS = target.protocol === "https:";
|
||||||
|
const lib = isTLS ? https : http;
|
||||||
|
|
||||||
|
/* Copy request headers but rewrite Host / Origin / Referer */
|
||||||
|
const headers = { ...clientReq.headers, host: target.host };
|
||||||
|
if (headers.origin) headers.origin = target.origin;
|
||||||
|
if (headers.referer) {
|
||||||
|
try {
|
||||||
|
const ref = new URL(headers.referer);
|
||||||
|
headers.referer = target.origin + ref.pathname + ref.search;
|
||||||
|
} catch {
|
||||||
|
delete headers.referer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers["if-none-match"] && needsInjection(target.pathname))
|
||||||
|
delete headers["if-none-match"];
|
||||||
|
|
||||||
|
const upOpts = {
|
||||||
|
protocol: target.protocol,
|
||||||
|
hostname: target.hostname,
|
||||||
|
port: target.port || (isTLS ? 443 : 80),
|
||||||
|
path: target.pathname + target.search,
|
||||||
|
method: clientReq.method,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const upReq = lib.request(upOpts, (upRes) => {
|
||||||
|
const inject = needsInjection(target.pathname);
|
||||||
|
|
||||||
|
if (!inject) {
|
||||||
|
clientRes.writeHead(upRes.statusCode, upRes.headers);
|
||||||
|
return void upRes.pipe(clientRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
upRes.on("data", (c) => chunks.push(c));
|
||||||
|
upRes.on("end", () => {
|
||||||
|
try {
|
||||||
|
const merged = Buffer.concat(chunks);
|
||||||
|
const patched = injectHTML(merged);
|
||||||
|
|
||||||
|
const hdrs = {
|
||||||
|
...upRes.headers,
|
||||||
|
"content-length": Buffer.byteLength(patched),
|
||||||
|
};
|
||||||
|
clientRes.writeHead(upRes.statusCode, hdrs);
|
||||||
|
clientRes.end(patched);
|
||||||
|
} catch (e) {
|
||||||
|
clientRes.writeHead(500, { "content-type": "text/plain" });
|
||||||
|
clientRes.end("Injection failed: " + e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
clientReq.pipe(upReq);
|
||||||
|
upReq.on("error", (e) => {
|
||||||
|
clientRes.writeHead(502, { "content-type": "text/plain" });
|
||||||
|
clientRes.end("Upstream error: " + e.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------- */
|
||||||
|
/* 2. WebSocket / generic Upgrade tunnelling */
|
||||||
|
/* ----------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
server.on("upgrade", (req, socket, _head) => {
|
||||||
|
let target;
|
||||||
|
try {
|
||||||
|
target = buildTargetURL(req);
|
||||||
|
} catch (err) {
|
||||||
|
socket.write("HTTP/1.1 400 Bad Request\r\n\r\n" + err.message);
|
||||||
|
return socket.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTLS = target.protocol === "https:";
|
||||||
|
const headers = { ...req.headers, host: target.host };
|
||||||
|
if (headers.origin) headers.origin = target.origin;
|
||||||
|
|
||||||
|
const upReq = (isTLS ? https : http).request({
|
||||||
|
protocol: target.protocol,
|
||||||
|
hostname: target.hostname,
|
||||||
|
port: target.port || (isTLS ? 443 : 80),
|
||||||
|
path: target.pathname + target.search,
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
upReq.on("upgrade", (upRes, upSocket, upHead) => {
|
||||||
|
socket.write(
|
||||||
|
"HTTP/1.1 101 Switching Protocols\r\n" +
|
||||||
|
Object.entries(upRes.headers)
|
||||||
|
.map(([k, v]) => `${k}: ${v}`)
|
||||||
|
.join("\r\n") +
|
||||||
|
"\r\n\r\n",
|
||||||
|
);
|
||||||
|
if (upHead && upHead.length) socket.write(upHead);
|
||||||
|
|
||||||
|
upSocket.pipe(socket).pipe(upSocket);
|
||||||
|
});
|
||||||
|
|
||||||
|
upReq.on("error", () => socket.destroy());
|
||||||
|
upReq.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
server.listen(LISTEN_PORT, LISTEN_HOST, () => {
|
||||||
|
parentPort?.postMessage(
|
||||||
|
`proxy-server-start url=http://${LISTEN_HOST}:${LISTEN_PORT}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user