Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
8 changes: 7 additions & 1 deletion src/core/run/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,16 @@ function directoryListing(path: string): Set<string> {
);
}

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;
}
Expand Down
109 changes: 109 additions & 0 deletions tests/unit/core/deno-std-cache.test.ts
Original file line number Diff line number Diff line change
@@ -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 });

Check warning on line 15 in tests/unit/core/deno-std-cache.test.ts

View workflow job for this annotation

GitHub Actions / runtime-boundary

tests: +2 Deno.mkdirSync in tests/unit/core/deno-std-cache.test.ts; src/deno_ral has no replacement yet, so nothing to change in this PR. A maintainer accepts the new count before merging; this job stays red until then (see the job summary).
Deno.writeTextFileSync(file, "");

Check warning on line 16 in tests/unit/core/deno-std-cache.test.ts

View workflow job for this annotation

GitHub Actions / runtime-boundary

tests: +1 Deno.writeTextFileSync in tests/unit/core/deno-std-cache.test.ts; src/deno_ral has no replacement yet, so nothing to change in this PR. A maintainer accepts the new count before merging; this job stays red until then (see the job summary).
};

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);
},
);
Loading