From b7b511be9de0d3221671009c53f7461026a6e2df Mon Sep 17 00:00:00 2001 From: Jim van der Voort Date: Thu, 10 Sep 2026 12:01:43 +0200 Subject: [PATCH 1/3] Add Caddy custom-domains deploy destination Add a "caddy" destination alongside "custom" for the new Caddy-based custom-domains setup (elastic-infra ansible/vars/apps/custom-domains.yml). It uploads the same files as the old external nginx server, but rewrites the nginx SSI directives to Caddy template placeholders on upload, since Caddy's templates directive injects the request hostname and the proxy-product query parameters at serve time. The caddy-ssi transform fails the deploy if a build contains stray Go template delimiters or an SSI directive without a known replacement, so a bad build cannot turn into per-request 500s. Uploads go via a temporary file so the transformed bytes are what lands on the server, and the post-upload verification compares against the transformed content. A --caddy-only flag deploys exclusively to the new setup, leaving the CDN and the old external server untouched during the migration. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01ApznmYeffWY6YojKXhEJDW --- deploy.mts | 332 +++++++++++++++++++++++++++++-------------- test/deploy.test.mts | 74 ++++++++++ 2 files changed, 303 insertions(+), 103 deletions(-) diff --git a/deploy.mts b/deploy.mts index b595327..3576af4 100644 --- a/deploy.mts +++ b/deploy.mts @@ -10,16 +10,21 @@ import { spinner, } from "@clack/prompts"; import { spawn } from "node:child_process"; -import { readdir, readFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; +import { mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; export type ScriptChoice = "default" | "auto-events"; -export type Destination = "cdn" | "custom"; +export type Destination = "cdn" | "custom" | "caddy"; export type EmbedVariant = "latest" | "sri" | "light" | "proxy"; export type FileStatus = "create" | "replace" | "unchanged" | "blocked"; type FileKind = "javascript" | "source-map"; -type ContentTransform = "none" | "cdn-sri-javascript" | "cdn-sri-map"; +type ContentTransform = + | "none" + | "cdn-sri-javascript" + | "cdn-sri-map" + | "caddy-ssi"; export interface DeploySelections { scripts: ScriptChoice[]; @@ -67,6 +72,34 @@ interface DeploymentResult { const ROOT = dirname(fileURLToPath(import.meta.url)); const CUSTOM_HOST = "app@external.simpleanalytics.com"; const CUSTOM_ROOT = "/var/www/default"; +// New Caddy-based custom-domains setup (elastic-infra ansible/vars/apps/custom-domains.yml). +// Caddy injects the request hostname at serve time with its templates +// directive, so the nginx SSI directives are rewritten to Go template +// placeholders (or their static defaults) on upload. +const CADDY_HOST = "user@esapp05.simpleanalytics.com"; +const CADDY_ROOT = "/home/user/apps/custom-domains-www"; +const CADDY_SSI_REPLACEMENTS: ReadonlyArray = [ + ['', "{{.Req.Host}}"], + // Substituted from the proxy-product query parameters at serve time + // (docs.simpleanalytics.com/proxy), like nginx's `set $proxy_hostname + // $arg_hostname`. Custom-domain requests carry no such args, so these + // render as "" there — matching the old nginx vhost's defined-but-empty + // variables (its SSI defaults never applied). Go's Query.Get + // percent-decodes, which covers nginx's %2F-rewrite hack too. + ['', '{{.Req.URL.Query.Get "hostname"}}'], + ['', '{{.Req.URL.Query.Get "path"}}'], +]; + +interface RemoteTarget { + host: string; + root: string; +} + +function remoteTarget(destination: "custom" | "caddy"): RemoteTarget { + return destination === "custom" + ? { host: CUSTOM_HOST, root: CUSTOM_ROOT } + : { host: CADDY_HOST, root: CADDY_ROOT }; +} const CDN_PUBLIC_ROOT = "https://scripts.simpleanalyticscdn.com"; const CDN_STORAGE_ROOT = "https://storage.bunnycdn.com/sa-cdn"; const CDN_PURGE_URL = @@ -127,8 +160,13 @@ function addScriptPair( kind: "source-map", localPath: `${localPath}.map`, remotePath: `${remotePath}.map`, + // Source maps carry the SSI directive too, so caddy-ssi applies to both. transform: - transform === "cdn-sri-javascript" ? "cdn-sri-map" : "none", + transform === "cdn-sri-javascript" + ? "cdn-sri-map" + : transform === "caddy-ssi" + ? "caddy-ssi" + : "none", version, }); } @@ -145,6 +183,21 @@ export function createManifest( selections.variants.includes(variant); const needsSri = hasScript("default") && hasVariant("sri"); + // The old (nginx) and new (Caddy) custom-domain servers host the same + // files; Caddy uploads additionally rewrite the SSI directives. + const serverDestinations = (["custom", "caddy"] as const).filter(hasDestination); + const addServerPair = ( + input: Omit[1], "destination">, + ) => { + for (const destination of serverDestinations) { + addScriptPair(files, { + ...input, + destination, + transform: destination === "caddy" ? "caddy-ssi" : input.transform, + }); + } + }; + if (needsSri && version === undefined) { throw new Error("An SRI version is required when SRI is selected."); } @@ -169,24 +222,19 @@ export function createManifest( }); } - if (hasDestination("custom")) { - addScriptPair(files, { - destination: "custom", - localPath: "dist/latest/custom/latest.js", - remotePath: "latest.js", - }); - addScriptPair(files, { - destination: "custom", - localPath: "dist/latest/custom/e.js", - remotePath: "events.js", - }); - addScriptPair(files, { - destination: "custom", - localPath: "dist/latest/custom/latest.dev.js", - remotePath: "latest.dev.js", - sourceMap: false, - }); - } + addServerPair({ + localPath: "dist/latest/custom/latest.js", + remotePath: "latest.js", + }); + addServerPair({ + localPath: "dist/latest/custom/e.js", + remotePath: "events.js", + }); + addServerPair({ + localPath: "dist/latest/custom/latest.dev.js", + remotePath: "latest.dev.js", + sourceMap: false, + }); } if (needsSri) { @@ -201,15 +249,12 @@ export function createManifest( }); } - if (hasDestination("custom")) { - addScriptPair(files, { - destination: "custom", - immutable: true, - localPath: `dist/v${version}/custom/app.js`, - remotePath: `v${version}/app.js`, - version, - }); - } + addServerPair({ + immutable: true, + localPath: `dist/v${version}/custom/app.js`, + remotePath: `v${version}/app.js`, + version, + }); } if (hasScript("default") && hasVariant("light")) { @@ -221,39 +266,33 @@ export function createManifest( }); } - if (hasDestination("custom")) { - addScriptPair(files, { - destination: "custom", - localPath: "dist/latest/custom/light.js", - remotePath: "light.js", - }); + addServerPair({ + localPath: "dist/latest/custom/light.js", + remotePath: "light.js", + }); - if (needsSri) { - addScriptPair(files, { - destination: "custom", - immutable: true, - localPath: `dist/v${version}/custom/light.js`, - remotePath: `v${version}/light.js`, - version, - }); - } + if (needsSri) { + addServerPair({ + immutable: true, + localPath: `dist/v${version}/custom/light.js`, + remotePath: `v${version}/light.js`, + version, + }); } } if ( hasScript("default") && hasVariant("proxy") && - hasDestination("custom") + serverDestinations.length > 0 ) { - addScriptPair(files, { - destination: "custom", + addServerPair({ localPath: "dist/latest/custom/proxy.js", remotePath: "proxy.js", }); if (needsSri) { - addScriptPair(files, { - destination: "custom", + addServerPair({ immutable: true, localPath: `dist/v${version}/custom/proxy.js`, remotePath: `v${version}/proxy.js`, @@ -271,22 +310,18 @@ export function createManifest( }); } - if (hasDestination("custom")) { - addScriptPair(files, { - destination: "custom", - localPath: "dist/latest/custom/auto-events.js", - remotePath: "auto-events.js", - }); + addServerPair({ + localPath: "dist/latest/custom/auto-events.js", + remotePath: "auto-events.js", + }); - if (needsSri) { - addScriptPair(files, { - destination: "custom", - immutable: true, - localPath: `dist/v${version}/custom/auto-events.js`, - remotePath: `v${version}/auto-events.js`, - version, - }); - } + if (needsSri) { + addServerPair({ + immutable: true, + localPath: `dist/v${version}/custom/auto-events.js`, + remotePath: `v${version}/auto-events.js`, + version, + }); } } @@ -314,6 +349,27 @@ async function discoverSriVersion(): Promise { export function transformContent(file: DeployFile, content: Buffer): Buffer { if (file.transform === "none") return content; + + if (file.transform === "caddy-ssi") { + let source = content.toString("utf8"); + // Caddy's templates directive would choke on (or execute) stray actions, + // turning a bad build into per-request 500s — fail the deploy instead. + if (source.includes("{{")) { + throw new Error( + `${file.localPath} contains Go template delimiters ("{{"); Caddy's templates directive cannot serve it safely.`, + ); + } + for (const [pattern, replacement] of CADDY_SSI_REPLACEMENTS) { + source = source.replaceAll(pattern, replacement); + } + if (source.includes("","","")', + ), + ).toString(), + 'a("{{.Req.Host}}","{{.Req.URL.Query.Get "hostname"}}","{{.Req.URL.Query.Get "path"}}")', + ); + }); + + it("passes through files without directives unchanged", () => { + const content = Buffer.from("plain();\n"); + assert.equal(transformContent(caddyFile, content).toString(), "plain();\n"); + }); + + it("rejects stray Go template delimiters", () => { + assert.throws( + () => transformContent(caddyFile, Buffer.from('var a = "{{oops"')), + /Go template delimiters/, + ); + }); + + it("rejects SSI directives without a replacement", () => { + assert.throws( + () => + transformContent( + caddyFile, + Buffer.from('a("")'), + ), + /no Caddy replacement/, + ); + }); +}); From b6382b54a62e2314f3c39bda6a231ccccbea62c8 Mon Sep 17 00:00:00 2001 From: Simple Analytics Codex Date: Wed, 16 Sep 2026 15:07:07 +0200 Subject: [PATCH 2/3] Escape Caddy proxy template values --- deploy.mts | 13 ++++++++++--- test/deploy.test.mts | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/deploy.mts b/deploy.mts index 3576af4..f422cdb 100644 --- a/deploy.mts +++ b/deploy.mts @@ -85,9 +85,16 @@ const CADDY_SSI_REPLACEMENTS: ReadonlyArray = [ // $arg_hostname`. Custom-domain requests carry no such args, so these // render as "" there — matching the old nginx vhost's defined-but-empty // variables (its SSI defaults never applied). Go's Query.Get - // percent-decodes, which covers nginx's %2F-rewrite hack too. - ['', '{{.Req.URL.Query.Get "hostname"}}'], - ['', '{{.Req.URL.Query.Get "path"}}'], + // percent-decodes, which covers nginx's %2F-rewrite hack too. Escape the + // decoded values before inserting them into the script's string literal. + [ + '', + '{{js (.Req.URL.Query.Get "hostname")}}', + ], + [ + '', + '{{js (.Req.URL.Query.Get "path")}}', + ], ]; interface RemoteTarget { diff --git a/test/deploy.test.mts b/test/deploy.test.mts index 28c02a1..16b78c6 100644 --- a/test/deploy.test.mts +++ b/test/deploy.test.mts @@ -253,7 +253,7 @@ describe("Caddy custom-domains destination", () => { 'a("","","")', ), ).toString(), - 'a("{{.Req.Host}}","{{.Req.URL.Query.Get "hostname"}}","{{.Req.URL.Query.Get "path"}}")', + 'a("{{.Req.Host}}","{{js (.Req.URL.Query.Get "hostname")}}","{{js (.Req.URL.Query.Get "path")}}")', ); }); From e82ef66746cd6157ce1a1278fa29fa435a5d880d Mon Sep 17 00:00:00 2001 From: "simple-analytics-ai[bot]" <313929673+simple-analytics-ai[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:52:09 +0200 Subject: [PATCH 3/3] Remove retired custom deployment target (#119) Co-authored-by: Simple Analytics Codex --- deploy.mts | 155 +++++++++++-------------------------------- test/deploy.test.mts | 46 +++++++------ 2 files changed, 66 insertions(+), 135 deletions(-) diff --git a/deploy.mts b/deploy.mts index f422cdb..6c87c4d 100644 --- a/deploy.mts +++ b/deploy.mts @@ -16,7 +16,7 @@ import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; export type ScriptChoice = "default" | "auto-events"; -export type Destination = "cdn" | "custom" | "caddy"; +export type Destination = "cdn" | "caddy"; export type EmbedVariant = "latest" | "sri" | "light" | "proxy"; export type FileStatus = "create" | "replace" | "unchanged" | "blocked"; type FileKind = "javascript" | "source-map"; @@ -70,9 +70,6 @@ interface DeploymentResult { } const ROOT = dirname(fileURLToPath(import.meta.url)); -const CUSTOM_HOST = "app@external.simpleanalytics.com"; -const CUSTOM_ROOT = "/var/www/default"; -// New Caddy-based custom-domains setup (elastic-infra ansible/vars/apps/custom-domains.yml). // Caddy injects the request hostname at serve time with its templates // directive, so the nginx SSI directives are rewritten to Go template // placeholders (or their static defaults) on upload. @@ -97,16 +94,6 @@ const CADDY_SSI_REPLACEMENTS: ReadonlyArray = [ ], ]; -interface RemoteTarget { - host: string; - root: string; -} - -function remoteTarget(destination: "custom" | "caddy"): RemoteTarget { - return destination === "custom" - ? { host: CUSTOM_HOST, root: CUSTOM_ROOT } - : { host: CADDY_HOST, root: CADDY_ROOT }; -} const CDN_PUBLIC_ROOT = "https://scripts.simpleanalyticscdn.com"; const CDN_STORAGE_ROOT = "https://storage.bunnycdn.com/sa-cdn"; const CDN_PURGE_URL = @@ -190,19 +177,15 @@ export function createManifest( selections.variants.includes(variant); const needsSri = hasScript("default") && hasVariant("sri"); - // The old (nginx) and new (Caddy) custom-domain servers host the same - // files; Caddy uploads additionally rewrite the SSI directives. - const serverDestinations = (["custom", "caddy"] as const).filter(hasDestination); - const addServerPair = ( + const addCaddyPair = ( input: Omit[1], "destination">, ) => { - for (const destination of serverDestinations) { - addScriptPair(files, { - ...input, - destination, - transform: destination === "caddy" ? "caddy-ssi" : input.transform, - }); - } + if (!hasDestination("caddy")) return; + addScriptPair(files, { + ...input, + destination: "caddy", + transform: "caddy-ssi", + }); }; if (needsSri && version === undefined) { @@ -229,15 +212,15 @@ export function createManifest( }); } - addServerPair({ + addCaddyPair({ localPath: "dist/latest/custom/latest.js", remotePath: "latest.js", }); - addServerPair({ + addCaddyPair({ localPath: "dist/latest/custom/e.js", remotePath: "events.js", }); - addServerPair({ + addCaddyPair({ localPath: "dist/latest/custom/latest.dev.js", remotePath: "latest.dev.js", sourceMap: false, @@ -256,7 +239,7 @@ export function createManifest( }); } - addServerPair({ + addCaddyPair({ immutable: true, localPath: `dist/v${version}/custom/app.js`, remotePath: `v${version}/app.js`, @@ -273,13 +256,13 @@ export function createManifest( }); } - addServerPair({ + addCaddyPair({ localPath: "dist/latest/custom/light.js", remotePath: "light.js", }); if (needsSri) { - addServerPair({ + addCaddyPair({ immutable: true, localPath: `dist/v${version}/custom/light.js`, remotePath: `v${version}/light.js`, @@ -291,15 +274,15 @@ export function createManifest( if ( hasScript("default") && hasVariant("proxy") && - serverDestinations.length > 0 + hasDestination("caddy") ) { - addServerPair({ + addCaddyPair({ localPath: "dist/latest/custom/proxy.js", remotePath: "proxy.js", }); if (needsSri) { - addServerPair({ + addCaddyPair({ immutable: true, localPath: `dist/v${version}/custom/proxy.js`, remotePath: `v${version}/proxy.js`, @@ -317,13 +300,13 @@ export function createManifest( }); } - addServerPair({ + addCaddyPair({ localPath: "dist/latest/custom/auto-events.js", remotePath: "auto-events.js", }); if (needsSri) { - addServerPair({ + addCaddyPair({ immutable: true, localPath: `dist/v${version}/custom/auto-events.js`, remotePath: `v${version}/auto-events.js`, @@ -534,9 +517,8 @@ function shellQuote(value: string): string { return `'${value.replaceAll("'", `'"'"'`)}'`; } -async function readCustomFiles( +async function readCaddyFiles( remotePaths: string[], - target: RemoteTarget, ): Promise> { const uniquePaths = [...new Set(remotePaths)]; if (uniquePaths.length === 0) return new Map(); @@ -546,7 +528,7 @@ async function readCustomFiles( const command = [ `for relative_path in ${pathArguments}; do`, `printf '__SA_FILE__:%s\\n' "$relative_path";`, - `full_path=${shellQuote(target.root)}/$relative_path;`, + `full_path=${shellQuote(CADDY_ROOT)}/$relative_path;`, 'if test -f "$full_path"; then', `printf '__SA_DATA__:'; base64 "$full_path" | tr -d '\\n'; printf '\\n';`, "else printf '__SA_MISSING__\\n'; fi;", @@ -554,7 +536,7 @@ async function readCustomFiles( ].join(" "); const { stdout } = await runCommand( "ssh", - ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", target.host, command], + ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", CADDY_HOST, command], { timeout: 45_000 }, ); @@ -581,7 +563,7 @@ async function readCustomFiles( for (const remotePath of uniquePaths) { if (!result.has(remotePath)) { - throw new Error(`Custom server did not return a result for ${remotePath}.`); + throw new Error(`Caddy server did not return a result for ${remotePath}.`); } } return result; @@ -593,24 +575,18 @@ async function inspectFiles(files: PreparedFile[]): Promise { .filter((file) => file.destination === destination) .map((file) => file.remotePath); - const [cdnEntries, customFiles, caddyFiles] = await Promise.all([ + const [cdnEntries, caddyFiles] = await Promise.all([ Promise.all( pathsFor("cdn").map( async (remotePath) => [remotePath, await readCdnFile(remotePath)] as const, ), ), - readCustomFiles(pathsFor("custom"), remoteTarget("custom")), - readCustomFiles(pathsFor("caddy"), remoteTarget("caddy")), + readCaddyFiles(pathsFor("caddy")), ]); const cdnFiles = new Map(cdnEntries); return files.map((file) => { - const remoteFiles = - file.destination === "cdn" - ? cdnFiles - : file.destination === "custom" - ? customFiles - : caddyFiles; + const remoteFiles = file.destination === "cdn" ? cdnFiles : caddyFiles; const remoteContent = remoteFiles.get(file.remotePath) ?? null; return { ...file, @@ -626,8 +602,7 @@ async function inspectFiles(files: PreparedFile[]): Promise { function remoteLabel(file: DeployFile): string { if (file.destination === "cdn") return `${CDN_PUBLIC_ROOT}/${file.remotePath}`; - const target = remoteTarget(file.destination); - return `${target.host}:${target.root}/${file.remotePath}`; + return `${CADDY_HOST}:${CADDY_ROOT}/${file.remotePath}`; } function statusLabel(status: FileStatus): string { @@ -669,11 +644,10 @@ function renderPreview( ); const destinationLabels: Record = { - caddy: "Custom domain (new Caddy setup)", + caddy: "Caddy", cdn: "CDN", - custom: "Custom domain", }; - for (const destination of ["cdn", "custom", "caddy"] as const) { + for (const destination of ["cdn", "caddy"] as const) { const destinationFiles = files.filter( (file) => file.destination === destination, ); @@ -729,10 +703,7 @@ async function readCurrentFile( if (file.destination === "cdn") { return readCdnFile(file.remotePath, credentials?.accessKey); } - const remoteFiles = await readCustomFiles( - [file.remotePath], - remoteTarget(file.destination), - ); + const remoteFiles = await readCaddyFiles([file.remotePath]); return remoteFiles.get(file.remotePath) ?? null; } @@ -756,38 +727,10 @@ async function uploadCdnFile( } } -async function uploadCustomFile(file: PreparedFile): Promise { - assertSafeRemotePath(file.remotePath); - if (file.transform !== "none") { - throw new Error(`Custom uploads cannot transform ${file.localPath}.`); - } - - if (file.immutable) { - const remoteDirectory = dirname(`${CUSTOM_ROOT}/${file.remotePath}`); - await runCommand("ssh", [ - "-o", - "BatchMode=yes", - "-o", - "ConnectTimeout=10", - CUSTOM_HOST, - `sudo mkdir -p -- ${shellQuote(remoteDirectory)}`, - ]); - } - - const args = ["--quiet", "--rsync-path=sudo rsync"]; - if (file.immutable) args.push("--ignore-existing"); - args.push( - resolve(ROOT, file.localPath), - `${CUSTOM_HOST}:${CUSTOM_ROOT}/${file.remotePath}`, - ); - await runCommand("rsync", args, { timeout: 120_000 }); -} - async function uploadCaddyFile(file: PreparedFile): Promise { assertSafeRemotePath(file.remotePath); - // Unlike the old server, the deployed bytes differ from the local file - // (SSI directives become Caddy template placeholders), so upload the + // SSI directives become Caddy template placeholders, so upload the // transformed content from a temporary file. const remoteDirectory = dirname(`${CADDY_ROOT}/${file.remotePath}`); await runCommand("ssh", [ @@ -829,20 +772,12 @@ async function verifyUploadedFiles( credentials?: CdnCredentials, ): Promise { const cdnFiles = files.filter((file) => file.destination === "cdn"); - const customFiles = files.filter((file) => file.destination === "custom"); const caddyFiles = files.filter((file) => file.destination === "caddy"); - const [cdnContents, customContents, caddyContents] = await Promise.all([ + const [cdnContents, caddyContents] = await Promise.all([ Promise.all( cdnFiles.map((file) => readCdnFile(file.remotePath, credentials?.accessKey)), ), - readCustomFiles( - customFiles.map((file) => file.remotePath), - remoteTarget("custom"), - ), - readCustomFiles( - caddyFiles.map((file) => file.remotePath), - remoteTarget("caddy"), - ), + readCaddyFiles(caddyFiles.map((file) => file.remotePath)), ]); for (const [index, file] of cdnFiles.entries()) { @@ -850,11 +785,6 @@ async function verifyUploadedFiles( throw new Error(`CDN verification failed for ${file.remotePath}.`); } } - for (const file of customFiles) { - if (!customContents.get(file.remotePath)?.equals(file.localContent)) { - throw new Error(`Custom-domain verification failed for ${file.remotePath}.`); - } - } for (const file of caddyFiles) { if (!caddyContents.get(file.remotePath)?.equals(file.localContent)) { throw new Error(`Caddy verification failed for ${file.remotePath}.`); @@ -910,10 +840,8 @@ async function deployFiles( if (file.destination === "cdn") { if (!credentials) throw new Error("Missing CDN credentials."); await uploadCdnFile(file, credentials); - } else if (file.destination === "caddy") { - await uploadCaddyFile(file); } else { - await uploadCustomFile(file); + await uploadCaddyFile(file); } progress.stop(`Uploaded ${file.remotePath} to ${file.destination}`); } catch (error) { @@ -983,11 +911,9 @@ function selectedValue(value: T | symbol): T { async function main(): Promise { intro("Simple Analytics script deployment"); - // --caddy-only deploys exclusively to the new Caddy custom-domains setup, - // leaving the CDN and the old external server untouched. const caddyOnly = process.argv.includes("--caddy-only"); if (caddyOnly) { - log.info("Deploying only to the new Caddy custom-domains setup (--caddy-only)."); + log.info("Deploying only to Caddy (--caddy-only)."); } const dryRun = selectedValue( @@ -1017,12 +943,11 @@ async function main(): Promise { ? ["caddy"] : selectedValue( await multiselect({ - initialValues: ["cdn", "custom"], + initialValues: ["cdn", "caddy"], message: "Where do you want to deploy these scripts?", options: [ { label: "CDN", value: "cdn" }, - { label: "Custom domain", value: "custom" }, - { label: "Custom domain (new Caddy setup)", value: "caddy" }, + { label: "Caddy", value: "caddy" }, ], required: true, }), @@ -1035,7 +960,7 @@ async function main(): Promise { { label: "SRI", value: "sri" }, { label: "Light", value: "light" }, ]; - if (destinations.includes("custom") || destinations.includes("caddy")) { + if (destinations.includes("caddy")) { options.push({ label: "Proxy", value: "proxy" }); } variants = selectedValue( @@ -1093,9 +1018,9 @@ async function main(): Promise { const result = await deployFiles(previews, credentials); renderDeploymentResult(result); - if (variants.includes("sri") && destinations.includes("custom")) { + if (variants.includes("sri")) { log.warn( - "Add the SRI version to /etc/nginx/sa-client-site-with-ssl.conf and update https://docs.simpleanalytics.com/sri.", + "Update https://docs.simpleanalytics.com/sri with the SRI version.", ); } outro("Scripts deployed successfully."); diff --git a/test/deploy.test.mts b/test/deploy.test.mts index 16b78c6..170a6a9 100644 --- a/test/deploy.test.mts +++ b/test/deploy.test.mts @@ -16,7 +16,7 @@ import { describe("deployment manifest", () => { it("maps latest and its legacy companions to both destinations", () => { const files = createManifest({ - destinations: ["cdn", "custom"], + destinations: ["cdn", "caddy"], scripts: ["default"], variants: ["latest"], }); @@ -29,11 +29,11 @@ describe("deployment manifest", () => { "cdn:hello.js", "cdn:hello.js.map", "cdn:latest.dev.js", - "custom:latest.js", - "custom:latest.js.map", - "custom:events.js", - "custom:events.js.map", - "custom:latest.dev.js", + "caddy:latest.js", + "caddy:latest.js.map", + "caddy:events.js", + "caddy:events.js.map", + "caddy:latest.dev.js", ], ); }); @@ -41,7 +41,7 @@ describe("deployment manifest", () => { it("includes every selected current and versioned file", () => { const files = createManifest( { - destinations: ["cdn", "custom"], + destinations: ["cdn", "caddy"], scripts: ["default", "auto-events"], variants: ["latest", "sri", "light", "proxy"], }, @@ -49,6 +49,12 @@ describe("deployment manifest", () => { ); assert.equal(files.length, 30); + assert.equal( + files + .filter((file) => file.destination === "caddy") + .every((file) => file.transform === "caddy-ssi"), + true, + ); assert.deepEqual( files .filter((file) => file.immutable) @@ -56,21 +62,21 @@ describe("deployment manifest", () => { [ "cdn:sri/v12.js", "cdn:sri/v12.js.map", - "custom:v12/app.js", - "custom:v12/app.js.map", - "custom:v12/light.js", - "custom:v12/light.js.map", - "custom:v12/proxy.js", - "custom:v12/proxy.js.map", - "custom:v12/auto-events.js", - "custom:v12/auto-events.js.map", + "caddy:v12/app.js", + "caddy:v12/app.js.map", + "caddy:v12/light.js", + "caddy:v12/light.js.map", + "caddy:v12/proxy.js", + "caddy:v12/proxy.js.map", + "caddy:v12/auto-events.js", + "caddy:v12/auto-events.js.map", ], ); }); it("deploys standalone auto-events without implicit SRI files", () => { const files = createManifest({ - destinations: ["cdn", "custom"], + destinations: ["cdn", "caddy"], scripts: ["auto-events"], variants: [], }); @@ -80,8 +86,8 @@ describe("deployment manifest", () => { [ "cdn:auto-events.js", "cdn:auto-events.js.map", - "custom:auto-events.js", - "custom:auto-events.js.map", + "caddy:auto-events.js", + "caddy:auto-events.js.map", ], ); assert.equal(files.some((file) => file.immutable), false); @@ -118,7 +124,7 @@ describe("deployment manifest", () => { it("never invents a source map for latest.dev.js", () => { const files = createManifest({ - destinations: ["cdn", "custom"], + destinations: ["cdn", "caddy"], scripts: ["default"], variants: ["latest"], }); @@ -208,7 +214,7 @@ describe("preview helpers", () => { }); describe("Caddy custom-domains destination", () => { - it("mirrors the custom files with the caddy-ssi transform", () => { + it("maps Caddy-only files with the caddy-ssi transform", () => { const files = createManifest( { destinations: ["caddy"],