- 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.
149 lines
4.8 KiB
JavaScript
149 lines
4.8 KiB
JavaScript
/**
|
|
* Dev : sert chaque dist-<slug>/ sous /slides/<slug>/ (sources = dossiers à la racine du dépôt).
|
|
* La racine `/slides/<slug>/` (et `/slides/<slug>`) redirige en 302 vers `/slides/<slug>/1` (route Slidev).
|
|
*/
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import sirv from 'sirv'
|
|
import { listPresentationSlugs } from '../../tools/list-slugs.mjs'
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
|
|
|
function slidesUrlPrefix(vpBase) {
|
|
if (!vpBase || vpBase === '/') return '/slides'
|
|
return `${String(vpBase).replace(/\/+$/, '')}/slides`
|
|
}
|
|
|
|
/** Requêtes que le middleware ne doit pas confondre avec la SPA Slidev (sinon HTML → erreur MIME sur les modules). */
|
|
function isViteOrSourceLikeRequest(rawUrl) {
|
|
let u
|
|
try {
|
|
u = new URL(rawUrl, 'http://vitepress.local')
|
|
} catch {
|
|
return false
|
|
}
|
|
const q = u.searchParams
|
|
if (q.has('import') || q.has('raw') || q.has('url') || q.has('worker') || q.has('sharedworker')) {
|
|
return true
|
|
}
|
|
const p = u.pathname
|
|
return /\.(md|mdx|vue|ts|mts|cts|tsx|jsx)($|\?)/i.test(p)
|
|
}
|
|
|
|
/**
|
|
* Après échec sirv : ne pas laisser VitePress répondre en HTML pour des scripts / modules.
|
|
* Pour une navigation document, servir le index.html du deck (router Slidev en history).
|
|
*/
|
|
function send404(res) {
|
|
res.statusCode = 404
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
|
res.end('Not found')
|
|
}
|
|
|
|
function wantsAssetNotHtml(req) {
|
|
const dest = req.headers['sec-fetch-dest']
|
|
if (dest === 'script' || dest === 'style' || dest === 'image' || dest === 'font') return true
|
|
const accept = req.headers.accept || ''
|
|
if (accept.includes('text/javascript') || accept.includes('application/javascript')) return true
|
|
return false
|
|
}
|
|
|
|
/** Racine du deck Slidev (`/` ou `/index.html`) : le routeur attend `/1` pour la première planche. */
|
|
function isSlideDeckEntrySubpath(sub) {
|
|
const n = sub.replace(/\/+$/, '') || '/'
|
|
return n === '/' || n === '/index.html'
|
|
}
|
|
|
|
export function vitePluginSlideDists({ base: vpBase }) {
|
|
const prefix = slidesUrlPrefix(vpBase)
|
|
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
const re = new RegExp(`^${escaped}\\/([^/]+)(.*)$`)
|
|
/** @type {Map<string, ReturnType<typeof sirv>>} */
|
|
const sirvBySlug = new Map()
|
|
/** @type {Map<string, string>} */
|
|
const indexHtmlBySlug = new Map()
|
|
const warnedMissing = new Set()
|
|
|
|
return {
|
|
name: 'talks-slide-dists',
|
|
enforce: 'pre',
|
|
apply: 'serve',
|
|
configureServer(server) {
|
|
server.middlewares.use((req, res, next) => {
|
|
const raw = req.originalUrl || req.url || '/'
|
|
const pathPart = raw.split('?')[0]
|
|
const m = pathPart.match(re)
|
|
if (!m) return next()
|
|
const slug = m[1]
|
|
const subRaw = m[2] || '/'
|
|
if (!listPresentationSlugs(repoRoot).includes(slug)) return next()
|
|
|
|
const distDir = path.join(repoRoot, `dist-${slug}`)
|
|
if (!fs.existsSync(path.join(distDir, 'index.html'))) {
|
|
if (!warnedMissing.has(slug)) {
|
|
warnedMissing.add(slug)
|
|
server.config.logger.warn(
|
|
`[talks-slide-dists] dist-${slug}/ manquant ou sans index.html — exécutez « make run » ou « npm run build » puis réessayez.`,
|
|
)
|
|
}
|
|
return next()
|
|
}
|
|
|
|
const sub = subRaw.startsWith('/') ? subRaw : `/${subRaw}`
|
|
const query = raw.includes('?') ? `?${raw.split('?')[1]}` : ''
|
|
|
|
if (
|
|
(req.method === 'GET' || req.method === 'HEAD') &&
|
|
isSlideDeckEntrySubpath(sub) &&
|
|
!isViteOrSourceLikeRequest(raw)
|
|
) {
|
|
res.statusCode = 302
|
|
res.setHeader('Location', `${prefix}/${slug}/1${query}`)
|
|
res.end()
|
|
return
|
|
}
|
|
|
|
if (isViteOrSourceLikeRequest(raw)) {
|
|
return send404(res)
|
|
}
|
|
|
|
const origUrl = req.url
|
|
delete req._parsedUrl
|
|
req.url = sub + query
|
|
|
|
let handler = sirvBySlug.get(slug)
|
|
if (!handler) {
|
|
handler = sirv(distDir, { dev: true, etag: true, maxAge: 0 })
|
|
sirvBySlug.set(slug, handler)
|
|
}
|
|
|
|
handler(req, res, () => {
|
|
req.url = origUrl
|
|
delete req._parsedUrl
|
|
|
|
if (isViteOrSourceLikeRequest(raw)) {
|
|
return send404(res)
|
|
}
|
|
if (wantsAssetNotHtml(req)) {
|
|
return send404(res)
|
|
}
|
|
if (req.method === 'GET') {
|
|
let html = indexHtmlBySlug.get(slug)
|
|
if (html === undefined) {
|
|
const p = path.join(distDir, 'index.html')
|
|
html = fs.readFileSync(p, 'utf8')
|
|
indexHtmlBySlug.set(slug, html)
|
|
}
|
|
res.statusCode = 200
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
res.end(html)
|
|
return
|
|
}
|
|
next()
|
|
})
|
|
})
|
|
},
|
|
}
|
|
}
|