@@ -116,12 +116,11 @@ async function resolveConfig(
116116 const configPath = join ( cwd , "typst-gather.toml" ) ;
117117 if ( existsSync ( configPath ) ) {
118118 info ( `Using config: ${ configPath } ` ) ;
119- // Return the config file path - rust will parse it directly
120- // We still parse minimally to validate and show info
121- const content = Deno . readTextFileSync ( configPath ) ;
122- const config = parseSimpleToml ( content ) ;
123- config . configFile = configPath ;
124- return config ;
119+ return {
120+ configFile : configPath ,
121+ destination : "" ,
122+ discover : [ ] ,
123+ } ;
125124 }
126125
127126 // No config file - try to auto-detect from _extension.yml
@@ -164,105 +163,58 @@ async function resolveConfig(
164163 } ;
165164}
166165
167- function parseSimpleToml ( content : string ) : TypstGatherConfig {
168- const lines = content . split ( "\n" ) ;
169- let rootdir : string | undefined ;
170- let destination = "" ;
171- const discover : string [ ] = [ ] ;
172-
173- for ( const line of lines ) {
174- const trimmed = line . trim ( ) ;
175-
176- // Parse rootdir
177- const rootdirMatch = trimmed . match ( / ^ r o o t d i r \s * = \s * " ( [ ^ " ] + ) " / ) ;
178- if ( rootdirMatch ) {
179- rootdir = rootdirMatch [ 1 ] ;
180- continue ;
181- }
182-
183- // Parse destination
184- const destMatch = trimmed . match ( / ^ d e s t i n a t i o n \s * = \s * " ( [ ^ " ] + ) " / ) ;
185- if ( destMatch ) {
186- destination = destMatch [ 1 ] ;
187- continue ;
188- }
189-
190- // Parse discover as string
191- const discoverStrMatch = trimmed . match ( / ^ d i s c o v e r \s * = \s * " ( [ ^ " ] + ) " / ) ;
192- if ( discoverStrMatch ) {
193- discover . push ( discoverStrMatch [ 1 ] ) ;
194- continue ;
195- }
196-
197- // Parse discover as array (simple single-line parsing)
198- const discoverArrMatch = trimmed . match ( / ^ d i s c o v e r \s * = \s * \[ ( [ ^ \] ] + ) \] / ) ;
199- if ( discoverArrMatch ) {
200- const items = discoverArrMatch [ 1 ] . split ( "," ) ;
201- for ( const item of items ) {
202- const match = item . trim ( ) . match ( / " ( [ ^ " ] + ) " / ) ;
203- if ( match ) {
204- discover . push ( match [ 1 ] ) ;
205- }
206- }
207- }
208- }
209-
210- return { rootdir, destination, discover } ;
211- }
212-
213- interface DiscoveredImport {
166+ export interface AnalyzeImport {
167+ namespace : string ;
214168 name : string ;
215169 version : string ;
216- sourceFile : string ;
170+ source : string ;
171+ direct : boolean ;
217172}
218173
219- interface DiscoveryResult {
220- preview : DiscoveredImport [ ] ;
221- local : DiscoveredImport [ ] ;
222- scannedFiles : string [ ] ;
174+ export interface AnalyzeResult {
175+ imports : AnalyzeImport [ ] ;
176+ files : string [ ] ;
223177}
224178
225- function discoverImportsFromFiles ( files : string [ ] ) : DiscoveryResult {
226- const result : DiscoveryResult = {
227- preview : [ ] ,
228- local : [ ] ,
229- scannedFiles : [ ] ,
230- } ;
179+ function typstGatherBinaryPath ( ) : string {
180+ const binaryName = isWindows ? "typst-gather.exe" : "typst-gather" ;
181+ const binary = Deno . env . get ( "QUARTO_TYPST_GATHER" ) ||
182+ architectureToolsPath ( binaryName ) ;
231183
232- // Regex to match @namespace /name:version imports
233- // Note: #include is for files, not packages, so we only match #import
234- const importRegex = / # i m p o r t \s + " @ ( \w + ) \/ ( [ ^ : ] + ) : ( [ ^ " ] + ) " / g;
184+ if ( ! existsSync ( binary ) ) {
185+ throw new Error (
186+ `typst-gather binary not found.\n` +
187+ `Run ./configure.sh to build and install it.` ,
188+ ) ;
189+ }
235190
236- for ( const file of files ) {
237- if ( ! existsSync ( file ) ) continue ;
238- if ( ! file . endsWith ( ".typ" ) ) continue ;
191+ return binary ;
192+ }
239193
240- const filename = file . split ( "/" ) . pop ( ) || file ;
241- result . scannedFiles . push ( filename ) ;
194+ async function runAnalyze ( tomlConfig : string ) : Promise < AnalyzeResult > {
195+ const binary = typstGatherBinaryPath ( ) ;
196+
197+ const result = await execProcess (
198+ {
199+ cmd : binary ,
200+ args : [ "analyze" , "-" ] ,
201+ stdout : "piped" ,
202+ stderr : "piped" ,
203+ } ,
204+ tomlConfig ,
205+ ) ;
242206
243- try {
244- const content = Deno . readTextFileSync ( file ) ;
245- let match ;
246- while ( ( match = importRegex . exec ( content ) ) !== null ) {
247- const [ , namespace , name , version ] = match ;
248- const entry = { name, version, sourceFile : filename } ;
249-
250- if ( namespace === "preview" ) {
251- result . preview . push ( entry ) ;
252- } else if ( namespace === "local" ) {
253- result . local . push ( entry ) ;
254- }
255- }
256- } catch {
257- // Skip files that can't be read
258- }
207+ if ( ! result . success ) {
208+ throw new Error (
209+ result . stderr || "typst-gather analyze failed" ,
210+ ) ;
259211 }
260212
261- return result ;
213+ return JSON . parse ( result . stdout ! ) as AnalyzeResult ;
262214}
263215
264- function generateConfigContent (
265- discovery : DiscoveryResult ,
216+ export function generateConfigFromAnalysis (
217+ result : AnalyzeResult ,
266218 rootdir ?: string ,
267219) : string {
268220 const lines : string [ ] = [ ] ;
@@ -278,31 +230,31 @@ function generateConfigContent(
278230 lines . push ( "" ) ;
279231
280232 // Discover section
281- if ( discovery . scannedFiles . length > 0 ) {
282- if ( discovery . scannedFiles . length === 1 ) {
283- lines . push ( `discover = "${ toTomlPath ( discovery . scannedFiles [ 0 ] ) } "` ) ;
284- } else {
285- const files = discovery . scannedFiles . map ( ( f ) => `"${ toTomlPath ( f ) } "` )
286- . join ( ", " ) ;
287- lines . push ( `discover = [${ files } ]` ) ;
288- }
233+ if ( result . files . length === 1 ) {
234+ lines . push ( `discover = "${ toTomlPath ( result . files [ 0 ] ) } "` ) ;
235+ } else if ( result . files . length > 1 ) {
236+ const files = result . files . map ( ( f ) => `"${ toTomlPath ( f ) } "` ) . join ( ", " ) ;
237+ lines . push ( `discover = [${ files } ]` ) ;
289238 } else {
290239 lines . push ( '# discover = "template.typ" # Add your .typ files here' ) ;
291240 }
292241
293242 lines . push ( "" ) ;
294243
295244 // Preview section (commented out - packages will be auto-discovered)
245+ const previewImports = result . imports . filter ( ( i ) =>
246+ i . namespace === "preview"
247+ ) ;
296248 lines . push ( "# Preview packages are auto-discovered from imports." ) ;
297249 lines . push ( "# Uncomment to pin specific versions:" ) ;
298250 lines . push ( "# [preview]" ) ;
299- if ( discovery . preview . length > 0 ) {
300- // Deduplicate
251+ if ( previewImports . length > 0 ) {
301252 const seen = new Set < string > ( ) ;
302- for ( const { name, version } of discovery . preview ) {
253+ for ( const { name, version, direct , source } of previewImports ) {
303254 if ( ! seen . has ( name ) ) {
304255 seen . add ( name ) ;
305- lines . push ( `# ${ name } = "${ version } "` ) ;
256+ const suffix = direct ? "" : ` # via ${ source } ` ;
257+ lines . push ( `# ${ name } = "${ version } "${ suffix } ` ) ;
306258 }
307259 }
308260 } else {
@@ -312,21 +264,24 @@ function generateConfigContent(
312264 lines . push ( "" ) ;
313265
314266 // Local section
267+ const localImports = result . imports . filter (
268+ ( i ) => i . namespace === "local" && i . direct ,
269+ ) ;
315270 lines . push (
316271 "# Local packages (@local namespace) must be configured manually." ,
317272 ) ;
318- if ( discovery . local . length > 0 ) {
273+ if ( localImports . length > 0 ) {
319274 lines . push ( "# Found @local imports:" ) ;
320275 const seen = new Set < string > ( ) ;
321- for ( const { name, version, sourceFile } of discovery . local ) {
276+ for ( const { name, version, source } of localImports ) {
322277 if ( ! seen . has ( name ) ) {
323278 seen . add ( name ) ;
324- lines . push ( `# @local/${ name } :${ version } (in ${ sourceFile } )` ) ;
279+ lines . push ( `# @local/${ name } :${ version } (in ${ source } )` ) ;
325280 }
326281 }
327282 lines . push ( "[local]" ) ;
328283 seen . clear ( ) ;
329- for ( const { name } of discovery . local ) {
284+ for ( const { name } of localImports ) {
330285 if ( ! seen . has ( name ) ) {
331286 seen . add ( name ) ;
332287 lines . push ( `${ name } = "/path/to/${ name } " # TODO: set correct path` ) ;
@@ -373,14 +328,18 @@ async function initConfig(): Promise<void> {
373328 info ( `Found extension: ${ extensionDir } ` ) ;
374329 }
375330
376- // Discover imports from the files
377- const discovery = discoverImportsFromFiles ( typFiles ) ;
331+ // Build analyze config with discover paths
332+ const discoverArray = typFiles . map ( ( f ) => `"${ toTomlPath ( f ) } "` ) . join ( ", " ) ;
333+ const analyzeConfig = `discover = [${ discoverArray } ]\n` ;
334+
335+ // Run typst-gather analyze to discover imports
336+ const analysis = await runAnalyze ( analyzeConfig ) ;
378337
379338 // Calculate relative path from cwd to extension dir for rootdir
380339 const rootdir = relative ( Deno . cwd ( ) , extensionDir ) ;
381340
382- // Generate config content
383- const configContent = generateConfigContent ( discovery , rootdir ) ;
341+ // Generate config content from analysis
342+ const configContent = generateConfigFromAnalysis ( analysis , rootdir ) ;
384343
385344 // Write config file
386345 try {
@@ -390,23 +349,30 @@ async function initConfig(): Promise<void> {
390349 Deno . exit ( 1 ) ;
391350 }
392351
352+ const previewImports = analysis . imports . filter (
353+ ( i ) => i . namespace === "preview" ,
354+ ) ;
355+ const localImports = analysis . imports . filter (
356+ ( i ) => i . namespace === "local" && i . direct ,
357+ ) ;
358+
393359 info ( "Created typst-gather.toml" ) ;
394- if ( discovery . scannedFiles . length > 0 ) {
395- info ( ` Scanned: ${ discovery . scannedFiles . join ( ", " ) } ` ) ;
360+ if ( analysis . files . length > 0 ) {
361+ info ( ` Scanned: ${ analysis . files . join ( ", " ) } ` ) ;
396362 }
397- if ( discovery . preview . length > 0 ) {
398- info ( ` Found ${ discovery . preview . length } @preview import(s)` ) ;
363+ if ( previewImports . length > 0 ) {
364+ info ( ` Found ${ previewImports . length } @preview import(s)` ) ;
399365 }
400- if ( discovery . local . length > 0 ) {
366+ if ( localImports . length > 0 ) {
401367 info (
402- ` Found ${ discovery . local . length } @local import(s) - configure paths in [local] section` ,
368+ ` Found ${ localImports . length } @local import(s) - configure paths in [local] section` ,
403369 ) ;
404370 }
405371
406372 info ( "" ) ;
407373 info ( "Next steps:" ) ;
408374 info ( " 1. Review and edit typst-gather.toml" ) ;
409- if ( discovery . local . length > 0 ) {
375+ if ( localImports . length > 0 ) {
410376 info ( " 2. Add paths for @local packages in [local] section" ) ;
411377 }
412378 info ( " 3. Run: quarto call typst-gather" ) ;
@@ -442,37 +408,21 @@ export const typstGatherCommand = new Command()
442408 Deno . exit ( 1 ) ;
443409 }
444410
445- if ( ! config . destination ) {
446- console . error ( "No destination specified in configuration." ) ;
447- Deno . exit ( 1 ) ;
448- }
449-
450- if ( config . discover . length === 0 ) {
451- console . error ( "No files to discover imports from." ) ;
452- Deno . exit ( 1 ) ;
453- }
454-
455- // Find typst-gather binary in standard tools location
456- const binaryName = isWindows ? "typst-gather.exe" : "typst-gather" ;
457- const typstGatherBinary = architectureToolsPath ( binaryName ) ;
458- if ( ! existsSync ( typstGatherBinary ) ) {
459- console . error (
460- `typst-gather binary not found.\n` +
461- `Run ./configure.sh to build and install it.` ,
462- ) ;
463- Deno . exit ( 1 ) ;
464- }
411+ const typstGatherBinary = typstGatherBinaryPath ( ) ;
465412
466- // Determine config file to use
467- let configFileToUse : string ;
468- let tempConfig : string | null = null ;
413+ info ( `Running typst-gather...` ) ;
469414
415+ // Run typst-gather gather
416+ let result ;
470417 if ( config . configFile ) {
471- // Use existing config file directly - rust will parse [local], [preview], etc.
472- configFileToUse = config . configFile ;
418+ // Existing config file — pass directly
419+ result = await execProcess ( {
420+ cmd : typstGatherBinary ,
421+ args : [ "gather" , config . configFile ] ,
422+ cwd : Deno . cwd ( ) ,
423+ } ) ;
473424 } else {
474- // Create a temporary TOML config file for auto-detected config
475- tempConfig = Deno . makeTempFileSync ( { suffix : ".toml" } ) ;
425+ // Auto-detected — pipe config on stdin
476426 const discoverArray = config . discover . map ( ( p ) => `"${ toTomlPath ( p ) } "` )
477427 . join ( ", " ) ;
478428 let tomlContent = "" ;
@@ -481,26 +431,15 @@ export const typstGatherCommand = new Command()
481431 }
482432 tomlContent += `destination = "${ toTomlPath ( config . destination ) } "\n` ;
483433 tomlContent += `discover = [${ discoverArray } ]\n` ;
484- Deno . writeTextFileSync ( tempConfig , tomlContent ) ;
485- configFileToUse = tempConfig ;
486- }
487-
488- info ( `Running typst-gather...` ) ;
489434
490- // Run typst-gather
491- const result = await execProcess ( {
492- cmd : typstGatherBinary ,
493- args : [ configFileToUse ] ,
494- cwd : Deno . cwd ( ) ,
495- } ) ;
496-
497- // Clean up temp file if we created one
498- if ( tempConfig ) {
499- try {
500- Deno . removeSync ( tempConfig ) ;
501- } catch {
502- // Ignore cleanup errors
503- }
435+ result = await execProcess (
436+ {
437+ cmd : typstGatherBinary ,
438+ args : [ "gather" , "-" ] ,
439+ cwd : Deno . cwd ( ) ,
440+ } ,
441+ tomlContent ,
442+ ) ;
504443 }
505444
506445 if ( ! result . success ) {
0 commit comments