Skip to content

Commit c6a5f24

Browse files
committed
Export the schedule from the site's own modules
The script duplicated the grouping logic and had drifted to a schedule that no longer existed. It now loads the real modules through Vite.
1 parent 9de5817 commit c6a5f24

2 files changed

Lines changed: 114 additions & 124 deletions

File tree

schedule-export-2026.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# /dev/mtl 2026 — Programme
2+
3+
| Time | Track 1 (FR) | Track 2 (EN) | Track 3 (Communautés) |
4+
| --- | --- | --- | --- |
5+
| 08:00 AM | Accueil & Café ☕ | | |
6+
| 08:30 AM | **Keynote**<br>_À venir_ | | |
7+
| 09:30 AM | Pause | | |
8+
| 10:00 AM | **Le Context Engineering commence bien avant le prompt**<br>Thomas Salmon | **Software is not text -- it's a living hypergraph**<br>Zackary Therrien | **De Age of Empires II aux taxis électriques: Bâtir une intelligence sans IA**<br>Hugues Lamy<br>Montréal Ruby |
9+
| 10:45 AM | Pause | | |
10+
| 11:00 AM | **De la mise en place au coup de feu**<br>Arnaud Oltra | **NeuroModular AI Architecture™ Designing Intelligent Systems That Think, Adapt, and Scale**<br>Samantha St-Louis (Allegrini) | **Introduction to GenServers with Elixir**<br>Nurul Islam<br>Elixir Montréal |
11+
| 11:45 AM | Repas (inclus) 🍱 | | |
12+
| 01:15 PM | **Si Claude écrit le code, qu'est-ce qui fait encore de moi un développeur?**<br>Sebastien Castiel | **The Laptop Is the Perimeter: How Attackers Target Developers to Breach the Software Supply Chain**<br>Dwayne McDaniel | **_Titre à venir_**<br>Stefania Pecore<br>Women Techmakers Montréal |
13+
| 02:00 PM | Pause | | |
14+
| 02:15 PM | **Donner une mémoire à un robot grâce aux graphes**<br>Damien Soichet & François Cailleau | **Latency, Cost, Quality in AI Systems: Pick Your Victim**<br>Imad-eddine Charchar | **_Titre à venir_**<br>_À venir_<br>Montréal JUG |
15+
| 03:00 PM | Pause | | |
16+
| 03:15 PM | **Au-delà du code : Comment j’ai appris à réfléchir comme une développeuse orientée produit**<br>Dina Kada | **Your system will break. It just depends when.**<br>Reza Madabadi | **_Titre à venir_**<br>_À venir_<br>CNCF |
17+
| 04:00 PM | Pause | | |
18+
| 04:15 PM | **7 Principes d'Ingénierie Agentique**<br>Carl Lapierre | **Your CLI has a new user: the LLM**<br>Margaret Gu | **Full-stack Observability with React & OpenTelemetry**<br>Glodie Juvenal Pengele<br>React Montréal |
19+
| 05:00 PM | Fin 👋 | | |

scripts/export-schedule-to-markdown.js

Lines changed: 95 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -5,155 +5,126 @@ import { readdir, readFile, writeFile } from "fs/promises";
55
import { dirname, join } from "path";
66
import { fileURLToPath } from "url";
77
import { promisify } from "util";
8+
import { createServer } from "vite";
89

910
const 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

Comments
 (0)