@hajimism/dek 0.5.1 → 0.6.1
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 +38 -0
- package/package.json +10 -3
- package/src/cli/build.ts +3 -1
- package/src/cli/check.ts +20 -11
- package/src/cli/commands.ts +24 -10
- package/src/cli/contract.ts +28 -2
- package/src/cli/flags.ts +5 -0
- package/src/cli/lint.ts +53 -1
- package/src/cli/sync.ts +21 -6
- package/src/cli/text.ts +20 -2
- package/src/core/build.ts +4 -0
- package/src/core/config-keys.ts +7 -2
- package/src/core/config.ts +63 -0
- package/src/core/diagnostic.ts +9 -1
- package/src/core/document.ts +28 -3
- package/src/core/lint/script.ts +2 -4
- package/src/core/page.ts +1 -0
- package/src/core/plugin-host.ts +709 -0
- package/src/core/plugin.ts +442 -0
- package/src/core/sarif.ts +47 -5
- package/src/core/script-lines.ts +11 -0
- package/src/core/sync.ts +4 -2
- package/src/plugin-api.ts +126 -0
- package/src/plugins/study/check.ts +128 -0
- package/src/plugins/study/main.ts +47 -0
- package/src/plugins/study/manifest.ts +32 -0
- package/src/plugins/study/page.ts +295 -0
- package/src/plugins/study/place.ts +88 -0
- package/src/plugins/study/sarif.ts +30 -0
- package/src/plugins/study/study.ts +137 -0
- package/src/plugins/study/text.ts +55 -0
- package/src/plugins/study/yaml.ts +226 -0
- package/src/plugins/words/check.ts +109 -0
- package/src/plugins/words/lines.ts +26 -0
- package/src/plugins/words/list.ts +288 -0
- package/src/plugins/words/main.ts +50 -0
- package/src/plugins/words/manifest.ts +36 -0
- package/src/plugins/words/match.ts +60 -0
- package/src/plugins/words/sarif.ts +37 -0
- package/src/plugins/words/text.ts +352 -0
- package/src/plugins/words/yaml.ts +225 -0
- package/src/runtime/player.ts +18 -2
- package/src/runtime/plugins.ts +236 -0
- package/src/runtime/start.ts +16 -1
- package/src/runtime/window.ts +3 -0
- package/src/server/deck-snapshot.ts +32 -1
- package/src/server/dev.ts +7 -1
- package/src/server/pages.ts +9 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
import { PAGE_ID } from "../core/page.ts";
|
|
4
|
+
import type { Position } from "../core/step.ts";
|
|
5
|
+
import type { DekPage, PageApi, PageMount, PagePosition } from "../plugin-api.ts";
|
|
6
|
+
import { isLetterKey, isTextEntry } from "./step.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Page API 1 (ADR 33): `window.dek.plugin`, which a plugin's page calls once it runs, after the
|
|
10
|
+
* player. Each plugin gets a layer over every slide, in the slide's own pixels, kept in `#deck`
|
|
11
|
+
* beside the slides so that it scales and moves with them, and is captured with them in a view
|
|
12
|
+
* transition, while a slide's own markup, which lint checked, is never touched.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** What the page carries for each plugin: its declared keys and its data files. */
|
|
16
|
+
type PluginConfig = Record<
|
|
17
|
+
string,
|
|
18
|
+
{ keys: string[]; data: Record<string, { text: string; version: string }> }
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
export type PluginHost = {
|
|
22
|
+
/** Show the layers of the slide at `pos`; runs inside each move, as the stage draws it. */
|
|
23
|
+
render(pos: Position): void;
|
|
24
|
+
/** The deck settled at `pos`: every plugin hears where it stands. */
|
|
25
|
+
settled(pos: Position): void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type Registered = {
|
|
29
|
+
name: string;
|
|
30
|
+
change: Array<(position: PagePosition) => void>;
|
|
31
|
+
mount: Array<(mount: PageMount) => void>;
|
|
32
|
+
layers: Map<string, HTMLElement>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const LAYER_CLASS = "dek-plugin-layer";
|
|
36
|
+
const PAGE_API = 1;
|
|
37
|
+
|
|
38
|
+
export function createPluginHost(options: {
|
|
39
|
+
deck: string;
|
|
40
|
+
slides: Array<{ slug: string; title: string; beats: unknown[] }>;
|
|
41
|
+
position: () => Position;
|
|
42
|
+
}): PluginHost {
|
|
43
|
+
// The plugins' block comes after the player in the page, so it is read when a plugin asks.
|
|
44
|
+
let config: PluginConfig | undefined;
|
|
45
|
+
const registered: Registered[] = [];
|
|
46
|
+
const keys = new Map<string, { plugin: string; listener: () => void }>();
|
|
47
|
+
|
|
48
|
+
const at = (pos: Position): PagePosition => ({
|
|
49
|
+
slug: options.slides[pos.slideIndex]?.slug ?? "",
|
|
50
|
+
index: pos.slideIndex,
|
|
51
|
+
beat: pos.beatIndex,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** Runs a plugin's code; what it throws is the plugin's, and the player goes on. */
|
|
55
|
+
const guard = (name: string, run: () => void): void => {
|
|
56
|
+
try {
|
|
57
|
+
run();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`dek: plugin ${name} threw`, error);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const slideEl = (slug: string): HTMLElement | null =>
|
|
64
|
+
document.querySelector<HTMLElement>(
|
|
65
|
+
`#${PAGE_ID.deck} > .slide[data-slug="${CSS.escape(slug)}"]`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
/** The plugin's layer over `slug`, made on first use, after the slides in `#deck`. */
|
|
69
|
+
const layerOf = (plugin: Registered, slug: string): HTMLElement => {
|
|
70
|
+
let host = plugin.layers.get(slug);
|
|
71
|
+
if (!host) {
|
|
72
|
+
host = document.createElement("div");
|
|
73
|
+
host.className = LAYER_CLASS;
|
|
74
|
+
host.dataset.plugin = plugin.name;
|
|
75
|
+
host.dataset.slug = slug;
|
|
76
|
+
host.setAttribute("aria-hidden", "true");
|
|
77
|
+
// The layer is the slide's box; only what a plugin draws in it takes the pointer.
|
|
78
|
+
host.style.cssText = "position:absolute;inset:0;pointer-events:none;display:none";
|
|
79
|
+
host.attachShadow({ mode: "open" });
|
|
80
|
+
document.getElementById(PAGE_ID.deck)?.append(host);
|
|
81
|
+
plugin.layers.set(slug, host);
|
|
82
|
+
}
|
|
83
|
+
return host;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const mount = (plugin: Registered, slug: string): void => {
|
|
87
|
+
const root = slideEl(slug);
|
|
88
|
+
const layer = layerOf(plugin, slug).shadowRoot;
|
|
89
|
+
if (!root || !layer) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
layer.replaceChildren();
|
|
93
|
+
for (const listener of plugin.mount) {
|
|
94
|
+
guard(plugin.name, () => listener({ slug, root, layer, view: "stage" }));
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const render = (pos: Position): void => {
|
|
99
|
+
const slug = options.slides[pos.slideIndex]?.slug;
|
|
100
|
+
for (const plugin of registered) {
|
|
101
|
+
for (const [layerSlug, host] of plugin.layers) {
|
|
102
|
+
host.style.display = layerSlug === slug ? "block" : "none";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const settled = (pos: Position): void => {
|
|
108
|
+
for (const plugin of registered) {
|
|
109
|
+
for (const listener of plugin.change) {
|
|
110
|
+
guard(plugin.name, () => listener(at(pos)));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const dek: DekPage = {
|
|
116
|
+
plugin(name, pluginOptions, setup) {
|
|
117
|
+
if (pluginOptions?.api !== PAGE_API) {
|
|
118
|
+
console.error(
|
|
119
|
+
`dek: plugin ${name} asks for page API ${String(pluginOptions?.api)}; this dek gives ${PAGE_API}`,
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
config ??= readConfig();
|
|
124
|
+
const declared = config[name];
|
|
125
|
+
if (!declared || registered.some((plugin) => plugin.name === name)) {
|
|
126
|
+
console.error(`dek: plugin ${name} is not one this page carries`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const plugin: Registered = { name, change: [], mount: [], layers: new Map() };
|
|
130
|
+
const api: PageApi = {
|
|
131
|
+
deck: { name: options.deck },
|
|
132
|
+
slides: options.slides.map(({ slug, title }) => ({ slug, title })),
|
|
133
|
+
current: () => at(options.position()),
|
|
134
|
+
on(event: "change" | "mount", listener: (value: never) => void) {
|
|
135
|
+
if (event === "change" || event === "mount") {
|
|
136
|
+
plugin[event].push(listener as never);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
shown: (element) => element.closest("[data-step]:not(.is-shown)") === null,
|
|
140
|
+
data: (path) => declared.data[path] ?? null,
|
|
141
|
+
key(letter, listener) {
|
|
142
|
+
if (!declared.keys.includes(letter) || keys.has(letter)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
keys.set(letter, { plugin: name, listener });
|
|
146
|
+
return true;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
let ok = true;
|
|
150
|
+
try {
|
|
151
|
+
setup(api);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
ok = false;
|
|
154
|
+
console.error(`dek: plugin ${name} threw`, error);
|
|
155
|
+
}
|
|
156
|
+
if (!ok) {
|
|
157
|
+
for (const [letter, bound] of keys) {
|
|
158
|
+
if (bound.plugin === name) {
|
|
159
|
+
keys.delete(letter);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
registered.push(plugin);
|
|
165
|
+
for (const { slug } of options.slides) {
|
|
166
|
+
mount(plugin, slug);
|
|
167
|
+
}
|
|
168
|
+
render(options.position());
|
|
169
|
+
for (const listener of plugin.change) {
|
|
170
|
+
guard(name, () => listener(at(options.position())));
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
window.dek = dek;
|
|
175
|
+
|
|
176
|
+
document.addEventListener("keydown", (event) => {
|
|
177
|
+
if (isTextEntry(event.target)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
for (const [letter, bound] of keys) {
|
|
181
|
+
if (isLetterKey(event, letter)) {
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
guard(bound.plugin, bound.listener);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// A live reload swaps a slide for a new element: its layers start over on the new one.
|
|
190
|
+
const deckEl = document.getElementById(PAGE_ID.deck);
|
|
191
|
+
if (deckEl && typeof MutationObserver !== "undefined") {
|
|
192
|
+
new MutationObserver((records) => {
|
|
193
|
+
const replaced = new Set<string>();
|
|
194
|
+
for (const record of records) {
|
|
195
|
+
for (const node of record.addedNodes) {
|
|
196
|
+
if (
|
|
197
|
+
node instanceof HTMLElement &&
|
|
198
|
+
node.classList.contains("slide") &&
|
|
199
|
+
node.dataset.slug
|
|
200
|
+
) {
|
|
201
|
+
replaced.add(node.dataset.slug);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (replaced.size === 0) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (const plugin of registered) {
|
|
209
|
+
for (const slug of replaced) {
|
|
210
|
+
mount(plugin, slug);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
render(options.position());
|
|
214
|
+
settled(options.position());
|
|
215
|
+
}).observe(deckEl, { childList: true });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// A printed deck is the slides alone.
|
|
219
|
+
const style = document.createElement("style");
|
|
220
|
+
style.textContent = `@media print { .${LAYER_CLASS} { display: none !important; } }`;
|
|
221
|
+
document.head.append(style);
|
|
222
|
+
|
|
223
|
+
return { render, settled };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readConfig(): PluginConfig {
|
|
227
|
+
const text = document.getElementById(PAGE_ID.plugins)?.textContent;
|
|
228
|
+
if (!text) {
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
return JSON.parse(text) as PluginConfig;
|
|
233
|
+
} catch {
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
}
|
package/src/runtime/start.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { CreateDevLine } from "./dev-line.ts";
|
|
|
9
9
|
import { bindInput } from "./input.ts";
|
|
10
10
|
import { createLaserView, type LaserView } from "./laser-view.ts";
|
|
11
11
|
import { createNavigator } from "./navigator.ts";
|
|
12
|
+
import { createPluginHost } from "./plugins.ts";
|
|
12
13
|
import { createPresenterView } from "./presenter-view.ts";
|
|
13
14
|
import { createRailView } from "./rail-view.ts";
|
|
14
15
|
import { deckChannelName } from "./routes.ts";
|
|
@@ -41,6 +42,7 @@ export function startPlayer(createDevLine?: CreateDevLine): void {
|
|
|
41
42
|
presenter.render(pos);
|
|
42
43
|
rail.markCurrent(pos.slideIndex);
|
|
43
44
|
marks?.render(pos);
|
|
45
|
+
plugins.render(pos);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -92,13 +94,26 @@ export function startPlayer(createDevLine?: CreateDevLine): void {
|
|
|
92
94
|
stage.showMove(from, to);
|
|
93
95
|
}
|
|
94
96
|
},
|
|
95
|
-
afterMove:
|
|
97
|
+
afterMove: () => {
|
|
98
|
+
presenter.startClock();
|
|
99
|
+
// A move that another is queued behind has not settled; the last one says where it stands.
|
|
100
|
+
if (positionsEqual(nav.target(), nav.position())) {
|
|
101
|
+
plugins.settled(nav.position());
|
|
102
|
+
}
|
|
103
|
+
},
|
|
96
104
|
route: (next) => control.request(next, "remote"),
|
|
97
105
|
...(laser ? { onPointer: laser.receive } : {}),
|
|
98
106
|
});
|
|
99
107
|
|
|
100
108
|
const marks = dev?.marks;
|
|
101
109
|
|
|
110
|
+
// Plugins' pages run after the player and call window.dek.plugin, which this defines.
|
|
111
|
+
const plugins = createPluginHost({
|
|
112
|
+
deck: page.deck,
|
|
113
|
+
slides,
|
|
114
|
+
position: () => nav.position(),
|
|
115
|
+
});
|
|
116
|
+
|
|
102
117
|
const rail = createRailView({
|
|
103
118
|
slides,
|
|
104
119
|
stage,
|
package/src/runtime/window.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Position } from "../core/step.ts";
|
|
2
|
+
import type { DekPage } from "../plugin-api.ts";
|
|
2
3
|
|
|
3
4
|
/** How capture tools seek the current slide's script, the way they seek Web Animations. */
|
|
4
5
|
export type DekMotionHandle = {
|
|
@@ -17,6 +18,8 @@ declare global {
|
|
|
17
18
|
dekGo?: (next: Position | null | undefined) => Promise<void>;
|
|
18
19
|
dekMotion?: DekMotionHandle;
|
|
19
20
|
dekLive?: (raw: unknown) => Promise<void>;
|
|
21
|
+
/** Page API 1, for plugins' pages (ADR 33). */
|
|
22
|
+
dek?: DekPage;
|
|
20
23
|
/** The go the video recorder started and has not awaited yet. */
|
|
21
24
|
__dekPendingGo?: Promise<void>;
|
|
22
25
|
/** The animations that go started, so capture tools seek those and nothing older. */
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
3
|
import { deckPaths } from "../core/deck-paths.ts";
|
|
4
4
|
import type { LiveEvent } from "../core/live-protocol.ts";
|
|
5
|
+
import { deckProjectRoot } from "../core/path.ts";
|
|
6
|
+
import { deckData, pinnedPlugins } from "../core/plugin-host.ts";
|
|
5
7
|
import { listSlideFiles, type SlideSidecar } from "../core/resolve.ts";
|
|
6
8
|
|
|
7
9
|
/** Modification times by name; a file that is gone is left out. */
|
|
@@ -19,6 +21,11 @@ export type DeckSnapshot = {
|
|
|
19
21
|
javascript: Mtimes;
|
|
20
22
|
/** `voice/voice.toml`, `voice/dict.toml`, and `voice/pin/*`. */
|
|
21
23
|
voice: Mtimes;
|
|
24
|
+
/**
|
|
25
|
+
* Each pinned plugin's lock, and each data file it declares in this deck, keyed by the path
|
|
26
|
+
* from the project's root: what the page's plugin layer is made from (ADR 33).
|
|
27
|
+
*/
|
|
28
|
+
plugins: Mtimes;
|
|
22
29
|
};
|
|
23
30
|
|
|
24
31
|
/** Which slides to lint: one slug, or the whole deck when it is left out. */
|
|
@@ -43,9 +50,29 @@ export function takeSnapshot(deckDir: string): DeckSnapshot {
|
|
|
43
50
|
scripts: slideMtimes(deckDir, ".ts"),
|
|
44
51
|
javascript: slideMtimes(deckDir, ".js"),
|
|
45
52
|
voice: voiceMtimes(paths.voice),
|
|
53
|
+
plugins: pluginMtimes(deckDir),
|
|
46
54
|
};
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
function pluginMtimes(deckDir: string): Mtimes {
|
|
58
|
+
const root = deckProjectRoot(deckDir);
|
|
59
|
+
const times: Mtimes = {};
|
|
60
|
+
const note = (path: string) => {
|
|
61
|
+
const time = mtime(path);
|
|
62
|
+
if (time !== 0) {
|
|
63
|
+
times[relative(root, path).split(sep).join("/")] = time;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
// A dek.toml mid-edit names no plugin; the next scan sees them again.
|
|
67
|
+
for (const { name, manifest } of pinnedPlugins(root)) {
|
|
68
|
+
note(join(root, "plugins", name, "plugin.json"));
|
|
69
|
+
for (const { path } of manifest ? deckData(manifest) : []) {
|
|
70
|
+
note(join(deckDir, path));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return times;
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
/**
|
|
50
77
|
* What changed from `prev` to `next`, as the events a page hears and the work that follows.
|
|
51
78
|
* Any change counts, an older mtime too: a file restored from a copy is still an edit.
|
|
@@ -88,6 +115,10 @@ export function diffSnapshot(prev: DeckSnapshot, next: DeckSnapshot): DeckChange
|
|
|
88
115
|
}
|
|
89
116
|
}
|
|
90
117
|
}
|
|
118
|
+
// A plugin's page reads its data once, as the page loads, so a change reloads the page.
|
|
119
|
+
if (!sync && changedKeys(prev.plugins, next.plugins).length > 0) {
|
|
120
|
+
events.push({ type: "reload-script", slugs: [] });
|
|
121
|
+
}
|
|
91
122
|
const synth = sync || changedKeys(prev.voice, next.voice).length > 0;
|
|
92
123
|
return { sync, events, ...(diagnose ? { diagnose } : {}), synth };
|
|
93
124
|
}
|
package/src/server/dev.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { DekError } from "../core/error.ts";
|
|
|
3
3
|
import type { PlaywrightRunner } from "../core/playwright.ts";
|
|
4
4
|
import { isRefName, refReadOnlyError, requireProject } from "../core/ref.ts";
|
|
5
5
|
import { locateDeck, type Project } from "../core/resolve.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
annotateScript,
|
|
8
|
+
livePlayerScript,
|
|
9
|
+
liveReloadScript,
|
|
10
|
+
pluginPageScripts,
|
|
11
|
+
} from "../runtime/player.ts";
|
|
7
12
|
import {
|
|
8
13
|
clearance,
|
|
9
14
|
keepToOwnOrigin,
|
|
@@ -84,6 +89,7 @@ export async function startDevServer(options: DevServerOptions): Promise<DevServ
|
|
|
84
89
|
playerScript: await livePlayerScript(),
|
|
85
90
|
liveReloadScript: liveReloadScript(),
|
|
86
91
|
annotateScript: await annotateScript(),
|
|
92
|
+
pluginScripts: await pluginPageScripts(),
|
|
87
93
|
};
|
|
88
94
|
|
|
89
95
|
const pages = createDevPages({
|
package/src/server/pages.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderDeckHtml } from "../core/document.ts";
|
|
2
|
+
import { pluginPages } from "../core/plugin-host.ts";
|
|
2
3
|
import type { Project } from "../core/resolve.ts";
|
|
3
4
|
import { errorPage, htmlResponse, renderIndexHtml } from "./routes.ts";
|
|
4
5
|
|
|
@@ -13,7 +14,13 @@ export type DevPages = {
|
|
|
13
14
|
|
|
14
15
|
/** The pages a dev server renders, each kept until `clear`. */
|
|
15
16
|
export function createDevPages(options: {
|
|
16
|
-
embed: {
|
|
17
|
+
embed: {
|
|
18
|
+
playerScript: string;
|
|
19
|
+
liveReloadScript: string;
|
|
20
|
+
annotateScript: string;
|
|
21
|
+
/** The pages of the plugins dek ships, compiled, by name. */
|
|
22
|
+
pluginScripts?: Record<string, string>;
|
|
23
|
+
};
|
|
17
24
|
remote: boolean;
|
|
18
25
|
/** Embedded on the presenter page, so its stream and moves are let through. */
|
|
19
26
|
password: string | undefined;
|
|
@@ -39,6 +46,7 @@ export function createDevPages(options: {
|
|
|
39
46
|
cached(`${dir}:${mode}`, () =>
|
|
40
47
|
renderDeckHtml(dir, {
|
|
41
48
|
playerScript: embed.playerScript,
|
|
49
|
+
plugins: pluginPages(options.project().root, dir, embed.pluginScripts ?? {}),
|
|
42
50
|
target: {
|
|
43
51
|
kind: "dev",
|
|
44
52
|
mode,
|