Add Makefile, package.json, and initial configuration for VitePress and Slidev integration
- Introduced a Makefile for streamlined commands related to building, running, and managing presentations. - Created package.json to define project metadata, dependencies, and scripts for development and testing. - Added .gitignore entries to exclude build artifacts and temporary files from version control. - Included playwright.config.mjs for end-to-end testing setup with Playwright. - Added README.md for project documentation, outlining setup instructions and development guidelines.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { listPresentationSlugs } from './list-slugs.mjs'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.resolve(__dirname, '..')
|
||||
|
||||
const slugs = listPresentationSlugs(root)
|
||||
if (!slugs.length) {
|
||||
console.log('[build-all-decks] aucune présentation')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
for (const slug of slugs) {
|
||||
execFileSync(process.execPath, [path.join(root, 'tools', 'build-deck.mjs'), slug], {
|
||||
stdio: 'inherit',
|
||||
cwd: root,
|
||||
env: process.env,
|
||||
})
|
||||
}
|
||||
console.log('[build-all-decks] terminé')
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build Slidev pour un slug → dossier dist-<slug>/ à la racine du dépôt.
|
||||
* Usage : node tools/build-deck.mjs <slug>
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { sitePath } from './site-base.mjs'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.resolve(__dirname, '..')
|
||||
|
||||
function publishBuild(wipDir, finalDir) {
|
||||
fs.mkdirSync(finalDir, { recursive: true })
|
||||
fs.cpSync(wipDir, finalDir, { recursive: true, force: true })
|
||||
fs.rmSync(wipDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
function slidevBin() {
|
||||
const p = path.join(root, 'node_modules', '.bin', 'slidev')
|
||||
if (fs.existsSync(p)) return p
|
||||
return 'slidev'
|
||||
}
|
||||
|
||||
const slug = process.argv[2]
|
||||
if (!slug || slug.startsWith('wip_')) {
|
||||
console.error('usage: node tools/build-deck.mjs <slug>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const entry = path.join(root, 'slides', slug, 'slides.md')
|
||||
const metaPath = path.join(root, 'slides', slug, 'meta.json')
|
||||
if (!fs.existsSync(entry) || !fs.existsSync(metaPath)) {
|
||||
console.error(`[build-deck] slides/${slug}/ incomplet (slides.md + meta.json requis)`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const finalDir = path.join(root, `dist-${slug}`)
|
||||
const wipDir = path.join(root, '.slide-wip', slug)
|
||||
const base = sitePath(`/slides/${slug}/`)
|
||||
if (!base.endsWith('/')) throw new Error(`base invalide: ${base}`)
|
||||
|
||||
fs.rmSync(wipDir, { recursive: true, force: true })
|
||||
fs.mkdirSync(wipDir, { recursive: true })
|
||||
/* Ne pas supprimer slides/<slug>/index.html avant le build : Slidev s’attend à pouvoir unlink ce fichier en fin de build. */
|
||||
console.log(`[build-deck] ${slug} → dist-${slug}/ (base=${base})`)
|
||||
try {
|
||||
execFileSync(
|
||||
slidevBin(),
|
||||
['build', entry, '--out', wipDir, '--base', base],
|
||||
{ stdio: 'inherit', cwd: root, env: process.env },
|
||||
)
|
||||
publishBuild(wipDir, finalDir)
|
||||
/* Slidev peut écrire un index.html à côté de slides.md — inutile et bruyant au build suivant */
|
||||
try {
|
||||
fs.rmSync(path.join(root, 'slides', slug, 'index.html'), { force: true })
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
fs.rmSync(wipDir, { recursive: true, force: true })
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* npm run dev:portal — sync + VitePress dev avec base=/ (ignore SITE_BASE sauf SKIP_PORTAL_DEV=1).
|
||||
*/
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawn, execFileSync } from 'node:child_process'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.resolve(__dirname, '..')
|
||||
const runEnv =
|
||||
process.env.SKIP_PORTAL_DEV === '1'
|
||||
? process.env
|
||||
: { ...process.env, TALKS_PORTAL_DEV: '1' }
|
||||
|
||||
execFileSync(process.execPath, [path.join(root, 'tools', 'sync-portal.mjs')], {
|
||||
stdio: 'inherit',
|
||||
cwd: root,
|
||||
env: runEnv,
|
||||
})
|
||||
|
||||
const vitepressBin = path.join(root, 'node_modules', '.bin', 'vitepress')
|
||||
const child = spawn(vitepressBin, ['dev', 'portal'], {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
env: runEnv,
|
||||
shell: process.platform === 'win32',
|
||||
})
|
||||
|
||||
function die() {
|
||||
try {
|
||||
child.kill('SIGTERM')
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
process.on('SIGINT', die)
|
||||
process.on('SIGTERM', die)
|
||||
child.on('exit', (code) => process.exit(code ?? 0))
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Dossiers de présentation sous slides/ (hors wip_*), avec meta.json + slides.md.
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
export function listPresentationSlugs(root) {
|
||||
const slides = path.join(root, 'slides')
|
||||
if (!fs.existsSync(slides)) return []
|
||||
return fs
|
||||
.readdirSync(slides, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory() && !d.name.startsWith('wip_') && !d.name.startsWith('_'))
|
||||
.map((d) => d.name)
|
||||
.filter((slug) => {
|
||||
return (
|
||||
fs.existsSync(path.join(slides, slug, 'meta.json')) &&
|
||||
fs.existsSync(path.join(slides, slug, 'slides.md'))
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* create | delete | clone | rename — cohérence slides + portail (sync).
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.resolve(__dirname, '..')
|
||||
|
||||
function sync() {
|
||||
execFileSync(process.execPath, [path.join(root, 'tools', 'sync-portal.mjs')], {
|
||||
stdio: 'inherit',
|
||||
cwd: root,
|
||||
env: process.env,
|
||||
})
|
||||
}
|
||||
|
||||
function assertSlug(s, label = 'slug') {
|
||||
if (!s || typeof s !== 'string') throw new Error(`${label} manquant`)
|
||||
if (!/^[a-z0-9-]+$/.test(s)) throw new Error(`${label} invalide (a-z 0-9 -): ${s}`)
|
||||
if (s.length < 2) throw new Error(`${label} trop court`)
|
||||
if (s === 'portal') throw new Error('slug réservé')
|
||||
}
|
||||
|
||||
const cmd = process.argv[2]
|
||||
const a = process.argv[3]
|
||||
const b = process.argv[4]
|
||||
|
||||
try {
|
||||
if (cmd === 'create') {
|
||||
assertSlug(a)
|
||||
const dir = path.join(root, 'slides', a)
|
||||
if (fs.existsSync(dir)) throw new Error(`existe déjà: slides/${a}/`)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'meta.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
title: 'Nouvelle présentation',
|
||||
description: '',
|
||||
order: 99,
|
||||
tags: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
'utf8',
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'slides.md'),
|
||||
['---', 'title: Nouvelle présentation', '---', '', '# Bienvenue', ''].join('\n'),
|
||||
'utf8',
|
||||
)
|
||||
sync()
|
||||
console.log(`[create] slides/${a}/ — lancez make run ou make build`)
|
||||
} else if (cmd === 'delete') {
|
||||
assertSlug(a)
|
||||
fs.rmSync(path.join(root, 'slides', a), { recursive: true, force: true })
|
||||
fs.rmSync(path.join(root, `dist-${a}`), { recursive: true, force: true })
|
||||
sync()
|
||||
console.log(`[delete] ${a} retiré`)
|
||||
} else if (cmd === 'clone') {
|
||||
assertSlug(a, 'source')
|
||||
assertSlug(b, 'dest')
|
||||
const src = path.join(root, 'slides', a)
|
||||
const dst = path.join(root, 'slides', b)
|
||||
if (!fs.existsSync(src)) throw new Error(`source absente: slides/${a}/`)
|
||||
if (fs.existsSync(dst)) throw new Error(`dest existe: slides/${b}/`)
|
||||
fs.cpSync(src, dst, { recursive: true })
|
||||
const metaPath = path.join(dst, 'meta.json')
|
||||
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'))
|
||||
meta.title = `${meta.title || a} (copie)`
|
||||
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf8')
|
||||
sync()
|
||||
console.log(`[clone] ${a} → ${b} — build: node tools/build-deck.mjs ${b}`)
|
||||
} else if (cmd === 'rename') {
|
||||
assertSlug(a, 'ancien')
|
||||
assertSlug(b, 'nouveau')
|
||||
const src = path.join(root, 'slides', a)
|
||||
const dst = path.join(root, 'slides', b)
|
||||
if (!fs.existsSync(src)) throw new Error(`slides/${a}/ absent`)
|
||||
if (fs.existsSync(dst)) throw new Error(`slides/${b}/ existe déjà`)
|
||||
fs.renameSync(src, dst)
|
||||
const metaPath = path.join(dst, 'meta.json')
|
||||
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'))
|
||||
delete meta.slug
|
||||
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf8')
|
||||
const oldDist = path.join(root, `dist-${a}`)
|
||||
const newDist = path.join(root, `dist-${b}`)
|
||||
if (fs.existsSync(oldDist)) {
|
||||
fs.renameSync(oldDist, newDist)
|
||||
}
|
||||
sync()
|
||||
console.log(`[rename] ${a} → ${b}`)
|
||||
} else {
|
||||
console.error('usage: node tools/presentation-cli.mjs <create|delete|clone|rename> [args]')
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(String(e.message || e))
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Logique pure partagée : génération markdown, lecture meta, échappements.
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
export const FEATURED_START = '<!-- FEATURED_START -->'
|
||||
export const FEATURED_END = '<!-- FEATURED_END -->'
|
||||
|
||||
export function escMd(s) {
|
||||
if (typeof s !== 'string') return ''
|
||||
return s.replace(/\|/g, '\\|')
|
||||
}
|
||||
|
||||
export function escHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
export function pickFeatured(items) {
|
||||
const marked = items.filter((p) => p.featured).sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
if (marked.length) return marked.slice(0, 6)
|
||||
return [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).slice(0, 3)
|
||||
}
|
||||
|
||||
/** @param {Function} sitePath - ex. import { sitePath } from './site-base.mjs' */
|
||||
export function buildFeaturedHtml(items, sitePath) {
|
||||
const featured = pickFeatured(items)
|
||||
if (!featured.length) return '<!-- (aucune présentation) -->'
|
||||
const lines = [
|
||||
'<section class="talks-section talks-section--home" aria-labelledby="featured-heading">',
|
||||
' <h2 id="featured-heading" class="section-title">Présentations</h2>',
|
||||
' <div class="talks-grid">',
|
||||
]
|
||||
for (const p of featured) {
|
||||
const href = sitePath(`/presentations/${p.slug}/`)
|
||||
const meta = `Présentation · ${p.slug}`
|
||||
lines.push(` <a class="talk-card" href="${escHtml(href)}">`)
|
||||
lines.push(` <h2>${escHtml(p.title)}</h2>`)
|
||||
lines.push(` <p>${escHtml(p.description || '')}</p>`)
|
||||
lines.push(` <span class="meta">${escHtml(meta)}</span>`)
|
||||
lines.push(' </a>')
|
||||
}
|
||||
lines.push(' </div>', '</section>')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
export function readAllMeta(slidesRoot) {
|
||||
if (!fs.existsSync(slidesRoot)) return []
|
||||
const dirs = fs
|
||||
.readdirSync(slidesRoot, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory() && !d.name.startsWith('wip_') && !d.name.startsWith('_'))
|
||||
.map((d) => d.name)
|
||||
|
||||
const list = []
|
||||
for (const dir of dirs) {
|
||||
const metaPath = path.join(slidesRoot, dir, 'meta.json')
|
||||
if (!fs.existsSync(metaPath)) continue
|
||||
const raw = fs.readFileSync(metaPath, 'utf8')
|
||||
const meta = JSON.parse(raw)
|
||||
if (meta.slug && meta.slug !== dir) {
|
||||
console.warn(`[sync] slug "${meta.slug}" ≠ dossier "${dir}", le dossier fait foi pour les URLs Slidev`)
|
||||
}
|
||||
list.push({ ...meta, slug: dir, folder: dir })
|
||||
}
|
||||
list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
||||
return list
|
||||
}
|
||||
|
||||
/** @param {Function} sitePath */
|
||||
export function generateListPageMarkdown(items, sitePath) {
|
||||
const lines = [
|
||||
'---',
|
||||
'title: Présentations',
|
||||
'outline: deep',
|
||||
'---',
|
||||
'',
|
||||
'# Présentations',
|
||||
'',
|
||||
'Decks disponibles — fiche portail et build Slidev statique.',
|
||||
'',
|
||||
'<div class="talks-grid">',
|
||||
]
|
||||
for (const p of items) {
|
||||
const detail = sitePath(`/presentations/${p.slug}/`)
|
||||
/** Première planche (`/1`) + classe `vp-raw` : le routeur VitePress ignore ces liens (navigation réelle vers Slidev). */
|
||||
const deck = sitePath(`/slides/${p.slug}/1`)
|
||||
const tags = (p.tags || []).map((t) => `\`${escMd(t)}\``).join(' · ')
|
||||
lines.push(' <article class="talk-card talk-card--list">')
|
||||
lines.push(` <h2><a href="${detail}">${escMd(p.title)}</a></h2>`)
|
||||
lines.push(` <p>${escMd(p.description || '')}</p>`)
|
||||
if (tags) lines.push(` <p class="meta">${tags}</p>`)
|
||||
lines.push(' <div class="talk-card__actions">')
|
||||
lines.push(` <a class="vp-button" href="${detail}">Fiche</a>`)
|
||||
lines.push(
|
||||
` <a class="vp-button vp-button--alt vp-raw" href="${deck}" rel="noopener">Slides</a>`,
|
||||
)
|
||||
lines.push(' </div>')
|
||||
lines.push(' </article>')
|
||||
}
|
||||
lines.push('</div>', '')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/** @param {Function} sitePath */
|
||||
export function generateDetailPageMarkdown(p, sitePath) {
|
||||
/** `vp-raw` : VitePress ne intercepte pas le clic (sinon 404 SPA sur /slides/…). */
|
||||
const deck = sitePath(`/slides/${p.slug}/1`)
|
||||
const tags = (p.tags || []).map((t) => `\`${t}\``).join(' · ')
|
||||
const lines = [
|
||||
'---',
|
||||
`title: ${p.title.replace(/"/g, '\\"')}`,
|
||||
'outline: deep',
|
||||
'---',
|
||||
'',
|
||||
`# ${p.title}`,
|
||||
'',
|
||||
...(tags ? [`**Tags :** ${tags}`, ''] : []),
|
||||
'## Résumé',
|
||||
'',
|
||||
p.description || '_Pas de description._',
|
||||
'',
|
||||
'## Accès',
|
||||
'',
|
||||
`<p class="pres-actions">`,
|
||||
` <a class="vp-button vp-raw" href="${deck}">Voir les slides</a>`,
|
||||
` <a class="vp-button vp-button--alt vp-raw" href="${deck}" target="_blank" rel="noopener noreferrer">Ouvrir dans un nouvel onglet</a>`,
|
||||
`</p>`,
|
||||
'',
|
||||
'## Partager les slides',
|
||||
'',
|
||||
'Depuis la salle, scannez le code pour ouvrir les slides sur mobile.',
|
||||
'',
|
||||
`<QrEncode path="${deck}" />`,
|
||||
'',
|
||||
'::: info À propos',
|
||||
'Les slides sont servies en SPA statique (Slidev). Les chemins d’assets suivent le `base` défini au build.',
|
||||
':::',
|
||||
'',
|
||||
]
|
||||
return lines.filter((l) => l !== undefined).join('\n')
|
||||
}
|
||||
|
||||
export function patchIndexFeaturedContent(indexContent, items, sitePath) {
|
||||
const i0 = indexContent.indexOf(FEATURED_START)
|
||||
const i1 = indexContent.indexOf(FEATURED_END)
|
||||
if (i0 === -1 || i1 === -1 || i1 <= i0) {
|
||||
return { ok: false, content: indexContent }
|
||||
}
|
||||
const before = indexContent.slice(0, i0 + FEATURED_START.length)
|
||||
const after = indexContent.slice(i1)
|
||||
const block = buildFeaturedHtml(items, sitePath)
|
||||
const content = `${before}\n\n${block}\n\n${after}`
|
||||
return { ok: true, content }
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* make run : sync + build tous les decks, puis VitePress dev + watch slides/
|
||||
* → rebuild ciblé + sync pour refléter les sources.
|
||||
*/
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawn, execFileSync } from 'node:child_process'
|
||||
import chokidar from 'chokidar'
|
||||
import { listPresentationSlugs } from './list-slugs.mjs'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.resolve(__dirname, '..')
|
||||
const slidesDir = path.join(root, 'slides')
|
||||
|
||||
/** Aligne sync, decks et VitePress sur base=/ (ignore SITE_BASE du .env). Voir tools/site-base.mjs */
|
||||
const runEnv =
|
||||
process.env.SKIP_PORTAL_DEV === '1'
|
||||
? process.env
|
||||
: { ...process.env, TALKS_PORTAL_DEV: '1' }
|
||||
|
||||
function sync() {
|
||||
execFileSync(process.execPath, [path.join(root, 'tools', 'sync-portal.mjs')], {
|
||||
stdio: 'inherit',
|
||||
cwd: root,
|
||||
env: runEnv,
|
||||
})
|
||||
}
|
||||
|
||||
function buildDeck(slug) {
|
||||
execFileSync(process.execPath, [path.join(root, 'tools', 'build-deck.mjs'), slug], {
|
||||
stdio: 'inherit',
|
||||
cwd: root,
|
||||
env: runEnv,
|
||||
})
|
||||
}
|
||||
|
||||
function buildAllDecks() {
|
||||
const slugs = listPresentationSlugs(root)
|
||||
for (const slug of slugs) buildDeck(slug)
|
||||
}
|
||||
|
||||
sync()
|
||||
buildAllDecks()
|
||||
|
||||
const pending = new Set()
|
||||
let flushTimer = null
|
||||
function flush() {
|
||||
flushTimer = null
|
||||
sync()
|
||||
for (const slug of pending) {
|
||||
try {
|
||||
buildDeck(slug)
|
||||
} catch {
|
||||
/* build-deck a déjà loggé */
|
||||
}
|
||||
}
|
||||
pending.clear()
|
||||
}
|
||||
|
||||
chokidar
|
||||
.watch(slidesDir, {
|
||||
ignoreInitial: true,
|
||||
ignored: (p) => path.basename(p).startsWith('.') || p.includes(`${path.sep}wip_`),
|
||||
})
|
||||
.on('all', (_evt, fp) => {
|
||||
const rel = path.relative(slidesDir, fp)
|
||||
const slug = rel.split(path.sep)[0]
|
||||
if (!slug || slug.startsWith('wip_')) return
|
||||
pending.add(slug)
|
||||
clearTimeout(flushTimer)
|
||||
flushTimer = setTimeout(flush, 450)
|
||||
})
|
||||
|
||||
const vitepressBin = path.join(root, 'node_modules', '.bin', 'vitepress')
|
||||
const child = spawn(vitepressBin, ['dev', 'portal'], {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
env: runEnv,
|
||||
shell: process.platform === 'win32',
|
||||
})
|
||||
|
||||
function die() {
|
||||
try {
|
||||
child.kill('SIGTERM')
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
process.on('SIGINT', die)
|
||||
process.on('SIGTERM', die)
|
||||
child.on('exit', (code) => process.exit(code ?? 0))
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Préfixe URL du site, aligné sur VitePress `base`.
|
||||
* Racine : SITE_BASE=/ ou absent
|
||||
* Sous-chemin (ex. GitHub Pages) : SITE_BASE=/mon-repo/
|
||||
*
|
||||
* En dev local (`TALKS_PORTAL_DEV=1`, ex. make run), SITE_BASE est ignoré pour
|
||||
* éviter les modules .md servis en text/html (mismatch base / URLs).
|
||||
* Pour tester un sous-chemin en dev : SKIP_PORTAL_DEV=1 SITE_BASE=/repo/ npm run dev:portal
|
||||
*/
|
||||
export function getSiteBase() {
|
||||
if (process.env.TALKS_PORTAL_DEV === '1') return ''
|
||||
const raw = process.env.SITE_BASE?.trim() || '/'
|
||||
if (raw === '/') return ''
|
||||
return raw.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
/** Valeur `base` VitePress : `/` ou `/mon-projet/` */
|
||||
export function vitepressAppBase() {
|
||||
if (process.env.TALKS_PORTAL_DEV === '1') return '/'
|
||||
const raw = process.env.SITE_BASE?.trim()
|
||||
if (!raw || raw === '/') return '/'
|
||||
return raw.endsWith('/') ? raw : `${raw}/`
|
||||
}
|
||||
|
||||
/** Chemin absolu du site : /slides/foo/ */
|
||||
export function sitePath(absolutePath) {
|
||||
const p = absolutePath.startsWith('/') ? absolutePath : `/${absolutePath}`
|
||||
const b = getSiteBase()
|
||||
return b ? `${b}${p}` : p
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Sert dist-portal + chaque dist-<slug> sous /slides/<slug>/ (tests E2E, prod locale).
|
||||
* Comme le plugin Vite en dev : racine du deck → 302 vers /slides/<slug>/1 ; routes Slidev (/1, /2…) via SPA (sirv single).
|
||||
*/
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import sirv from 'sirv'
|
||||
import { listPresentationSlugs } from './list-slugs.mjs'
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const port = Number(process.env.PREVIEW_PORT || 4174)
|
||||
|
||||
const portalStatic = sirv(path.join(root, 'dist-portal'), { etag: true })
|
||||
const slideSirv = new Map()
|
||||
|
||||
function getSirv(slug) {
|
||||
if (!slideSirv.has(slug)) {
|
||||
slideSirv.set(
|
||||
slug,
|
||||
sirv(path.join(root, `dist-${slug}`), {
|
||||
etag: true,
|
||||
single: 'index.html',
|
||||
}),
|
||||
)
|
||||
}
|
||||
return slideSirv.get(slug)
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const u = new URL(req.url || '/', 'http://127.0.0.1')
|
||||
const pathname = u.pathname
|
||||
const m = pathname.match(/^\/slides\/([^/]+)(.*)$/)
|
||||
if (m) {
|
||||
const slug = m[1]
|
||||
const rest = m[2] || '/'
|
||||
const slugs = new Set(listPresentationSlugs(root))
|
||||
if (!slugs.has(slug)) {
|
||||
res.statusCode = 404
|
||||
res.end('not found')
|
||||
return
|
||||
}
|
||||
const atDeckRoot = rest === '/' || rest === ''
|
||||
if (atDeckRoot && (req.method === 'GET' || req.method === 'HEAD')) {
|
||||
res.statusCode = 302
|
||||
res.setHeader('Location', `/slides/${slug}/1${u.search || ''}`)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
const orig = req.url
|
||||
req.url = rest + (u.search || '')
|
||||
getSirv(slug)(req, res, () => {
|
||||
req.url = orig
|
||||
res.statusCode = 404
|
||||
res.end()
|
||||
})
|
||||
return
|
||||
}
|
||||
portalStatic(req, res, () => {
|
||||
res.statusCode = 404
|
||||
res.end('not found')
|
||||
})
|
||||
})
|
||||
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
console.log(`[static-preview] http://127.0.0.1:${port}/`)
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Lit chaque slides/<slug>/meta.json, écrit presentations.json, index featured, liste + fiches.
|
||||
* Retire les fiches portal obsolètes.
|
||||
*
|
||||
* TALKS_ROOT (optionnel) : racine du dépôt — tests.
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { sitePath } from './site-base.mjs'
|
||||
import {
|
||||
readAllMeta,
|
||||
generateListPageMarkdown,
|
||||
generateDetailPageMarkdown,
|
||||
patchIndexFeaturedContent,
|
||||
} from './presentation-contracts.mjs'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = process.env.TALKS_ROOT ? path.resolve(process.env.TALKS_ROOT) : path.resolve(__dirname, '..')
|
||||
const slidesRoot = path.join(root, 'slides')
|
||||
const portalData = path.join(root, 'portal', '.vitepress', 'data')
|
||||
const presentationsDir = path.join(root, 'portal', 'presentations')
|
||||
|
||||
function pruneStalePresentationDirs(items) {
|
||||
const keep = new Set(items.map((p) => p.slug))
|
||||
if (!fs.existsSync(presentationsDir)) return
|
||||
for (const d of fs.readdirSync(presentationsDir, { withFileTypes: true })) {
|
||||
if (!d.isDirectory()) continue
|
||||
if (!keep.has(d.name)) {
|
||||
fs.rmSync(path.join(presentationsDir, d.name), { recursive: true, force: true })
|
||||
console.warn(`[sync] fiche retirée (plus de slides/${d.name}/): presentations/${d.name}/`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const items = readAllMeta(slidesRoot)
|
||||
fs.mkdirSync(portalData, { recursive: true })
|
||||
fs.writeFileSync(path.join(portalData, 'presentations.json'), JSON.stringify(items, null, 2), 'utf8')
|
||||
|
||||
const indexPath = path.join(root, 'portal', 'index.md')
|
||||
if (fs.existsSync(indexPath)) {
|
||||
const raw = fs.readFileSync(indexPath, 'utf8')
|
||||
const { ok, content } = patchIndexFeaturedContent(raw, items, sitePath)
|
||||
if (!ok) {
|
||||
console.warn('[sync] marqueurs FEATURED_START / FEATURED_END absents ou invalides dans portal/index.md')
|
||||
} else {
|
||||
fs.writeFileSync(indexPath, content, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(presentationsDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(presentationsDir, 'index.md'), generateListPageMarkdown(items, sitePath), 'utf8')
|
||||
|
||||
for (const p of items) {
|
||||
const dir = path.join(presentationsDir, p.slug)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
fs.writeFileSync(path.join(dir, 'index.md'), generateDetailPageMarkdown(p, sitePath), 'utf8')
|
||||
}
|
||||
|
||||
pruneStalePresentationDirs(items)
|
||||
|
||||
console.log(`[sync] ${items.length} présentation(s) — portal mis à jour`)
|
||||
}
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user