From a0be56997155cb778c6c72fd95dd3beacdff83bb Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 16 Dec 2025 13:58:49 -0600 Subject: [PATCH 1/4] Manage projectcontext objects correctly on preview --- package/scripts/common/quarto | 4 +-- src/command/preview/cmd.ts | 3 +- src/command/preview/preview-shiny.ts | 6 ++-- src/command/preview/preview.ts | 37 ++++++++++++-------- src/command/render/render-shared.ts | 14 ++++---- src/command/serve/cmd.ts | 2 +- src/execute/jupyter/jupyter.ts | 18 +++++++++- src/project/project-context.ts | 13 ++++--- src/project/serve/serve.ts | 2 +- src/project/types/single-file/single-file.ts | 4 +-- 10 files changed, 63 insertions(+), 40 deletions(-) diff --git a/package/scripts/common/quarto b/package/scripts/common/quarto index 0585a391f46..b33439e697a 100755 --- a/package/scripts/common/quarto +++ b/package/scripts/common/quarto @@ -187,9 +187,9 @@ QUARTO_DENO_OPTIONS="--unstable-ffi --unstable-kv --no-config --no-lock ${QUARTO # --enable-experimental-regexp-engine is required for /regex/l, https://github.com/quarto-dev/quarto-cli/issues/9737 if [ "$QUARTO_DENO_V8_OPTIONS" != "" ]; then - QUARTO_DENO_V8_OPTIONS="--enable-experimental-regexp-engine,--max-old-space-size=8192,--max-heap-size=8192,${QUARTO_DENO_V8_OPTIONS}" + QUARTO_DENO_V8_OPTIONS="--enable-experimental-regexp-engine,--max-old-space-size=8192,--max-heap-size=8192,--stack-trace-limit=100,${QUARTO_DENO_V8_OPTIONS}" else - QUARTO_DENO_V8_OPTIONS="--enable-experimental-regexp-engine,--max-old-space-size=8192,--max-heap-size=8192" + QUARTO_DENO_V8_OPTIONS="--enable-experimental-regexp-engine,--max-old-space-size=8192,--max-heap-size=8192,--stack-trace-limit=100" fi if [ "$QUARTO_DENO_EXTRA_OPTIONS" == "" ]; then diff --git a/src/command/preview/cmd.ts b/src/command/preview/cmd.ts index 8effb9d3472..b37e8ea4c80 100644 --- a/src/command/preview/cmd.ts +++ b/src/command/preview/cmd.ts @@ -292,7 +292,7 @@ export const previewCommand = new Command() services.cleanup(); } })(); - const format = await previewFormat(file, flags.to, formats, project); + const format = await previewFormat(file, project, flags.to, formats); // see if this is server: shiny document and if it is then forward to previewShiny if (isHtmlOutput(parseFormatString(format).baseFormat)) { @@ -314,6 +314,7 @@ export const previewCommand = new Command() format, pandocArgs: args, watchInputs: options.watchInputs!, + project, }); exitWithCleanup(result.code); throw new Error(); // unreachable diff --git a/src/command/preview/preview-shiny.ts b/src/command/preview/preview-shiny.ts index e9d120c79f1..8c55ed1906e 100644 --- a/src/command/preview/preview-shiny.ts +++ b/src/command/preview/preview-shiny.ts @@ -33,18 +33,16 @@ import { } from "../../core/http.ts"; import { findOpenPort } from "../../core/port.ts"; import { handleHttpRequests } from "../../core/http-server.ts"; -import { kLocalhost } from "../../core/port-consts.ts"; import { normalizePath } from "../../core/path.ts"; import { previewMonitorResources } from "../../core/quarto.ts"; import { renderServices } from "../render/render-services.ts"; import { RenderFlags } from "../render/types.ts"; import { notebookContext } from "../../render/notebook/notebook-context.ts"; -import { isIpynbOutput } from "../../config/format.ts"; export interface PreviewShinyOptions extends RunOptions { pandocArgs: string[]; watchInputs: boolean; - project?: ProjectContext; + project: ProjectContext; } export async function previewShiny(options: PreviewShinyOptions) { @@ -128,8 +126,8 @@ function runPreviewControlService( return normalizePath(options.input) === normalizePath(prevReq.path) && await previewRenderRequestIsCompatible( prevReq, - options.format, options.project, + options.format, ); }; diff --git a/src/command/preview/preview.ts b/src/command/preview/preview.ts index 8617beefad4..279635c2842 100644 --- a/src/command/preview/preview.ts +++ b/src/command/preview/preview.ts @@ -85,7 +85,7 @@ import { import { isJupyterNotebook } from "../../core/jupyter/jupyter.ts"; import { watchForFileChanges } from "../../core/watch.ts"; import { previewMonitorResources } from "../../core/quarto.ts"; -import { exitWithCleanup } from "../../core/cleanup.ts"; +import { exitWithCleanup, onCleanup } from "../../core/cleanup.ts"; import { extensionFilesFromDirs, inputExtensionDirs, @@ -151,11 +151,15 @@ export async function preview( ) { const nbContext = notebookContext(); // see if this is project file - const project = await projectContext(file, nbContext); + const project = (await projectContext(file, nbContext)) || + (await singleFileProjectContext(file, nbContext)); + onCleanup(() => { + project.cleanup(); + }); // determine the target format if there isn't one in the command line args // (current we force the use of an html or pdf based format) - const format = await previewFormat(file, flags.to, undefined, project); + const format = await previewFormat(file, project, flags.to, undefined); setPreviewFormat(format, flags, pandocArgs); // render for preview (create function we can pass to watcher then call it) @@ -224,6 +228,7 @@ export async function preview( options.port!, reloader, changeHandler.render, + project, ) : project ? projectHtmlFileRequestHandler( @@ -241,6 +246,7 @@ export async function preview( result.format, reloader, changeHandler.render, + project, ); // open browser if this is a browseable format @@ -340,17 +346,17 @@ export function previewRenderRequest( export async function previewRenderRequestIsCompatible( request: PreviewRenderRequest, + project: ProjectContext, format?: string, - project?: ProjectContext, ) { if (request.version === 1) { return true; // rstudio manages its own request compatibility state } else { const reqFormat = await previewFormat( request.path, + project, request.format, undefined, - project, ); return reqFormat === format; } @@ -359,20 +365,20 @@ export async function previewRenderRequestIsCompatible( // determine the format to preview export async function previewFormat( file: string, + project: ProjectContext, format?: string, formats?: Record, - project?: ProjectContext, ) { if (format) { return format; } - const nbContext = notebookContext(); - project = project || (await singleFileProjectContext(file, nbContext)); + // const nbContext = notebookContext(); + // project = project || (await singleFileProjectContext(file, nbContext)); formats = formats || await withRenderServices( - nbContext, + project.notebookContext, (services: RenderServices) => - renderFormats(file, services, "all", project!), + renderFormats(file, services, "all", project), ); format = Object.keys(formats)[0] || "html"; return format; @@ -418,7 +424,6 @@ export async function renderForPreview( pandocArgs: string[], project?: ProjectContext, ): Promise { - // render const renderResult = await render(file, { services, @@ -426,7 +431,7 @@ export async function renderForPreview( pandocArgs: pandocArgs, previewServer: true, setProjectDir: project !== undefined, - }); + }, project); if (renderResult.error) { throw renderResult.error; } @@ -689,6 +694,7 @@ function htmlFileRequestHandler( format: Format, reloader: HttpDevServer, renderHandler: (to?: string) => Promise, + context: ProjectContext, ) { return httpFileRequestHandler( htmlFileRequestHandlerOptions( @@ -699,6 +705,7 @@ function htmlFileRequestHandler( format, reloader, renderHandler, + context, ), ); } @@ -711,7 +718,7 @@ function htmlFileRequestHandlerOptions( format: Format, devserver: HttpDevServer, renderHandler: (to?: string) => Promise, - project?: ProjectContext, + project: ProjectContext, ): HttpFileRequestOptions { // if we an alternate format on the fly we need to do a full re-render // to get the correct state back. this flag will be set whenever @@ -742,7 +749,7 @@ function htmlFileRequestHandlerOptions( prevReq && existsSync(prevReq.path) && normalizePath(prevReq.path) === normalizePath(inputFile) && - await previewRenderRequestIsCompatible(prevReq, flags.to) + await previewRenderRequestIsCompatible(prevReq, project, flags.to) ) { // don't wait for the promise so the // caller gets an immediate reply @@ -853,6 +860,7 @@ function pdfFileRequestHandler( port: number, reloader: HttpDevServer, renderHandler: () => Promise, + project: ProjectContext, ) { // start w/ the html handler (as we still need it's http reload injection) const pdfOptions = htmlFileRequestHandlerOptions( @@ -863,6 +871,7 @@ function pdfFileRequestHandler( format, reloader, renderHandler, + project, ); // pdf customizations diff --git a/src/command/render/render-shared.ts b/src/command/render/render-shared.ts index 63841a09383..52a4650e274 100644 --- a/src/command/render/render-shared.ts +++ b/src/command/render/render-shared.ts @@ -33,20 +33,23 @@ import { kTextPlain } from "../../core/mime.ts"; import { normalizePath } from "../../core/path.ts"; import { notebookContext } from "../../render/notebook/notebook-context.ts"; import { singleFileProjectContext } from "../../project/types/single-file/single-file.ts"; -import { assert } from "testing/asserts"; +import { ProjectContext } from "../../project/types.ts"; export async function render( path: string, options: RenderOptions, + pContext?: ProjectContext, ): Promise { // one time initialization of yaml validators setInitializer(initYamlIntelligenceResourcesFromFilesystem); await initState(); - const nbContext = notebookContext(); + const nbContext = pContext?.notebookContext || notebookContext(); // determine target context/files - let context = await projectContext(path, nbContext, options); + // let context = await projectContext(path, nbContext, options); + let context = pContext || (await projectContext(path, nbContext, options)) || + (await singleFileProjectContext(path, nbContext, options)); // Create a synthetic project when --output-dir is used without a project file // This creates a temporary .quarto directory to manage the render, which must @@ -61,6 +64,7 @@ export async function render( // set env var if requested if (context && options.setProjectDir) { + // FIXME we can't set environment variables like this with asyncs flying around Deno.env.set("QUARTO_PROJECT_DIR", context.dir); } @@ -98,10 +102,6 @@ export async function render( // validate that we didn't get any project-only options validateDocumentRenderFlags(options.flags); - assert(!context, "Expected no context here"); - // NB: singleFileProjectContext is currently not fully-featured - context = await singleFileProjectContext(path, nbContext, options); - // otherwise it's just a file render const result = await renderFiles( [{ path }], diff --git a/src/command/serve/cmd.ts b/src/command/serve/cmd.ts index ca3042fcc39..2e51022d07f 100644 --- a/src/command/serve/cmd.ts +++ b/src/command/serve/cmd.ts @@ -75,7 +75,7 @@ export const serveCommand = new Command() (services: RenderServices) => renderFormats(input, services, undefined, context), ); - const format = await previewFormat(input, undefined, formats, context); + const format = await previewFormat(input, context, undefined, formats); const result = await serve({ input, diff --git a/src/execute/jupyter/jupyter.ts b/src/execute/jupyter/jupyter.ts index a98971c7187..378515454cb 100644 --- a/src/execute/jupyter/jupyter.ts +++ b/src/execute/jupyter/jupyter.ts @@ -317,7 +317,23 @@ title: "Title" // of additional changes to our file handling code (without changes, // our output files would be called $FILE.quarto.html, which // is not what we want). So for now, we'll use .quarto_ipynb - const notebook = join(fileDir, fileStem + ".quarto_ipynb"); + let counter: number | undefined = undefined; + let notebook = join( + fileDir, + `${fileStem}.quarto_ipynb${counter ? "_" + String(counter) : ""}`, + ); + + while (existsSync(notebook)) { + if (!counter) { + counter = 1; + } else { + ++counter; + } + notebook = join( + fileDir, + `${fileStem}.quarto_ipynb${counter ? "_" + String(counter) : ""}`, + ); + } const target = { source: file, input: notebook, diff --git a/src/project/project-context.ts b/src/project/project-context.ts index 119a5e69989..71a04150a52 100644 --- a/src/project/project-context.ts +++ b/src/project/project-context.ts @@ -105,7 +105,6 @@ import { createProjectCache } from "../core/cache/cache.ts"; import { createTempContext } from "../core/temp.ts"; import { onCleanup } from "../core/cleanup.ts"; -import { once } from "../core/once.ts"; import { Zod } from "../resources/types/zod/schema-types.ts"; import { ExternalEngine } from "../resources/types/schema-types.ts"; @@ -366,11 +365,11 @@ export async function projectContext( previewServer: renderOptions?.previewServer, diskCache: await createProjectCache(join(dir, ".quarto")), temp, - cleanup: once(() => { + cleanup: () => { cleanupFileInformationCache(result); result.diskCache.close(); temp.cleanup(); - }), + }, }; // see if the project [kProjectType] wants to filter the project config @@ -458,11 +457,11 @@ export async function projectContext( previewServer: renderOptions?.previewServer, diskCache: await createProjectCache(join(dir, ".quarto")), temp, - cleanup: once(() => { + cleanup: () => { cleanupFileInformationCache(result); result.diskCache.close(); temp.cleanup(); - }), + }, }; const { files, engines } = await projectInputFiles( result, @@ -538,11 +537,11 @@ export async function projectContext( previewServer: renderOptions?.previewServer, diskCache: await createProjectCache(join(temp.baseDir, ".quarto")), temp, - cleanup: once(() => { + cleanup: () => { cleanupFileInformationCache(context); context.diskCache.close(); temp.cleanup(); - }), + }, }; if (Deno.statSync(path).isDirectory) { const { files, engines } = await projectInputFiles(context); diff --git a/src/project/serve/serve.ts b/src/project/serve/serve.ts index 13d968ac6cd..8c4a029a5fb 100644 --- a/src/project/serve/serve.ts +++ b/src/project/serve/serve.ts @@ -810,7 +810,7 @@ function previewControlChannelRequestHandler( ); if ( prevReq && - (await previewRenderRequestIsCompatible(prevReq, flags.to, project)) + (await previewRenderRequestIsCompatible(prevReq, project, flags.to)) ) { if (isProjectInputFile(prevReq.path, project!)) { const services = renderServices(notebookContext()); diff --git a/src/project/types/single-file/single-file.ts b/src/project/types/single-file/single-file.ts index aec26a0569a..1aa24b6c308 100644 --- a/src/project/types/single-file/single-file.ts +++ b/src/project/types/single-file/single-file.ts @@ -86,10 +86,10 @@ export async function singleFileProjectContext( isSingleFile: true, diskCache: await createProjectCache(projectCacheBaseDir), temp, - cleanup: once(() => { + cleanup: () => { cleanupFileInformationCache(result); result.diskCache.close(); - }), + }, }; if (renderOptions) { result.config = { From 3efceaf8785e0cdd67cdf61489c459f7ebdf099c Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 16 Dec 2025 15:20:27 -0600 Subject: [PATCH 2/4] communicate single-file source correctly to lua filters --- src/command/render/filters.ts | 24 ++++++++++++++-------- src/resources/filters/quarto-pre/shiny.lua | 5 +++-- src/resources/pandoc/datadir/init.lua | 13 +++--------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/command/render/filters.ts b/src/command/render/filters.ts index 13edc229530..863d3d9603a 100644 --- a/src/command/render/filters.ts +++ b/src/command/render/filters.ts @@ -85,7 +85,7 @@ import { quartoConfig } from "../../core/quarto.ts"; import { metadataNormalizationFilterActive } from "./normalize.ts"; import { kCodeAnnotations } from "../../format/html/format-html-shared.ts"; import { projectOutputDir } from "../../project/project-shared.ts"; -import { relative } from "../../deno_ral/path.ts"; +import { basename, relative } from "../../deno_ral/path.ts"; import { citeIndexFilterParams } from "../../project/project-cites.ts"; import { debug } from "../../deno_ral/log.ts"; import { kJatsSubarticle } from "../../format/jats/format-jats-types.ts"; @@ -660,7 +660,11 @@ async function quartoFilterParams( params[kHasResourcePath] = hasResourcePath; // The source document - params[kQuartoSource] = options.source; + if (options.project.isSingleFile) { + params[kQuartoSource] = basename(options.source); + } else { + params[kQuartoSource] = options.source; + } // profile as an array params[kQuartoProfile.toLowerCase()] = activeProfiles(); @@ -860,7 +864,7 @@ async function resolveFilterExtension( } let pathToResolve: string | null = null; - + if (typeof filter === "string") { pathToResolve = filter; } else if (typeof filter === "object" && filter.path) { @@ -869,7 +873,9 @@ async function resolveFilterExtension( if (pathToResolve) { // The filter string points to an executable file which exists - if (existsSync(pathToResolve) && !Deno.statSync(pathToResolve).isDirectory) { + if ( + existsSync(pathToResolve) && !Deno.statSync(pathToResolve).isDirectory + ) { return filter; } @@ -896,17 +902,19 @@ async function resolveFilterExtension( if (typeof filter === "string") { return extensionFilters; } else if (isFilterEntryPoint(filter)) { - return extensionFilters.map(extFilter => { + return extensionFilters.map((extFilter) => { if (typeof extFilter === "string") { return { - type: extFilter.endsWith(".lua") ? "lua" : "json" as "lua" | "json", + type: extFilter.endsWith(".lua") + ? "lua" + : "json" as "lua" | "json", path: extFilter, - at: filter.at + at: filter.at, }; } else { return { ...extFilter, - at: filter.at + at: filter.at, }; } }); diff --git a/src/resources/filters/quarto-pre/shiny.lua b/src/resources/filters/quarto-pre/shiny.lua index ccf2cb5e64f..ad04d017dca 100644 --- a/src/resources/filters/quarto-pre/shiny.lua +++ b/src/resources/filters/quarto-pre/shiny.lua @@ -112,6 +112,7 @@ function server_shiny() end, Pandoc = function(doc) + print(quarto.doc.output_file) codeCells["html_file"] = pandoc.path.split_extension( pandoc.path.filename(quarto.doc.output_file) ) .. ".html" @@ -123,7 +124,7 @@ function server_shiny() end -- Write the code cells to a temporary file. - codeCellsOutfile = pandoc.path.split_extension(quarto.doc.output_file) .. "-cells.tmp.json" + local codeCellsOutfile = pandoc.path.split_extension(quarto.doc.output_file) .. "-cells.tmp.json" local file = io.open(codeCellsOutfile, "w") if file == nil then error("Error opening file: " .. codeCellsOutfile .. " for writing.") @@ -132,7 +133,7 @@ function server_shiny() file:close() -- Convert the json file to app.py by calling `shiny convert-cells`. - appOutfile = pandoc.path.join({ + local appOutfile = pandoc.path.join({ pandoc.path.directory(quarto.doc.output_file), "app.py" }); diff --git a/src/resources/pandoc/datadir/init.lua b/src/resources/pandoc/datadir/init.lua index dd052fa1a76..a02c83cb0c9 100644 --- a/src/resources/pandoc/datadir/init.lua +++ b/src/resources/pandoc/datadir/init.lua @@ -645,16 +645,9 @@ local function inputFile() return source else local projectDir = projectDirectory() - if projectDir then - return pandoc.path.join({projectDir, source}) - else - -- outside of a project, quarto already changes - -- pwd to the file's directory prior to calling pandoc, - -- so we should just use the filename - -- https://github.com/quarto-dev/quarto-cli/issues/7424 - local path_parts = pandoc.path.split(source) - return pandoc.path.join({pandoc.system.get_working_directory(), path_parts[#path_parts]}) - end + -- we now always have a projectDir, even in single-file settings + assert(projectDir) + return pandoc.path.join({projectDir, source}) end end From 50291e762608a517ee5de4d980ed9f047efd4793 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 16 Dec 2025 15:30:36 -0600 Subject: [PATCH 3/4] sometimes projectDirectory can still be nil --- src/resources/pandoc/datadir/init.lua | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/resources/pandoc/datadir/init.lua b/src/resources/pandoc/datadir/init.lua index a02c83cb0c9..02dd13c6b85 100644 --- a/src/resources/pandoc/datadir/init.lua +++ b/src/resources/pandoc/datadir/init.lua @@ -645,9 +645,16 @@ local function inputFile() return source else local projectDir = projectDirectory() - -- we now always have a projectDir, even in single-file settings - assert(projectDir) - return pandoc.path.join({projectDir, source}) + if projectDir then + return pandoc.path.join({projectDir, source}) + else + -- outside of a project, quarto already changes + -- pwd to the file's directory prior to calling pandoc, + -- so we should just use the filename + -- https://github.com/quarto-dev/quarto-cli/issues/7424 + local path_parts = pandoc.path.split(source) + return pandoc.path.join({pandoc.system.get_working_directory(), path_parts[#path_parts]}) + end end end @@ -655,6 +662,10 @@ local function outputFile() local projectOutDir = projectOutputDirectory() if projectOutDir then local projectDir = projectDirectory() + print("---") + print(projectDir) + print(pandoc.path.directory(inputFile())) + print("---") if projectDir then local input = pandoc.path.directory(inputFile()) local relativeDir = pandoc.path.make_relative(input, projectDir) From f89c530848308b30b1c167646cc1c5bc09424b40 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Thu, 18 Dec 2025 08:22:10 -0600 Subject: [PATCH 4/4] remove stray prints --- src/resources/filters/quarto-post/foldcode.lua | 1 - src/resources/filters/quarto-post/typst.lua | 1 - src/resources/filters/quarto-pre/shiny.lua | 1 - src/resources/pandoc/datadir/init.lua | 4 ---- 4 files changed, 7 deletions(-) diff --git a/src/resources/filters/quarto-post/foldcode.lua b/src/resources/filters/quarto-post/foldcode.lua index ab68552c298..35a9ac50dc5 100644 --- a/src/resources/filters/quarto-post/foldcode.lua +++ b/src/resources/filters/quarto-post/foldcode.lua @@ -106,7 +106,6 @@ function fold_code_and_lift_codeblocks() }) if need_to_move_dl then assert(prev_annotated_code_block_scaffold) - print(prev_annotated_code_block_scaffold) prev_annotated_code_block_scaffold.content:insert(div) return {} end diff --git a/src/resources/filters/quarto-post/typst.lua b/src/resources/filters/quarto-post/typst.lua index 6380dc8fee7..8a55ec11c94 100644 --- a/src/resources/filters/quarto-post/typst.lua +++ b/src/resources/filters/quarto-post/typst.lua @@ -25,7 +25,6 @@ function render_typst() m["toc-indent"] = option("toc-indent") if m["number-depth"] then number_depth = tonumber(pandoc.utils.stringify(m["number-depth"])) - print(number_depth) end return m end diff --git a/src/resources/filters/quarto-pre/shiny.lua b/src/resources/filters/quarto-pre/shiny.lua index ad04d017dca..28e2f7514a1 100644 --- a/src/resources/filters/quarto-pre/shiny.lua +++ b/src/resources/filters/quarto-pre/shiny.lua @@ -112,7 +112,6 @@ function server_shiny() end, Pandoc = function(doc) - print(quarto.doc.output_file) codeCells["html_file"] = pandoc.path.split_extension( pandoc.path.filename(quarto.doc.output_file) ) .. ".html" diff --git a/src/resources/pandoc/datadir/init.lua b/src/resources/pandoc/datadir/init.lua index 02dd13c6b85..dd052fa1a76 100644 --- a/src/resources/pandoc/datadir/init.lua +++ b/src/resources/pandoc/datadir/init.lua @@ -662,10 +662,6 @@ local function outputFile() local projectOutDir = projectOutputDirectory() if projectOutDir then local projectDir = projectDirectory() - print("---") - print(projectDir) - print(pandoc.path.directory(inputFile())) - print("---") if projectDir then local input = pandoc.path.directory(inputFile()) local relativeDir = pandoc.path.make_relative(input, projectDir)