Repository navigation
All @cloudhal/* packages and create-cloudhal at 0.6.2.
Changes
-
createMiniflareDoSqlite() sends its Durable Object calls over kept-alive connections instead of opening a new TCP connection per call through Miniflare 4's dispatchFetch, so long suites no longer exhaust local ports on macOS (#116).
-
Migration
0029_entry_list_values(RFC-0042 §4, #110):entry_list_values, the indexed list-field values of entries (field, presence, number, text, collection-sort key, creation time, node, locale and status) with a query index and a per-node index, andentry_list_state, each listed collection's readiness (definition fingerprint, ready flag, backfill epoch and cursor). DDL only; existing data is untouched. -
The
./testingsubpath now ships. The published packages pointed./testingatdist/src/testing.jsand.d.ts, but the build left those files out, so importing@cloudhal/{storage,analytics,comments,schedules}/testingfailed with ERR_MODULE_NOT_FOUND. The suites run on your own vitest, 3 or later, which is an optional peer dependency.@cloudhal/storage's Miniflare helpers also load your ownminiflare, an optional peer of any version. The ranges are wide on purpose. When a direct dependency's optional peer does not match the copy in your project, npm stops with ERESOLVE, even if nothing imports./testing. No semver range matches the prerelease miniflare that wrangler currently brings in, so with a narrower range npm would install a second miniflare beside it. -
importSite(behindcloudhal pull, the admin bundle routecloudhal pushuses, andcloudhal import) now checks a bundle's project row before inserting it: it must be the site node's project (projectId), a valid Project, with itsidandrevcolumns matching its body. Before, a bundle could plant any projects row. -
Dependency patch updates (#115).
@cloudhal/apidepends on Better Auth 1.7.12 and on exactly the versions it uses itself:@better-auth/coreand@better-auth/api-key1.7.12,@better-auth/utils0.5.0 andbetter-call1.4.4.@cloudhal/cloudflareasks for zod ^4.6.7 (GHSA-mrhx-c7xc-m3w5 is fixed in 4.6.6).@cloudhal/core-tsand@cloudhal/astro-rendereruse marked ^18.1.1.@cloudhal/core-tsand@cloudhal/schedulesuse temporal-polyfill 1.0.6.@cloudhal/astro-rendereruses vite ^8.3.5. The Studio shipped in@cloudhal/cliis built with React 19.3.1, TanStack Router 1.170.46 and TanStack Query 5.104.5. marked 18.1.1 splits a few unusual markdown inputs differently: a blockquote ends before an unquoted line that follows a heading, rule or HTML block, and indented HTML blocks interrupt a paragraph. A stored body like that keeps rendering and shows virtual block ids until its next write. -
RFC-0042 §4, #110:
queryContentEntries(the API'sGET /entries?where=…, Studio's lists, MCPlist_entries) answerswhere,orderBy, status, locale,limitandoffsetin SQL from the newentry_list_valuesside table once a collection is ready, instead of reading and evaluating the whole collection; results are the shared evaluator's, which stays the fallback (term filters, collections not ready, operands SQL cannot compare exactly) and the oracle of a seeded property suite on SQLite, D1 and Durable Object SQLite. On the 10,000-entry benchmark the store path drops from 124 ms CPU / 10,001 rows (SQLite) and 176 ms / 10,003 rows read (D1) to 0.6 ms / 22 rows and 5.3 ms / 5,068 rows read; a page at offset 4,980 costs about the same as the first.The store keeps the table exact. Entry creates, updates, moves and deletes write an entry's rows (its
listFieldsvalues asresolveFieldsreads them) in their own batch, and an original's write rewrites its same-site translations' rows; site writes and field operations that change a collection's list definitions reset its readiness in the same batch, and imports reset the site's. A race never refuses a write: it marks the collection not ready. NewrebuildListValues(sql, options)is the resumable, budgeted backfill (guarded per chunk, safe to run concurrently;budget.statementsbounds every statement it issues,startrotates where a run begins,tidyremoves rows of unlisted collections);updateSite, finished field operations andimportSiterun it for what they reset, andopenLocalStoreruns it on open. A field operation's revert resets every listed collection whose values its restored bodies change.deleteContentEntrybehaves as before.revertTagsToTermsnow leaves an entry that was edited after the migration's before-snapshot (by a concurrent write, or before a resumed run) as it is and reports it inedited, instead of restoring its earlier fields, status or collection. -
importSite, used bycloudhal pull, by the admin bundle routecloudhal pushuses, and bycloudhal import, now runs every check before it writes anything, the bootstrap conflict and the 1000-statement limit included, and plans the media ledger with them. A refused bundle leaves the target as it was; before, node bodies could land ahead of a conflict and snapshots ahead of the size limit. Only a storage failure can stop it between its writes, and importing the bundle again completes it. New checks, each before the first write:- Every node body the target lacks must be a valid contract document of the kind of each snapshot ref that points to it, with that ref's id and rev, of the bundle's site (precompiled validators). A body no snapshot references is refused. Before, a body was only checked against its hash, so a crafted bundle could put invalid content behind the published snapshot.
- Node and snapshot ids may not belong to another site of the target, each appears once, and no two nodes share a kind and key or a translation group and locale (the node batch's unique keys).
- Every takedown entry has its fields with their types (
reason,legal,by,at). - The media ledger (
snapshot_blobs) reads the target's own asset bodies, never the bundle's text for a body the target already holds.
New
planImport(to, bundle, options)returns what an import would change, without writing:- the nodes added, changed and removed;
- the snapshots and node bodies to add;
- whether the project row is new;
- the listed media that are present, missing, of another size or taken down on the target;
- the bundle's takedowns, those whose bytes the target holds (an import deletes them) and those its ledger does not hold yet.
With
report: trueit returns a bootstrap conflict and missing media instead of throwing them. A node body that is not JSON is refused asblob <hash> is not JSONinstead of with the parser's message. -
New
restoreBundle(from, snapshot): a site bundle of one snapshot's nodes, rebuilt from the stored node bodies, forimportSite(…, { mode: "replace" }), which checks it like any bundle. The site's working copy and published pointer become that snapshot's again (cloudhal import --revert, RFC-0039). Revs only move forward: a node the site holds at the snapshot's rev or a later one, with other content, comes back at the rev after its current one, so a Studio tab or agent still holding a rev that node had gets a conflict instead of saving over it; a node whose content already is the snapshot's is left as it is. It lists the media those nodes need and carries the store's takedowns among them, so taken-down content stays down; a missing node body, or one that is not the node its snapshot ref names, is refused before anything is written.exportSite's output is unchanged. -
list_entrieswithorderByandlimitlets the store page (in SQL once the collection's list values are ready, RFC-0042 §4, #110) instead of loading every match. Arguments, cursors and output are unchanged; withoutorderBythe list keeps its last-updated order. -
render_page's long-line and input-label checks handle whitespace correctly again. A plain template literal ate the backslash, so the in-page script received
/s/and/s+/instead of/\s/and/\s+/. As a result, long-line counted the letter "s" as blank and every real space and indentation run as text. input-label splitaria-labelledbyon the letter "s" and reported labelled inputs as unlabelled. The check parts are nowString.rawtemplates. -
The admin Worker's minute tick gains a
lists.backfillsystem job (every five minutes): a budgetedrebuildListValuesfor listed collections that are new, changed, reset by a race or older than migration 0029, sized to the tick plan. Its declared cost (16 statements on the free plan, 150 on paid) is the run's statement budget, which the backfill enforces on every statement, discovery included, and each run starts at another waiting collection. On the paid plan it also removes rows of collections that list no fields any more, never those of a collection listed while it ran. -
New
cloudhal export(RFC-0039, #47) writes one site of the local project to a single.cloudhalfile. The file is a gzipped tar holding the site's content graph (nodes, snapshots and the node bodies they reference, as NDJSON parts), its media files and its takedown ledger, with every file's SHA-256 listed in the manifest. The flags:--out <file>names the file, and replaces a file already there once the new one is complete. By default the file is<site-slug>-<YYYYMMDD>.cloudhalin the current folder, dated by the newest timestamp among the rows it holds (deleting content alone does not move that date; the next edit or snapshot does). A file already at the default name is replaced only by the same archive; any other (a later edit the same day, other flags, a deletion) is left alone and the export asks for--out.--site <id>exports another site listed incloudhal.jsonsites.--site,--no-mediaand--latest-onlyare refused on every other command.--no-mediawrites the graph only.--latest-onlykeeps only the published and the latest snapshot, and only the media they and the current content need. It is refused while a field operation is unfinished (cloudhal fields continue|revert).
A damaged or missing local media file stops the export (run
cloudhal media verify); so do Ctrl-C and SIGTERM. Either way no file is written and an existing one is left as it was. Exporting an unchanged site again with the same CLI and Node version gives the same bytes. An archive never contains user accounts, sessions, roles, agent keys, comments, readers, analytics, the audit log, scheduled releases, automations, schedule runs, pending approvals, open edit suggestions, deployment secrets or webhook credentials, and the export prints this list. Its history does name who made each change (the user or agent of each snapshot, upload, block edit, review and takedown). -
New
cloudhal import <file> [--dry-run](RFC-0039, #47) reads a.cloudhalfile written bycloudhal exportinto the local project, keeping the site's id.- It reads the file twice. First it verifies every file's size and SHA-256, every row and every node body (each must be the node its snapshot refs name), and checks the archive against the project. A damaged or edited file, invalid content, an archive of another site, a site whose nodes differ, or missing media is therefore refused before anything is written. Then it reads the file again, refusing it if it changed in between, copies the media the project lacks and imports the graph, keeping the archive's published snapshot as
cloudhal pulldoes. - By default a bootstrap import. The archive's site must be
cloudhal.json'ssiteIdor listed insites, and the project must not hold it yet or hold exactly its nodes; missing snapshots, node bodies and media are added. A site whose nodes differ is refused;--replacereplaces them (see its own note). - An archive written with
--no-mediaimports only when the project already has its media. - Taken-down content stays down: the archive's takedowns are recorded and their bytes never written, a local copy of content the archive's ledger took down is deleted (the summary says how many), and an archive carrying content this project has taken down is refused.
--dry-runprints the manifest and what the import would add, change or remove (nodes by kind and title, snapshots, media to copy), writes nothing, and exits 1 when the import would be refused.- Ctrl-C and SIGTERM stop the import before the graph is written; media already copied stay in the blob store, unreferenced, and the next import reuses them. A storage failure while the graph is written says the import stopped partway; running it again completes it, search index included.
- Opening an existing local store applies pending migrations, as every command does; a project without one gets none from a dry run or a refused import.
- It reads the file twice. First it verifies every file's size and SHA-256, every row and every node body (each must be the node its snapshot refs name), and checks the archive against the project. A damaged or edited file, invalid content, an archive of another site, a site whose nodes differ, or missing media is therefore refused before anything is written. Then it reads the file again, refusing it if it changed in between, copies the media the project lacks and imports the graph, keeping the archive's published snapshot as
-
cloudhal import <file> --replace [--dry-run](RFC-0039 rollout step 4, #47) makes the local site's nodes the archive's, its published snapshot included, ascloudhal pull --replacedoes.cloudhal pushstill keeps a deployment's own published snapshot, so a local replace never moves production. Snapshots are only ever added.- When it changes a site the project already holds, the current graph is saved first as a snapshot labelled
before import <file>and the result asimport <file>; the summary prints(undo: cloudhal import --revert <id>). A new or unchanged site gets no snapshot; a site the plan found unchanged is checked again just before the write, and refused if another process changed it meanwhile. A run that stopped after the first snapshot reuses it the next time. - The archive's nodes keep their revs, as with
pull --replace. A Studio tab or agent still holding a node at the rev the archive gives it can save over it without a conflict, so the summary counts those nodes and says to reload open Studio tabs. - New
cloudhal import --revert <snapshot-id> [--dry-run]brings the site back to abefore importsnapshot, its published snapshot included, saving the current graph first asbefore revert import <file>, so a revert can itself be reverted. Its revs only move forward: a node edited since comes back at the rev after its current one, so a stale editor gets a conflict. Studio's rollback only moves the published pointer to an earlier snapshot, which cannot undo a replace. - An archive of a site
cloudhal.jsondoes not list switches itssiteId, and the old id's place insites, to the archive's, editing only those values in the file (layout and other keys stay as written), once the import has landed. The old site stays in the local store, unlisted and untouched; writing the old id back by hand undoes the switch (--revertrestores nodes only), and the summary namescloudhal site deleteto remove the old site for good. A deployed project is warned that its deployment serves the old site, and that a deployment of the imported site is updated from the project that deployed it. - Refused before anything is written: open releases of the site (cancel them in Studio), an unfinished field operation, a hostname another local site serves (now a refusal
--dry-runreports, for bootstrap imports too), and an archive site idcloudhal.jsoncannot name. --dry-runshows the snapshots it would take, the published-snapshot change and thecloudhal.jsonchange; it exits 0, or 1 when the import would be refused.--cancel-schedulesis now refused by every command butcloudhal push.
- When it changes a site the project already holds, the current graph is saved first as a snapshot labelled
-
Studio shows Ended (collection rule) beside the status of a published entry whose collection's expiry has passed (RFC-0042 §3.3, #113), in the collection list and in the entry editor's header. Until now such an entry looked plainly published while the site had already left it out of lists, or answered "not found". The badge explains itself on hover, from the keyboard and with a tap on touch screens. It names the date field, when the entry ended in the site's time zone, and what the site does: the page keeps its address but leaves lists, feeds, the sitemap and search, or the page answers "not found". Drafts, embargoed entries and entries whose own "Hide after" has passed get no badge (their own dates show in the editor's Schedule card). A translation reads its original's date only while the original is published, as the site does. The badge and its words load only for collections that have an expiry. Translations of the new words are AI drafts awaiting review.
Upgrade
npm install @cloudhal/cli@0.6.2 @cloudhal/core-ts@0.6.2 @cloudhal/astro-renderer@0.6.2 @cloudhal/cloudflare@0.6.2
npx cloudhal deployDocs: https://cloudhal.dev/docs/start/installation#upgrading