Skip to content

Commit 521960c

Browse files
authored
Merge pull request #12384 from quarto-dev/bugfix/12380
manuscript - do not cloneDeep Kv. create safeCloneDeep
2 parents d52d6e7 + bca2d32 commit 521960c

18 files changed

+165
-79
lines changed

src/command/render/pandoc-html.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
} from "../../config/types.ts";
2020
import { ProjectContext } from "../../project/types.ts";
2121

22-
import { TempContext } from "../../core/temp.ts";
2322
import { cssImports, cssResources } from "../../core/css.ts";
2423
import { cleanSourceMappingUrl, compileSass } from "../../core/sass.ts";
2524

@@ -91,10 +90,12 @@ export async function resolveSassBundles(
9190
const maybeBrandBundle = bundlesWithBrand.find((bundle) =>
9291
bundle.key === "brand"
9392
);
94-
assert(!maybeBrandBundle ||
95-
!maybeBrandBundle.user?.find((v) => v === "brand") &&
96-
!maybeBrandBundle.dark?.user?.find((v) => v === "brand"));
97-
let foundBrand = {light: false, dark: false};
93+
assert(
94+
!maybeBrandBundle ||
95+
!maybeBrandBundle.user?.find((v) => v === "brand") &&
96+
!maybeBrandBundle.dark?.user?.find((v) => v === "brand"),
97+
);
98+
const foundBrand = { light: false, dark: false };
9899
const bundles: SassBundle[] = bundlesWithBrand.filter((bundle) =>
99100
bundle.key !== "brand"
100101
).map((bundle) => {
@@ -106,12 +107,18 @@ export async function resolveSassBundles(
106107
bundle.user!.splice(userBrand, 1, ...(maybeBrandBundle?.user || []));
107108
foundBrand.light = true;
108109
}
109-
const darkBrand = bundle.dark?.user?.findIndex((layer) => layer === "brand");
110+
const darkBrand = bundle.dark?.user?.findIndex((layer) =>
111+
layer === "brand"
112+
);
110113
if (darkBrand && darkBrand !== -1) {
111114
if (!cloned) {
112115
bundle = cloneDeep(bundle);
113116
}
114-
bundle.dark!.user!.splice(darkBrand, 1, ...(maybeBrandBundle?.dark?.user || []))
117+
bundle.dark!.user!.splice(
118+
darkBrand,
119+
1,
120+
...(maybeBrandBundle?.dark?.user || []),
121+
);
115122
foundBrand.dark = true;
116123
}
117124
return bundle as SassBundle;
@@ -122,18 +129,19 @@ export async function resolveSassBundles(
122129
key: "brand",
123130
user: !foundBrand.light && maybeBrandBundle?.user as SassLayer[] || [],
124131
dark: !foundBrand.dark && maybeBrandBundle?.dark?.user && {
125-
user: maybeBrandBundle.dark.user as SassLayer[],
126-
default: maybeBrandBundle.dark.default
127-
} || undefined
132+
user: maybeBrandBundle.dark.user as SassLayer[],
133+
default: maybeBrandBundle.dark.default,
134+
} || undefined,
128135
});
129136
}
130137

131138
// See if any bundles are providing dark specific css
132139
const hasDark = bundles.some((bundle) => bundle.dark !== undefined);
133-
defaultStyle =
134-
bundles.some((bundle) => bundle.dark !== undefined && bundle.dark.default)
135-
? "dark"
136-
: "light";
140+
defaultStyle = bundles.some((bundle) =>
141+
bundle.dark !== undefined && bundle.dark.default
142+
)
143+
? "dark"
144+
: "light";
137145
const targets: SassTarget[] = [{
138146
name: `${dependency}.min.css`,
139147
bundles: (bundles as any),

src/command/render/project.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ import { fileExecutionEngine } from "../../execute/engine.ts";
8181
import { projectContextForDirectory } from "../../project/project-context.ts";
8282
import { ProjectType } from "../../project/types/types.ts";
8383
import { ProjectConfig as ProjectConfig_Project } from "../../resources/types/schema-types.ts";
84-
import { Extension } from "../../extension/types.ts";
8584

8685
const noMutationValidations = (
8786
projType: ProjectType,

src/command/render/render-contexts.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,21 +89,6 @@ import {
8989
import { ExtensionContext } from "../../extension/types.ts";
9090
import { NotebookContext } from "../../render/notebook/notebook-types.ts";
9191

92-
// we can't naively ld.cloneDeep everything
93-
// because that destroys class instances
94-
// with private members
95-
//
96-
// Currently, that's ProjectContext.
97-
//
98-
// TODO: Ideally, we shouldn't be copying the RenderContext at all.
99-
export function copyRenderContext(
100-
context: RenderContext,
101-
): RenderContext {
102-
return {
103-
...ld.cloneDeep(context),
104-
project: context.project,
105-
};
106-
}
10792
export async function resolveFormatsFromMetadata(
10893
metadata: Metadata,
10994
input: string,

src/command/render/render-files.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import { outputRecipe } from "./output.ts";
4848

4949
import { renderPandoc } from "./render.ts";
5050
import { PandocRenderCompletion, RenderServices } from "./types.ts";
51-
import { copyRenderContext, renderContexts } from "./render-contexts.ts";
51+
import { renderContexts } from "./render-contexts.ts";
5252
import { renderProgress } from "./render-info.ts";
5353
import {
5454
ExecutedFile,
@@ -114,6 +114,7 @@ import {
114114
} from "../../project/project-shared.ts";
115115
import { NotebookContext } from "../../render/notebook/notebook-types.ts";
116116
import { setExecuteEnvironment } from "../../execute/environment.ts";
117+
import { safeCloneDeep } from "../../core/safe-clone-deep.ts";
117118

118119
export async function renderExecute(
119120
context: RenderContext,
@@ -503,7 +504,7 @@ async function renderFileInternal(
503504

504505
for (const format of Object.keys(contexts)) {
505506
pushTiming("render-context");
506-
const context = copyRenderContext(contexts[format]); // since we're going to mutate it...
507+
const context = safeCloneDeep(contexts[format]); // since we're going to mutate it...
507508

508509
// disquality some documents from server: shiny
509510
if (isServerShiny(context.format) && context.project) {

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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 Maps
28+
if (obj instanceof Map) {
29+
const clonedMap = new Map();
30+
for (const [key, value] of obj.entries()) {
31+
clonedMap.set(key, safeCloneDeep(value));
32+
}
33+
return clonedMap as T;
34+
}
35+
36+
// Handle Sets
37+
if (obj instanceof Set) {
38+
const clonedSet = new Set();
39+
for (const value of obj.values()) {
40+
clonedSet.add(safeCloneDeep(value));
41+
}
42+
return clonedSet as T;
43+
}
44+
45+
// Handle regular objects
46+
const result = {} as T;
47+
for (const key in obj) {
48+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
49+
result[key] = safeCloneDeep(obj[key]);
50+
}
51+
}
52+
53+
return result;
54+
}

src/core/sass.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { existsSync } from "../deno_ral/fs.ts";
88
import { join } from "../deno_ral/path.ts";
99

1010
import { quartoCacheDir } from "./appdirs.ts";
11-
import { TempContext } from "./temp.ts";
1211

1312
import { SassBundleLayers, SassLayer } from "../config/types.ts";
1413
import { dartCompile } from "./dart-sass.ts";
@@ -384,6 +383,9 @@ export async function compileWithCache(
384383
const result = await memoizedGetVarsBlock(project, input);
385384
return input + "\n" + result;
386385
} catch (e) {
386+
if (e.name !== "SCSSParsingError") {
387+
throw e;
388+
}
387389
console.warn("Error adding css vars block", e);
388390
console.warn(
389391
"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/execute/engine.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Copyright (C) 2020-2022 Posit Software, PBC
55
*/
66

7-
import { extname, join, resolve } from "../deno_ral/path.ts";
7+
import { extname, join } from "../deno_ral/path.ts";
88

99
import * as ld from "../core/lodash.ts";
1010

@@ -23,7 +23,6 @@ import { kMdExtensions, markdownEngine } from "./markdown.ts";
2323
import { ExecutionEngine, kQmdExtensions } from "./types.ts";
2424
import { languagesInMarkdown } from "./engine-shared.ts";
2525
import { languages as handlerLanguages } from "../core/handlers/base.ts";
26-
import { MappedString } from "../core/lib/text-types.ts";
2726
import { RenderContext, RenderFlags } from "../command/render/types.ts";
2827
import { mergeConfigs } from "../core/config.ts";
2928
import { ProjectContext } from "../project/types.ts";

src/project/project-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ import { NotebookContext } from "../render/notebook/notebook-types.ts";
102102
import { MappedString } from "../core/mapped-text.ts";
103103
import { makeTimedFunctionAsync } from "../core/performance/function-times.ts";
104104
import { createProjectCache } from "../core/cache/cache.ts";
105-
import { createTempContext, globalTempContext } from "../core/temp.ts";
105+
import { createTempContext } from "../core/temp.ts";
106106

107107
export async function projectContext(
108108
path: string,

0 commit comments

Comments
 (0)