Skip to content

Commit 2c8963a

Browse files
committed
manuscript - do not cloneDeep Kv. create safeCloneDeep
1 parent 2546027 commit 2c8963a

File tree

9 files changed

+74
-7
lines changed

9 files changed

+74
-7
lines changed

src/core/cache/cache.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ import {
1919
type ImmediateBufferCacheEntry,
2020
type ImmediateStringCacheEntry,
2121
} from "./cache-types.ts";
22+
import { Cloneable } from "../safe-clone-deep.ts";
2223
export { type ProjectCache } from "./cache-types.ts";
2324

2425
const currentCacheVersion = "1";
2526
const requiredQuartoVersions: Record<string, string> = {
2627
"1": ">1.7.0",
2728
};
2829

29-
class ProjectCacheImpl {
30+
class ProjectCacheImpl implements Cloneable<ProjectCacheImpl> {
3031
projectScratchDir: string;
3132
index: Deno.Kv | null;
3233

@@ -35,6 +36,10 @@ class ProjectCacheImpl {
3536
this.index = null;
3637
}
3738

39+
clone() {
40+
return this;
41+
}
42+
3843
close() {
3944
if (this.index) {
4045
this.index.close();

src/core/safe-clone-deep.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* safe-clone-deep.ts
3+
*
4+
* CloneDeep that uses object's own cloning methods when available
5+
*
6+
* Copyright (C) 2025 Posit Software, PBC
7+
*/
8+
9+
export interface Cloneable<T> {
10+
clone(): T;
11+
}
12+
13+
export function safeCloneDeep<T>(obj: T): T {
14+
if (obj === null || typeof obj !== "object") {
15+
return obj;
16+
}
17+
18+
// Handle arrays
19+
if (Array.isArray(obj)) {
20+
return obj.map((item) => safeCloneDeep(item)) as T;
21+
}
22+
23+
if (obj && ("clone" in obj) && typeof obj.clone === "function") {
24+
return obj.clone();
25+
}
26+
27+
// Handle regular objects
28+
const result = {} as T;
29+
for (const key in obj) {
30+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
31+
result[key] = safeCloneDeep(obj[key]);
32+
}
33+
}
34+
35+
return result;
36+
}

src/core/sass.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ export async function compileWithCache(
384384
const result = await memoizedGetVarsBlock(project, input);
385385
return input + "\n" + result;
386386
} catch (e) {
387+
if (e.name !== "SCSSParsingError") {
388+
throw e;
389+
}
387390
console.warn("Error adding css vars block", e);
388391
console.warn(
389392
"The resulting CSS file will not have SCSS color variables exported as CSS.",

src/core/sass/add-css-vars.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,22 @@ import { propagateDeclarationTypes } from "./analyzer/declaration-types.ts";
1515
import { getVariableDependencies } from "./analyzer/get-dependencies.ts";
1616

1717
const { getSassAst } = makeParserModule(parse);
18+
19+
export class SCSSParsingError extends Error {
20+
constructor(message: string) {
21+
super(`SCSS Parsing Error: ${message}`);
22+
this.name = "SCSSParsingError";
23+
}
24+
}
25+
1826
export const cssVarsBlock = (scssSource: string) => {
19-
const ast = propagateDeclarationTypes(cleanSassAst(getSassAst(scssSource)));
27+
let astOriginal;
28+
try {
29+
astOriginal = getSassAst(scssSource);
30+
} catch (e) {
31+
throw new SCSSParsingError(e.message);
32+
}
33+
const ast = propagateDeclarationTypes(cleanSassAst(astOriginal));
2034
const deps = getVariableDependencies(ast);
2135

2236
const output: string[] = [":root {"];

src/core/sass/cache.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ import { TempContext } from "../temp.ts";
1414
import { safeRemoveIfExists } from "../path.ts";
1515
import * as log from "../../deno_ral/log.ts";
1616
import { onCleanup } from "../cleanup.ts";
17+
import { Cloneable } from "../safe-clone-deep.ts";
1718

18-
class SassCache {
19+
class SassCache implements Cloneable<SassCache> {
1920
kv: Deno.Kv;
2021
path: string;
2122

23+
clone() {
24+
return this;
25+
}
26+
2227
constructor(kv: Deno.Kv, path: string) {
2328
this.kv = kv;
2429
this.path = path;

src/render/notebook/notebook-contributor-html.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { isQmdFile } from "../../execute/qmd.ts";
5151
import { dirAndStem } from "../../core/path.ts";
5252
import { projectOutputDir } from "../../project/project-shared.ts";
5353
import { existsSync } from "../../deno_ral/fs.ts";
54+
import { safeCloneDeep } from "../../core/safe-clone-deep.ts";
5455

5556
export const htmlNotebookContributor: NotebookContributor = {
5657
resolve: resolveHtmlNotebook,
@@ -85,7 +86,7 @@ function resolveHtmlNotebook(
8586
executedFile: ExecutedFile,
8687
notebookMetadata?: NotebookMetadata,
8788
) {
88-
const resolved = ld.cloneDeep(executedFile) as ExecutedFile;
89+
const resolved = safeCloneDeep(executedFile) as ExecutedFile;
8990

9091
// Set the output file
9192
resolved.recipe.format.pandoc[kOutputFile] = `${outputFile(nbAbsPath)}`;

src/render/notebook/notebook-contributor-ipynb.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { ipynbTitleTemplatePath } from "../../format/ipynb/format-ipynb.ts";
3434
import { projectOutputDir } from "../../project/project-shared.ts";
3535
import { existsSync } from "../../deno_ral/fs.ts";
3636
import { dirname, join, relative } from "../../deno_ral/path.ts";
37+
import { safeCloneDeep } from "../../core/safe-clone-deep.ts";
3738

3839
export const outputNotebookContributor: NotebookContributor = {
3940
resolve: resolveOutputNotebook,
@@ -67,7 +68,7 @@ function resolveOutputNotebook(
6768
executedFile: ExecutedFile,
6869
_notebookMetadata?: NotebookMetadata,
6970
) {
70-
const resolved = ld.cloneDeep(executedFile);
71+
const resolved = safeCloneDeep(executedFile);
7172
resolved.recipe.format.pandoc[kOutputFile] = outputFile(nbAbsPath);
7273
resolved.recipe.output = resolved.recipe.format.pandoc[kOutputFile];
7374

src/render/notebook/notebook-contributor-jats.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import * as ld from "../../core/lodash.ts";
3737

3838
import { error } from "../../deno_ral/log.ts";
3939
import { Format } from "../../config/types.ts";
40+
import { safeCloneDeep } from "../../core/safe-clone-deep.ts";
4041

4142
export const jatsContributor: NotebookContributor = {
4243
resolve: resolveJats,
@@ -56,7 +57,7 @@ function resolveJats(
5657
executedFile: ExecutedFile,
5758
_notebookMetadata?: NotebookMetadata,
5859
) {
59-
const resolved = ld.cloneDeep(executedFile);
60+
const resolved = safeCloneDeep(executedFile);
6061
const to =
6162
resolved.recipe.format.render[kVariant]?.includes("+element_citations")
6263
? "jats+element_citations"

src/render/notebook/notebook-contributor-qmd.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { ipynbTitleTemplatePath } from "../../format/ipynb/format-ipynb.ts";
3838
import { projectScratchPath } from "../../project/project-scratch.ts";
3939
import { ensureDirSync, existsSync } from "../../deno_ral/fs.ts";
4040
import { dirname, join, relative } from "../../deno_ral/path.ts";
41+
import { safeCloneDeep } from "../../core/safe-clone-deep.ts";
4142

4243
export const qmdNotebookContributor: NotebookContributor = {
4344
resolve: resolveOutputNotebook,
@@ -86,7 +87,7 @@ function resolveOutputNotebook(
8687
executedFile: ExecutedFile,
8788
_notebookMetadata?: NotebookMetadata,
8889
) {
89-
const resolved = ld.cloneDeep(executedFile);
90+
const resolved = safeCloneDeep(executedFile);
9091
resolved.recipe.format.pandoc[kOutputFile] = ipynbOutputFile(nbAbsPath);
9192
resolved.recipe.output = resolved.recipe.format.pandoc[kOutputFile];
9293

0 commit comments

Comments
 (0)