From 10bf713ded232f4b39a2001baa1aafbdaa7b2341 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 29 Oct 2026 16:46:56 +0100 Subject: [PATCH 1/3] Keep the deno_std cache when Deno removes SQLite sidecars Since Deno 2.4.5 (Quarto 1.9), `deno cache` can exit before closing its SQLite cache databases while building the bundled deno_std seed. The seed then ships un-checkpointed `-shm`/`-wal` files next to `dep_analysis_cache_v2` and `node_analysis_cache_v2`. When a later runtime Deno process closes one of these databases cleanly, SQLite deletes the sidecars from the user copy. The next `initDenoCache()` sees seed paths missing from the user copy, wipes the cache and recopies the seed, dropping the downloaded npm packages. Offline runs then fail with "npm package not found in cache". Skip the sidecars of Deno's `*_cache_v2` databases when comparing the seed listing with the user listing. The match is narrow so package files that happen to end in `-wal` or `-shm` still count as drift, and a missing database file still triggers a reset. The copy keeps sidecars, since a seed database with an un-checkpointed WAL is only valid together with its WAL. --- src/core/run/deno.ts | 8 +- tests/unit/core/deno-std-cache.test.ts | 109 +++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 tests/unit/core/deno-std-cache.test.ts diff --git a/src/core/run/deno.ts b/src/core/run/deno.ts index add8c21b782..9303e89635e 100644 --- a/src/core/run/deno.ts +++ b/src/core/run/deno.ts @@ -81,10 +81,16 @@ function directoryListing(path: string): Set { ); } -function directoryListingsMatch(path1: string, path2: string): boolean { +// Deno may leave these in the seed and delete them from the user copy on a clean close +const kSqliteSidecar = /_cache_v2-(shm|wal|journal)$/; + +export function directoryListingsMatch(path1: string, path2: string): boolean { const listing1 = directoryListing(path1), listing2 = directoryListing(path2); for (const path of listing1) { + if (kSqliteSidecar.test(path)) { + continue; + } if (!listing2.has(path)) { return false; } diff --git a/tests/unit/core/deno-std-cache.test.ts b/tests/unit/core/deno-std-cache.test.ts new file mode 100644 index 00000000000..12d77d626bf --- /dev/null +++ b/tests/unit/core/deno-std-cache.test.ts @@ -0,0 +1,109 @@ +/* + * deno-std-cache.test.ts + * + * Copyright (C) 2026 Posit Software, PBC + */ + +import { dirname, join } from "../../../src/deno_ral/path.ts"; +import { directoryListingsMatch } from "../../../src/core/run/deno.ts"; +import { unitTest } from "../../test.ts"; +import { assertEquals } from "../../asserts.ts"; +import { withTempDir } from "../../utils.ts"; + +const touch = (root: string, rel: string) => { + const file = join(root, rel); + Deno.mkdirSync(dirname(file), { recursive: true }); + Deno.writeTextFileSync(file, ""); +}; + +const populate = (root: string, rels: string[]) => { + Deno.mkdirSync(root, { recursive: true }); + rels.forEach((rel) => touch(root, rel)); +}; + +const compare = (seedFiles: string[], userFiles: string[]) => + withTempDir((dir) => { + const seed = join(dir, "seed"); + const user = join(dir, "user"); + populate(seed, seedFiles); + populate(user, userFiles); + return directoryListingsMatch(seed, user); + }); + +// Deno can exit `deno cache` without closing its SQLite cache databases, so the +// bundled seed may hold -shm/-wal files that a later clean close deletes from +// the user copy. Those must not count as a missing seed file. +unitTest( + "directoryListingsMatch - ignores SQLite cache sidecars missing from the user copy", + async () => { + const result = await compare( + [ + "cache/dep_analysis_cache_v2", + "cache/dep_analysis_cache_v2-shm", + "cache/dep_analysis_cache_v2-wal", + "cache/v8_code_cache_v2", + "cache/v8_code_cache_v2-wal", + "cache/npm/x/y.json", + ], + [ + "cache/dep_analysis_cache_v2", + "cache/v8_code_cache_v2", + "cache/npm/x/y.json", + ], + ); + assertEquals(result, true); + }, +); + +unitTest( + "directoryListingsMatch - reports a missing ordinary file", + async () => { + const result = await compare( + [ + "cache/dep_analysis_cache_v2", + "cache/dep_analysis_cache_v2-shm", + "cache/dep_analysis_cache_v2-wal", + "cache/v8_code_cache_v2", + "cache/v8_code_cache_v2-wal", + "cache/npm/x/y.json", + "cache/npm/x/z.json", + ], + [ + "cache/dep_analysis_cache_v2", + "cache/v8_code_cache_v2", + "cache/npm/x/z.json", + ], + ); + assertEquals(result, false); + }, +); + +unitTest( + "directoryListingsMatch - does not ignore package files with sidecar-like names", + async () => { + const result = await compare( + [ + "cache/dep_analysis_cache_v2", + "cache/npm/x/y.json", + "cache/npm/x/foo-wal", + ], + ["cache/dep_analysis_cache_v2", "cache/npm/x/y.json"], + ); + assertEquals(result, false); + }, +); + +unitTest( + "directoryListingsMatch - reports a missing cache database", + async () => { + const result = await compare( + [ + "cache/dep_analysis_cache_v2", + "cache/dep_analysis_cache_v2-wal", + "cache/npm/x/y.json", + ], + ["cache/npm/x/y.json"], + ); + assertEquals(result, false); + }, +); From da1bfd8cbd413425498e1be66e4d4a29b63bbb61 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 29 Oct 2026 17:20:16 +0100 Subject: [PATCH 2/3] Add changelog entry for #15227 --- news/changelog-1.11.md | 1 + 1 file changed, 1 insertion(+) diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 1b80a1502a7..1dfd7680c96 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -208,3 +208,4 @@ All changes included in 1.11: - ([#15157](https://github.com/quarto-dev/quarto-cli/issues/15157)): Fix `quarto preview` and other commands sometimes starting a local server on port 4190, which browsers refuse to connect to. - ([#15180](https://github.com/quarto-dev/quarto-cli/pull/15180)): Fix older Quarto versions failing to render with `RangeError: could not deserialize value` once a Quarto built on a newer Deno has used the same cache. Also fix a project cache that already has entries failing to clear with `Path must be a string`. - ([#15198](https://github.com/quarto-dev/quarto-cli/issues/15198)): Fix `quarto add`, `quarto use template`, `quarto use brand` and `quarto install tinytex` failing to unzip on Windows when a Group Policy blocks running `.bat` files from the temporary folder, and when a path contains an apostrophe. +- ([#15227](https://github.com/quarto-dev/quarto-cli/pull/15227)): Fix TypeScript scripts run by Quarto, such as `quarto run` scripts, project pre- and post-render scripts and Jupyter filters, losing their downloaded npm packages on Windows, which made them fail when run offline (regression introduced in v1.9). From a556a41c6dbcbcf1af23f7ac942b4336ff2719e7 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 30 Oct 2026 11:35:57 +0100 Subject: [PATCH 3/3] Accept the runtime boundary increase from the deno_std cache test The test creates fixture directories with ensureDirSync from deno_ral, so the only new Deno.* call is Deno.writeTextFileSync, which has no deno_ral replacement yet. --- tests/unit/core/deno-std-cache.test.ts | 5 +++-- tools/runtime-boundary-baseline.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/core/deno-std-cache.test.ts b/tests/unit/core/deno-std-cache.test.ts index 12d77d626bf..4b0fca45572 100644 --- a/tests/unit/core/deno-std-cache.test.ts +++ b/tests/unit/core/deno-std-cache.test.ts @@ -4,6 +4,7 @@ * Copyright (C) 2026 Posit Software, PBC */ +import { ensureDirSync } from "../../../src/deno_ral/fs.ts"; import { dirname, join } from "../../../src/deno_ral/path.ts"; import { directoryListingsMatch } from "../../../src/core/run/deno.ts"; import { unitTest } from "../../test.ts"; @@ -12,12 +13,12 @@ import { withTempDir } from "../../utils.ts"; const touch = (root: string, rel: string) => { const file = join(root, rel); - Deno.mkdirSync(dirname(file), { recursive: true }); + ensureDirSync(dirname(file)); Deno.writeTextFileSync(file, ""); }; const populate = (root: string, rels: string[]) => { - Deno.mkdirSync(root, { recursive: true }); + ensureDirSync(root); rels.forEach((rel) => touch(root, rel)); }; diff --git a/tools/runtime-boundary-baseline.json b/tools/runtime-boundary-baseline.json index 0607c709f10..729d34db7b6 100644 --- a/tools/runtime-boundary-baseline.json +++ b/tools/runtime-boundary-baseline.json @@ -16,7 +16,7 @@ "stdImportFiles": 3 }, "tests": { - "denoCalls": 874, + "denoCalls": 875, "stdImportFiles": 176 } }