|
1 | 1 | /* |
2 | 2 | * dart-sass.ts |
3 | 3 | * |
4 | | - * Copyright (C) 2020-2022 Posit Software, PBC |
| 4 | + * Copyright (C) 2020-2025 Posit Software, PBC |
5 | 5 | */ |
6 | 6 | import { join } from "../deno_ral/path.ts"; |
7 | 7 |
|
8 | 8 | import { architectureToolsPath } from "./resources.ts"; |
9 | 9 | import { execProcess } from "./process.ts"; |
10 | | -import { ProcessResult } from "./process-types.ts"; |
11 | 10 | import { TempContext } from "./temp.ts"; |
12 | 11 | import { lines } from "./text.ts"; |
13 | 12 | import { debug, info } from "../deno_ral/log.ts"; |
14 | 13 | import { existsSync } from "../deno_ral/fs.ts"; |
15 | 14 | import { warnOnce } from "./log.ts"; |
16 | 15 | import { isWindows } from "../deno_ral/platform.ts"; |
17 | | -import { requireQuoting, safeWindowsExec } from "./windows.ts"; |
18 | 16 |
|
19 | 17 | export function dartSassInstallDir() { |
20 | 18 | return architectureToolsPath("dart-sass"); |
@@ -60,80 +58,91 @@ export async function dartCompile( |
60 | 58 | */ |
61 | 59 | export interface DartCommandOptions { |
62 | 60 | /** |
63 | | - * Override the sass executable path. |
64 | | - * Primarily used for testing with spaced paths. |
| 61 | + * Override the dart-sass install directory. |
| 62 | + * Used for testing with non-standard paths (spaces, accented characters). |
65 | 63 | */ |
66 | | - sassPath?: string; |
| 64 | + installDir?: string; |
67 | 65 | } |
68 | 66 |
|
69 | | -export async function dartCommand( |
70 | | - args: string[], |
71 | | - options?: DartCommandOptions, |
72 | | -) { |
73 | | - const resolvePath = () => { |
| 67 | +/** |
| 68 | + * Resolve the dart-sass command and its base arguments. |
| 69 | + * |
| 70 | + * On Windows, calls dart.exe + sass.snapshot directly instead of going |
| 71 | + * through sass.bat. The bundled sass.bat is a thin wrapper generated by |
| 72 | + * dart_cli_pkg that just runs: |
| 73 | + * "%SCRIPTPATH%\src\dart.exe" "%SCRIPTPATH%\src\sass.snapshot" %arguments% |
| 74 | + * |
| 75 | + * Template source: |
| 76 | + * https://github.com/google/dart_cli_pkg/blob/main/lib/src/templates/standalone/executable.bat.mustache |
| 77 | + * Upstream issue to ship standalone .exe instead of .bat + dart.exe: |
| 78 | + * https://github.com/google/dart_cli_pkg/issues/67 |
| 79 | + * |
| 80 | + * Bypassing sass.bat avoids multiple .bat file issues on Windows: |
| 81 | + * - Deno quoting bugs with spaced paths (#13997) |
| 82 | + * - cmd.exe OEM code page misreading UTF-8 accented paths (#14267) |
| 83 | + * - Enterprise group policy blocking .bat execution (#6651) |
| 84 | + */ |
| 85 | +function resolveSassCommand(options?: DartCommandOptions): { |
| 86 | + cmd: string; |
| 87 | + baseArgs: string[]; |
| 88 | +} { |
| 89 | + const installDir = options?.installDir; |
| 90 | + if (installDir == null) { |
| 91 | + // Only check env var override when no explicit installDir is provided. |
| 92 | + // If QUARTO_DART_SASS doesn't exist on disk, fall through to use the |
| 93 | + // bundled dart-sass at the default architectureToolsPath. |
74 | 94 | const dartOverrideCmd = Deno.env.get("QUARTO_DART_SASS"); |
75 | 95 | if (dartOverrideCmd) { |
76 | 96 | if (!existsSync(dartOverrideCmd)) { |
77 | 97 | warnOnce( |
78 | 98 | `Specified QUARTO_DART_SASS does not exist, using built in dart sass.`, |
79 | 99 | ); |
80 | 100 | } else { |
81 | | - return dartOverrideCmd; |
| 101 | + return { cmd: dartOverrideCmd, baseArgs: [] }; |
82 | 102 | } |
83 | 103 | } |
| 104 | + } |
84 | 105 |
|
85 | | - const command = isWindows ? "sass.bat" : "sass"; |
86 | | - return architectureToolsPath(join("dart-sass", command)); |
87 | | - }; |
88 | | - const sass = options?.sassPath ?? resolvePath(); |
89 | | - |
90 | | - // Process result helper (shared by Windows and non-Windows paths) |
91 | | - const processResult = (result: ProcessResult): string | undefined => { |
92 | | - if (result.success) { |
93 | | - if (result.stderr) { |
94 | | - info(result.stderr); |
95 | | - } |
96 | | - return result.stdout; |
97 | | - } else { |
98 | | - debug(`[DART path] : ${sass}`); |
99 | | - debug(`[DART args] : ${args.join(" ")}`); |
100 | | - debug(`[DART stdout] : ${result.stdout}`); |
101 | | - debug(`[DART stderr] : ${result.stderr}`); |
102 | | - |
103 | | - const errLines = lines(result.stderr || ""); |
104 | | - // truncate the last 2 lines (they include a pointer to the temp file containing |
105 | | - // all of the concatenated sass, which is more or less incomprehensible for users. |
106 | | - const errMsg = errLines.slice(0, errLines.length - 2).join("\n"); |
107 | | - throw new Error("Theme file compilation failed:\n\n" + errMsg); |
108 | | - } |
109 | | - }; |
| 106 | + const sassDir = installDir ?? architectureToolsPath("dart-sass"); |
110 | 107 |
|
111 | | - // On Windows, use safeWindowsExec to handle paths with spaces |
112 | | - // (e.g., when Quarto is installed in C:\Program Files\) |
113 | | - // See https://github.com/quarto-dev/quarto-cli/issues/13997 |
114 | 108 | if (isWindows) { |
115 | | - const quoted = requireQuoting([sass, ...args]); |
116 | | - const result = await safeWindowsExec( |
117 | | - quoted.args[0], |
118 | | - quoted.args.slice(1), |
119 | | - (cmd: string[]) => { |
120 | | - return execProcess({ |
121 | | - cmd: cmd[0], |
122 | | - args: cmd.slice(1), |
123 | | - stdout: "piped", |
124 | | - stderr: "piped", |
125 | | - }); |
126 | | - }, |
127 | | - ); |
128 | | - return processResult(result); |
| 109 | + return { |
| 110 | + cmd: join(sassDir, "src", "dart.exe"), |
| 111 | + baseArgs: [join(sassDir, "src", "sass.snapshot")], |
| 112 | + }; |
129 | 113 | } |
130 | 114 |
|
131 | | - // Non-Windows: direct execution |
| 115 | + return { cmd: join(sassDir, "sass"), baseArgs: [] }; |
| 116 | +} |
| 117 | + |
| 118 | +export async function dartCommand( |
| 119 | + args: string[], |
| 120 | + options?: DartCommandOptions, |
| 121 | +) { |
| 122 | + const { cmd, baseArgs } = resolveSassCommand(options); |
| 123 | + |
132 | 124 | const result = await execProcess({ |
133 | | - cmd: sass, |
134 | | - args, |
| 125 | + cmd, |
| 126 | + args: [...baseArgs, ...args], |
135 | 127 | stdout: "piped", |
136 | 128 | stderr: "piped", |
137 | 129 | }); |
138 | | - return processResult(result); |
| 130 | + |
| 131 | + if (result.success) { |
| 132 | + if (result.stderr) { |
| 133 | + info(result.stderr); |
| 134 | + } |
| 135 | + return result.stdout; |
| 136 | + } else { |
| 137 | + debug(`[DART cmd] : ${cmd}`); |
| 138 | + debug(`[DART args] : ${[...baseArgs, ...args].join(" ")}`); |
| 139 | + debug(`[DART stdout] : ${result.stdout}`); |
| 140 | + debug(`[DART stderr] : ${result.stderr}`); |
| 141 | + |
| 142 | + const errLines = lines(result.stderr || ""); |
| 143 | + // truncate the last 2 lines (they include a pointer to the temp file containing |
| 144 | + // all of the concatenated sass, which is more or less incomprehensible for users. |
| 145 | + const errMsg = errLines.slice(0, errLines.length - 2).join("\n"); |
| 146 | + throw new Error("Theme file compilation failed:\n\n" + errMsg); |
| 147 | + } |
139 | 148 | } |
0 commit comments