diff --git a/.gitignore b/.gitignore index 04293f5..becf13f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ node_modules temp .vitepress/dist .vitepress/cache +storage +rp diff --git a/.vscode/dictionary.txt b/.vscode/dictionary.txt index 30013fc..6e48230 100644 --- a/.vscode/dictionary.txt +++ b/.vscode/dictionary.txt @@ -20,6 +20,7 @@ outdir pausable pkgx Postcardware +postcompile prefetch preinstall socio diff --git a/bin/cli.ts b/bin/cli.ts index fb8ed56..7899702 100644 --- a/bin/cli.ts +++ b/bin/cli.ts @@ -13,6 +13,7 @@ interface Options { to?: string keyPath?: string certPath?: string + verbose?: boolean } cli @@ -21,6 +22,7 @@ cli .option('--to ', 'The URL to proxy to') .option('--keyPath ', 'Absolute path to the SSL key') .option('--certPath ', 'Absolute path to the SSL certificate') + .option('--verbose', 'Enable verbose logging', { default: false }) .example('reverse-proxy start --from localhost:3000 --to my-project.localhost') .example('reverse-proxy start --from localhost:3000 --to localhost:3001') .example('reverse-proxy start --from localhost:3000 --to my-project.test --keyPath /absolute/path/to/key --certPath /absolute/path/to/cert') @@ -59,33 +61,38 @@ cli .example('sudo reverse-proxy update:etc-hosts') .example('sudo reverse-proxy update-etc-hosts') .action(async () => { - log.info('Ensuring /etc/hosts file covers the proxy domains...') + log.info('Ensuring /etc/hosts file covers the proxy domain/s...') + const hostsFilePath = os.platform() === 'win32' ? 'C:\\Windows\\System32\\drivers\\etc\\hosts' : '/etc/hosts' if (config && typeof config === 'object') { const entriesToAdd = Object.entries(config).map(([from, to]) => `127.0.0.1 ${to} # reverse-proxy mapping for ${from}`) + // Ensure "127.0.0.1 localhost" is in the array + entriesToAdd.push('127.0.0.1 localhost # essential localhost mapping') + try { let currentHostsContent = readFileSync(hostsFilePath, 'utf8') let updated = false for (const entry of entriesToAdd) { - const to = entry.split(' ')[1] + const [ip, host] = entry.split(' ', 2) + // Use a regex to match the line with any amount of whitespace between IP and host + const regex = new RegExp(`^${ip}\\s+${host.split(' ')[0]}(\\s|$)`, 'm') // Check if the entry (domain) is already in the file - if (!currentHostsContent.includes(to)) { - // If not, append it + if (!regex.test(currentHostsContent)) { + // If not, append it currentHostsContent += `\n${entry}` updated = true } else { - log.info(`Entry for ${to} already exists in the hosts file.`) + log.info(`Entry for ${host} already exists in the hosts file.`) } } if (updated) { writeFileSync(hostsFilePath, currentHostsContent, 'utf8') - log.success('Hosts file updated with latest proxy domains.') } else { diff --git a/bun.lockb b/bun.lockb index 31a9f25..a7caada 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 21b2a55..5788a9f 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -2,13 +2,13 @@ import { defineConfig } from 'vitepress' // https://vitepress.dev/reference/site-config export default defineConfig({ - title: "Reverse Proxy", - description: "A better developer environment.", + title: 'Reverse Proxy', + description: 'A better developer environment.', themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Home', link: '/' }, - { text: 'Examples', link: '/markdown-examples' } + { text: 'Examples', link: '/markdown-examples' }, ], sidebar: [ @@ -16,13 +16,13 @@ export default defineConfig({ text: 'Examples', items: [ { text: 'Markdown Examples', link: '/markdown-examples' }, - { text: 'Runtime API Examples', link: '/api-examples' } - ] - } + { text: 'Runtime API Examples', link: '/api-examples' }, + ], + }, ], socialLinks: [ - { icon: 'github', link: 'https://github.com/vuejs/vitepress' } - ] - } + { icon: 'github', link: 'https://github.com/vuejs/vitepress' }, + ], + }, }) diff --git a/docs/index.md b/docs/index.md index 1503d93..c7a7353 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,4 +22,3 @@ features: - title: Feature C details: Lorem ipsum dolor sit amet, consectetur adipiscing elit --- - diff --git a/package.json b/package.json index e43117a..19b65b9 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,9 @@ "src" ], "scripts": { - "build": "bun build.ts && bun run compile", - "compile": "bun build ./bin/cli.ts --compile --minify --sourcemap --outfile dist/reverse-proxy && mv ./bin/reverse-proxy ./dist/reverse-proxy", + "build": "bun scripts/build.ts && bun run compile", + "compile": "bun build ./bin/cli.ts --compile --external rollup --minify --sourcemap --outfile dist/reverse-proxy", + "postcompile": "bun ./scripts/post-compile.ts", "lint": "eslint .", "lint:fix": "bunx eslint . --fix", "fresh": "bunx rimraf node_modules/ bun.lock && bun i", @@ -61,12 +62,12 @@ "docs:preview": "vitepress preview docs" }, "dependencies": { - "@stacksjs/cli": "^0.59.9", - "@stacksjs/storage": "^0.59.9", + "@stacksjs/cli": "^0.59.11", + "@stacksjs/storage": "^0.59.11", "c12": "^1.9.0" }, "devDependencies": { - "@stacksjs/development": "^0.59.9", + "@stacksjs/development": "^0.59.11", "@types/bun": "^1.0.8", "@types/node": "^20.11.24", "bun-plugin-dts-auto": "^0.10.0", diff --git a/build.ts b/scripts/build.ts similarity index 65% rename from build.ts rename to scripts/build.ts index 60305a7..b37bdc3 100644 --- a/build.ts +++ b/scripts/build.ts @@ -1,17 +1,23 @@ +import path from 'node:path' import { $ } from 'bun' import dts from 'bun-plugin-dts-auto' +import { log } from '@stacksjs/logging' -console.log('Building...') +log.info('Building...') + +$.cwd(path.resolve(import.meta.dir, '..')) +await $`rm -rf ./dist` await Bun.build({ entrypoints: ['./src/index.ts', './bin/cli.ts'], outdir: './dist', format: 'esm', target: 'bun', + external: ['rollup', 'fsevents'], plugins: [ dts({ - cwd: import.meta.dir, + cwd: path.resolve(import.meta.dir, '..'), }), ], }) @@ -22,7 +28,5 @@ await $`cp ./dist/bin/cli.js ./dist/cli.js` await $`rm -rf ./dist/bin` await $`cp ./bin/cli.d.ts ./dist/cli.d.ts` // while bun-plugin-dts-auto doesn't support bin files well await $`rm ./bin/cli.d.ts` -await $`cp ./bin/reverse-proxy ./dist/reverse-proxy` -await $`rm ./bin/reverse-proxy` -console.log('Build done!') +log.success('Built') diff --git a/scripts/post-compile.ts b/scripts/post-compile.ts new file mode 100644 index 0000000..c38c258 --- /dev/null +++ b/scripts/post-compile.ts @@ -0,0 +1,4 @@ +import { $ } from 'bun' + +await $`mv ./bin/reverse-proxy ./dist/reverse-proxy` +await $`cp ./dist/reverse-proxy ./rp` diff --git a/src/start.ts b/src/start.ts index 1d84e73..618004d 100644 --- a/src/start.ts +++ b/src/start.ts @@ -2,13 +2,12 @@ import * as net from 'node:net' import * as http from 'node:http' import * as https from 'node:https' import * as fs from 'node:fs' -import process from 'node:process' import type { Buffer } from 'node:buffer' -import path from 'node:path' -import { bold, green, log, runCommand } from '@stacksjs/cli' +import { path } from '@stacksjs/path' +import { bold, dim, green, log, runCommand } from '@stacksjs/cli' import { version } from '../package.json' -interface Option { +export interface Option { from?: string // domain to proxy from, defaults to localhost:3000 to?: string // domain to proxy to, defaults to stacks.localhost keyPath?: string // absolute path to the key @@ -21,38 +20,8 @@ type Options = Option | Option[] export async function startServer(option: Option = { from: 'localhost:3000', to: 'stacks.localhost' }): Promise { log.debug('Starting Reverse Proxy Server') - let key: Buffer | undefined - let cert: Buffer | undefined - - const keyPath = option.keyPath ?? path.resolve(import.meta.dir, `keys/localhost-key.pem`) - if (fs.existsSync(keyPath)) - key = fs.readFileSync(keyPath) - else - log.debug('No SSL key found') - - const certPath = option.certPath ?? path.resolve(import.meta.dir, `keys/localhost.pem`) - if (fs.existsSync(certPath)) - cert = fs.readFileSync(certPath) - else - log.debug('No SSL certificate found') - - if (!fs.existsSync(keyPath) || fs.existsSync(certPath)) { - log.info('A valid SSL key & certificate was not found') - log.info('Creating a self-signed certificate...') - - // self-sign a certificate using mkcert - const keysPath = path.resolve(import.meta.dir, 'keys') - if (!fs.existsSync(keysPath)) - fs.mkdirSync(keysPath) - - await runCommand('mkcert -install', { - cwd: keysPath, - }) - - await runCommand(`mkcert *.${option.to}`, { - cwd: keysPath, - }) - } + // Ensure the SSL key and certificate exist + const { key, cert } = await ensureCertificates(option) // Parse the option.from URL to dynamically set hostname and port const fromUrl = new URL(option.from ? (option.from.startsWith('http') ? option.from : `http://${option.from}`) : 'http://localhost:3000') @@ -74,7 +43,7 @@ export async function startServer(option: Option = { from: 'localhost:3000', to: }) } -function setupReverseProxy({ key, cert, hostname, port, option }: { key?: Buffer, cert?: Buffer, hostname: string, port: number, option: Option }): void { +export function setupReverseProxy({ key, cert, hostname, port, option }: { key?: Buffer, cert?: Buffer, hostname: string, port: number, option: Option }): void { // This server will act as a reverse proxy const httpsServer = https.createServer({ key, cert }, (req, res) => { // Define the target server's options @@ -113,7 +82,7 @@ function setupReverseProxy({ key, cert, hostname, port, option }: { key?: Buffer // eslint-disable-next-line no-console console.log('') // eslint-disable-next-line no-console - console.log(` ${green('➜')} ${option.from} ➜ ${option.to}`) + console.log(` ${green('➜')} ${dim(option.from!)} ${dim('➜')} https://${option.to}`) }) // http to https redirect @@ -121,7 +90,7 @@ function setupReverseProxy({ key, cert, hostname, port, option }: { key?: Buffer startHttpRedirectServer() } -function startHttpRedirectServer(): void { +export function startHttpRedirectServer(): void { http.createServer((req, res) => { res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` }) res.end() @@ -142,3 +111,29 @@ export function startProxies(options?: Options): void { startServer(options) } } + +export async function ensureCertificates(option: Option): Promise<{ key: Buffer, cert: Buffer }> { + const sslBasePath = path.homeDir('.stacks/ssl') + const keysPath = path.resolve(sslBasePath, 'keys') + await fs.promises.mkdir(keysPath, { recursive: true }) + + const keyPath = option.keyPath ?? path.resolve(keysPath, `${option.to}-key.pem`) + const certPath = option.certPath ?? path.resolve(keysPath, `${option.to}.pem`) + + let key: Buffer | undefined + let cert: Buffer | undefined + + try { + key = await fs.promises.readFile(keyPath) + cert = await fs.promises.readFile(certPath) + } + catch (error) { + log.info('A valid SSL key & certificate was not found, creating a self-signed certificate...') + await runCommand('mkcert -install', { cwd: keysPath }) + await runCommand(`mkcert ${option.to}`, { cwd: keysPath }) + key = await fs.promises.readFile(keyPath) + cert = await fs.promises.readFile(certPath) + } + + return { key, cert } +} diff --git a/test/index.test.ts b/test/index.test.ts index 8c8c8a2..5bbfbd1 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,4 +1,3 @@ -// @ts-expect-error types are somehow missing import { describe, expect, it } from 'bun:test' describe('should', () => { diff --git a/tsconfig.json b/tsconfig.json index 58efc4a..d9a0183 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,9 @@ "strict": true, "strictNullChecks": true, "noFallthroughCasesInSwitch": true, + "declaration": true, "noEmit": true, + "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "verbatimModuleSyntax": true,