@workshops.de/slidev-toolkit 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -2
- package/bin/workshop-slides.mjs +230 -0
- package/package.json +25 -4
package/README.md
CHANGED
|
@@ -1,3 +1,87 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @workshops.de/slidev-toolkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared build, dev and deploy tooling for the workshops.de Slidev slide repositories. Slide repositories
|
|
4
|
+
depend on this package instead of maintaining their own build scripts, deploy workflow and core
|
|
5
|
+
dependencies (Slidev, Playwright, TypeScript, Vue). A fix or dependency update happens here once and
|
|
6
|
+
reaches every repository with one version bump.
|
|
7
|
+
|
|
8
|
+
## What it provides
|
|
9
|
+
|
|
10
|
+
- **`workshop-slides` CLI**
|
|
11
|
+
- `workshop-slides build [--base /<repo>] [--no-pdf]`: builds every deck into `dist/`, exports a PDF
|
|
12
|
+
per deck and makes deep links like `/<deck>/presenter/1` work on GitHub Pages.
|
|
13
|
+
- `workshop-slides dev [<lesson-number>|all]`: starts the Slidev dev server (interactive menu without
|
|
14
|
+
an argument).
|
|
15
|
+
- `workshop-slides install-browser`: installs the Chromium version that matches the bundled
|
|
16
|
+
Playwright (npm 11 does not run Playwright's own install script).
|
|
17
|
+
- **Pinned core dependencies:** `@slidev/cli`, `playwright-chromium`, `typescript` (6, required by
|
|
18
|
+
Slidev's Twoslash setup) and `vue`.
|
|
19
|
+
- **Reusable deploy workflow:** `.github/workflows/deploy.yml`, called from each slide repository.
|
|
20
|
+
|
|
21
|
+
Two repository layouts are detected automatically:
|
|
22
|
+
|
|
23
|
+
| Layout | Detected by | Output |
|
|
24
|
+
| ----------- | -------------------------------------------------------------- | ----------------------------------------------- |
|
|
25
|
+
| Multi-deck | `lessons/<NN-name>/*.md` with `base:` and `out:` in headmatter | `dist/<deck>/`, root `index.html` + `404.html` |
|
|
26
|
+
| Single-deck | `slides.md` in the repository root | `dist/` (Slidev's own `404.html`) |
|
|
27
|
+
|
|
28
|
+
## Using it in a slide repository
|
|
29
|
+
|
|
30
|
+
`package.json`: only the toolkit, the theme and the addons the decks use:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"scripts": {
|
|
35
|
+
"dev": "workshop-slides dev",
|
|
36
|
+
"build": "workshop-slides build",
|
|
37
|
+
"export": "slidev export"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@workshops.de/slidev-theme": "2.1.0",
|
|
41
|
+
"slidev-addon-tldraw": "^0.5.1",
|
|
42
|
+
"slidev-addon-window-mockup": "0.3.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@workshops.de/slidev-toolkit": "^1.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`.nvmrc`: `v24`
|
|
51
|
+
|
|
52
|
+
`.github/workflows/deploy.yml`:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
name: Deploy pages
|
|
56
|
+
|
|
57
|
+
on:
|
|
58
|
+
workflow_dispatch:
|
|
59
|
+
push:
|
|
60
|
+
branches: [main]
|
|
61
|
+
|
|
62
|
+
permissions:
|
|
63
|
+
contents: read
|
|
64
|
+
pages: write
|
|
65
|
+
id-token: write
|
|
66
|
+
|
|
67
|
+
concurrency:
|
|
68
|
+
group: pages
|
|
69
|
+
cancel-in-progress: false
|
|
70
|
+
|
|
71
|
+
jobs:
|
|
72
|
+
deploy:
|
|
73
|
+
uses: workshops-de/slidev-toolkit/.github/workflows/deploy.yml@v1
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Then set **Settings → Pages → Source → GitHub Actions** once per repository.
|
|
77
|
+
|
|
78
|
+
## Releasing
|
|
79
|
+
|
|
80
|
+
1. Bump `version` in `package.json` and commit.
|
|
81
|
+
2. Push a tag `vX.Y.Z`. The release workflow publishes to npm via trusted publishing (OIDC, no
|
|
82
|
+
token; configured on npmjs.com for `workshops-de/slidev-toolkit` and `release.yml`) and moves the
|
|
83
|
+
major tag (`v1`) that slide repositories use for the reusable workflow.
|
|
84
|
+
3. Dependabot in the slide repositories picks up the new version.
|
|
85
|
+
|
|
86
|
+
Breaking changes (a new Slidev major that needs content changes, a changed CLI) get a new major version,
|
|
87
|
+
so repositories move over deliberately.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// workshop-slides: shared build, dev and deploy tooling for workshops.de Slidev repositories.
|
|
3
|
+
//
|
|
4
|
+
// Two repository layouts are supported and detected automatically:
|
|
5
|
+
// multi-deck: 00-index.md + lessons/<NN-name>/<file>.md, each with `base:` and `out:` in its
|
|
6
|
+
// headmatter. Every deck is built into its own subfolder of dist/.
|
|
7
|
+
// single-deck: slides.md in the repository root, built into dist/.
|
|
8
|
+
//
|
|
9
|
+
// Commands:
|
|
10
|
+
// workshop-slides build [--base /<repo>] [--no-pdf] build all decks (+ PDFs) into dist/
|
|
11
|
+
// workshop-slides dev [<lesson-number>|all] start the Slidev dev server
|
|
12
|
+
// workshop-slides install-browser install the Chromium used for PDF export
|
|
13
|
+
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import readline from 'node:readline/promises';
|
|
19
|
+
|
|
20
|
+
const root = process.cwd();
|
|
21
|
+
const requireFromProject = createRequire(path.join(root, 'package.json'));
|
|
22
|
+
const color = (code) => (text) => (process.stdout.isTTY ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
23
|
+
const green = color('32');
|
|
24
|
+
const yellow = color('33');
|
|
25
|
+
const cyan = color('36');
|
|
26
|
+
const red = color('31');
|
|
27
|
+
|
|
28
|
+
function fail(message) {
|
|
29
|
+
console.error(`${red('Error:')} ${message}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Resolve a package bin from the project's node_modules. Transitive dependencies (like
|
|
34
|
+
// @slidev/cli when it comes in through this toolkit) are not linked into node_modules/.bin,
|
|
35
|
+
// so `npx slidev` would fetch a different version from the registry instead.
|
|
36
|
+
function resolveBin(pkg, binName) {
|
|
37
|
+
let manifestPath;
|
|
38
|
+
try {
|
|
39
|
+
manifestPath = requireFromProject.resolve(`${pkg}/package.json`);
|
|
40
|
+
} catch {
|
|
41
|
+
fail(`${pkg} is not installed. Run npm install first.`);
|
|
42
|
+
}
|
|
43
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
44
|
+
const bin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.[binName];
|
|
45
|
+
if (!bin) fail(`${pkg} has no "${binName}" executable.`);
|
|
46
|
+
return path.join(path.dirname(manifestPath), bin);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function run(pkg, binName, args, { allowFailure = false } = {}) {
|
|
50
|
+
const result = spawnSync(process.execPath, [resolveBin(pkg, binName), ...args], {
|
|
51
|
+
cwd: root,
|
|
52
|
+
stdio: 'inherit',
|
|
53
|
+
});
|
|
54
|
+
if (result.status !== 0 && !allowFailure) process.exit(result.status ?? 1);
|
|
55
|
+
return result.status === 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const slidev = (args, options) => run('@slidev/cli', 'slidev', args, options);
|
|
59
|
+
|
|
60
|
+
function headmatter(file) {
|
|
61
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
62
|
+
if (lines[0].trim() !== '---') return {};
|
|
63
|
+
const data = {};
|
|
64
|
+
for (const line of lines.slice(1)) {
|
|
65
|
+
if (line.trim() === '---') break;
|
|
66
|
+
const match = line.match(/^(base|out):\s*(.+?)\s*$/);
|
|
67
|
+
if (match) data[match[1]] = match[2].replace(/^['"]|['"]$/g, '');
|
|
68
|
+
}
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function lessonDecks() {
|
|
73
|
+
const lessonsDir = path.join(root, 'lessons');
|
|
74
|
+
if (!fs.existsSync(lessonsDir)) return [];
|
|
75
|
+
return fs
|
|
76
|
+
.readdirSync(lessonsDir, { withFileTypes: true })
|
|
77
|
+
.filter((entry) => entry.isDirectory())
|
|
78
|
+
.map((entry) => entry.name)
|
|
79
|
+
.sort()
|
|
80
|
+
.flatMap((dir) =>
|
|
81
|
+
fs
|
|
82
|
+
.readdirSync(path.join(lessonsDir, dir))
|
|
83
|
+
.filter((file) => file.endsWith('.md'))
|
|
84
|
+
.sort()
|
|
85
|
+
.map((file) => ({ dir, file: path.join('lessons', dir, file) })),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function layout() {
|
|
90
|
+
if (lessonDecks().length > 0) return 'multi';
|
|
91
|
+
if (fs.existsSync(path.join(root, 'slides.md'))) return 'single';
|
|
92
|
+
fail('No decks found: expected lessons/<NN-name>/*.md or slides.md in the repository root.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseArgs(argv) {
|
|
96
|
+
const options = { base: '', pdf: true, rest: [] };
|
|
97
|
+
for (let i = 0; i < argv.length; i++) {
|
|
98
|
+
if (argv[i] === '--base') options.base = (argv[++i] ?? '').replace(/\/+$/, '');
|
|
99
|
+
else if (argv[i] === '--no-pdf') options.pdf = false;
|
|
100
|
+
else options.rest.push(argv[i]);
|
|
101
|
+
}
|
|
102
|
+
return options;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function exportPdf(entry, output) {
|
|
106
|
+
console.log(`${green('Exporting PDF')} ${entry} → ${output}`);
|
|
107
|
+
const ok = slidev(['export', entry, '--output', output, '--timeout', '60000'], { allowFailure: true });
|
|
108
|
+
console.log(ok ? ` ${green('✓')} PDF exported` : ` ${yellow('⚠')} PDF export failed (is Chromium installed?)`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildMulti({ base, pdf }) {
|
|
112
|
+
const decks = [{ dir: null, file: '00-index.md' }, ...lessonDecks()].filter(({ file }) =>
|
|
113
|
+
fs.existsSync(path.join(root, file)),
|
|
114
|
+
);
|
|
115
|
+
for (const { dir, file } of decks) {
|
|
116
|
+
const { base: deckBase, out } = headmatter(path.join(root, file));
|
|
117
|
+
if (!deckBase || !out) {
|
|
118
|
+
console.log(`${yellow('Skipping')} ${file} (missing base or out in headmatter)`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const fullBase = `${base}${deckBase}`;
|
|
122
|
+
console.log(`${green('Building')} ${file}\n ➤ base: ${fullBase}\n ➤ out : ${out}`);
|
|
123
|
+
slidev(['build', file, '--base', fullBase, '--out', out]);
|
|
124
|
+
if (pdf && dir) exportPdf(file, path.join('dist', dir, 'slides.pdf'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const dist = path.join(root, 'dist');
|
|
128
|
+
fs.mkdirSync(dist, { recursive: true });
|
|
129
|
+
const indexUrl = `${base}/00-index/`;
|
|
130
|
+
fs.writeFileSync(
|
|
131
|
+
path.join(dist, 'index.html'),
|
|
132
|
+
`<!DOCTYPE html>
|
|
133
|
+
<html>
|
|
134
|
+
<head>
|
|
135
|
+
<meta charset="UTF-8">
|
|
136
|
+
<meta http-equiv="refresh" content="0; url=${indexUrl}">
|
|
137
|
+
<title>Redirecting...</title>
|
|
138
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
<p>Redirecting to <a href="${indexUrl}">slide decks</a>...</p>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|
|
143
|
+
`,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// GitHub Pages only serves the 404.html at the site root, but every deck is its own SPA in a
|
|
147
|
+
// subfolder (Slidev's own 404.html lands in dist/<deck>/ and is never used). This page loads the
|
|
148
|
+
// matching deck's index.html in place, so the URL stays intact and the deck router opens the
|
|
149
|
+
// requested route, e.g. /<deck>/presenter/1.
|
|
150
|
+
fs.writeFileSync(
|
|
151
|
+
path.join(dist, '404.html'),
|
|
152
|
+
`<!DOCTYPE html>
|
|
153
|
+
<html>
|
|
154
|
+
<head>
|
|
155
|
+
<meta charset="UTF-8">
|
|
156
|
+
<title>Loading…</title>
|
|
157
|
+
<script>
|
|
158
|
+
(function () {
|
|
159
|
+
var base = '${base}/';
|
|
160
|
+
var path = location.pathname;
|
|
161
|
+
var deck = path.indexOf(base) === 0 ? path.slice(base.length).split('/')[0] : '';
|
|
162
|
+
function notFound() {
|
|
163
|
+
document.title = 'Not found';
|
|
164
|
+
document.body.innerHTML = '<h1>404</h1><p><a href="' + base + '">Back to the overview</a></p>';
|
|
165
|
+
}
|
|
166
|
+
if (!deck) { document.addEventListener('DOMContentLoaded', notFound); return; }
|
|
167
|
+
fetch(base + deck + '/index.html')
|
|
168
|
+
.then(function (r) { if (!r.ok) throw new Error(r.status); return r.text(); })
|
|
169
|
+
.then(function (html) { document.open(); document.write(html); document.close(); })
|
|
170
|
+
.catch(notFound);
|
|
171
|
+
})();
|
|
172
|
+
</script>
|
|
173
|
+
</head>
|
|
174
|
+
<body></body>
|
|
175
|
+
</html>
|
|
176
|
+
`,
|
|
177
|
+
);
|
|
178
|
+
console.log(`${green('Created')} dist/index.html and dist/404.html`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildSingle({ base, pdf }) {
|
|
182
|
+
// Slidev writes its own dist/404.html (a copy of index.html), which covers deep links here.
|
|
183
|
+
console.log(`${green('Building')} slides.md\n ➤ base: ${base}/\n ➤ out : dist`);
|
|
184
|
+
slidev(['build', 'slides.md', '--base', `${base}/`, '--out', 'dist']);
|
|
185
|
+
if (pdf) exportPdf('slides.md', path.join('dist', 'slides.pdf'));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function dev(choice) {
|
|
189
|
+
if (layout() === 'single') return slidev(['slides.md', '--open']);
|
|
190
|
+
|
|
191
|
+
const lessons = lessonDecks().filter(({ dir }, i, all) => all.findIndex((d) => d.dir === dir) === i);
|
|
192
|
+
const hasAll = fs.existsSync(path.join(root, 'slides.md'));
|
|
193
|
+
if (!choice) {
|
|
194
|
+
console.log(`\n${cyan('Workshop slides: development server')}\n`);
|
|
195
|
+
if (hasAll) console.log(` ${green('0')}) All lessons (slides.md) [default]`);
|
|
196
|
+
for (const { dir } of lessons) console.log(` ${green(dir.split('-')[0])}) ${dir.replace(/^[^-]*-/, '')}`);
|
|
197
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
198
|
+
choice = (await rl.question(`\nEnter your choice${hasAll ? ' [0]' : ''}: `)).trim() || (hasAll ? '0' : '');
|
|
199
|
+
rl.close();
|
|
200
|
+
}
|
|
201
|
+
if (choice === '0' || choice === 'all') {
|
|
202
|
+
if (!hasAll) fail('slides.md not found in the repository root.');
|
|
203
|
+
return slidev(['slides.md', '--open']);
|
|
204
|
+
}
|
|
205
|
+
const lesson = lessons.find(({ dir }) => dir.split('-')[0] === choice.padStart(2, '0'));
|
|
206
|
+
if (!lesson) fail(`No lesson found starting with '${choice}'.`);
|
|
207
|
+
return slidev([lesson.file, '--open']);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const [command, ...argv] = process.argv.slice(2);
|
|
211
|
+
switch (command) {
|
|
212
|
+
case 'build': {
|
|
213
|
+
const options = parseArgs(argv);
|
|
214
|
+
(layout() === 'multi' ? buildMulti : buildSingle)(options);
|
|
215
|
+
console.log(cyan('Build complete!'));
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case 'dev':
|
|
219
|
+
await dev(argv[0]);
|
|
220
|
+
break;
|
|
221
|
+
case 'install-browser':
|
|
222
|
+
run('playwright-chromium', 'playwright', ['install', 'chromium']);
|
|
223
|
+
break;
|
|
224
|
+
default:
|
|
225
|
+
console.log(`Usage:
|
|
226
|
+
workshop-slides build [--base /<repo>] [--no-pdf] build all decks (+ PDFs) into dist/
|
|
227
|
+
workshop-slides dev [<lesson-number>|all] start the Slidev dev server
|
|
228
|
+
workshop-slides install-browser install Chromium for the PDF export`);
|
|
229
|
+
process.exit(command ? 1 : 0);
|
|
230
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workshops.de/slidev-toolkit",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared build, dev and deploy tooling for workshops.de Slidev slide repositories",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/workshops-de/slidev-toolkit.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"workshop-slides": "bin/workshop-slides.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=24",
|
|
19
|
+
"npm": ">=11"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@slidev/cli": "^53.1.0",
|
|
23
|
+
"playwright-chromium": "^1.65.0",
|
|
24
|
+
"typescript": "^6.0.3",
|
|
25
|
+
"vue": "^3.5.45"
|
|
26
|
+
}
|
|
27
|
+
}
|