File size: 4,962 Bytes
391c43e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
 * Pure helpers for deployment URL/asset-path resolution.
 *
 * Kept separate from static-builder.ts so they carry no server-only / filesystem
 * dependencies and can be unit-tested directly.
 */

import { VirtualFile } from '@/lib/vfs/types';

export interface DeploymentServing {
  /** True when the deployment is served at a root (needs root-relative asset paths). */
  servedAtRoot: boolean;
  /** Base URL used for SEO (sitemap, canonical, Open Graph). */
  baseUrl: string;
}

/**
 * Decide how a deployment is served, which controls its asset path style and
 * SEO URLs:
 * - custom domain → served at that domain's root (root-relative paths)
 * - static proxy (Caddy subdomains) active AND a slug → served at the slug
 *   subdomain root (root-relative paths)
 * - otherwise → served under /deployments/{id}/ by the Next route (prefixed
 *   paths). A slug alone doesn't imply root serving, since publish always
 *   assigns one.
 */
export function resolveDeploymentServing(
  deployment: { slug?: string; customDomain?: string },
  deploymentId: string,
  opts: { staticProxyEnabled: boolean; appUrl: string }
): DeploymentServing {
  const servedViaSubdomain = opts.staticProxyEnabled && !!deployment.slug;
  const servedAtRoot = !!deployment.customDomain || servedViaSubdomain;
  const instanceDomain = opts.appUrl.replace(/^https?:\/\//, '');
  const baseUrl = deployment.customDomain
    ? `https://${deployment.customDomain}`
    : servedViaSubdomain
    ? `https://${deployment.slug}.${instanceDomain}`
    : `${opts.appUrl}/deployments/${deploymentId}`;
  return { servedAtRoot, baseUrl };
}

/**
 * Replace both blob URLs and file path references with deployment-prefixed absolute paths
 */
export function replaceAssetPathsWithDeploymentPrefix(
  content: string,
  blobUrlToPath: Map<string, string>,
  allFiles: VirtualFile[],
  deploymentId: string,
  servedAtRoot?: boolean
): string {
  let result = content;

  // If served at domain root (custom domain or subdomain slug), use root-relative paths.
  // Otherwise use deployment-prefixed paths for direct /deployments/{id}/ access.
  const pathPrefix = servedAtRoot ? '' : `/deployments/${deploymentId}`;

  // First, replace all blob URLs with appropriate paths
  for (const [blobUrl, filePath] of blobUrlToPath) {
    const absolutePath = `${pathPrefix}${filePath}`;
    result = result.replace(new RegExp(escapeRegex(blobUrl), 'g'), absolutePath);
  }

  // Helper to check if path is already prefixed with deployment path
  const isAlreadyPrefixed = (path: string) =>
    pathPrefix && path.startsWith(pathPrefix);

  // Rewrite all internal absolute paths for HTML files
  // Pattern matches: href="/anything.html" or href="/anything.htm"
  result = result.replace(
    /href=(["'])(\/[^"']*\.html?)\1/g,
    (match, quote, filePath) => {
      if (isAlreadyPrefixed(filePath)) {
        return match;
      }
      return `href=${quote}${pathPrefix}${filePath}${quote}`;
    }
  );

  // Rewrite asset directory paths (styles, scripts, assets, images, fonts, js, css)
  const assetDirPattern = /(?:href|src)=(["'])(\/(?:styles|scripts|assets|images|fonts|js|css)\/[^"']+)\1/g;
  result = result.replace(assetDirPattern, (match, quote, filePath) => {
    if (isAlreadyPrefixed(filePath)) {
      return match;
    }
    return match.replace(filePath, `${pathPrefix}${filePath}`);
  });

  // Rewrite root-level asset references (e.g., /bundle.js, /bundle.css, /favicon.ico)
  const rootAssetPattern = /(?:href|src)=(["'])(\/[^"'\/]+\.(?:js|css|json|xml|ico|png|jpg|jpeg|gif|svg|webp|woff|woff2|ttf|eot))\1/g;
  result = result.replace(rootAssetPattern, (match, quote, filePath) => {
    if (isAlreadyPrefixed(filePath)) {
      return match;
    }
    return match.replace(filePath, `${pathPrefix}${filePath}`);
  });

  // Rewrite CSS url() references for asset directories
  result = result.replace(
    /url\(['"]?(\/(?:styles|scripts|assets|images|fonts|js|css)\/[^'")]+)['"]?\)/g,
    (match, filePath) => {
      if (isAlreadyPrefixed(filePath)) {
        return match;
      }
      return match.replace(filePath, `${pathPrefix}${filePath}`);
    }
  );

  // Handle relative HTML paths (e.g., href="about.html") - convert to absolute with prefix
  result = result.replace(
    /href=(["'])([^"':/][^"']*\.html?)\1/g,
    (match, quote, filePath) => {
      // Skip if it looks like an already-processed path or external
      if (filePath.startsWith('/') || filePath.includes('://')) {
        return match;
      }
      return `href=${quote}${pathPrefix}/${filePath}${quote}`;
    }
  );

  // Handle root path href="/" - rewrite to deployment prefix
  if (pathPrefix) {
    result = result.replace(
      /href=(["'])\/\1/g,
      (match, quote) => `href=${quote}${pathPrefix}/${quote}`
    );
  }

  return result;
}

/**
 * Escape special regex characters
 */
function escapeRegex(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}