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). 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..4b0fca45572 --- /dev/null +++ b/tests/unit/core/deno-std-cache.test.ts @@ -0,0 +1,110 @@ +/* + * deno-std-cache.test.ts + * + * 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"; +import { assertEquals } from "../../asserts.ts"; +import { withTempDir } from "../../utils.ts"; + +const touch = (root: string, rel: string) => { + const file = join(root, rel); + ensureDirSync(dirname(file)); + Deno.writeTextFileSync(file, ""); +}; + +const populate = (root: string, rels: string[]) => { + ensureDirSync(root); + 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); + }, +); 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 } }