publichost
Version:
Make your localhost public. Tunnel HTTP and WebSocket traffic to your local machine from any domain name.
237 lines (231 loc) • 8.5 kB
JavaScript
// src/constants.ts
var DEFAULT_CLIENT_PORT = "3000";
var CONFIG_FILE_NAME = process.env.IS_LOCAL_SERVER === "true" || process.env.IS_TEST === "true" ? ".publichost.dev.json" : ".publichost.json";
var INITIAL_CONFIG = {
subdomains: {}
};
var DEFAULT_START_OPTIONS = {
isHttps: false,
localhostAppPort: DEFAULT_CLIENT_PORT
};
// src/libs/ConfigManager.ts
import { homedir } from "os";
import { join } from "path";
import { pathExistsSync, readJSONSync, writeJSONSync } from "fs-extra/esm";
var ConfigManager = class {
configPath;
constructor() {
this.configPath = join(homedir(), CONFIG_FILE_NAME);
}
getWorkspaceConfig(workspacePath) {
const config = this.#load();
const configEntry = Object.entries(config.subdomains).find(
([, workspaceConfig]) => workspaceConfig.workspacePath === workspacePath
);
if (configEntry) {
return configEntry[1];
}
if (workspacePath.includes("/")) {
const parentDirectory = workspacePath.split("/").slice(0, -1).join("/");
return this.getWorkspaceConfig(parentDirectory);
}
return void 0;
}
setWorkspaceConfig(newOrNextWorkspaceConfig) {
const config = { ...this.#load() };
config.subdomains[newOrNextWorkspaceConfig.subdomain] = newOrNextWorkspaceConfig;
this.#save(config);
}
#load() {
if (!pathExistsSync(this.configPath)) {
writeJSONSync(this.configPath, INITIAL_CONFIG, { spaces: 2 });
}
return readJSONSync(this.configPath);
}
#save(nextConfig) {
writeJSONSync(this.configPath, nextConfig, { spaces: 2 });
}
};
var configManager = new ConfigManager();
// src/commands/start.ts
import axios, { AxiosError } from "axios";
import { B } from "bhala";
// ../common/src/ClientMessage.types.ts
var ClientMessage;
((ClientMessage2) => {
let Type;
((Type2) => {
Type2["ERROR"] = "ERROR";
Type2["REGISTER"] = "REGISTER";
Type2["RESPONSE"] = "RESPONSE";
})(Type = ClientMessage2.Type || (ClientMessage2.Type = {}));
})(ClientMessage || (ClientMessage = {}));
// ../common/src/ServerMessage.types.ts
var ServerMessage;
((ServerMessage2) => {
let Type;
((Type2) => {
Type2["ERROR"] = "ERROR";
Type2["REGISTERED"] = "REGISTERED";
Type2["REQUEST"] = "REQUEST";
})(Type = ServerMessage2.Type || (ServerMessage2.Type = {}));
})(ServerMessage || (ServerMessage = {}));
// src/commands/start.ts
import { WebSocket } from "ws";
function start(publicHostServerHost, subdomain, apiKey, options) {
const controlledOptions = { ...DEFAULT_START_OPTIONS, ...options };
const { isHttps, localhostAppPort } = controlledOptions;
const localhostAppBaseUrl = process.env.IS_TEST === "true" ? "https://jsonplaceholder.typicode.com" : `http${isHttps ? "s" : ""}://localhost:${localhostAppPort}`;
const webSocketScheme = process.env.IS_LOCAL_SERVER === "true" || process.env.IS_TEST === "true" ? "ws" : "wss";
const webSocketUrl = `${webSocketScheme}://${publicHostServerHost}/${subdomain}`;
const ws = new WebSocket(webSocketUrl, {
headers: {
"x-api-key": apiKey
}
});
ws.on("open", () => {
B.log(
"[PublicHost Client]",
`[${subdomain}]`,
`Connected to PublicHost Server on ${webSocketUrl}.`,
"Registering subdomain..."
);
ws.send(JSON.stringify({ type: ClientMessage.Type.REGISTER, subdomain }));
});
ws.on("ping", () => {
ws.pong(void 0, void 0, () => {
});
});
ws.on("message", async (data) => {
try {
const serverMessage = JSON.parse(data);
switch (serverMessage.type) {
case ServerMessage.Type.REGISTERED: {
B.success("[PublicHost Client]", `[${subdomain}]`, "Subdomain registered.");
B.info(
"[PublicHost Client]",
`[${subdomain}]`,
`You can now access your localhost app at https://${subdomain}.${publicHostServerHost}.`
);
return;
}
case ServerMessage.Type.ERROR: {
B.error("[PublicHost Client]", `[${subdomain}]`, `PublicHost Server sent an error: ${serverMessage.error}.`);
return;
}
case ServerMessage.Type.REQUEST:
break;
default: {
B.error("[PublicHost Client]", `[${subdomain}]`, `Invalid message type: ${serverMessage.type}.`);
return;
}
}
const serverRequestMessage = serverMessage;
B.log(
"[PublicHost Client]",
`[${subdomain}]`,
`[${serverRequestMessage.id}]`,
`\u27A1\uFE0F Forwarding HTTP Request ${serverMessage.request.method} ${serverMessage.request.url} to Localhost App.`
);
const cleanHeaders = Object.fromEntries(
Object.entries(serverRequestMessage.request.headers).filter(([key]) => !["host"].includes(key))
);
try {
const localhostAppResponse = await axios({
method: serverRequestMessage.request.method,
url: `${localhostAppBaseUrl}${serverRequestMessage.request.url}`,
headers: cleanHeaders,
data: serverRequestMessage.request.rawBody
});
B.log(
"[PublicHost Client]",
`[${subdomain}]`,
`[${serverRequestMessage.id}]`,
`\u2B05\uFE0F Forwarding HTTP Response ${localhostAppResponse.status} for ${serverMessage.request.method} ${serverMessage.request.url} to PublicHost Server.`
);
const clientResponseMessage = {
id: serverRequestMessage.id,
type: ClientMessage.Type.RESPONSE,
response: {
status: localhostAppResponse.status,
headers: localhostAppResponse.headers,
rawBody: localhostAppResponse.data
}
};
ws.send(JSON.stringify(clientResponseMessage));
} catch (err) {
if (!(err instanceof AxiosError && err.response)) {
B.error("[PublicHost Client]", `[${subdomain}]`, `[${serverRequestMessage.id}]`, "An unknown error occurred.");
const clientResponseMessage2 = {
id: serverRequestMessage.id,
type: ClientMessage.Type.RESPONSE,
response: {
status: 500,
headers: {},
rawBody: ""
}
};
ws.send(JSON.stringify(clientResponseMessage2));
return;
}
B.warn(
"[PublicHost Client]",
`[${subdomain}]`,
`[${serverRequestMessage.id}]`,
`\u2B05\uFE0F Forwarding HTTP Response ${err.response.status ?? 500} to PublicHost Server.`
);
B.debug(
"[PublicHost Client]",
`[${subdomain}]`,
`[${serverRequestMessage.id}]`,
`Error reponse body: ${err.response.data ? JSON.stringify(err.response.data) : err}`
);
const clientResponseMessage = {
id: serverRequestMessage.id,
type: ClientMessage.Type.RESPONSE,
response: {
status: err.response.status,
headers: err.response.headers ?? {},
rawBody: err.response.data
}
};
ws.send(JSON.stringify(clientResponseMessage));
}
} catch (err) {
B.error("[PublicHost Client]", `[${subdomain}]`, "An unknown error occurred.");
B.debug("[PublicHost Client]", `[${subdomain}]`, `Error: ${err}.`);
}
});
ws.on("close", () => {
B.log("[PublicHost Client]", `[${subdomain}]`, "Connection closed by PublicHost Server.", "Reconnecting in 5s...");
setTimeout(() => {
B.log("[PublicHost Client]", `[${subdomain}]`, "Reconnecting to PublicHost Server...");
start(publicHostServerHost, subdomain, apiKey, options);
}, 5e3);
});
ws.on("error", (error) => {
B.error("[PublicHost Client]", `[${subdomain}]`, `PublicHost Server connection error: ${error}.`);
});
}
function startFromConfig() {
const workspacePath = process.cwd();
const workspaceConfig = configManager.getWorkspaceConfig(workspacePath);
if (!workspaceConfig) {
B.error(
"[PublicHost Client]",
`No configuration found for the current workspace or any of its parents: \`${workspacePath}\`.`
);
B.info("[PublicHost Client]", "Run `ph init` to initialize a new configuration for this workspace.");
return;
}
const { apiKey, options, publicHostServerHost, subdomain } = workspaceConfig;
start(publicHostServerHost, subdomain, apiKey, options);
}
export {
DEFAULT_CLIENT_PORT,
DEFAULT_START_OPTIONS,
configManager,
start,
startFromConfig
};
//# sourceMappingURL=chunk-7NWSCVNS.js.map