@@ -5,155 +5,126 @@ import { readdir, readFile, writeFile } from "fs/promises";
55import { dirname , join } from "path" ;
66import { fileURLToPath } from "url" ;
77import { promisify } from "util" ;
8+ import { createServer } from "vite" ;
89
910const execAsync = promisify ( exec ) ;
11+ const rootDir = join ( dirname ( fileURLToPath ( import . meta. url ) ) , ".." ) ;
1012
11- const __filename = fileURLToPath ( import . meta. url ) ;
12- const __dirname = dirname ( __filename ) ;
13-
14- // Pauses from src/assets/pauses.ts
15- const pauses = [
16- "2025-11-28T08:30:00" ,
17- "2025-11-28T10:00:00" ,
18- "2025-11-28T11:00:00" ,
19- "2025-11-28T12:00:00" ,
20- "2025-11-28T13:45:00" ,
21- "2025-11-28T14:45:00" ,
22- "2025-11-28T15:45:00" ,
23- ] ;
24-
25- // Format time like the React component does
26- function formatTime ( timeString ) {
27- const date = new Date ( timeString ) ;
28- const hours = date . getHours ( ) ;
29- const minutes = date . getMinutes ( ) ;
30- const ampm = hours >= 12 ? "PM" : "AM" ;
31- const displayHours = hours % 12 || 12 ;
32- return `${ displayHours . toString ( ) . padStart ( 2 , "0" ) } :${ minutes . toString ( ) . padStart ( 2 , "0" ) } ${ ampm } ` ;
33- }
34-
35- // Group speakers by time (same logic as groupSpeakersByTime)
36- function groupSpeakersByTime ( speakers , pauses ) {
37- const grouped = { } ;
38-
39- speakers . forEach ( ( speaker ) => {
40- const time = speaker . time ;
41- const trackIndex = speaker . track - 1 ;
42-
43- if ( ! grouped [ time ] ) {
44- grouped [ time ] = { time, tracks : [ ] } ;
45- }
13+ const year = process . argv [ 2 ] ?? "2026" ;
14+ const language = process . argv [ 3 ] ?? "fr" ;
4615
47- const session = grouped [ time ] ;
48-
49- while ( session . tracks . length <= trackIndex ) {
50- session . tracks . push ( null ) ;
51- }
16+ main ( ) ;
5217
53- session . tracks [ trackIndex ] = speaker ;
18+ async function main ( ) {
19+ // Going through Vite lets the script reuse the very modules the site runs on,
20+ // instead of duplicating the schedule logic and letting it drift
21+ const vite = await createServer ( {
22+ root : rootDir ,
23+ server : { middlewareMode : true } ,
24+ appType : "custom" ,
25+ logLevel : "warn" ,
5426 } ) ;
5527
56- pauses . forEach ( ( pauseTime ) => {
57- const time = pauseTime ;
28+ try {
29+ const { groupSpeakersByTime } = await vite . ssrLoadModule (
30+ "/src/utils/groupSpeakers.ts" ,
31+ ) ;
32+ const { pauses } = await vite . ssrLoadModule ( "/src/assets/pauses.ts" ) ;
33+ const { formatTime } = await vite . ssrLoadModule (
34+ "/src/components/Talks/formatTime.ts" ,
35+ ) ;
36+ const { trackNames } = await vite . ssrLoadModule ( "/src/constants/tracks.ts" ) ;
37+
38+ const speakers = await loadSpeakers ( year ) ;
39+ // Pauses are only defined for the current edition, so exporting a past year
40+ // should leave them out rather than mix in another day's breaks
41+ const day = speakers [ 0 ] ?. time . slice ( 0 , "YYYY-MM-DD" . length ) ;
42+ const schedule = groupSpeakersByTime (
43+ speakers ,
44+ pauses . filter ( ( pause ) => pause . time . startsWith ( day ) ) ,
45+ ) ;
46+
47+ const markdown = toMarkdown ( schedule , { formatTime, trackNames } ) ;
48+ const outputPath = join ( rootDir , `schedule-export-${ year } .md` ) ;
49+ await writeFile ( outputPath , markdown , "utf-8" ) ;
5850
59- if ( ! grouped [ time ] ) {
60- grouped [ time ] = { time, tracks : [ ] , isPause : true } ;
51+ // Aligning the table columns is nice to have, not worth failing the export
52+ try {
53+ await execAsync ( `npx prettier --write ${ JSON . stringify ( outputPath ) } ` ) ;
54+ } catch ( error ) {
55+ console . warn ( `⚠️ Prettier could not format the table: ${ error . message } ` ) ;
6156 }
62- } ) ;
63-
64- return Object . values ( grouped ) . sort (
65- ( a , b ) => new Date ( a . time ) . getTime ( ) - new Date ( b . time ) . getTime ( ) ,
66- ) ;
67- }
68-
69- // Get pause text based on time
70- function getPauseText ( formattedTime ) {
71- if ( formattedTime === "12:00 PM" ) {
72- return "Repas (inclus) 🍱 / Lunch (included) 🍱" ;
73- } else if ( formattedTime === "08:30 AM" ) {
74- return "Accueil & Café ☕ / Greetings & Coffee ☕" ;
75- }
76- return "Pause / Break" ;
77- }
7857
79- // Format speaker info for table cell
80- function formatSpeaker ( speaker ) {
81- if ( ! speaker ) {
82- return "" ;
58+ console . log (
59+ `✅ ${ speakers . length } sessions exported to ${ outputPath . replace ( rootDir + "/" , "" ) } ` ,
60+ ) ;
61+ } finally {
62+ await vite . close ( ) ;
8363 }
84- return `**${ speaker . title } **<br>${ speaker . name } ` ;
8564}
8665
87- // Load all speakers from JSON files
88- async function loadSpeakers ( ) {
89- const speakersDir = join ( __dirname , ".." , "src" , "assets" , "speakers-2025" ) ;
66+ async function loadSpeakers ( year ) {
67+ const speakersDir = join ( rootDir , "src" , "assets" , `speakers-${ year } ` ) ;
9068 const files = await readdir ( speakersDir ) ;
91- const jsonFiles = files . filter ( ( file ) => file . endsWith ( ".json" ) ) ;
9269
93- const speakers = await Promise . all (
94- jsonFiles . map ( async ( file ) => {
95- const content = await readFile ( join ( speakersDir , file ) , "utf-8" ) ;
96- return JSON . parse ( content ) ;
97- } ) ,
70+ return Promise . all (
71+ files
72+ . filter ( ( file ) => file . endsWith ( ".json" ) )
73+ . map ( async ( file ) =>
74+ JSON . parse ( await readFile ( join ( speakersDir , file ) , "utf-8" ) ) ,
75+ ) ,
9876 ) ;
99-
100- return speakers ;
10177}
10278
103- // Generate markdown table
104- function generateMarkdownTable ( schedule ) {
105- let markdown = "# /dev/mtl 2025 - Schedule\n\n" ;
106- markdown += "**Date:** November 28, 2025\n\n" ;
107- markdown += "| Time | Track 1 (FR) | Track 2 (EN) | Track 3 (Mixed) |\n" ;
108- markdown += "|------|--------------|--------------|------------------|\n" ;
109-
110- schedule . forEach ( ( session ) => {
111- const formattedTime = formatTime ( session . time ) ;
112-
113- if ( session . isPause ) {
114- const pauseText = getPauseText ( formattedTime ) ;
115- markdown += `| ${ formattedTime } | ${ pauseText } | | |\n` ;
116- } else {
117- const track1 = formatSpeaker ( session . tracks [ 0 ] ) ;
118- const track2 = formatSpeaker ( session . tracks [ 1 ] ) ;
119- const track3 = formatSpeaker ( session . tracks [ 2 ] ) ;
120- markdown += `| ${ formattedTime } | ${ track1 } | ${ track2 } | ${ track3 } |\n` ;
121- }
122- } ) ;
79+ function toMarkdown ( schedule , { formatTime, trackNames } ) {
80+ const headers = [ "Time" , ...trackNames . map ( ( track ) => track [ language ] ) ] ;
12381
124- return markdown ;
125- }
82+ const rows = schedule . map ( ( session ) => {
83+ const time = formatTime ( session . time ) ;
12684
127- // Main function
128- async function main ( ) {
129- try {
130- console . log ( "Loading speakers..." ) ;
131- const speakers = await loadSpeakers ( ) ;
132- console . log ( `Loaded ${ speakers . length } speakers` ) ;
85+ if ( session . kind === "pause" ) {
86+ return [ time , session . label [ language ] , "" , "" ] ;
87+ }
13388
134- console . log ( "Grouping by time..." ) ;
135- const schedule = groupSpeakersByTime ( speakers , pauses ) ;
136- console . log ( `Generated ${ schedule . length } schedule sessions` ) ;
89+ const tracks = trackNames . map ( ( _ , index ) =>
90+ formatTalk ( session . tracks [ index ] ?? [ ] ) ,
91+ ) ;
13792
138- console . log ( "Generating markdown ..." ) ;
139- const markdown = generateMarkdownTable ( schedule ) ;
93+ return [ time , ...tracks ] ;
94+ } ) ;
14095
141- const outputPath = join ( __dirname , ".." , "schedule-export.md" ) ;
142- await writeFile ( outputPath , markdown , "utf-8" ) ;
96+ return [
97+ `# /dev/mtl ${ year } — ${ language === "fr" ? "Programme" : "Schedule" } ` ,
98+ "" ,
99+ toTable ( headers , rows ) ,
100+ ] . join ( "\n" ) ;
101+ }
143102
144- console . log ( "Formatting with Prettier..." ) ;
145- try {
146- await execAsync ( `npx prettier --write ${ outputPath } ` ) ;
147- console . log ( `\n✅ Schedule exported and formatted: ${ outputPath } ` ) ;
148- } catch ( prettierError ) {
149- console . log ( `\n✅ Schedule exported to: ${ outputPath } ` ) ;
150- console . warn ( "⚠️ Prettier formatting failed:" , prettierError . message ) ;
151- }
152- } catch ( error ) {
153- console . error ( "Error:" , error ) ;
154- process . exit ( 1 ) ;
103+ function formatTalk ( speakers ) {
104+ if ( speakers . length === 0 ) {
105+ return "" ;
155106 }
107+
108+ const [ talk ] = speakers ;
109+ const presenters = speakers
110+ . filter ( ( speaker ) => speaker . name !== "" )
111+ . map ( ( speaker ) => speaker . name ) ;
112+
113+ return [
114+ `**${ talk . title || "_Titre à venir_" } **` ,
115+ presenters . join ( " & " ) || "_À venir_" ,
116+ talk . community ,
117+ ]
118+ . filter ( Boolean )
119+ . join ( "<br>" ) ;
156120}
157121
158- main ( ) ;
122+ function toTable ( headers , rows ) {
123+ const line = ( cells ) => `| ${ cells . join ( " | " ) } |` ;
159124
125+ return [
126+ line ( headers ) ,
127+ line ( headers . map ( ( ) => "---" ) ) ,
128+ ...rows . map ( line ) ,
129+ ] . join ( "\n" ) ;
130+ }
0 commit comments