Skip to content

Commit 98be8a6

Browse files
authored
Improve pre/post render script logging and add output control env vars (#12444)
Add `Running script` prefix to script execution logging for clearer output. Pass two new environment variables to pre/post render scripts so they can adapt their output: - QUARTO_PROJECT_SCRIPT_PROGRESS: "1" when progress can be shown, "0" otherwise - QUARTO_PROJECT_SCRIPT_QUIET: "1" when --quiet is active, "0" otherwise Consolidate handler/non-handler code paths in runScripts using a default handler with nullish coalescing, and move environment initialization before the loop.
1 parent d36fe54 commit 98be8a6

File tree

13 files changed

+215
-40
lines changed

13 files changed

+215
-40
lines changed

llm-docs/testing-patterns.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,62 @@ testQuartoCmd(
8888
- Use absolute paths with `join()` for file verification
8989
- Clean up output directories in teardown
9090

91+
### Pre/Post Render Script Tests
92+
93+
For testing pre-render or post-render scripts that run during project rendering:
94+
95+
```typescript
96+
import { docs } from "../../utils.ts";
97+
import { join } from "../../../src/deno_ral/path.ts";
98+
import { existsSync } from "../../../src/deno_ral/fs.ts";
99+
import { testQuartoCmd } from "../../test.ts";
100+
import { noErrors, validJsonWithFields } from "../../verify.ts";
101+
import { safeRemoveIfExists } from "../../../src/core/path.ts";
102+
103+
const projectDir = docs("project/prepost/my-test");
104+
const projectDirAbs = join(Deno.cwd(), projectDir);
105+
const dumpPath = join(projectDirAbs, "output.json");
106+
const outDir = join(projectDirAbs, "_site");
107+
108+
testQuartoCmd(
109+
"render",
110+
[projectDir],
111+
[
112+
noErrors,
113+
validJsonWithFields(dumpPath, {
114+
expected: "value",
115+
}),
116+
],
117+
{
118+
teardown: async () => {
119+
safeRemoveIfExists(dumpPath);
120+
if (existsSync(outDir)) {
121+
await Deno.remove(outDir, { recursive: true });
122+
}
123+
},
124+
},
125+
);
126+
```
127+
128+
**Fixture structure:**
129+
130+
```
131+
tests/docs/project/prepost/my-test/
132+
├── _quarto.yml # project config with pre-render/post-render scripts
133+
├── index.qmd # minimal page (website needs at least one)
134+
├── check-env.ts # pre/post-render script (Deno TypeScript)
135+
└── .gitignore # exclude .quarto/ and *.quarto_ipynb
136+
```
137+
138+
**Key points:**
139+
- Pre/post-render scripts run as subprocesses with `cwd` set to the project directory
140+
- Scripts access environment variables via `Deno.env.get()` and can write files for verification
141+
- Use `validJsonWithFields` for JSON file verification (parses and compares field values exactly)
142+
- Use `ensureFileRegexMatches` for non-JSON files or when regex matching is needed
143+
- The file dump pattern (script writes JSON, test reads it) is useful for verifying env vars and other runtime state
144+
- Clean up both the dump file and the output directory in teardown
145+
- Existing fixtures: `tests/docs/project/prepost/` (mutate-render-list, invalid-mutate, extension, issue-10828, script-env-vars)
146+
91147
### Extension Template Tests
92148

93149
For testing `quarto use template`:
@@ -164,6 +220,43 @@ folderExists(path: string)
164220
directoryEmptyButFor(dir: string, allowedFiles: string[])
165221
```
166222

223+
### Content Verifiers
224+
225+
```typescript
226+
// Regex match on file contents (matches required, noMatches must be absent)
227+
ensureFileRegexMatches(file: string, matches: (string | RegExp)[], noMatches?: (string | RegExp)[])
228+
229+
// Regex match on CSS files linked from HTML
230+
ensureCssRegexMatches(file: string, matches: (string | RegExp)[], noMatches?: (string | RegExp)[])
231+
232+
// Check HTML elements exist or don't exist (CSS selectors)
233+
ensureHtmlElements(file: string, noElements: string[], elements: string[])
234+
235+
// Verify JSON structure has expected fields (parses JSON, compares values with deep equality)
236+
validJsonWithFields(file: string, fields: Record<string, unknown>)
237+
238+
// Check output message at specific log level
239+
printsMessage(options: { level: string, regex: RegExp })
240+
```
241+
242+
### Assertion Helpers
243+
244+
```typescript
245+
// Assert path exists (throws if missing)
246+
verifyPath(path: string)
247+
248+
// Assert path does NOT exist (throws if present)
249+
verifyNoPath(path: string)
250+
```
251+
252+
### Cleanup Helpers
253+
254+
```typescript
255+
// Safe file removal (no error if missing) - from src/core/path.ts
256+
import { safeRemoveIfExists } from "../../../src/core/path.ts";
257+
safeRemoveIfExists(path: string)
258+
```
259+
167260
### Path Helpers
168261

169262
```typescript

news/changelog-1.9.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ All changes included in 1.9:
109109

110110
## Projects
111111

112+
- ([#12444](https://github.com/quarto-dev/quarto-cli/pull/12444)): Improve pre/post render script logging with `Running script` prefix and add `QUARTO_PROJECT_SCRIPT_PROGRESS` and `QUARTO_PROJECT_SCRIPT_QUIET` environment variables so scripts can adapt their output.
112113
- ([#13892](https://github.com/quarto-dev/quarto-cli/issues/13892)): Fix `output-dir: ./` deleting entire project directory. `output-dir` must be a subdirectory of the project directory and check is now better to avoid deleting the project itself when it revolves to the same path.
113114

114115
### `website`

src/command/render/project.ts

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import { Format } from "../../config/types.ts";
7878
import { fileExecutionEngine } from "../../execute/engine.ts";
7979
import { projectContextForDirectory } from "../../project/project-context.ts";
8080
import { ProjectType } from "../../project/types/types.ts";
81+
import { RunHandlerOptions } from "../../core/run/types.ts";
8182

8283
const noMutationValidations = (
8384
projType: ProjectType,
@@ -937,52 +938,61 @@ async function runScripts(
937938
quiet: boolean,
938939
env?: { [key: string]: string },
939940
) {
941+
// initialize the environment if needed
942+
if (env) {
943+
env = {
944+
...env,
945+
};
946+
} else {
947+
env = {};
948+
}
949+
if (!env) throw new Error("should never get here");
950+
951+
// Pass some argument as environment
952+
env["QUARTO_PROJECT_SCRIPT_PROGRESS"] = progress ? "1" : "0";
953+
env["QUARTO_PROJECT_SCRIPT_QUIET"] = quiet ? "1" : "0";
954+
940955
for (let i = 0; i < scripts.length; i++) {
941956
const args = parseShellRunCommand(scripts[i]);
942957
const script = args[0];
943958

944959
if (progress && !quiet) {
945-
info(colors.bold(colors.blue(`${script}`)));
960+
info(colors.bold(colors.blue(`Running script '${script}'`)));
946961
}
947962

948-
const handler = handlerForScript(script);
949-
if (handler) {
950-
if (env) {
951-
env = {
952-
...env,
953-
};
954-
} else {
955-
env = {};
956-
}
957-
if (!env) throw new Error("should never get here");
958-
const input = Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES");
959-
const output = Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES");
960-
if (input) {
961-
env["QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES"] = input;
962-
}
963-
if (output) {
964-
env["QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES"] = output;
965-
}
963+
const handler = handlerForScript(script) ?? {
964+
run: async (
965+
script: string,
966+
args: string[],
967+
_stdin?: string,
968+
options?: RunHandlerOptions,
969+
) => {
970+
return await execProcess({
971+
cmd: script,
972+
args: args,
973+
cwd: options?.cwd,
974+
stdout: options?.stdout,
975+
env: options?.env,
976+
});
977+
},
978+
};
966979

967-
const result = await handler.run(script, args.splice(1), undefined, {
968-
cwd: projDir,
969-
stdout: quiet ? "piped" : "inherit",
970-
env,
971-
});
972-
if (!result.success) {
973-
throw new Error();
974-
}
975-
} else {
976-
const result = await execProcess({
977-
cmd: args[0],
978-
args: args.slice(1),
979-
cwd: projDir,
980-
stdout: quiet ? "piped" : "inherit",
981-
env,
982-
});
983-
if (!result.success) {
984-
throw new Error();
985-
}
980+
const input = Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES");
981+
const output = Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES");
982+
if (input) {
983+
env["QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES"] = input;
984+
}
985+
if (output) {
986+
env["QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES"] = output;
987+
}
988+
989+
const result = await handler.run(script, args.slice(1), undefined, {
990+
cwd: projDir,
991+
stdout: quiet ? "piped" : "inherit",
992+
env,
993+
});
994+
if (!result.success) {
995+
throw new Error();
986996
}
987997
}
988998
if (scripts.length > 0) {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/.quarto/
2+
**/*.quarto_ipynb
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
/.quarto/
2+
3+
**/*.quarto_ipynb
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
/.quarto/
2+
3+
**/*.quarto_ipynb
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
/.quarto/
2+
3+
**/*.quarto_ipynb
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/.quarto/
2+
**/*.quarto_ipynb
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
project:
2+
type: website
3+
pre-render: check-env.ts
4+
5+
website:
6+
title: "script-env-vars"
7+
8+
format:
9+
html:
10+
theme: cosmo
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
title: About
3+
---
4+
5+
About page

0 commit comments

Comments
 (0)