diff --git a/.env.example b/.env.example index faf75bae70..fe10d24a15 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,20 @@ # Veryfront Renderer - Local Development # ====================================== # Copy this file to .env and fill in your credentials. -# Get OAuth credentials from 1Password: "Veryfront OAuth Credentials" -# OAuth Credentials (for proxy mode: deno task proxy) -OAUTH_CLIENT_ID= -OAUTH_CLIENT_SECRET= -OAUTH_PREVIEW_CLIENT_ID= -OAUTH_PREVIEW_CLIENT_SECRET= +# Project Token (for deno task dev) +VERYFRONT_API_TOKEN=your_token_here +VERYFRONT_PROJECT_SLUG=your-project # API Configuration VERYFRONT_API_BASE_URL=https://api.veryfront.com -# Direct Mode (deno task dev) - uncomment to use -# VERYFRONT_API_TOKEN=your_token_here -# VERYFRONT_PROJECT_SLUG=your-project +# Multi-project Mode (deno task dev:multi) - Veryfront staff only +# Get OAuth credentials from 1Password: "Veryfront OAuth Credentials" +# OAUTH_CLIENT_ID= +# OAUTH_CLIENT_SECRET= +# OAUTH_PREVIEW_CLIENT_ID= +# OAUTH_PREVIEW_CLIENT_SECRET= # Logging (debug | info | warn | error) LOG_LEVEL=info diff --git a/CLAUDE.md b/CLAUDE.md index 5e4e381fde..d1ab518034 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,8 +74,8 @@ Client → Renderer → API ### Renderer (Deno) ```bash -deno task dev # Start dev server (HMR) - direct mode -deno task proxy # Start proxy + renderer together (proxy mode) +deno task dev # Start dev server (single project) +deno task dev:multi # Multi-project mode (Veryfront staff) deno task build # Production build deno task test # Run all tests deno task test:unit # Run unit tests only @@ -168,9 +168,9 @@ cp .env.local.example .env.local # 2. Fill in OAuth credentials (from 1Password: "Veryfront OAuth Credentials") -# 3. Run in either mode: -deno task dev # Direct mode (single project) -deno task proxy # Proxy mode (multi-project) +# 3. Run: +deno task dev # Single project mode +deno task dev:multi # Multi-project (Veryfront staff) ``` ### Debug Endpoints @@ -212,7 +212,7 @@ logcli query '{namespace="veryfront-production", container="renderer"} |= "error | File | Purpose | |------|---------| | `.env.local.example` | Template for local development config | -| `scripts/dev-proxy.ts` | Launcher for `deno task proxy` | +| `scripts/dev-proxy.ts` | Launcher for `deno task dev:multi` | | `proxy/main.ts` | Proxy entry point | | `proxy/token-manager.ts` | OAuth token lifecycle | | `src/server/production-server.ts` | Production server | diff --git a/deno.json b/deno.json index f16ea71185..6f3ea736ae 100644 --- a/deno.json +++ b/deno.json @@ -158,7 +158,7 @@ "tasks": { "setup": "deno run --allow-all scripts/setup.ts", "dev": "deno run --allow-all --unstable-net --unstable-worker-options src/cli/main.ts dev", - "proxy": "deno run --allow-all scripts/dev-proxy.ts", + "dev:multi": "deno run --allow-all scripts/dev-proxy.ts", "build": "deno compile --allow-all --output ../../bin/veryfront src/cli/main.ts", "build:npm": "deno run -A scripts/build-npm.ts", "release": "deno run -A scripts/release.ts", diff --git a/scripts/dev-proxy.ts b/scripts/dev-proxy.ts index 12687c33fe..1ce26598f5 100644 --- a/scripts/dev-proxy.ts +++ b/scripts/dev-proxy.ts @@ -80,13 +80,12 @@ await new Promise((r) => setTimeout(r, 2000)); // Clean startup banner console.log(); console.log(c(ANSI.dim, "─".repeat(40))); -console.log(` ${c(ANSI.bold + ANSI.cyan, "veryfront")} ${c(ANSI.dim, "proxy mode")}`); +console.log(` ${c(ANSI.bold + ANSI.cyan, "Veryfront")} ${c(ANSI.dim, "Multi-Project Mode")}`); console.log(c(ANSI.dim, "─".repeat(40))); console.log(); -console.log(` ${c(ANSI.green, "●")} Proxy ${c(ANSI.cyan, `http://localhost:${PROXY_PORT}`)}`); -console.log(` ${c(ANSI.green, "●")} Renderer ${c(ANSI.cyan, `http://localhost:${RENDERER_PORT}`)}`); +console.log(` ${c(ANSI.green, "●")} Open: ${c(ANSI.cyan, `http://{project}.lvh.me:${PROXY_PORT}/`)}`); console.log(); -console.log(` ${c(ANSI.dim, "Open:")} ${c(ANSI.cyan, `http://{project}.veryfront.dev:${PROXY_PORT}/`)}`); +console.log(` ${c(ANSI.dim, "Example:")} ${c(ANSI.cyan, `http://blank.lvh.me:${PROXY_PORT}/`)}`); console.log(); console.log(c(ANSI.dim, ` Press Ctrl+C to stop`)); console.log(); diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 365e5f2632..f018b18377 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -4,14 +4,12 @@ import { compileAllMDX, watchMDX } from "@veryfront/build/compiler/mdx-compiler/index.ts"; import { ErrorCode, VeryfrontError } from "@veryfront/errors/index.ts"; -import { LOCALHOST } from "@veryfront/config"; import { bold, cyan, dim, green } from "@veryfront/compat/console"; import { join } from "@veryfront/platform/compat/path/index.ts"; import { getAdapter } from "@veryfront/platform/adapters/detect.ts"; import { cliLogger } from "@veryfront/utils"; import { getConfig } from "@veryfront/config"; import { createDevServer } from "@veryfront/server/dev-server.ts"; -import { getNetworkInterfaces } from "@veryfront/platform/compat/process.ts"; import { runAIConfigValidation } from "@veryfront/ai/utils/config-validator.ts"; import { discoverAll } from "@veryfront/ai/utils/discovery.ts"; import { exitProcess, registerTerminationSignals } from "../utils/index.ts"; @@ -25,21 +23,6 @@ export interface DevOptions { // Alias for backward compatibility export type DevCommandOptions = DevOptions; -async function getLocalIP(): Promise { - try { - const interfaces = await getNetworkInterfaces(); - for (const iface of interfaces) { - if (iface.family === "IPv4" && !iface.address.startsWith("127.")) { - return iface.address; - } - } - } catch (error) { - // Network interface enumeration may fail due to permissions - cliLogger.debug("Failed to get network interfaces:", error); - } - return LOCALHOST.HOSTNAME; -} - export async function devCommand(options: DevOptions) { const { port, projectDir, hmr = true } = options; @@ -191,20 +174,12 @@ export async function devCommand(options: DevOptions) { // Clean startup banner (skip in proxy mode - dev-proxy.ts prints its own) const isProxyMode = config?.fs?.veryfront?.proxyMode === true; if (!isProxyMode) { - const localIP = await getLocalIP(); - console.log(); console.log(dim("─".repeat(40))); - console.log(` ${bold(cyan("veryfront"))} ${dim("dev")}`); + console.log(` ${bold(cyan("Veryfront"))}`); console.log(dim("─".repeat(40))); console.log(); - console.log(` ${green("●")} Local ${cyan(`http://${LOCALHOST.HOSTNAME}:${finalPort}`)}`); - console.log(` ${green("●")} Network ${cyan(`http://${localIP}:${finalPort}`)}`); - if (enableHMR) { - console.log( - ` ${green("●")} HMR ${dim(`ws://${LOCALHOST.HOSTNAME}:${finalPort + 1}/`)}`, - ); - } + console.log(` ${green("●")} Open: ${cyan(`http://lvh.me:${finalPort}/`)}`); console.log(); console.log(dim(` Press Ctrl+C to stop`)); console.log(); diff --git a/src/cli/utils/index.test.ts b/src/cli/utils/index.test.ts index 6e17803484..339f047c42 100644 --- a/src/cli/utils/index.test.ts +++ b/src/cli/utils/index.test.ts @@ -14,7 +14,9 @@ import { // Strip ANSI escape codes from string function stripAnsi(str: string): string { - return str.replace(/\x1b\[[0-9;]*m/g, ""); + const ESC = String.fromCharCode(27); + const ansiPattern = new RegExp(ESC + "\\[[0-9;]*m", "g"); + return str.replace(ansiPattern, ""); } // Helper to capture console output diff --git a/src/server/utils/domain-parser.ts b/src/server/utils/domain-parser.ts index 2ad6de3c12..b6d7a61a42 100644 --- a/src/server/utils/domain-parser.ts +++ b/src/server/utils/domain-parser.ts @@ -3,12 +3,16 @@ * * Extracts project slug and branch from preview/development URLs. * Supports patterns: + * - {slug}.lvh.me:{port} (local development - recommended) + * - {slug}.preview.lvh.me:{port} * - {slug}.preview.veryfront.dev:{port} * - {slug}--{branch}.preview.veryfront.dev:{port} * - {slug}.veryfront.dev:{port} * - {slug}.preview.veryfront.com * - {slug}--{branch}.preview.veryfront.com * - {slug}.veryfront.com + * + * Note: lvh.me is preferred for local dev because .dev TLD forces HTTPS in browsers. */ export interface ParsedDomain { @@ -21,6 +25,11 @@ export interface ParsedDomain { type Environment = ParsedDomain["environment"]; +// Local development domains (lvh.me resolves to 127.0.0.1, veryfront.dev for HTTPS testing) +const LOCAL_DEV_DOMAINS = "lvh\\.me|veryfront\\.dev"; +// Production domains +const PROD_DOMAINS = "veryfront\\.com|veryfront\\.org"; + /** * Parse slug and optional branch from subdomain. * Branch pattern: {slug}--{branch} (double dash separator) @@ -54,72 +63,90 @@ export function parseProjectDomain(host: string): ParsedDomain { // Remove port if present const domain = host.replace(/:\d+$/, ""); - // veryfront.dev local development: {slug}.preview.veryfront.dev or {slug}--{branch}.preview.veryfront.dev - const devPreviewMatch = domain.match(/^([A-Za-z0-9-]+)\.preview\.veryfront\.dev$/); - if (devPreviewMatch?.[1]) { - const { slug, branch } = parseSlugAndBranch(devPreviewMatch[1]); + // Local development preview: {slug}.preview.{lvh.me|veryfront.dev} + const localPreviewMatch = domain.match( + new RegExp(`^([A-Za-z0-9-]+)\\.preview\\.(${LOCAL_DEV_DOMAINS})$`), + ); + if (localPreviewMatch?.[1]) { + const { slug, branch } = parseSlugAndBranch(localPreviewMatch[1]); return createParsedDomain(slug, branch, "preview", true, true); } - // Local production testing: {domain}.prod.veryfront.dev - // This pattern is treated as a custom domain for JIT production rendering - const devProdMatch = domain.match(/^([A-Za-z0-9.-]+)\.prod\.veryfront\.dev$/); - if (devProdMatch?.[1]) { + // Local production testing: {custom-domain}.prod.{lvh.me|veryfront.dev} + // Treated as custom domain for JIT production rendering + const localProdMatch = domain.match( + new RegExp(`^([A-Za-z0-9.-]+)\\.prod\\.(${LOCAL_DEV_DOMAINS})$`), + ); + if (localProdMatch?.[1]) { return createParsedDomain(null, null, "production", false, false); } - const devMatch = domain.match(/^([A-Za-z0-9-]+)\.veryfront\.dev$/); - if (devMatch?.[1]) { - const { slug, branch } = parseSlugAndBranch(devMatch[1]); + // Local development base: {slug}.{lvh.me|veryfront.dev} + const localBaseMatch = domain.match(new RegExp(`^([A-Za-z0-9-]+)\\.(${LOCAL_DEV_DOMAINS})$`)); + if (localBaseMatch?.[1]) { + const { slug, branch } = parseSlugAndBranch(localBaseMatch[1]); return createParsedDomain(slug, branch, "development", true, true); } - // Veryfront.com/org domains - const vfPreviewMatch = domain.match(/^([A-Za-z0-9-]+)\.preview\.veryfront\.(com|org)$/); - if (vfPreviewMatch?.[1]) { - const { slug, branch } = parseSlugAndBranch(vfPreviewMatch[1]); - return createParsedDomain(slug, branch, "preview", true, true); + // Plain local dev domains without slug + if (domain === "veryfront.dev" || domain === "lvh.me") { + return createParsedDomain(null, null, "development", true, true); } - const vfStagingMatch = domain.match(/^([A-Za-z0-9-]+)\.staging\.veryfront\.(com|org)$/); - if (vfStagingMatch?.[1]) { - return createParsedDomain(vfStagingMatch[1], null, "staging", true, false); + // Production preview: {slug}.preview.veryfront.{com|org} + const prodPreviewMatch = domain.match( + new RegExp(`^([A-Za-z0-9-]+)\\.preview\\.(${PROD_DOMAINS})$`), + ); + if (prodPreviewMatch?.[1]) { + const { slug, branch } = parseSlugAndBranch(prodPreviewMatch[1]); + return createParsedDomain(slug, branch, "preview", true, true); } - const vfProdMatch = domain.match(/^([A-Za-z0-9-]+)\.production\.veryfront\.(com|org)$/); - if (vfProdMatch?.[1]) { - return createParsedDomain(vfProdMatch[1], null, "production", true, false); + // Production staging: {slug}.staging.veryfront.{com|org} + const prodStagingMatch = domain.match( + new RegExp(`^([A-Za-z0-9-]+)\\.staging\\.(${PROD_DOMAINS})$`), + ); + if (prodStagingMatch?.[1]) { + return createParsedDomain(prodStagingMatch[1], null, "staging", true, false); } - // Handle environment-specific root domains (no project slug) - // preview.veryfront.com, staging.veryfront.com, production.veryfront.com - const vfEnvRootMatch = domain.match(/^(preview|staging|production)\.veryfront\.(com|org)$/); - if (vfEnvRootMatch?.[1]) { - const env = vfEnvRootMatch[1] as "preview" | "staging" | "production"; - return createParsedDomain(null, null, env, true, env === "preview"); + // Production explicit: {slug}.production.veryfront.{com|org} + const prodExplicitMatch = domain.match( + new RegExp(`^([A-Za-z0-9-]+)\\.production\\.(${PROD_DOMAINS})$`), + ); + if (prodExplicitMatch?.[1]) { + return createParsedDomain(prodExplicitMatch[1], null, "production", true, false); } - const vfBaseMatch = domain.match(/^([A-Za-z0-9-]+)\.veryfront\.(com|org)$/); - if (vfBaseMatch?.[1]) { - return createParsedDomain(vfBaseMatch[1], null, "production", true, false); + // Environment root domains (no slug): preview|staging|production.veryfront.{com|org} + const envRootMatch = domain.match( + new RegExp(`^(preview|staging|production)\\.(${PROD_DOMAINS})$`), + ); + if (envRootMatch?.[1]) { + const env = envRootMatch[1] as "preview" | "staging" | "production"; + return createParsedDomain(null, null, env, true, env === "preview"); } - // Plain veryfront.dev without slug - if (domain === "veryfront.dev") { - return createParsedDomain(null, null, "development", true, true); + // Production base: {slug}.veryfront.{com|org} + const prodBaseMatch = domain.match(new RegExp(`^([A-Za-z0-9-]+)\\.(${PROD_DOMAINS})$`)); + if (prodBaseMatch?.[1]) { + return createParsedDomain(prodBaseMatch[1], null, "production", true, false); } // Not a recognized domain pattern return createParsedDomain(null, null, null, false, false); } +/** All recognized veryfront domains */ +const ALL_DOMAINS = `${LOCAL_DEV_DOMAINS}|${PROD_DOMAINS}`; + /** - * Check if a domain is a valid veryfront domain + * Check if a domain is a valid veryfront domain (includes lvh.me for local dev) */ export function isVeryfrontDomain(host: string): boolean { const domain = host.replace(/:\d+$/, ""); - const pattern = /^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.(veryfront\.com|veryfront\.org|veryfront\.dev)$/; - return pattern.test(domain) || domain === "veryfront.dev"; + const pattern = new RegExp(`^[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\\.(${ALL_DOMAINS})$`); + return pattern.test(domain) || domain === "veryfront.dev" || domain === "lvh.me"; } /**