22 * dart-sass.test.ts
33 *
44 * Tests for dart-sass functionality.
5- * Validates fix for https://github.com/quarto-dev/quarto-cli/issues/13997
5+ * Validates fixes for:
6+ * https://github.com/quarto-dev/quarto-cli/issues/13997 (spaced paths)
7+ * https://github.com/quarto-dev/quarto-cli/issues/14267 (accented paths)
8+ * https://github.com/quarto-dev/quarto-cli/issues/6651 (enterprise .bat blocking)
69 *
710 * Copyright (C) 2020-2025 Posit Software, PBC
811 */
@@ -13,46 +16,53 @@ import { isWindows } from "../../src/deno_ral/platform.ts";
1316import { join } from "../../src/deno_ral/path.ts" ;
1417import { dartCommand , dartSassInstallDir } from "../../src/core/dart-sass.ts" ;
1518
19+ /**
20+ * Helper: create a junction to the real dart-sass install dir at `targetDir`.
21+ * Returns cleanup function to remove the junction.
22+ */
23+ async function createDartSassJunction ( targetDir : string ) {
24+ const sassInstallDir = dartSassInstallDir ( ) ;
25+ const result = await new Deno . Command ( "cmd" , {
26+ args : [ "/c" , "mklink" , "/J" , targetDir , sassInstallDir ] ,
27+ } ) . output ( ) ;
28+
29+ if ( ! result . success ) {
30+ const stderr = new TextDecoder ( ) . decode ( result . stderr ) ;
31+ throw new Error ( `Failed to create junction: ${ stderr } ` ) ;
32+ }
33+
34+ return async ( ) => {
35+ await new Deno . Command ( "cmd" , {
36+ args : [ "/c" , "rmdir" , targetDir ] ,
37+ } ) . output ( ) ;
38+ } ;
39+ }
40+
1641// Test that dartCommand handles spaced paths on Windows (issue #13997)
17- // The bug only triggers when BOTH the executable path AND arguments contain spaces .
42+ // dart.exe is called directly, bypassing sass.bat and its quoting issues .
1843unitTest (
1944 "dartCommand - handles spaced paths on Windows (issue #13997)" ,
2045 async ( ) => {
21- // Create directories with spaces for both sass and file arguments
2246 const tempBase = Deno . makeTempDirSync ( { prefix : "quarto_test_" } ) ;
2347 const spacedSassDir = join ( tempBase , "Program Files" , "dart-sass" ) ;
2448 const spacedProjectDir = join ( tempBase , "My Project" ) ;
25- const sassInstallDir = dartSassInstallDir ( ) ;
49+
50+ let removeJunction : ( ( ) => Promise < void > ) | undefined ;
2651
2752 try {
28- // Create directories
2953 Deno . mkdirSync ( join ( tempBase , "Program Files" ) , { recursive : true } ) ;
3054 Deno . mkdirSync ( spacedProjectDir , { recursive : true } ) ;
3155
32- // Create junction (Windows directory symlink) to actual dart-sass
33- const junctionResult = await new Deno . Command ( "cmd" , {
34- args : [ "/c" , "mklink" , "/J" , spacedSassDir , sassInstallDir ] ,
35- } ) . output ( ) ;
56+ removeJunction = await createDartSassJunction ( spacedSassDir ) ;
3657
37- if ( ! junctionResult . success ) {
38- const stderr = new TextDecoder ( ) . decode ( junctionResult . stderr ) ;
39- throw new Error ( `Failed to create junction: ${ stderr } ` ) ;
40- }
41-
42- // Create test SCSS file in spaced path (args with spaces)
4358 const inputScss = join ( spacedProjectDir , "test style.scss" ) ;
4459 const outputCss = join ( spacedProjectDir , "test style.css" ) ;
4560 Deno . writeTextFileSync ( inputScss , "body { color: red; }" ) ;
4661
47- const spacedSassPath = join ( spacedSassDir , "sass.bat" ) ;
48-
49- // This is the exact bug scenario: spaced exe path + spaced args
50- // Without the fix, this fails with "C:\...\Program" not recognized
5162 const result = await dartCommand ( [ inputScss , outputCss ] , {
52- sassPath : spacedSassPath ,
63+ installDir : spacedSassDir ,
5364 } ) ;
5465
55- // Verify compilation succeeded (no stdout expected for file-to-file compilation)
5666 assert (
5767 result === undefined || result === "" ,
5868 "Sass compile should succeed (no stdout for file-to-file compilation)" ,
@@ -62,14 +72,56 @@ unitTest(
6272 "Output CSS file should be created" ,
6373 ) ;
6474 } finally {
65- // Cleanup: remove junction first (rmdir for junctions), then temp directory
6675 try {
67- await new Deno . Command ( "cmd" , {
68- args : [ "/c" , "rmdir" , spacedSassDir ] ,
69- } ) . output ( ) ;
76+ if ( removeJunction ) await removeJunction ( ) ;
77+ await Deno . remove ( tempBase , { recursive : true } ) ;
78+ } catch ( e ) {
79+ console . debug ( "Test cleanup failed:" , e ) ;
80+ }
81+ }
82+ } ,
83+ { ignore : ! isWindows } ,
84+ ) ;
85+
86+ // Test that dartCommand handles accented characters in paths (issue #14267)
87+ // Accented chars in user paths (e.g., C:\Users\Sébastien\) broke when
88+ // dart-sass was invoked through a .bat wrapper with UTF-8/OEM mismatch.
89+ unitTest (
90+ "dartCommand - handles accented characters in paths (issue #14267)" ,
91+ async ( ) => {
92+ const tempBase = Deno . makeTempDirSync ( { prefix : "quarto_test_" } ) ;
93+ const accentedSassDir = join ( tempBase , "Sébastien" , "dart-sass" ) ;
94+ const accentedProjectDir = join ( tempBase , "Sébastien" , "project" ) ;
95+
96+ let removeJunction : ( ( ) => Promise < void > ) | undefined ;
97+
98+ try {
99+ Deno . mkdirSync ( join ( tempBase , "Sébastien" ) , { recursive : true } ) ;
100+ Deno . mkdirSync ( accentedProjectDir , { recursive : true } ) ;
101+
102+ removeJunction = await createDartSassJunction ( accentedSassDir ) ;
103+
104+ const inputScss = join ( accentedProjectDir , "style.scss" ) ;
105+ const outputCss = join ( accentedProjectDir , "style.css" ) ;
106+ Deno . writeTextFileSync ( inputScss , "body { color: blue; }" ) ;
107+
108+ const result = await dartCommand ( [ inputScss , outputCss ] , {
109+ installDir : accentedSassDir ,
110+ } ) ;
111+
112+ assert (
113+ result === undefined || result === "" ,
114+ "Sass compile should succeed with accented path" ,
115+ ) ;
116+ assert (
117+ Deno . statSync ( outputCss ) . isFile ,
118+ "Output CSS file should be created at accented path" ,
119+ ) ;
120+ } finally {
121+ try {
122+ if ( removeJunction ) await removeJunction ( ) ;
70123 await Deno . remove ( tempBase , { recursive : true } ) ;
71124 } catch ( e ) {
72- // Best effort cleanup - log for debugging if it fails
73125 console . debug ( "Test cleanup failed:" , e ) ;
74126 }
75127 }
0 commit comments