Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 3 additions & 4 deletions scripts/dev-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
29 changes: 2 additions & 27 deletions src/cli/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -25,21 +23,6 @@ export interface DevOptions {
// Alias for backward compatibility
export type DevCommandOptions = DevOptions;

async function getLocalIP(): Promise<string> {
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;

Expand Down Expand Up @@ -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();
Expand Down
4 changes: 3 additions & 1 deletion src/cli/utils/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
101 changes: 64 additions & 37 deletions src/server/utils/domain-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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";
}

/**
Expand Down