fb-slides 0.7.0-rc.0 → 0.7.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/CHANGELOG.md +11 -4
- package/docs/source-code.md +47 -17
- package/lib/config.mjs +14 -6
- package/lib/dev.mjs +40 -15
- package/package.json +1 -1
- package/runtime/deck.js +11 -2
- package/templates/starter/slides.config.js +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,7 @@ this — which makes it worth being able to see what arrived.
|
|
|
7
7
|
Entries are written as the work lands, under **Unreleased**; the release commit
|
|
8
8
|
that stamps the version renames that heading to the version and its date.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## 0.7.0 — 2026-09-07
|
|
11
11
|
|
|
12
12
|
- **A `source ↗` button on demo slides.** `editor: true` in `slides.config.js`
|
|
13
13
|
starts `code serve-web` — the web server built into VS Code — alongside the
|
|
@@ -17,10 +17,17 @@ that stamps the version renames that heading to the version and its date.
|
|
|
17
17
|
a folder demo, and for one embedded by URL from the `servers:` entry serving
|
|
18
18
|
that address — which is the first thing `url:` has ever been used for.
|
|
19
19
|
`<!-- demo: … | src: demo/cart -->` says it outright when nothing else knows.
|
|
20
|
-
The
|
|
21
|
-
|
|
20
|
+
The slide's address and the one in `servers:` need not match to the character:
|
|
21
|
+
the exact URL is tried first, then the origin, so a demo embedded at `#/` or
|
|
22
|
+
`/tools` still finds its folder.
|
|
23
|
+
The editor listens on 7100 — clear of 3000, 4200, 5173 and the rest of the band
|
|
24
|
+
the demos want, since the editor must never land on the app it exists to open —
|
|
25
|
+
and walks up from there if something holds it, because `serve-web` has no
|
|
26
|
+
`--strictPort` and would otherwise half-bind a busy port. A built
|
|
22
27
|
deck never carries the button: the link is a localhost address and a path on
|
|
23
|
-
your disk. `--no-editor` turns it off for one run.
|
|
28
|
+
your disk. `--no-editor` turns it off for one run. When the editor cannot
|
|
29
|
+
start — no `code` in `PATH`, port taken — the deck comes up as usual and says
|
|
30
|
+
why under its own URL, where the demo servers' banners cannot bury it.
|
|
24
31
|
[docs/source-code.md](docs/source-code.md)
|
|
25
32
|
|
|
26
33
|
## 0.6.3 — 2026-09-05
|
package/docs/source-code.md
CHANGED
|
@@ -17,13 +17,13 @@ export default {
|
|
|
17
17
|
};
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
That is the whole configuration. `dev` starts it alongside the deck, on
|
|
21
|
-
|
|
20
|
+
That is the whole configuration. `dev` starts it alongside the deck, on port
|
|
21
|
+
7100, and every demo slide grows the link.
|
|
22
22
|
|
|
23
23
|
```
|
|
24
24
|
$ npm run dev
|
|
25
25
|
|
|
26
|
-
↑ code serve-web → http://localhost:
|
|
26
|
+
↑ code serve-web → http://localhost:7100/
|
|
27
27
|
|
|
28
28
|
My Talk
|
|
29
29
|
→ http://localhost:4000/
|
|
@@ -32,10 +32,11 @@ $ npm run dev
|
|
|
32
32
|
Both defaults move if they have to:
|
|
33
33
|
|
|
34
34
|
```js
|
|
35
|
-
editor: { port:
|
|
35
|
+
editor: { port: 7300, command: 'code-insiders' },
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
`command` is a name in PATH
|
|
38
|
+
`command` is a name in PATH, or a path to the binary when the shell you start
|
|
39
|
+
the deck from does not carry it. `code-insiders` and `cursor` answer `serve-web`
|
|
39
40
|
too; whatever you name has to be a VS Code CLI, because that subcommand is what
|
|
40
41
|
this uses.
|
|
41
42
|
|
|
@@ -49,28 +50,30 @@ Start `dev` once at your desk after turning the key on. That is the whole
|
|
|
49
50
|
precaution, and it is why `editor:` is off in a fresh deck rather than on.
|
|
50
51
|
|
|
51
52
|
If `code` is not in PATH at all, the deck says so and carries on without the
|
|
52
|
-
button
|
|
53
|
+
button — see [When no slide has one](#when-no-slide-has-one) below.
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
⚠ code is not in PATH — the demo slides get no source button.
|
|
56
|
-
In VS Code: Shell Command: Install 'code' command in PATH.
|
|
57
|
-
```
|
|
55
|
+
## Why 7100
|
|
58
56
|
|
|
59
|
-
|
|
57
|
+
Because the demos own everything below it. 3000 is Next's default, 4200 is
|
|
58
|
+
`ng serve`'s, 5173 and 5174 are Vite's, 6006 is Storybook's, 8080 is everyone's.
|
|
59
|
+
The editor landing on the very app it exists to open is the one collision that
|
|
60
|
+
must not happen — and since `dev` starts the demo servers first, the editor is
|
|
61
|
+
the one that would lose.
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
second deck to be on.
|
|
63
|
+
It is not derived from the deck's port for the same reason: a deck on 4100 would
|
|
64
|
+
put its editor on 4200, which is `ng serve` waiting to happen.
|
|
64
65
|
|
|
65
66
|
`code serve-web` does not fail on a busy port the way the deck server does — a
|
|
66
67
|
process holding the IPv6 side leaves the IPv4 side free, both bind, and
|
|
67
68
|
`localhost` in the browser then lands on whichever the resolver picked. That is
|
|
68
69
|
the failure you find out about on stage. So the port is checked before the
|
|
69
|
-
editor
|
|
70
|
+
editor starts, and a taken one moves the editor along: 7101, 7102, up to ten
|
|
71
|
+
tries. Nobody types this address — the page is handed it — so a different number
|
|
72
|
+
costs nothing. Only ten taken ports in a row is a message:
|
|
70
73
|
|
|
71
74
|
```
|
|
72
|
-
⚠
|
|
73
|
-
|
|
75
|
+
⚠ source ↗ off: ports 7100–7109 are all in use.
|
|
76
|
+
Give the editor one of its own: `editor: { port: n }`.
|
|
74
77
|
```
|
|
75
78
|
|
|
76
79
|
## Where a slide's code is found
|
|
@@ -101,6 +104,12 @@ The third row is the one worth knowing about. A framework demo is embedded by
|
|
|
101
104
|
That pairing used to be a log line and nothing else. It is now also how the
|
|
102
105
|
button finds the code behind a running app.
|
|
103
106
|
|
|
107
|
+
The two addresses do not have to match to the character. A slide embeds an app
|
|
108
|
+
at the route the demo opens on — `http://localhost:5173/#/settings`, `/tools`,
|
|
109
|
+
a query string — while `url:` names where the app answers. The exact address is
|
|
110
|
+
tried first, and a miss falls back to the **origin**: same host and port, same
|
|
111
|
+
app, same sources. On a port shared by two entries, the first one wins.
|
|
112
|
+
|
|
104
113
|
The fourth row, `| src:`, is the escape hatch for what is left: a demo deployed
|
|
105
114
|
somewhere, or embedded from a server the config does not declare. It wins over
|
|
106
115
|
everything else when it is there.
|
|
@@ -108,6 +117,27 @@ everything else when it is there.
|
|
|
108
117
|
A demo whose folder is unknown simply has no button. Nothing breaks, and the
|
|
109
118
|
rest of the slide is unchanged.
|
|
110
119
|
|
|
120
|
+
## When no slide has one
|
|
121
|
+
|
|
122
|
+
The button is missing everywhere — not on one demo, on all of them — when the
|
|
123
|
+
editor never started. `dev` says so as the last thing it prints, under the deck
|
|
124
|
+
URL, because the demo servers write their own banners over anything said before
|
|
125
|
+
it:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
MCP UI
|
|
129
|
+
→ http://localhost:4100/
|
|
130
|
+
⚠ source ↗ off: code is not in PATH.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`code` living in `/usr/local/bin` and a shell that does not have that directory
|
|
134
|
+
in its `PATH` is the usual reason — an IDE's built-in terminal often does not.
|
|
135
|
+
Either start the deck from a shell that has it, or skip `PATH` altogether:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
editor: { command: '/usr/local/bin/code' },
|
|
139
|
+
```
|
|
140
|
+
|
|
111
141
|
## What it does not do
|
|
112
142
|
|
|
113
143
|
- **It is not in the built deck.** `build` never writes it out: the link is a
|
package/lib/config.mjs
CHANGED
|
@@ -21,16 +21,24 @@ const AUTO_STATIC = ['assets', 'demo', 'public', 'images', 'img'];
|
|
|
21
21
|
// Never served, never published — heavy, private, or generated.
|
|
22
22
|
export const ALWAYS_EXCLUDED = ['node_modules', '.git', '.angular', '.next', '.cache', '.DS_Store'];
|
|
23
23
|
|
|
24
|
+
// The editor's port. A fixed number rather than one derived from the deck's,
|
|
25
|
+
// because the band under 6000 belongs to the demos: 3000 is Next's, 4200 is
|
|
26
|
+
// `ng serve`'s, 5173 and 5174 are Vite's, 8080 is everyone's. An editor that
|
|
27
|
+
// lands on the app it exists to open is the one collision that must not happen
|
|
28
|
+
// — and `dev` starts the demo servers first, so it would be the editor that
|
|
29
|
+
// loses. Two decks at once are handled by walking up from here instead.
|
|
30
|
+
const EDITOR_PORT = 7100;
|
|
31
|
+
|
|
24
32
|
// `editor: true` turns the source button on with the defaults; `{ port, command }`
|
|
25
|
-
// changes them; leaving the key out is no button at all. The
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
const editorSpec = (value
|
|
33
|
+
// changes them; leaving the key out is no button at all. The command is a name in
|
|
34
|
+
// PATH — `code-insiders` and `cursor` answer `serve-web` too — or a path, for a
|
|
35
|
+
// shell whose PATH does not carry the editor.
|
|
36
|
+
const editorSpec = (value) => {
|
|
29
37
|
if (!value) return null;
|
|
30
38
|
const user = value === true ? {} : value;
|
|
31
39
|
return {
|
|
32
40
|
command: user.command ?? 'code',
|
|
33
|
-
port: Number(user.port ??
|
|
41
|
+
port: Number(user.port ?? EDITOR_PORT),
|
|
34
42
|
};
|
|
35
43
|
};
|
|
36
44
|
|
|
@@ -109,7 +117,7 @@ export const loadConfig = async (root = process.cwd(), overrides = {}) => {
|
|
|
109
117
|
// `--no-editor` is its own flag rather than a part of `--no-servers`: that one
|
|
110
118
|
// is for skipping a demo's dev server, and a demo that is not running is
|
|
111
119
|
// exactly when you want to be able to open its code.
|
|
112
|
-
config.editor = overrides.editor === false ? null : editorSpec(user.editor
|
|
120
|
+
config.editor = overrides.editor === false ? null : editorSpec(user.editor);
|
|
113
121
|
// The folder behind each demo embedded by URL, taken from `servers:` as the
|
|
114
122
|
// project wrote it rather than from the list `dev` will start: `--no-servers`
|
|
115
123
|
// empties that one, and a demo that is not running is exactly when its code is
|
package/lib/dev.mjs
CHANGED
|
@@ -66,9 +66,7 @@ const startSideServer = (spec, root, children) => {
|
|
|
66
66
|
// `serve-web` has no --strictPort, and it does not fail on a taken port the way
|
|
67
67
|
// the deck server does: another process holding the IPv6 side leaves the IPv4
|
|
68
68
|
// side free, both bind, and `localhost` in the browser then lands on whichever
|
|
69
|
-
// the resolver picked
|
|
70
|
-
// 4000 puts its editor on 4100, which is a perfectly ordinary port for the
|
|
71
|
-
// second deck. So ask first, and say which port to use instead.
|
|
69
|
+
// the resolver picked — the failure you find out about on stage. So ask first.
|
|
72
70
|
const portInUse = (port) =>
|
|
73
71
|
Promise.all(
|
|
74
72
|
['127.0.0.1', '::1'].map(
|
|
@@ -86,6 +84,17 @@ const portInUse = (port) =>
|
|
|
86
84
|
),
|
|
87
85
|
).then((answers) => answers.some(Boolean));
|
|
88
86
|
|
|
87
|
+
// A second deck, or anything else already sitting there, moves the editor along
|
|
88
|
+
// rather than turning it off: nobody types this address — the page is handed it.
|
|
89
|
+
const EDITOR_PORT_TRIES = 10;
|
|
90
|
+
|
|
91
|
+
const freePort = async (start) => {
|
|
92
|
+
for (let port = start; port < start + EDITOR_PORT_TRIES; port += 1) {
|
|
93
|
+
if (!(await portInUse(port))) return port;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
97
|
+
|
|
89
98
|
const hasCommand = (command) =>
|
|
90
99
|
new Promise((done) => {
|
|
91
100
|
const probe = spawn(command, ['--version'], { stdio: 'ignore' });
|
|
@@ -93,20 +102,32 @@ const hasCommand = (command) =>
|
|
|
93
102
|
probe.on('exit', (code) => done(code === 0));
|
|
94
103
|
});
|
|
95
104
|
|
|
105
|
+
// Returns `{ url }` when the editor is up, `{ note }` when it is not. The note
|
|
106
|
+
// is printed with the summary rather than here: the side servers spawn first
|
|
107
|
+
// with `stdio: 'inherit'`, and vite writing its banner over the one line that
|
|
108
|
+
// explains a missing button is how a presenter ends up hunting for it.
|
|
96
109
|
const startEditor = async (spec, children) => {
|
|
97
110
|
if (!(await hasCommand(spec.command))) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
return {
|
|
112
|
+
note: [
|
|
113
|
+
`⚠ source ↗ off: ${spec.command} is not in PATH.`,
|
|
114
|
+
`In VS Code: Shell Command: Install '${spec.command}' command in PATH —`,
|
|
115
|
+
`or name it outright: \`editor: { command: '/usr/local/bin/code' }\`.`,
|
|
116
|
+
],
|
|
117
|
+
};
|
|
101
118
|
}
|
|
102
119
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
const port = await freePort(spec.port);
|
|
121
|
+
if (port === null) {
|
|
122
|
+
return {
|
|
123
|
+
note: [
|
|
124
|
+
`⚠ source ↗ off: ports ${spec.port}–${spec.port + EDITOR_PORT_TRIES - 1} are all in use.`,
|
|
125
|
+
`Give the editor one of its own: \`editor: { port: n }\`.`,
|
|
126
|
+
],
|
|
127
|
+
};
|
|
107
128
|
}
|
|
108
129
|
|
|
109
|
-
const url = `http://localhost:${
|
|
130
|
+
const url = `http://localhost:${port}/`;
|
|
110
131
|
// First run downloads the server half of VS Code into ~/.vscode/cli, which is
|
|
111
132
|
// a hundred megabytes and a minute — hence `stdio: 'inherit'`, so it happens
|
|
112
133
|
// in front of whoever started the deck rather than silently before a talk.
|
|
@@ -116,7 +137,7 @@ const startEditor = async (spec, children) => {
|
|
|
116
137
|
[
|
|
117
138
|
'serve-web',
|
|
118
139
|
'--port',
|
|
119
|
-
String(
|
|
140
|
+
String(port),
|
|
120
141
|
// No token in the URL: the slide has to be able to link straight to a
|
|
121
142
|
// folder, and this listens on localhost only.
|
|
122
143
|
'--without-connection-token',
|
|
@@ -130,7 +151,7 @@ const startEditor = async (spec, children) => {
|
|
|
130
151
|
child.on('exit', (code) => {
|
|
131
152
|
if (code) console.warn(`\n ⚠ ${spec.command} serve-web stopped (exit ${code}) — no source button.\n`);
|
|
132
153
|
});
|
|
133
|
-
return url;
|
|
154
|
+
return { url };
|
|
134
155
|
};
|
|
135
156
|
|
|
136
157
|
// What the page is built from, read again on every request. A config with a
|
|
@@ -197,9 +218,13 @@ export const dev = async (config, runtimeDir, reload) => {
|
|
|
197
218
|
const url = `http://localhost:${config.port}/`;
|
|
198
219
|
const children = [];
|
|
199
220
|
for (const spec of config.servers) startSideServer(spec, config.root, children);
|
|
200
|
-
|
|
221
|
+
const editor = config.editor ? await startEditor(config.editor, children) : {};
|
|
222
|
+
editorUrl = editor.url ?? null;
|
|
201
223
|
|
|
202
|
-
|
|
224
|
+
// The last thing printed, so nothing scrolls over it.
|
|
225
|
+
const summary = [``, ` ${config.title}`, ` → ${url}`];
|
|
226
|
+
if (editor.note) summary.push(...editor.note.map((line, i) => (i ? ` ${line}` : ` ${line}`)));
|
|
227
|
+
console.log(summary.join('\n') + '\n');
|
|
203
228
|
if (config.open) setTimeout(() => openBrowser(url), 400);
|
|
204
229
|
|
|
205
230
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
package/package.json
CHANGED
package/runtime/deck.js
CHANGED
|
@@ -76,8 +76,17 @@ const EDITOR = CFG.editor ?? null;
|
|
|
76
76
|
// `servers:` entry serves it — unless the slide said outright with `| src:`.
|
|
77
77
|
const sourceOf = (target, src, isUrl) => {
|
|
78
78
|
if (src) return src;
|
|
79
|
-
if (isUrl) return
|
|
80
|
-
|
|
79
|
+
if (!isUrl) return /^[./]/.test(target) ? target : `${DEMOS_DIR}${target}`;
|
|
80
|
+
const sources = EDITOR?.sources;
|
|
81
|
+
if (!sources) return null;
|
|
82
|
+
const exact = sources[target.replace(/\/$/, '')];
|
|
83
|
+
if (exact) return exact;
|
|
84
|
+
// A slide embeds an app at whatever route the demo opens on — `#/`, `/tools`,
|
|
85
|
+
// a query string — while `servers:` names the address the app answers at. The
|
|
86
|
+
// two rarely match to the character, so a miss falls back to the origin: one
|
|
87
|
+
// app per port is what a demo is, and on a shared port the first entry wins.
|
|
88
|
+
const { origin } = new URL(target);
|
|
89
|
+
return Object.entries(sources).find(([url]) => new URL(url).origin === origin)?.[1] ?? null;
|
|
81
90
|
};
|
|
82
91
|
|
|
83
92
|
// VS Code for the Web opens whatever `?folder=` names, and it wants a path on
|
|
@@ -44,9 +44,10 @@ export default {
|
|
|
44
44
|
// and nothing is embedded. Off by default because the very first run downloads
|
|
45
45
|
// VS Code's server half (~100 MB) and you do not want to discover that on
|
|
46
46
|
// stage: uncomment, run `dev` once at your desk, and it is cached from then on.
|
|
47
|
-
// `{ port:
|
|
48
|
-
//
|
|
49
|
-
// built deck never has it: the link is a localhost address and
|
|
47
|
+
// `{ port: 7300, command: 'code' }` to change either — the default is 7100,
|
|
48
|
+
// which is clear of the ports the demos want. `--no-editor` turns it off for
|
|
49
|
+
// one run, and a built deck never has it: the link is a localhost address and
|
|
50
|
+
// a path on disk.
|
|
50
51
|
editor: true,
|
|
51
52
|
|
|
52
53
|
// The corner signature. Uncomment to show it.
|