@cloudhal/cli 0.3.8 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/assets/starters/blog/package.json +4 -4
- package/dist/src/cli/commands-local.js +4 -0
- package/dist/src/cli/commands-remote.js +10 -6
- package/dist/src/cli/help.d.ts +1 -1
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli.d.ts +4 -0
- package/dist/src/cli.js +1 -0
- package/dist/src/cloudflare/config.d.ts +34 -0
- package/dist/src/cloudflare/config.js +7 -1
- package/dist/src/commands/deploy/prepare.js +1 -0
- package/dist/src/commands/deploy/resources.d.ts +10 -0
- package/dist/src/commands/deploy/resources.js +25 -0
- package/dist/src/commands/deploy/ship.js +25 -19
- package/dist/src/commands/domain.d.ts +6 -2
- package/dist/src/commands/domain.js +51 -10
- package/dist/src/commands/snapshot.d.ts +10 -2
- package/dist/src/commands/snapshot.js +26 -3
- package/dist/src/config.d.ts +3 -1
- package/dist/src/config.js +24 -11
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -163,7 +163,7 @@ Locally, `cloudhal studio` reads the project's SQLite (`analytics_events`), roll
|
|
|
163
163
|
|
|
164
164
|
**From Git (Workers Builds).** With `WORKERS_CI=1`, which Cloudflare sets in [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/), `cloudhal deploy` works from a fresh clone. It takes the names from the committed `wrangler.jsonc`, seeds the starter site when the checkout has none, keeps the secrets on the Workers and does not print the setup link. This is what the Deploy to Cloudflare button runs: see [docs/guides/deploy-button.md](../../docs/guides/deploy-button.md).
|
|
165
165
|
|
|
166
|
-
**Custom domain.** `cloudhal domain set news.example.com --studio studio.example.com` records the hostnames in `cloudhal.json`; the next `cloudhal deploy` routes them to the Workers as [Cloudflare Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) (the zone must be in the same Cloudflare account — Cloudflare creates the DNS record and certificate) and makes the site's one its primary hostname, so
|
|
166
|
+
**Custom domain.** `cloudhal domain set news.example.com --studio studio.example.com` records the hostnames in `cloudhal.json`; the next `cloudhal deploy` routes them to the Workers as [Cloudflare Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) (the zone must be in the same Cloudflare account — Cloudflare creates the DNS record and certificate) and makes the site's one its primary hostname on the deployment. `domain set` makes it primary in the local store right away, so `cloudhal dev`, `render` and `build` write it into canonical links, the sitemap and feeds before any deploy. The workers.dev addresses keep working. `cloudhal domain remove` and a deploy undo it. `--redirect www.news.example.com` (repeatable) adds hosts that answer with a 301 to the site's domain, path and query kept: the next deploy routes them to the site Worker as Custom Domains too, so Cloudflare creates their DNS records and certificates. `cloudhal domain remove redirects` drops them.
|
|
167
167
|
|
|
168
168
|
**Email (RFC-0011).** Add `"mail": { "from": "studio@example.com", "fromName": "Example Studio" }` to `cloudhal.json` and the next `cloudhal deploy` gives the admin Worker a [Cloudflare Email Service](https://developers.cloudflare.com/email-service/) `send_email` binding locked to that address. Invites can then be emailed, and staff can reset a forgotten password from the sign-in screen. Onboard the address's domain in Email Service first. Sending to any recipient needs Workers Paid (3,000 emails a month included); on Free, Email Service delivers only to verified destination addresses of your account. Locally, `cloudhal studio` prints these emails in the terminal instead of sending them.
|
|
169
169
|
|
|
@@ -14,18 +14,18 @@
|
|
|
14
14
|
"astro": "astro"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@cloudhal/astro-renderer": "0.3.
|
|
18
|
-
"@cloudhal/core-ts": "0.3.
|
|
17
|
+
"@cloudhal/astro-renderer": "0.3.10",
|
|
18
|
+
"@cloudhal/core-ts": "0.3.10",
|
|
19
19
|
"@tailwindcss/vite": "^4.3.3",
|
|
20
20
|
"astro": "7.3.6",
|
|
21
21
|
"better-sqlite3": "13.0.3",
|
|
22
22
|
"tailwindcss": "^4.3.3",
|
|
23
23
|
"@astrojs/cloudflare": "14.3.4",
|
|
24
|
-
"@cloudhal/cli": "0.3.
|
|
24
|
+
"@cloudhal/cli": "0.3.10"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"wrangler": "^4.148.0",
|
|
28
|
-
"@cloudhal/cloudflare": "0.3.
|
|
28
|
+
"@cloudhal/cloudflare": "0.3.10"
|
|
29
29
|
},
|
|
30
30
|
"trustedDependencies": [
|
|
31
31
|
"better-sqlite3",
|
|
@@ -45,6 +45,10 @@ const snapshotCommand = async (_rest, values, { projectDir }) => {
|
|
|
45
45
|
...(values.label ? { label: values.label } : {}),
|
|
46
46
|
...(values.publish ? { publish: true } : {}),
|
|
47
47
|
});
|
|
48
|
+
if (snap.unchanged) {
|
|
49
|
+
console.log(`nothing changed since ${snap.id} (live); no new snapshot`);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
48
52
|
console.log(
|
|
49
53
|
`${snap.id} nodes=${snap.nodes.length} +${snap.summary.added} ~${snap.summary.changed} -${snap.summary.removed}${values.publish ? " (published)" : ""}`,
|
|
50
54
|
);
|
|
@@ -60,23 +60,27 @@ const deployCommand = async (_rest, values, { projectDir }) => {
|
|
|
60
60
|
function changeDomain(rest , values , projectDir ) {
|
|
61
61
|
const [action, arg] = rest;
|
|
62
62
|
if (action === "set") {
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
const redirects = (values.redirect ?? []).flatMap((v) => v.split(","));
|
|
64
|
+
if (!arg && !values.studio && redirects.length === 0)
|
|
65
|
+
throw new Error(
|
|
66
|
+
"usage: cloudhal domain set <hostname> [--studio <hostname>] [--redirect <hostname>]…",
|
|
67
|
+
);
|
|
65
68
|
return domain({
|
|
66
69
|
projectDir,
|
|
67
70
|
set: {
|
|
68
71
|
...(arg ? { site: arg } : {}),
|
|
69
72
|
...(values.studio ? { studio: values.studio } : {}),
|
|
73
|
+
...(redirects.length > 0 ? { redirects } : {}),
|
|
70
74
|
},
|
|
71
75
|
});
|
|
72
76
|
}
|
|
73
77
|
if (action === "remove") {
|
|
74
78
|
const which = arg ?? "all";
|
|
75
|
-
if (!["site", "studio", "all"].includes(which))
|
|
76
|
-
throw new Error("usage: cloudhal domain remove [site|studio|all]");
|
|
79
|
+
if (!["site", "studio", "redirects", "all"].includes(which))
|
|
80
|
+
throw new Error("usage: cloudhal domain remove [site|studio|redirects|all]");
|
|
77
81
|
return domain({
|
|
78
82
|
projectDir,
|
|
79
|
-
remove: which === "all" ? ["site", "studio"] : [which ],
|
|
83
|
+
remove: which === "all" ? ["site", "studio", "redirects"] : [which ],
|
|
80
84
|
});
|
|
81
85
|
}
|
|
82
86
|
if (action === undefined) return domain({ projectDir });
|
|
@@ -87,7 +91,7 @@ const domainCommand = async (rest, values, { projectDir }) => {
|
|
|
87
91
|
const result = await changeDomain(rest, values, projectDir);
|
|
88
92
|
console.log(
|
|
89
93
|
result
|
|
90
|
-
? `site: ${result.site ? `https://${result.site}` : "(workers.dev)"}\nstudio: ${result.studio ? `https://${result.studio}` : "(workers.dev)"}`
|
|
94
|
+
? `site: ${result.site ? `https://${result.site}` : "(workers.dev)"}\nstudio: ${result.studio ? `https://${result.studio}` : "(workers.dev)"}${result.redirects ? `\n301: ${result.redirects.join(", ")} → https://${result.site}` : ""}`
|
|
91
95
|
: "no custom domains; the Workers answer on workers.dev",
|
|
92
96
|
);
|
|
93
97
|
if (rest[0])
|
package/dist/src/cli/help.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** `cloudhal --help`; every command it names has an entry in `COMMANDS` (cli.ts). */
|
|
2
|
-
export declare const HELP = "cloudhal \u2014 agent-native sites\n\nUsage:\n cloudhal init <dir> [--name <name>] [--starter blog] create a site project with a seeded local store\n cloudhal dev [--port <n>] [--host] live preview (drafts visible)\n cloudhal snapshot [--label <text>] [--publish] freeze the current graph (optionally publish)\n cloudhal build [--snapshot <id>] static HTML from the published/latest snapshot\n cloudhal studio [--port <n>] [--no-auth] API + Studio UI for this project (default :8787)\n cloudhal mcp [--mode plan|manual|edits|auto|bypass] [--scopes a,b] [--skills <dir>] [--preview-url <url>]\n MCP server over stdio for this project\n cloudhal deploy [--name <base>] [--dry-run] [--jurisdiction eu] [--scheduler cron|alarm] [--plan free|paid]\n [--force-downgrade] [--no-build-studio]\n site + admin Workers on Cloudflare (uses wrangler login);\n --jurisdiction eu keeps a newly created D1 database\n in the EU (cannot be changed later); --scheduler picks\n the minute cron trigger or the Durable Object alarm;\n --plan sizes scheduler ticks for the account's Workers\n plan (default free; remembered);\n --force-downgrade deploys older schedules code over\n open releases and time gates; from the monorepo a\n stale apps/studio/dist is rebuilt first\n (--no-build-studio: fail instead)\n cloudhal domain [set <hostname>] [--studio <hostname>] | remove [site|studio|all]\n custom domains for the next deploy\n cloudhal push [--replace] [--cancel-schedules] [--admin-url <url>]\n local site \u2192 deployment (refused while releases are\n open unless --cancel-schedules cancels them)\n cloudhal pull [--replace] [--admin-url <url>] deployment \u2192 local site\n cloudhal owner transfer --to <email> [--admin-url <url>]\n make a member the deployment's Owner\n (lockout recovery; uses the deployment token)\n cloudhal site list | create --name <name> --slug <slug> [--locale <tag>] [--project <id>] [--theme <name>] [--from <siteId>]\n | delete <siteId> --confirm <slug> [--remote] [--admin-url <url>]\n the installation's sites (RFC-0034): one Studio,\n many isolated sites; nothing is published\n cloudhal migrate tags-to-terms [--dry-run] [--revert <id>]\n move entry tags into tag terms (RFC-0002);\n resumable; --revert <id> undoes a run\n cloudhal media import-urls [--dry-run] [--include-markdown] [--allow-host <host>]\u2026 | --revert <id>\n copy linked images into the media library\n (RFC-0004); revertible\n cloudhal media verify [<assetId>] recompute media checksums\n cloudhal media gc [--dry-run] delete unreferenced media bytes (> 24 h old)\n cloudhal media placeholders [--remote] [--all] [--dry-run]\n draw missing image placeholders in local Chrome\n cloudhal analytics status [--remote] site analytics: settings, daily totals, budget\n cloudhal analytics rollup [--days <n>] [--remote] [--redo]\n compute daily totals (--remote backfills the\n deployment from WAE, up to 92 days)\n cloudhal analytics seed [--days <n>] [--views <n>] demo traffic in the local SQLite only\n cloudhal analytics export [--from <date> --to <date>] [--remote] [--out <file>]\n daily totals as CSV (default: 30 days)\n cloudhal comments status [--remote] reader comments: counts, pending, daily cap, purges\n cloudhal comments export [--remote] [--with-personal-data --out <file>]\n comments as JSON lines (stdout or --out)\n cloudhal comments import --in <file> [--remote] JSON lines back in; idempotent, safe to rerun\n cloudhal schedules status [--remote] the scheduler's health, open releases, gated pages\n cloudhal schedules list [--remote] [--limit <n>] the site's releases\n cloudhal schedules run-due [--remote] one scheduler tick now (self-hosting: run it from\n the system's cron every minute)\n cloudhal ai build-eval [--remote] [--models a,b] [--briefs landing,faq] [--out <file>]\n [--design] [--look] [--judge <model>|none]\n compare models on Build chat's turn, in memory\n (real model calls; RFC-0020 open question 7);\n --design renders each result: failed checks and\n a vision model's score (RFC-0021 21c); --look lets\n the model see its render first, as Build chat does\n cloudhal ai judge-check [--remote] [--judge <model>] score the reference pages (empty \u2192 strong) with the\n design judge; fails when it does not rank them\n cloudhal render <path|url> [--width 1440,390] [--scheme dark] [--motion static|reduce|full]\n [--frames <n>] [--base <url>] [--out <dir>]\n screenshots and render_page's checks for a page\n (of cloudhal dev by default; RFC-0024)\n cloudhal motion audit [--path <p>] the motion each page asks for, against its budgets\n cloudhal tokens export [--format dtcg] [--out <file>] the site's design tokens as a W3C Design Tokens file\n (Figma, Style Dictionary; RFC-0023)\n cloudhal chrome get [--file <f>] | set --file <f> the site's header, footer and brand as JSON\n (RFC-0025)\n cloudhal search reindex [--remote] rebuild the search index from the live snapshot\n (publishing keeps it current; for restored backups)\n cloudhal --help\n";
|
|
2
|
+
export declare const HELP = "cloudhal \u2014 agent-native sites\n\nUsage:\n cloudhal init <dir> [--name <name>] [--starter blog] create a site project with a seeded local store\n cloudhal dev [--port <n>] [--host] live preview (drafts visible)\n cloudhal snapshot [--label <text>] [--publish] freeze the current graph (optionally publish)\n cloudhal build [--snapshot <id>] static HTML from the published/latest snapshot\n cloudhal studio [--port <n>] [--no-auth] API + Studio UI for this project (default :8787)\n cloudhal mcp [--mode plan|manual|edits|auto|bypass] [--scopes a,b] [--skills <dir>] [--preview-url <url>]\n MCP server over stdio for this project\n cloudhal deploy [--name <base>] [--dry-run] [--jurisdiction eu] [--scheduler cron|alarm] [--plan free|paid]\n [--force-downgrade] [--no-build-studio]\n site + admin Workers on Cloudflare (uses wrangler login);\n --jurisdiction eu keeps a newly created D1 database\n in the EU (cannot be changed later); --scheduler picks\n the minute cron trigger or the Durable Object alarm;\n --plan sizes scheduler ticks for the account's Workers\n plan (default free; remembered);\n --force-downgrade deploys older schedules code over\n open releases and time gates; from the monorepo a\n stale apps/studio/dist is rebuilt first\n (--no-build-studio: fail instead)\n cloudhal domain [set <hostname>] [--studio <hostname>] [--redirect <hostname>]\u2026 | remove [site|studio|redirects|all]\n custom domains for the next deploy\n cloudhal push [--replace] [--cancel-schedules] [--admin-url <url>]\n local site \u2192 deployment (refused while releases are\n open unless --cancel-schedules cancels them)\n cloudhal pull [--replace] [--admin-url <url>] deployment \u2192 local site\n cloudhal owner transfer --to <email> [--admin-url <url>]\n make a member the deployment's Owner\n (lockout recovery; uses the deployment token)\n cloudhal site list | create --name <name> --slug <slug> [--locale <tag>] [--project <id>] [--theme <name>] [--from <siteId>]\n | delete <siteId> --confirm <slug> [--remote] [--admin-url <url>]\n the installation's sites (RFC-0034): one Studio,\n many isolated sites; nothing is published\n cloudhal migrate tags-to-terms [--dry-run] [--revert <id>]\n move entry tags into tag terms (RFC-0002);\n resumable; --revert <id> undoes a run\n cloudhal media import-urls [--dry-run] [--include-markdown] [--allow-host <host>]\u2026 | --revert <id>\n copy linked images into the media library\n (RFC-0004); revertible\n cloudhal media verify [<assetId>] recompute media checksums\n cloudhal media gc [--dry-run] delete unreferenced media bytes (> 24 h old)\n cloudhal media placeholders [--remote] [--all] [--dry-run]\n draw missing image placeholders in local Chrome\n cloudhal analytics status [--remote] site analytics: settings, daily totals, budget\n cloudhal analytics rollup [--days <n>] [--remote] [--redo]\n compute daily totals (--remote backfills the\n deployment from WAE, up to 92 days)\n cloudhal analytics seed [--days <n>] [--views <n>] demo traffic in the local SQLite only\n cloudhal analytics export [--from <date> --to <date>] [--remote] [--out <file>]\n daily totals as CSV (default: 30 days)\n cloudhal comments status [--remote] reader comments: counts, pending, daily cap, purges\n cloudhal comments export [--remote] [--with-personal-data --out <file>]\n comments as JSON lines (stdout or --out)\n cloudhal comments import --in <file> [--remote] JSON lines back in; idempotent, safe to rerun\n cloudhal schedules status [--remote] the scheduler's health, open releases, gated pages\n cloudhal schedules list [--remote] [--limit <n>] the site's releases\n cloudhal schedules run-due [--remote] one scheduler tick now (self-hosting: run it from\n the system's cron every minute)\n cloudhal ai build-eval [--remote] [--models a,b] [--briefs landing,faq] [--out <file>]\n [--design] [--look] [--judge <model>|none]\n compare models on Build chat's turn, in memory\n (real model calls; RFC-0020 open question 7);\n --design renders each result: failed checks and\n a vision model's score (RFC-0021 21c); --look lets\n the model see its render first, as Build chat does\n cloudhal ai judge-check [--remote] [--judge <model>] score the reference pages (empty \u2192 strong) with the\n design judge; fails when it does not rank them\n cloudhal render <path|url> [--width 1440,390] [--scheme dark] [--motion static|reduce|full]\n [--frames <n>] [--base <url>] [--out <dir>]\n screenshots and render_page's checks for a page\n (of cloudhal dev by default; RFC-0024)\n cloudhal motion audit [--path <p>] the motion each page asks for, against its budgets\n cloudhal tokens export [--format dtcg] [--out <file>] the site's design tokens as a W3C Design Tokens file\n (Figma, Style Dictionary; RFC-0023)\n cloudhal chrome get [--file <f>] | set --file <f> the site's header, footer and brand as JSON\n (RFC-0025)\n cloudhal search reindex [--remote] rebuild the search index from the live snapshot\n (publishing keeps it current; for restored backups)\n cloudhal --help\n";
|
package/dist/src/cli/help.js
CHANGED
|
@@ -21,7 +21,7 @@ Usage:
|
|
|
21
21
|
open releases and time gates; from the monorepo a
|
|
22
22
|
stale apps/studio/dist is rebuilt first
|
|
23
23
|
(--no-build-studio: fail instead)
|
|
24
|
-
cloudhal domain [set <hostname>] [--studio <hostname>] | remove [site|studio|all]
|
|
24
|
+
cloudhal domain [set <hostname>] [--studio <hostname>] [--redirect <hostname>]… | remove [site|studio|redirects|all]
|
|
25
25
|
custom domains for the next deploy
|
|
26
26
|
cloudhal push [--replace] [--cancel-schedules] [--admin-url <url>]
|
|
27
27
|
local site → deployment (refused while releases are
|
package/dist/src/cli.d.ts
CHANGED
package/dist/src/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ export const OPTIONS = {
|
|
|
26
26
|
replace: { type: "boolean" },
|
|
27
27
|
"admin-url": { type: "string" },
|
|
28
28
|
studio: { type: "string" },
|
|
29
|
+
redirect: { type: "string", multiple: true },
|
|
29
30
|
revert: { type: "string" },
|
|
30
31
|
to: { type: "string" },
|
|
31
32
|
"include-markdown": { type: "boolean" },
|
|
@@ -102,6 +102,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
102
102
|
domain?: string;
|
|
103
103
|
/** RFC-0034 §5: every other site's hostnames, one Custom Domain route each. */
|
|
104
104
|
domains?: readonly string[];
|
|
105
|
+
/** Hosts that answer with a 301 to `domain` (#49), each routed as a Custom Domain too. */
|
|
106
|
+
redirects?: readonly string[];
|
|
105
107
|
analytics?: AnalyticsDeployment;
|
|
106
108
|
comments?: CommentsDeployment;
|
|
107
109
|
/** Extra hostnames the site answers on (custom domains): analytics and comments check them. */
|
|
@@ -150,6 +152,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
150
152
|
CLOUDHAL_ANALYTICS?: string;
|
|
151
153
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
152
154
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
155
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
156
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
153
157
|
} | {
|
|
154
158
|
CLOUDHAL_SEARCH_PROVIDER?: never;
|
|
155
159
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT?: never;
|
|
@@ -157,6 +161,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
157
161
|
CLOUDHAL_ANALYTICS?: string;
|
|
158
162
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
159
163
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
164
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
165
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
160
166
|
} | {
|
|
161
167
|
CLOUDHAL_SEARCH_PROVIDER: string;
|
|
162
168
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT: string;
|
|
@@ -164,6 +170,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
164
170
|
CLOUDHAL_ANALYTICS?: string;
|
|
165
171
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
166
172
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
173
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
174
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
167
175
|
} | {
|
|
168
176
|
CLOUDHAL_SEARCH_PROVIDER?: never;
|
|
169
177
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT?: never;
|
|
@@ -171,6 +179,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
171
179
|
CLOUDHAL_ANALYTICS?: string;
|
|
172
180
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
173
181
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
182
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
183
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
174
184
|
};
|
|
175
185
|
cache: {
|
|
176
186
|
enabled: boolean;
|
|
@@ -223,6 +233,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
223
233
|
CLOUDHAL_ANALYTICS?: string;
|
|
224
234
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
225
235
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
236
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
237
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
226
238
|
} | {
|
|
227
239
|
CLOUDHAL_SEARCH_PROVIDER?: never;
|
|
228
240
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT?: never;
|
|
@@ -230,6 +242,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
230
242
|
CLOUDHAL_ANALYTICS?: string;
|
|
231
243
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
232
244
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
245
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
246
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
233
247
|
} | {
|
|
234
248
|
CLOUDHAL_SEARCH_PROVIDER: string;
|
|
235
249
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT: string;
|
|
@@ -237,6 +251,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
237
251
|
CLOUDHAL_ANALYTICS?: string;
|
|
238
252
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
239
253
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
254
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
255
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
240
256
|
} | {
|
|
241
257
|
CLOUDHAL_SEARCH_PROVIDER?: never;
|
|
242
258
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT?: never;
|
|
@@ -244,6 +260,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
244
260
|
CLOUDHAL_ANALYTICS?: string;
|
|
245
261
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
246
262
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
263
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
264
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
247
265
|
};
|
|
248
266
|
cache: {
|
|
249
267
|
enabled: boolean;
|
|
@@ -296,6 +314,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
296
314
|
CLOUDHAL_ANALYTICS?: string;
|
|
297
315
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
298
316
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
317
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
318
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
299
319
|
} | {
|
|
300
320
|
CLOUDHAL_SEARCH_PROVIDER?: never;
|
|
301
321
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT?: never;
|
|
@@ -303,6 +323,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
303
323
|
CLOUDHAL_ANALYTICS?: string;
|
|
304
324
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
305
325
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
326
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
327
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
306
328
|
} | {
|
|
307
329
|
CLOUDHAL_SEARCH_PROVIDER: string;
|
|
308
330
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT: string;
|
|
@@ -310,6 +332,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
310
332
|
CLOUDHAL_ANALYTICS?: string;
|
|
311
333
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
312
334
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
335
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
336
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
313
337
|
} | {
|
|
314
338
|
CLOUDHAL_SEARCH_PROVIDER?: never;
|
|
315
339
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT?: never;
|
|
@@ -317,6 +341,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
317
341
|
CLOUDHAL_ANALYTICS?: string;
|
|
318
342
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
319
343
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
344
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
345
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
320
346
|
};
|
|
321
347
|
cache: {
|
|
322
348
|
enabled: boolean;
|
|
@@ -366,6 +392,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
366
392
|
CLOUDHAL_ANALYTICS?: string;
|
|
367
393
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
368
394
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
395
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
396
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
369
397
|
} | {
|
|
370
398
|
CLOUDHAL_SEARCH_PROVIDER?: never;
|
|
371
399
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT?: never;
|
|
@@ -373,6 +401,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
373
401
|
CLOUDHAL_ANALYTICS?: string;
|
|
374
402
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
375
403
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
404
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
405
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
376
406
|
} | {
|
|
377
407
|
CLOUDHAL_SEARCH_PROVIDER: string;
|
|
378
408
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT: string;
|
|
@@ -380,6 +410,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
380
410
|
CLOUDHAL_ANALYTICS?: string;
|
|
381
411
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
382
412
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
413
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
414
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
383
415
|
} | {
|
|
384
416
|
CLOUDHAL_SEARCH_PROVIDER?: never;
|
|
385
417
|
CLOUDHAL_SEARCH_MONTHLY_LIMIT?: never;
|
|
@@ -387,6 +419,8 @@ export declare function siteWorkerConfig(n: DeploymentNames & {
|
|
|
387
419
|
CLOUDHAL_ANALYTICS?: string;
|
|
388
420
|
CLOUDHAL_ANALYTICS_DEFAULTS?: string;
|
|
389
421
|
CLOUDHAL_SITE_HOSTS?: string;
|
|
422
|
+
CLOUDHAL_REDIRECT_HOSTS?: string;
|
|
423
|
+
CLOUDHAL_REDIRECT_TO?: string;
|
|
390
424
|
};
|
|
391
425
|
cache: {
|
|
392
426
|
enabled: boolean;
|
|
@@ -180,6 +180,8 @@ export function siteWorkerConfig(
|
|
|
180
180
|
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
|
|
184
|
+
|
|
183
185
|
|
|
184
186
|
|
|
185
187
|
|
|
@@ -191,6 +193,7 @@ export function siteWorkerConfig(
|
|
|
191
193
|
const a = n.analytics;
|
|
192
194
|
const c = n.comments?.available ? n.comments : undefined;
|
|
193
195
|
const hosts = [...new Set([...(n.siteHosts ?? []), ...(a?.siteHosts ?? [])])];
|
|
196
|
+
const redirects = n.domain ? (n.redirects ?? []) : [];
|
|
194
197
|
const ratelimits = [
|
|
195
198
|
// Optional cross-isolate beacon limit (RFC-0006 §5.5): one key per client (an HMAC, never the IP).
|
|
196
199
|
...(a?.rateLimit
|
|
@@ -237,8 +240,11 @@ export function siteWorkerConfig(
|
|
|
237
240
|
: {}),
|
|
238
241
|
// RFC-0006 §5.2 / RFC-0007 §6.2 step 2: the same-origin checks accept these hosts too.
|
|
239
242
|
...((a || c) && hosts.length > 0 ? { CLOUDHAL_SITE_HOSTS: hosts.join(",") } : {}),
|
|
243
|
+
...(n.domain && redirects.length > 0
|
|
244
|
+
? { CLOUDHAL_REDIRECT_HOSTS: redirects.join(","), CLOUDHAL_REDIRECT_TO: n.domain }
|
|
245
|
+
: {}),
|
|
240
246
|
},
|
|
241
|
-
...customDomain(n.domain, n.domains),
|
|
247
|
+
...customDomain(n.domain, [...(n.domains ?? []), ...redirects]),
|
|
242
248
|
// Workers Cache: published pages are cached until a publish purges `cloudhal-site-<id>`.
|
|
243
249
|
cache: { enabled: true },
|
|
244
250
|
observability: observabilitySettings(n.observability),
|
|
@@ -292,6 +292,7 @@ export async function buildSite(
|
|
|
292
292
|
...(served.siteIds ? { siteIds: served.siteIds } : {}),
|
|
293
293
|
...(served.hosts.length > 0 ? { domains: served.hosts } : {}),
|
|
294
294
|
...(cfg.domains?.site ? { domain: cfg.domains.site, siteHosts: [cfg.domains.site] } : {}),
|
|
295
|
+
...(cfg.domains?.redirects ? { redirects: cfg.domains.redirects } : {}),
|
|
295
296
|
...(input.analytics ? { analytics: input.analytics } : {}),
|
|
296
297
|
comments: input.comments,
|
|
297
298
|
observability: input.observability,
|
|
@@ -24,5 +24,15 @@ export declare const BUNDLE_LIFECYCLE_DAYS = 30;
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function ensureBundleLifecycle(run: WranglerRunner, cwd: string, bucket: string, log: (l: string) => void): Promise<void>;
|
|
26
26
|
export declare function waitForHealth(adminUrl: string, f: typeof fetch, log: (line: string) => void, timeoutMs: number): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Runs `step` again while it fails with 401. Right after a deploy the edge can still answer from
|
|
29
|
+
* the previous admin Worker version, which holds the previous deployment token (Workers Builds
|
|
30
|
+
* makes a new one on every build), for a few seconds. Gives up after `timeoutMs`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function whileTokenPropagates<T>(step: () => Promise<T>, options: {
|
|
33
|
+
log: (line: string) => void;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
waitMs?: number;
|
|
36
|
+
}): Promise<T>;
|
|
27
37
|
/** Purges the site Worker's whole cache once (the script tag lives in cached HTML). */
|
|
28
38
|
export declare function purgeSite(siteUrl: string, token: string, f: typeof fetch, log: (line: string) => void): Promise<boolean>;
|
|
@@ -139,6 +139,31 @@ export async function waitForHealth(
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Runs `step` again while it fails with 401. Right after a deploy the edge can still answer from
|
|
144
|
+
* the previous admin Worker version, which holds the previous deployment token (Workers Builds
|
|
145
|
+
* makes a new one on every build), for a few seconds. Gives up after `timeoutMs`.
|
|
146
|
+
*/
|
|
147
|
+
export async function whileTokenPropagates (
|
|
148
|
+
step ,
|
|
149
|
+
options ,
|
|
150
|
+
) {
|
|
151
|
+
const deadline = Date.now() + (options.timeoutMs ?? 90_000);
|
|
152
|
+
let announced = false;
|
|
153
|
+
for (;;) {
|
|
154
|
+
try {
|
|
155
|
+
return await step();
|
|
156
|
+
} catch (e) {
|
|
157
|
+
if (!/\(401\)/.test(e instanceof Error ? e.message : "") || Date.now() > deadline) throw e;
|
|
158
|
+
if (!announced) {
|
|
159
|
+
options.log("the deployment does not accept the new token yet; retrying while it spreads…");
|
|
160
|
+
announced = true;
|
|
161
|
+
}
|
|
162
|
+
await new Promise((r) => setTimeout(r, options.waitMs ?? 5000));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
142
167
|
/** Purges the site Worker's whole cache once (the script tag lives in cached HTML). */
|
|
143
168
|
export async function purgeSite(
|
|
144
169
|
siteUrl ,
|
|
@@ -14,7 +14,7 @@ import { openLocalStore } from "@cloudhal/core-ts/local";
|
|
|
14
14
|
import { deployedUrls, isCronLimitError, must, WranglerError } from "../../cloudflare/wrangler.js";
|
|
15
15
|
import { pushSite, setPrimaryDomain } from "../sync.js";
|
|
16
16
|
|
|
17
|
-
import { purgeSite, waitForHealth } from "./resources.js";
|
|
17
|
+
import { purgeSite, waitForHealth, whileTokenPropagates } from "./resources.js";
|
|
18
18
|
import { CRON_REFUSED_MESSAGE } from "./schedules-guard.js";
|
|
19
19
|
|
|
20
20
|
|
|
@@ -111,29 +111,35 @@ export async function afterDeploy(
|
|
|
111
111
|
});
|
|
112
112
|
await l.close();
|
|
113
113
|
}
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
log,
|
|
118
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
119
|
-
});
|
|
120
|
-
// A custom domain becomes the site's primary hostname (the workers.dev one stays secondary).
|
|
121
|
-
if (cfg.domains?.site) {
|
|
122
|
-
await setPrimaryDomain({
|
|
114
|
+
const retry = (step ) => whileTokenPropagates(step, { log });
|
|
115
|
+
const result = await retry(() =>
|
|
116
|
+
pushSite({
|
|
123
117
|
projectDir: options.projectDir,
|
|
124
|
-
|
|
118
|
+
ifAbsent: true,
|
|
119
|
+
log,
|
|
125
120
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
// A custom domain becomes the site's primary hostname (the workers.dev one stays secondary).
|
|
124
|
+
const domain = cfg.domains?.site;
|
|
125
|
+
if (domain) {
|
|
126
|
+
await retry(() =>
|
|
127
|
+
setPrimaryDomain({
|
|
128
|
+
projectDir: options.projectDir,
|
|
129
|
+
hostname: domain,
|
|
130
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
131
|
+
}),
|
|
129
132
|
);
|
|
133
|
+
log(`site domain: https://${domain} (Cloudflare issues the certificate on first use)`);
|
|
130
134
|
} else if (previous?.siteDomain && siteUrl) {
|
|
131
135
|
// The custom domain was removed from cloudhal.json: workers.dev is primary again.
|
|
132
|
-
await
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
await retry(() =>
|
|
137
|
+
setPrimaryDomain({
|
|
138
|
+
projectDir: options.projectDir,
|
|
139
|
+
hostname: new URL(siteUrl).hostname,
|
|
140
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
137
143
|
log(
|
|
138
144
|
`site domain ${previous.siteDomain} removed from cloudhal.json; if Cloudflare still lists it under the Worker's Domains & Routes, remove it there`,
|
|
139
145
|
);
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { type ProjectConfig } from "../config.js";
|
|
2
|
-
export type DomainTarget = "site" | "studio";
|
|
2
|
+
export type DomainTarget = "site" | "studio" | "redirects";
|
|
3
3
|
export interface DomainOptions {
|
|
4
4
|
readonly projectDir: string;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Hostnames to set (others stay as they are). `redirects` replaces the list of hosts that
|
|
7
|
+
* answer with a 301 to the site's domain (#49).
|
|
8
|
+
*/
|
|
6
9
|
readonly set?: {
|
|
7
10
|
readonly site?: string;
|
|
8
11
|
readonly studio?: string;
|
|
12
|
+
readonly redirects?: readonly string[];
|
|
9
13
|
};
|
|
10
14
|
/** Domains to forget. */
|
|
11
15
|
readonly remove?: readonly DomainTarget[];
|
|
@@ -2,16 +2,28 @@
|
|
|
2
2
|
* `cloudhal domain` — the custom domains `cloudhal deploy` routes to this project's Workers
|
|
3
3
|
* (Cloudflare Custom Domains: the zone must be in the same Cloudflare account; wrangler creates
|
|
4
4
|
* the DNS record and certificate). Kept in cloudhal.json so the setting travels with the project.
|
|
5
|
+
* The site's own hostname also becomes its primary domain in the local store, so `cloudhal build`
|
|
6
|
+
* writes it into canonical links, the sitemap and feeds before any deploy (#48); `deploy` does the
|
|
7
|
+
* same on the deployment.
|
|
5
8
|
*/
|
|
6
|
-
import
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { isHostname, normalizeHostname, withPrimaryDomain } from "@cloudhal/core-ts";
|
|
11
|
+
import { openLocalStore } from "@cloudhal/core-ts/local";
|
|
7
12
|
import { readProjectConfig, writeProjectConfig } from "../config.js";
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
15
27
|
|
|
16
28
|
|
|
17
29
|
|
|
@@ -27,18 +39,47 @@ export function checkHostname(input ) {
|
|
|
27
39
|
|
|
28
40
|
export async function domain(options ) {
|
|
29
41
|
const cfg = await readProjectConfig(options.projectDir);
|
|
30
|
-
const next
|
|
42
|
+
const next = {
|
|
43
|
+
...cfg.domains,
|
|
44
|
+
};
|
|
31
45
|
for (const target of options.remove ?? []) delete next[target];
|
|
46
|
+
// redirects go to the site's domain: without it they have nowhere to go
|
|
47
|
+
if (options.remove?.includes("site")) delete next.redirects;
|
|
32
48
|
if (options.set?.site) next.site = checkHostname(options.set.site);
|
|
33
49
|
if (options.set?.studio) next.studio = checkHostname(options.set.studio);
|
|
50
|
+
if (options.set?.redirects)
|
|
51
|
+
next.redirects = [...new Set(options.set.redirects.map(checkHostname))];
|
|
34
52
|
if (next.site && next.site === next.studio)
|
|
35
53
|
throw new Error("the site and Studio need different hostnames");
|
|
54
|
+
checkRedirects(next);
|
|
55
|
+
if (!next.redirects?.length) delete next.redirects;
|
|
56
|
+
const any = next.site || next.studio || next.redirects;
|
|
36
57
|
if (options.set || options.remove) {
|
|
37
58
|
const { domains: _old, ...rest } = cfg;
|
|
38
|
-
await writeProjectConfig(options.projectDir, {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
await writeProjectConfig(options.projectDir, { ...rest, ...(any ? { domains: next } : {}) });
|
|
60
|
+
}
|
|
61
|
+
if (next.site && options.set?.site) await makePrimary(options.projectDir, cfg, next.site);
|
|
62
|
+
return any ? next : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Redirect hosts need a site domain to go to, and are neither it nor Studio's. */
|
|
66
|
+
function checkRedirects(d ) {
|
|
67
|
+
if (!d.redirects?.length) return;
|
|
68
|
+
if (!d.site)
|
|
69
|
+
throw new Error("redirects need the site's own domain: cloudhal domain set <hostname>");
|
|
70
|
+
for (const host of d.redirects)
|
|
71
|
+
if (host === d.site || host === d.studio)
|
|
72
|
+
throw new Error(`${host} is already the site's or Studio's domain; it cannot redirect`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `hostname` becomes the local site's primary domain (other domains stay, as secondary). */
|
|
76
|
+
async function makePrimary(projectDir , cfg , hostname ) {
|
|
77
|
+
const local = await openLocalStore(path.join(projectDir, cfg.dataDir));
|
|
78
|
+
try {
|
|
79
|
+
const site = await local.store.getSite(cfg.siteId);
|
|
80
|
+
if (site && site.domains.find((d) => d.primary)?.hostname !== hostname)
|
|
81
|
+
await local.store.updateSite({ ...site, domains: withPrimaryDomain(site.domains, hostname) });
|
|
82
|
+
} finally {
|
|
83
|
+
await local.close();
|
|
42
84
|
}
|
|
43
|
-
return next.site || next.studio ? next : undefined;
|
|
44
85
|
}
|
|
@@ -4,7 +4,15 @@ export interface SnapshotOptions {
|
|
|
4
4
|
readonly label?: string;
|
|
5
5
|
readonly publish?: boolean;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
8
|
-
export
|
|
7
|
+
/** A snapshot, or (`unchanged`) the live one when publishing would change nothing. */
|
|
8
|
+
export type SnapshotResult = Snapshot & {
|
|
9
|
+
readonly unchanged?: true;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Creates a snapshot of the live site (and optionally publishes it). Publishing a site whose
|
|
13
|
+
* graph matches its live snapshot makes no new one (#51): re-running a setup script does not pad
|
|
14
|
+
* the history with empty snapshots.
|
|
15
|
+
*/
|
|
16
|
+
export declare function snapshot(options: SnapshotOptions): Promise<SnapshotResult>;
|
|
9
17
|
/** The snapshot `cloudhal build` renders: the published one, else the latest, else a fresh one. */
|
|
10
18
|
export declare function resolveBuildSnapshot(projectDir: string): Promise<Snapshot>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { openLocalStore } from "@cloudhal/core-ts/local";
|
|
4
4
|
import { readProjectConfig } from "../config.js";
|
|
5
5
|
|
|
6
6
|
|
|
@@ -9,11 +9,22 @@ import { readProjectConfig } from "../config.js";
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/** A snapshot, or (`unchanged`) the live one when publishing would change nothing. */
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a snapshot of the live site (and optionally publishes it). Publishing a site whose
|
|
17
|
+
* graph matches its live snapshot makes no new one (#51): re-running a setup script does not pad
|
|
18
|
+
* the history with empty snapshots.
|
|
19
|
+
*/
|
|
20
|
+
export async function snapshot(options ) {
|
|
14
21
|
const cfg = await readProjectConfig(options.projectDir);
|
|
15
22
|
const local = await openLocalStore(path.join(options.projectDir, cfg.dataDir));
|
|
16
23
|
try {
|
|
24
|
+
if (options.publish) {
|
|
25
|
+
const live = await unchangedLive(local.store, cfg.siteId);
|
|
26
|
+
if (live) return { ...live, unchanged: true };
|
|
27
|
+
}
|
|
17
28
|
const snap = await local.store.createSnapshot(cfg.siteId, {
|
|
18
29
|
actor: { kind: "human", id: "user_localcli0001", name: "cloudhal CLI" },
|
|
19
30
|
reason: options.publish ? "publish" : "manual",
|
|
@@ -30,6 +41,18 @@ export async function snapshot(options ) {
|
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
/** The live snapshot when the site has one and nothing is pending against it. */
|
|
45
|
+
async function unchangedLive(
|
|
46
|
+
store ,
|
|
47
|
+
siteId ,
|
|
48
|
+
) {
|
|
49
|
+
const site = await store.getSite(siteId);
|
|
50
|
+
if (!site?.publishedSnapshotId) return undefined;
|
|
51
|
+
const d = await store.pendingChanges(siteId);
|
|
52
|
+
if (d.added.length || d.removed.length || d.changed.length) return undefined;
|
|
53
|
+
return (await store.getSnapshot(site.publishedSnapshotId)) ?? undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
/** The snapshot `cloudhal build` renders: the published one, else the latest, else a fresh one. */
|
|
34
57
|
export async function resolveBuildSnapshot(projectDir ) {
|
|
35
58
|
const cfg = await readProjectConfig(projectDir);
|
package/dist/src/config.d.ts
CHANGED
|
@@ -104,11 +104,13 @@ export interface ProjectConfig {
|
|
|
104
104
|
readonly dataDir: string;
|
|
105
105
|
/**
|
|
106
106
|
* Custom domains `cloudhal deploy` routes to the Workers (Cloudflare Custom Domains; the zone
|
|
107
|
-
* must be in the same account). The workers.dev addresses keep working.
|
|
107
|
+
* must be in the same account). The workers.dev addresses keep working. `redirects` answer with
|
|
108
|
+
* a 301 to `site` (#49), e.g. `www.example.com`.
|
|
108
109
|
*/
|
|
109
110
|
readonly domains?: {
|
|
110
111
|
readonly site?: string;
|
|
111
112
|
readonly studio?: string;
|
|
113
|
+
readonly redirects?: readonly string[];
|
|
112
114
|
};
|
|
113
115
|
/** Site analytics (RFC-0006); absent = off. */
|
|
114
116
|
readonly analytics?: AnalyticsConfig;
|
package/dist/src/config.js
CHANGED
|
@@ -117,9 +117,14 @@ const MAIL_ADDRESS = /^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/;
|
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
|
|
121
|
+
|
|
121
122
|
|
|
122
|
-
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
123
128
|
|
|
124
129
|
|
|
125
130
|
|
|
@@ -167,6 +172,21 @@ const OWN_KEYS = new Set([
|
|
|
167
172
|
|
|
168
173
|
const VITALS = [0, 1, 2, 5, 10, 20];
|
|
169
174
|
|
|
175
|
+
/** `domains`, leniently: values of the wrong type are left out. */
|
|
176
|
+
function parseDomains(input ) {
|
|
177
|
+
if (!input || typeof input !== "object") return undefined;
|
|
178
|
+
const d = input ;
|
|
179
|
+
const redirects = Array.isArray(d.redirects)
|
|
180
|
+
? d.redirects.filter((h) => typeof h === "string" && h !== "")
|
|
181
|
+
: [];
|
|
182
|
+
const out = {
|
|
183
|
+
...(typeof d.site === "string" && d.site ? { site: d.site } : {}),
|
|
184
|
+
...(typeof d.studio === "string" && d.studio ? { studio: d.studio } : {}),
|
|
185
|
+
...(redirects.length > 0 ? { redirects } : {}),
|
|
186
|
+
};
|
|
187
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
170
190
|
/** Leniently parsed, like `domains`: values of the wrong type are left out. */
|
|
171
191
|
export function parseAnalyticsConfig(input ) {
|
|
172
192
|
if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
|
|
@@ -297,7 +317,7 @@ export async function readProjectConfig(projectDir )
|
|
|
297
317
|
}
|
|
298
318
|
const cfg = JSON.parse(raw) ;
|
|
299
319
|
if (typeof cfg.siteId !== "string") throw new Error(`${CONFIG_FILE}: missing siteId`);
|
|
300
|
-
const domains = cfg.domains
|
|
320
|
+
const domains = parseDomains(cfg.domains);
|
|
301
321
|
const analytics = parseAnalyticsConfig(cfg.analytics);
|
|
302
322
|
const comments = parseCommentsConfig(cfg.comments);
|
|
303
323
|
const schedules = parseSchedulesConfig(cfg.schedules);
|
|
@@ -314,14 +334,7 @@ export async function readProjectConfig(projectDir )
|
|
|
314
334
|
? { installation: cfg.installation }
|
|
315
335
|
: {}),
|
|
316
336
|
dataDir: cfg.dataDir ?? ".cloudhal",
|
|
317
|
-
...(domains
|
|
318
|
-
? {
|
|
319
|
-
domains: {
|
|
320
|
-
...(typeof domains.site === "string" ? { site: domains.site } : {}),
|
|
321
|
-
...(typeof domains.studio === "string" ? { studio: domains.studio } : {}),
|
|
322
|
-
},
|
|
323
|
-
}
|
|
324
|
-
: {}),
|
|
337
|
+
...(domains ? { domains } : {}),
|
|
325
338
|
...(analytics ? { analytics } : {}),
|
|
326
339
|
...(comments ? { comments } : {}),
|
|
327
340
|
...(schedules ? { schedules } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudhal/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "cloudhal — create, run and build Cloudhal sites locally",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://cloudhal.dev",
|
|
@@ -27,16 +27,16 @@
|
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cloudhal/agent-runtime": "0.3.
|
|
31
|
-
"@cloudhal/analytics": "0.3.
|
|
32
|
-
"@cloudhal/api": "0.3.
|
|
33
|
-
"@cloudhal/cloudflare": "0.3.
|
|
34
|
-
"@cloudhal/comments": "0.3.
|
|
35
|
-
"@cloudhal/core-contracts": "0.3.
|
|
36
|
-
"@cloudhal/core-ts": "0.3.
|
|
37
|
-
"@cloudhal/mcp-server": "0.3.
|
|
38
|
-
"@cloudhal/schedules": "0.3.
|
|
39
|
-
"@cloudhal/storage": "0.3.
|
|
30
|
+
"@cloudhal/agent-runtime": "0.3.10",
|
|
31
|
+
"@cloudhal/analytics": "0.3.10",
|
|
32
|
+
"@cloudhal/api": "0.3.10",
|
|
33
|
+
"@cloudhal/cloudflare": "0.3.10",
|
|
34
|
+
"@cloudhal/comments": "0.3.10",
|
|
35
|
+
"@cloudhal/core-contracts": "0.3.10",
|
|
36
|
+
"@cloudhal/core-ts": "0.3.10",
|
|
37
|
+
"@cloudhal/mcp-server": "0.3.10",
|
|
38
|
+
"@cloudhal/schedules": "0.3.10",
|
|
39
|
+
"@cloudhal/storage": "0.3.10",
|
|
40
40
|
"@hono/node-server": "^2.1.4",
|
|
41
41
|
"better-sqlite3": "13.0.3"
|
|
42
42
|
},
|