@hajimism/dek 0.5.1 → 0.6.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 +18 -0
- package/package.json +10 -3
- package/src/cli/build.ts +3 -1
- 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/page.ts +1 -0
- package/src/core/plugin-host.ts +669 -0
- package/src/core/plugin.ts +436 -0
- package/src/core/sarif.ts +47 -5
- package/src/core/sync.ts +3 -1
- 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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Release notes for `@hajimism/dek`. From 0.1.0 on, release-please writes this file from the Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [0.6.0](https://github.com/hajimism/dek/compare/v0.5.1...v0.6.0) (2026-11-07)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* a skipped check can now be "plugin" in --json (ADR 31).
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* let a plugin the user installed check a deck in dekc lint ([847822c](https://github.com/hajimism/dek/commit/847822cdfb34f4931c284b103007cb10d0dfc450))
|
|
15
|
+
* **plugins:** lay dek-study's sheet over the slides ([8886efc](https://github.com/hajimism/dek/commit/8886efc9b8a0b9bd4bc47290574654e79be32368))
|
|
16
|
+
* **plugins:** let a plugin keep data for the whole project in plugins/<name>/ ([84add8a](https://github.com/hajimism/dek/commit/84add8ab77918fdbfd60bfced6c18a103b1de0b1))
|
|
17
|
+
* **plugins:** ship dek-study inside dek as its first official plugin ([96595d5](https://github.com/hajimism/dek/commit/96595d5f3996c597a1c47564a736ef0ca5ae9413))
|
|
18
|
+
* **plugins:** ship dek-study inside dek, with a sheet of stickers over the slides ([855908c](https://github.com/hajimism/dek/commit/855908c03305aa8e8284c890bea0e19e25986994))
|
|
19
|
+
* **plugins:** ship dek-words, a check for a project's word list ([9eff6d1](https://github.com/hajimism/dek/commit/9eff6d13cb2727b0f0cf1aa8bdc9595ee4d5c1c1))
|
|
20
|
+
* **plugins:** ship dek-words, a check for a project's word list ([07d8f54](https://github.com/hajimism/dek/commit/07d8f54908ef8cd1e52cbf9cec5e576f929e55fb))
|
|
21
|
+
* **plugins:** show a plugin's page in the player and builds behind page API 1 ([b815ef6](https://github.com/hajimism/dek/commit/b815ef625444c572a13af1a6e0ab0dddfbffcb22))
|
|
22
|
+
|
|
5
23
|
## [0.5.1](https://github.com/hajimism/dek/compare/v0.5.0...v0.5.1) (2026-10-30)
|
|
6
24
|
|
|
7
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hajimism/dek",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A build system for talks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hajime Nakagawa (https://github.com/hajimism)",
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
"bin": {
|
|
9
9
|
"dekc": "src/cli.ts"
|
|
10
10
|
},
|
|
11
|
+
"exports": {
|
|
12
|
+
"./plugin": {
|
|
13
|
+
"types": "./src/plugin-api.ts",
|
|
14
|
+
"default": "./src/plugin-api.ts"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
11
18
|
"files": [
|
|
12
19
|
"src",
|
|
13
20
|
"skills",
|
|
@@ -61,12 +68,12 @@
|
|
|
61
68
|
"zod": "^4.6.5"
|
|
62
69
|
},
|
|
63
70
|
"devDependencies": {
|
|
64
|
-
"@biomejs/biome": "2.6.
|
|
71
|
+
"@biomejs/biome": "2.6.4",
|
|
65
72
|
"@happy-dom/global-registrator": "21.0.4",
|
|
66
73
|
"@types/bun": "^1.4.2",
|
|
67
74
|
"fast-check": "4.10.2",
|
|
68
75
|
"happy-dom": "21.0.4",
|
|
69
|
-
"knip": "6.
|
|
76
|
+
"knip": "6.43.0",
|
|
70
77
|
"playwright": "^1.63.0",
|
|
71
78
|
"publint": "0.3.25",
|
|
72
79
|
"typescript": "^7.0.2",
|
package/src/cli/build.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { DekError } from "../core/error.ts";
|
|
|
4
4
|
import { lintDeck, lintProject } from "../core/lint.ts";
|
|
5
5
|
import { parsePublicUrl } from "../core/ogp.ts";
|
|
6
6
|
import { PLAYWRIGHT_INSTALL, type PlaywrightRunner } from "../core/playwright.ts";
|
|
7
|
-
import { playerScript } from "../runtime/player.ts";
|
|
7
|
+
import { playerScript, pluginPageScripts } from "../runtime/player.ts";
|
|
8
8
|
import { RENDERED_RULES, type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
|
|
9
9
|
import type { DecksTarget } from "./scope.ts";
|
|
10
10
|
|
|
@@ -47,12 +47,14 @@ export async function buildCommand(
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
const script = await playerScript();
|
|
50
|
+
const pluginScripts = await pluginPageScripts();
|
|
50
51
|
const results = await Promise.all(
|
|
51
52
|
decks.map((entry) =>
|
|
52
53
|
buildDeck(
|
|
53
54
|
{ project, deck: entry },
|
|
54
55
|
{
|
|
55
56
|
playerScript: script,
|
|
57
|
+
pluginScripts,
|
|
56
58
|
rootDist: options.rootDist,
|
|
57
59
|
...(url ? { url } : {}),
|
|
58
60
|
...(options.public ? { public: true } : {}),
|
package/src/cli/commands.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasErrors } from "../core/diagnostic.ts";
|
|
1
|
+
import { hasErrors, RULES } from "../core/diagnostic.ts";
|
|
2
2
|
import { shellQuote } from "../core/escape.ts";
|
|
3
3
|
import type { HiddenText } from "../core/foreign-text.ts";
|
|
4
4
|
import { isRefName } from "../core/ref-name.ts";
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
formatNew,
|
|
28
28
|
formatPack,
|
|
29
29
|
formatPdf,
|
|
30
|
+
formatPluginNotes,
|
|
30
31
|
formatPptx,
|
|
31
32
|
formatRef,
|
|
32
33
|
formatReport,
|
|
@@ -658,23 +659,31 @@ export const COMMANDS = {
|
|
|
658
659
|
sync: result({
|
|
659
660
|
scope: "decks",
|
|
660
661
|
args: [],
|
|
661
|
-
flags: [],
|
|
662
|
-
usage: ["dekc sync [deck]"],
|
|
662
|
+
flags: ["accept"],
|
|
663
|
+
usage: ["dekc sync [deck] [--accept <plugin>]"],
|
|
663
664
|
summary:
|
|
664
665
|
"Create a skeleton for each section without slide HTML, refresh the skeletons\nnobody has edited since, and remove the ones whose section is gone.",
|
|
665
666
|
group: "Slide",
|
|
666
|
-
overview: [
|
|
667
|
+
overview: [
|
|
668
|
+
["dekc sync", "create missing skeletons; refresh or drop the ones nobody edited"],
|
|
669
|
+
["dekc sync --accept <plugin>", "pin a plugin whose manifest changed since its lock"],
|
|
670
|
+
],
|
|
667
671
|
agent: [
|
|
668
672
|
"dekc sync create missing skeletons; refresh or drop untouched ones; never edits your slides",
|
|
673
|
+
"dekc sync --accept <plugin> pin a plugin's changed manifest, once the user has read the change",
|
|
669
674
|
],
|
|
670
|
-
run: async ({ target }) => {
|
|
675
|
+
run: async ({ target, flags }) => {
|
|
671
676
|
const { syncCommand } = await import("./sync.ts");
|
|
672
|
-
return syncCommand(target);
|
|
677
|
+
return syncCommand(target, { accept: flags.accept });
|
|
673
678
|
},
|
|
674
679
|
output: {
|
|
675
680
|
text: (data) => formatCreated(data.created, data.updated, data.removed),
|
|
676
|
-
notes: (data, color) =>
|
|
681
|
+
notes: (data, color) =>
|
|
682
|
+
[formatKept(data.kept, color), formatPluginNotes(data.plugins, color)]
|
|
683
|
+
.filter(Boolean)
|
|
684
|
+
.join("\n") || undefined,
|
|
677
685
|
paths: (data, display) => ({
|
|
686
|
+
...data,
|
|
678
687
|
created: data.created.map(display),
|
|
679
688
|
updated: data.updated.map(display),
|
|
680
689
|
removed: data.removed.map(display),
|
|
@@ -703,14 +712,19 @@ export const COMMANDS = {
|
|
|
703
712
|
output: {
|
|
704
713
|
text: (data, color) => formatReport(data.diagnostics, { color }),
|
|
705
714
|
notes: (data, color) => formatSkipped(data.skipped, color) || undefined,
|
|
706
|
-
json: ({ rumdlSarif: _rumdlSarif, ...data }) => data,
|
|
715
|
+
json: ({ rumdlSarif: _rumdlSarif, pluginSarif: _pluginSarif, ...data }) => data,
|
|
707
716
|
paths: withDisplayDiagnostics,
|
|
708
717
|
// The rerun measures what this run did: without --visual, the errors it found may not show.
|
|
709
718
|
failure: (data, flags) =>
|
|
710
719
|
diagnosticFailure("lint", data, flags.visual ? "dekc lint --visual" : "dekc lint"),
|
|
720
|
+
// dek's run holds its own rules; rumdl and each plugin have runs of their own.
|
|
711
721
|
sarif: (data) => {
|
|
712
|
-
const dek = data.diagnostics.filter((diagnostic) => diagnostic.id
|
|
713
|
-
return mergeSarif(
|
|
722
|
+
const dek = data.diagnostics.filter((diagnostic) => Object.hasOwn(RULES, diagnostic.id));
|
|
723
|
+
return mergeSarif(
|
|
724
|
+
toSarif(dek, { skipped: data.skipped ?? [] }),
|
|
725
|
+
data.rumdlSarif,
|
|
726
|
+
data.pluginSarif,
|
|
727
|
+
);
|
|
714
728
|
},
|
|
715
729
|
},
|
|
716
730
|
}),
|
package/src/cli/contract.ts
CHANGED
|
@@ -41,7 +41,8 @@ const Diagnostic = z
|
|
|
41
41
|
|
|
42
42
|
const SkippedCheck = z
|
|
43
43
|
.strictObject({
|
|
44
|
-
check: z.enum(["lint", "preview", "rumdl", "visual", "voice"]),
|
|
44
|
+
check: z.enum(["lint", "plugin", "preview", "rumdl", "visual", "voice"]),
|
|
45
|
+
plugin: z.string().optional().describe("For a plugin's check, its name in dek.toml [plugins]."),
|
|
45
46
|
reason: z.string(),
|
|
46
47
|
hint: z.string().optional().describe("What to do so that the check runs."),
|
|
47
48
|
})
|
|
@@ -421,7 +422,32 @@ export const RESULT_FIELDS = {
|
|
|
421
422
|
}),
|
|
422
423
|
z.strictObject({ action: z.literal("clear"), cleared: z.number().int() }),
|
|
423
424
|
]),
|
|
424
|
-
sync: z.strictObject({
|
|
425
|
+
sync: z.strictObject({
|
|
426
|
+
created: paths,
|
|
427
|
+
updated: paths,
|
|
428
|
+
removed: paths,
|
|
429
|
+
kept: paths,
|
|
430
|
+
plugins: z
|
|
431
|
+
.array(
|
|
432
|
+
z.strictObject({
|
|
433
|
+
name: z.string().describe("The plugin's name in dek.toml [plugins]."),
|
|
434
|
+
status: z
|
|
435
|
+
.enum(["accepted", "current", "changed", "missing", "invalid", "unsupported"])
|
|
436
|
+
.describe(
|
|
437
|
+
"accepted: pinned now; current: as pinned; changed: differs from its lock, which was left as it was; missing: not installed; invalid: printed no manifest dek can use; unsupported: needs a newer dek.",
|
|
438
|
+
),
|
|
439
|
+
version: z.string().optional().describe("The installed plugin's version."),
|
|
440
|
+
changes: z
|
|
441
|
+
.array(z.string())
|
|
442
|
+
.optional()
|
|
443
|
+
.describe("For changed: what differs from the lock, one change each."),
|
|
444
|
+
reason: z.string().optional(),
|
|
445
|
+
hint: z.string().optional(),
|
|
446
|
+
}),
|
|
447
|
+
)
|
|
448
|
+
.optional()
|
|
449
|
+
.describe("Each plugin dek.toml names, in name order; absent when it names none."),
|
|
450
|
+
}),
|
|
425
451
|
lint: z.strictObject({ diagnostics, skipped, unsandboxed }),
|
|
426
452
|
cues: z.strictObject({
|
|
427
453
|
name: z.string(),
|
package/src/cli/flags.ts
CHANGED
|
@@ -52,6 +52,11 @@ export const FLAGS = {
|
|
|
52
52
|
note: "for JSON, pass --json",
|
|
53
53
|
text: "print diagnostics as SARIF",
|
|
54
54
|
},
|
|
55
|
+
accept: {
|
|
56
|
+
type: "string",
|
|
57
|
+
value: "PLUGIN",
|
|
58
|
+
text: "pin the installed plugin's manifest though it changed from plugins/<name>/plugin.json",
|
|
59
|
+
},
|
|
55
60
|
before: { type: "string", value: "SLUG", text: "move the section before this one" },
|
|
56
61
|
after: { type: "string", value: "SLUG", text: "move the section after this one" },
|
|
57
62
|
step: { type: "string", value: "ID|N", text: "the beat to capture (default: the last)" },
|
package/src/cli/lint.ts
CHANGED
|
@@ -4,8 +4,9 @@ import type { Diagnostic } from "../core/diagnostic.ts";
|
|
|
4
4
|
import { shellQuote } from "../core/escape.ts";
|
|
5
5
|
import { lintDeck, lintProject, unreadableScriptDiagnostics } from "../core/lint.ts";
|
|
6
6
|
import { playwrightMissingError } from "../core/playwright.ts";
|
|
7
|
+
import { checkDeck, onProjectData, pluginChecks } from "../core/plugin-host.ts";
|
|
7
8
|
import { type RumdlRunner, type RumdlSkip, resolveRumdlBin, runRumdl } from "../core/rumdl.ts";
|
|
8
|
-
import { mergeSarif, type SarifLog } from "../core/sarif.ts";
|
|
9
|
+
import { mergeSarif, pluginSarif, type SarifLog } from "../core/sarif.ts";
|
|
9
10
|
import { syncDeck } from "../core/sync.ts";
|
|
10
11
|
import { lintVisualDeck } from "../core/visual.ts";
|
|
11
12
|
import { RENDERED_RULES, type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
|
|
@@ -16,6 +17,8 @@ export type LintCliResult = {
|
|
|
16
17
|
/** Checks that did not run, each with why and how to run it, so an empty list is not a pass. */
|
|
17
18
|
skipped?: SkippedCheck[];
|
|
18
19
|
rumdlSarif?: SarifLog;
|
|
20
|
+
/** A run per plugin that ran, for `--format sarif`. */
|
|
21
|
+
pluginSarif?: SarifLog;
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
/** `cwd` is where a skipped rumdl's hint says to run it from; `rumdl` runs it, dek's by default. */
|
|
@@ -32,6 +35,16 @@ export async function lintCommand(
|
|
|
32
35
|
let rumdlSarif: SarifLog | undefined;
|
|
33
36
|
// A rumdl that hung on one deck would hang on each, half a minute apiece.
|
|
34
37
|
let rumdlHung = false;
|
|
38
|
+
// The plugins as the project accepted them; one that hung is run on no deck after, as rumdl.
|
|
39
|
+
const plugins = await pluginChecks(project);
|
|
40
|
+
diagnostics.push(...plugins.diagnostics);
|
|
41
|
+
const pluginRuns = plugins.ready.map((plugin) => ({
|
|
42
|
+
...plugin,
|
|
43
|
+
diagnostics: [] as Diagnostic[],
|
|
44
|
+
hungOn: undefined as string | undefined,
|
|
45
|
+
skippedDecks: 0,
|
|
46
|
+
seen: new Set<string>(),
|
|
47
|
+
}));
|
|
35
48
|
|
|
36
49
|
for (const deck of decks) {
|
|
37
50
|
if (options.fix) {
|
|
@@ -50,6 +63,31 @@ export async function lintCommand(
|
|
|
50
63
|
rumdlSarif = rumdlSarif ? mergeSarif(rumdlSarif, markdown.sarif) : markdown.sarif;
|
|
51
64
|
}
|
|
52
65
|
}
|
|
66
|
+
for (const plugin of pluginRuns) {
|
|
67
|
+
if (plugin.hungOn !== undefined) {
|
|
68
|
+
plugin.skippedDecks += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const outcome = await checkDeck(plugin, project, deck);
|
|
72
|
+
// A finding on the project's data comes back from each deck; it is reported once (ADR 35).
|
|
73
|
+
const fresh = outcome.diagnostics.filter((finding) => {
|
|
74
|
+
if (!onProjectData(finding, project.root, plugin.name)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const { id, path, line, column, message } = finding;
|
|
78
|
+
const key = JSON.stringify([id, path, line, column, message]);
|
|
79
|
+
if (plugin.seen.has(key)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
plugin.seen.add(key);
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
diagnostics.push(...fresh);
|
|
86
|
+
plugin.diagnostics.push(...fresh.filter(({ id }) => id !== "DEK046"));
|
|
87
|
+
if (outcome.hung) {
|
|
88
|
+
plugin.hungOn = deck.name;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
53
91
|
if (options.visual) {
|
|
54
92
|
const visual = await lintVisualDeck({ project, deck });
|
|
55
93
|
if (visual === null) {
|
|
@@ -63,6 +101,19 @@ export async function lintCommand(
|
|
|
63
101
|
diagnostics,
|
|
64
102
|
...skippedChecks([
|
|
65
103
|
...(rumdlSkip ? [rumdlSkip] : []),
|
|
104
|
+
...plugins.skipped,
|
|
105
|
+
...pluginRuns.flatMap((plugin): SkippedCheck[] =>
|
|
106
|
+
plugin.skippedDecks > 0
|
|
107
|
+
? [
|
|
108
|
+
{
|
|
109
|
+
check: "plugin",
|
|
110
|
+
plugin: plugin.name,
|
|
111
|
+
reason: `dek-${plugin.name} hung on ${plugin.hungOn}, so it ran on no deck after`,
|
|
112
|
+
hint: `tell the user: dek-${plugin.name} hangs, and its author can fix it`,
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
: [],
|
|
116
|
+
),
|
|
66
117
|
...(options.visual
|
|
67
118
|
? []
|
|
68
119
|
: [
|
|
@@ -72,6 +123,7 @@ export async function lintCommand(
|
|
|
72
123
|
]),
|
|
73
124
|
]),
|
|
74
125
|
...(rumdlSarif ? { rumdlSarif } : {}),
|
|
126
|
+
...(pluginRuns.length > 0 ? { pluginSarif: pluginSarif(pluginRuns) } : {}),
|
|
75
127
|
};
|
|
76
128
|
}
|
|
77
129
|
|
package/src/cli/sync.ts
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { type PluginSync, syncPlugins } from "../core/plugin-host.ts";
|
|
2
3
|
import { syncDeck } from "../core/sync.ts";
|
|
3
4
|
import { ignoreLocalState } from "./files.ts";
|
|
4
5
|
import type { DecksTarget } from "./scope.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Every file the sync wrote or removed: the decks' slides, then dek's own
|
|
8
|
-
* when it lacked what dek keeps for itself;
|
|
9
|
-
* which lint reports as DEK002.
|
|
8
|
+
* Every file the sync wrote or removed: the plugins' locks, the decks' slides, then dek's own
|
|
9
|
+
* files, then .gitignore when it lacked what dek keeps for itself; the slides it kept though their
|
|
10
|
+
* section is gone, which lint reports as DEK002; and what it did with each plugin dek.toml names.
|
|
10
11
|
*/
|
|
11
12
|
export type SyncCliResult = {
|
|
12
13
|
created: string[];
|
|
13
14
|
updated: string[];
|
|
14
15
|
removed: string[];
|
|
15
16
|
kept: string[];
|
|
17
|
+
plugins?: PluginSync[];
|
|
16
18
|
};
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* `accept` names a plugin whose changed manifest to pin anyway. The plugins go first, so the
|
|
22
|
+
* AGENTS.md the decks' sync writes lists them as they now stand.
|
|
23
|
+
*/
|
|
24
|
+
export async function syncCommand(
|
|
25
|
+
{ project, decks }: DecksTarget,
|
|
26
|
+
options: { accept?: string } = {},
|
|
27
|
+
): Promise<SyncCliResult> {
|
|
28
|
+
const plugins = await syncPlugins(project, options);
|
|
29
|
+
const result: SyncCliResult = {
|
|
30
|
+
created: [...plugins.created],
|
|
31
|
+
updated: [...plugins.updated],
|
|
32
|
+
removed: [],
|
|
33
|
+
kept: [],
|
|
34
|
+
};
|
|
20
35
|
const dek = { created: [] as string[], updated: [] as string[] };
|
|
21
36
|
for (const deck of decks) {
|
|
22
37
|
const synced = syncDeck({ project, deck });
|
|
@@ -33,5 +48,5 @@ export function syncCommand({ project, decks }: DecksTarget): SyncCliResult {
|
|
|
33
48
|
if (ignoreLocalState(project.root)) {
|
|
34
49
|
result.updated.push(join(project.root, ".gitignore"));
|
|
35
50
|
}
|
|
36
|
-
return result;
|
|
51
|
+
return plugins.plugins.length > 0 ? { ...result, plugins: plugins.plugins } : result;
|
|
37
52
|
}
|
package/src/cli/text.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// How each command's result reads as text; commands.ts says which command prints which.
|
|
2
2
|
// The pieces the dev server's log shares, such as diagnostics and tables, are in format.ts.
|
|
3
|
-
import type
|
|
3
|
+
import { type Diagnostic, type SkippedCheck, skippedLabel } from "../core/diagnostic.ts";
|
|
4
4
|
import { shellQuote } from "../core/escape.ts";
|
|
5
5
|
import { foreignLine, type HiddenText, TITLE_MAX } from "../core/foreign-text.ts";
|
|
6
6
|
import { compareCodeUnits } from "../core/order.ts";
|
|
7
|
+
import type { PluginSync } from "../core/plugin-host.ts";
|
|
7
8
|
import { unprinted } from "../core/prose.ts";
|
|
8
9
|
import { packFile, parseRefSource } from "../core/ref-name.ts";
|
|
9
10
|
import { formatClock } from "../core/timing.ts";
|
|
@@ -65,7 +66,7 @@ export function formatSkipped(skipped: SkippedCheck[] | undefined, color = false
|
|
|
65
66
|
const c = ansi(color);
|
|
66
67
|
return (skipped ?? [])
|
|
67
68
|
.flatMap((entry) => [
|
|
68
|
-
`${entry
|
|
69
|
+
`${skippedLabel(entry)}: skipped (${entry.reason})`,
|
|
69
70
|
...(entry.hint ? [` ${c.yellow("help:")} ${entry.hint}`] : []),
|
|
70
71
|
])
|
|
71
72
|
.join("\n");
|
|
@@ -118,6 +119,23 @@ export function formatKept(kept: string[], color = false): string {
|
|
|
118
119
|
.join("\n");
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The plugins sync could not pin as they are, for stderr: one whose manifest changed, with what
|
|
124
|
+
* changed, and one dek could not find or read, with why; a plugin pinned or current says nothing.
|
|
125
|
+
*/
|
|
126
|
+
export function formatPluginNotes(plugins: PluginSync[] | undefined, color = false): string {
|
|
127
|
+
const c = ansi(color);
|
|
128
|
+
return (plugins ?? [])
|
|
129
|
+
.filter(({ status }) => status !== "accepted" && status !== "current")
|
|
130
|
+
.flatMap((plugin) => [
|
|
131
|
+
plugin.status === "changed"
|
|
132
|
+
? `plugin ${plugin.name} changed: ${(plugin.changes ?? [plugin.reason ?? ""]).join("; ")}`
|
|
133
|
+
: `plugin ${plugin.name}: ${plugin.reason ?? plugin.status}`,
|
|
134
|
+
...(plugin.hint ? [` ${c.yellow("help:")} ${plugin.hint}`] : []),
|
|
135
|
+
])
|
|
136
|
+
.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
/**
|
|
122
140
|
* A theme.css init kept that is no dekc theme, for stderr: every deck copies it, so each would fail
|
|
123
141
|
* lint on its first run with a DEK015 per token.
|
package/src/core/build.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { loadConfig } from "./config.ts";
|
|
|
4
4
|
import { renderDeckDocument } from "./document.ts";
|
|
5
5
|
import { type DistOptions, distFile } from "./path.ts";
|
|
6
6
|
import type { PlaywrightRunner } from "./playwright.ts";
|
|
7
|
+
import { pluginPages } from "./plugin-host.ts";
|
|
7
8
|
import { asResolvedDeck, type ResolvedDeck } from "./resolve.ts";
|
|
8
9
|
import { removeInside, writeInside } from "./safe-fs.ts";
|
|
9
10
|
import { coverShot } from "./shot/still.ts";
|
|
@@ -22,6 +23,8 @@ export type BuildResult = { outPath: string } & (
|
|
|
22
23
|
|
|
23
24
|
export type BuildOptions = DistOptions & {
|
|
24
25
|
playerScript: string;
|
|
26
|
+
/** The pages of the plugins dek ships, compiled, by name; a pinned one's goes into the page. */
|
|
27
|
+
pluginScripts?: Record<string, string>;
|
|
25
28
|
/** Where dist/ is served from, ending in a slash; overrides `url` in dek.toml. */
|
|
26
29
|
url?: string;
|
|
27
30
|
/** For anyone with the link: the presenter view carries only what is said aloud. */
|
|
@@ -50,6 +53,7 @@ export async function buildDeck(
|
|
|
50
53
|
}
|
|
51
54
|
const html = renderDeckDocument(deck, {
|
|
52
55
|
playerScript: options.playerScript,
|
|
56
|
+
plugins: pluginPages(project.root, deck.dir, options.pluginScripts ?? {}),
|
|
53
57
|
target: {
|
|
54
58
|
kind: "build",
|
|
55
59
|
...(options.public ? { public: true } : {}),
|
package/src/core/config-keys.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DekToml, Pack, PluginTable, Voice } from "./config.ts";
|
|
2
3
|
import { splitLines } from "./lines.ts";
|
|
3
4
|
import { readFrontmatterYaml } from "./parse.ts";
|
|
4
5
|
import { Frontmatter } from "./schema.ts";
|
|
@@ -9,10 +10,14 @@ import { unrecognizedKeys } from "./zod.ts";
|
|
|
9
10
|
/** A key dek does not read, where it is, and the known key it most likely meant. */
|
|
10
11
|
export type UnknownKey = { key: string; line: number; suggestion?: string };
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* dek.toml's schema, but a key it does not read is an issue; `refs` and `plugins` still take any
|
|
15
|
+
* name, and a plugin's table, none of its own.
|
|
16
|
+
*/
|
|
13
17
|
const StrictDekToml = DekToml.extend({
|
|
14
18
|
voice: Voice.strict().optional(),
|
|
15
19
|
pack: Pack.strict().optional(),
|
|
20
|
+
plugins: z.record(z.string(), PluginTable.strict()).optional(),
|
|
16
21
|
}).strict();
|
|
17
22
|
|
|
18
23
|
/** Every key dek.toml may hold, as dotted paths. */
|
package/src/core/config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { DekError } from "./error.ts";
|
|
|
4
4
|
import { shellQuote } from "./escape.ts";
|
|
5
5
|
import { isPackLicense, PACK_LICENSES, type PackLicense } from "./licenses.ts";
|
|
6
6
|
import { parsePublicUrl } from "./ogp.ts";
|
|
7
|
+
import { pluginNameProblem } from "./plugin.ts";
|
|
7
8
|
import {
|
|
8
9
|
isPinFor,
|
|
9
10
|
isPlainRefName,
|
|
@@ -31,6 +32,12 @@ export const Pack = z.object({
|
|
|
31
32
|
.optional(),
|
|
32
33
|
});
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A plugin's table in `[plugins]`. It takes no key yet: a project chooses which of the plugins the
|
|
37
|
+
* user installed it uses, and never names a path, an argument, or a program for dek to run.
|
|
38
|
+
*/
|
|
39
|
+
export const PluginTable = z.object({});
|
|
40
|
+
|
|
34
41
|
export const DekToml = z.object({
|
|
35
42
|
url: z
|
|
36
43
|
.string()
|
|
@@ -71,6 +78,18 @@ export const DekToml = z.object({
|
|
|
71
78
|
})
|
|
72
79
|
.transform((refs) => refs as Record<string, string>)
|
|
73
80
|
.optional(),
|
|
81
|
+
// A name that is no plugin's is refused without being repeated, as a ref's is.
|
|
82
|
+
plugins: z
|
|
83
|
+
.record(z.string(), PluginTable)
|
|
84
|
+
.superRefine((plugins, ctx) => {
|
|
85
|
+
for (const name of Object.keys(plugins)) {
|
|
86
|
+
const problem = pluginNameProblem(name);
|
|
87
|
+
if (problem) {
|
|
88
|
+
ctx.addIssue({ code: "custom", path: [], message: problem });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.optional(),
|
|
74
93
|
});
|
|
75
94
|
|
|
76
95
|
const REFUSED_REF_KEY = "a key in [refs] names no ref";
|
|
@@ -107,6 +126,37 @@ function refusedRefKey(source: string): { line?: number } | undefined {
|
|
|
107
126
|
return entry === undefined ? {} : { line: entry.line };
|
|
108
127
|
}
|
|
109
128
|
|
|
129
|
+
/**
|
|
130
|
+
* The first name in [plugins] that is no plugin's, with what is wrong and its line. Like a ref's,
|
|
131
|
+
* the name may be someone else's text, so the error points at it and never repeats it.
|
|
132
|
+
*/
|
|
133
|
+
function refusedPluginKey(source: string): { problem: string; line?: number } | undefined {
|
|
134
|
+
let parsed: unknown;
|
|
135
|
+
try {
|
|
136
|
+
parsed = Bun.TOML.parse(source);
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const plugins = (parsed as { plugins?: unknown } | null)?.plugins;
|
|
141
|
+
if (plugins === null || typeof plugins !== "object") {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
for (const name of Object.keys(plugins)) {
|
|
145
|
+
const problem = pluginNameProblem(name);
|
|
146
|
+
if (problem === undefined) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const written = scanTomlKeys(source).filter(
|
|
150
|
+
(entry) => entry.path[0] === "plugins" && entry.path.length === 2,
|
|
151
|
+
);
|
|
152
|
+
const entry =
|
|
153
|
+
written.find((key) => key.path[1] === name) ??
|
|
154
|
+
written.find((key) => pluginNameProblem(key.path[1] ?? "") !== undefined);
|
|
155
|
+
return entry === undefined ? { problem } : { problem, line: entry.line };
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
110
160
|
type VoiceDefaults = {
|
|
111
161
|
engine: string;
|
|
112
162
|
speaker: string;
|
|
@@ -126,6 +176,8 @@ export type DekConfig = {
|
|
|
126
176
|
pack?: { license?: PackLicense };
|
|
127
177
|
/** `[refs]`: each ref name and what it is pinned to: a commit, or a pack's digest. */
|
|
128
178
|
refs?: Record<string, string>;
|
|
179
|
+
/** `[plugins]`: the name of each plugin the project uses, sorted, as dek runs them. */
|
|
180
|
+
plugins?: string[];
|
|
129
181
|
};
|
|
130
182
|
|
|
131
183
|
export function parseDekToml(source: string, path?: string): DekConfig {
|
|
@@ -133,6 +185,14 @@ export function parseDekToml(source: string, path?: string): DekConfig {
|
|
|
133
185
|
if (refused) {
|
|
134
186
|
throw new DekError(REFUSED_REF_KEY, { path, ...refused, hint: REFUSED_REF_HINT });
|
|
135
187
|
}
|
|
188
|
+
const plugin = refusedPluginKey(source);
|
|
189
|
+
if (plugin) {
|
|
190
|
+
throw new DekError(`a name in [plugins] is no plugin's: ${plugin.problem}`, {
|
|
191
|
+
path,
|
|
192
|
+
...(plugin.line === undefined ? {} : { line: plugin.line }),
|
|
193
|
+
hint: "rename the table to the <name> of the dek-<name> you installed, or delete it",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
136
196
|
const data = parseTomlWith(DekToml, source, {
|
|
137
197
|
label: "dek.toml",
|
|
138
198
|
anchor: "dek-toml",
|
|
@@ -155,6 +215,9 @@ export function parseDekToml(source: string, path?: string): DekConfig {
|
|
|
155
215
|
? { pack: data.pack.license ? { license: data.pack.license as PackLicense } : {} }
|
|
156
216
|
: {}),
|
|
157
217
|
...(data.refs && Object.keys(data.refs).length > 0 ? { refs: data.refs } : {}),
|
|
218
|
+
...(data.plugins && Object.keys(data.plugins).length > 0
|
|
219
|
+
? { plugins: Object.keys(data.plugins).sort() }
|
|
220
|
+
: {}),
|
|
158
221
|
};
|
|
159
222
|
}
|
|
160
223
|
|
package/src/core/diagnostic.ts
CHANGED
|
@@ -64,6 +64,7 @@ export const RULES = {
|
|
|
64
64
|
DEK043: { severity: "warning", scopes: ["deck"] },
|
|
65
65
|
DEK044: { severity: "warning", scopes: ["deck", "slide"] },
|
|
66
66
|
DEK045: { severity: "warning", scopes: ["deck"] },
|
|
67
|
+
DEK046: { severity: "error", scopes: ["deck"] },
|
|
67
68
|
} as const satisfies Record<string, { severity: Severity; scopes: readonly Scope[] }>;
|
|
68
69
|
|
|
69
70
|
export type RuleId = keyof typeof RULES;
|
|
@@ -113,7 +114,14 @@ export function hasErrors(diagnostics: Diagnostic[]): boolean {
|
|
|
113
114
|
*/
|
|
114
115
|
export type SkippedCheck = {
|
|
115
116
|
/** `preview` is a build's link preview image, which needs a URL and Playwright. */
|
|
116
|
-
check: "lint" | "preview" | "rumdl" | "visual" | "voice";
|
|
117
|
+
check: "lint" | "plugin" | "preview" | "rumdl" | "visual" | "voice";
|
|
118
|
+
/** The plugin, for a `plugin` check: its name in dek.toml `[plugins]`. */
|
|
119
|
+
plugin?: string;
|
|
117
120
|
reason: string;
|
|
118
121
|
hint?: string;
|
|
119
122
|
};
|
|
123
|
+
|
|
124
|
+
/** A skipped check as text names it: the check, or the plugin for a plugin's. */
|
|
125
|
+
export function skippedLabel(entry: SkippedCheck): string {
|
|
126
|
+
return entry.plugin === undefined ? entry.check : `plugin ${entry.plugin}`;
|
|
127
|
+
}
|
package/src/core/document.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { escapeAttr, escapeHtml } from "./escape.ts";
|
|
|
4
4
|
import { collectSlidesHtml, htmlShell } from "./html.ts";
|
|
5
5
|
import { type OgpImage, ogpHead } from "./ogp.ts";
|
|
6
6
|
import { PAGE_ID, type PageMode, pageConfigAttrs } from "./page.ts";
|
|
7
|
+
import type { PluginPage } from "./plugin.ts";
|
|
7
8
|
import {
|
|
8
9
|
nextPresenterTitle,
|
|
9
10
|
presenterSlides,
|
|
@@ -49,14 +50,19 @@ export type PageTarget =
|
|
|
49
50
|
/** The deck at `dir`, rendered for `target`. */
|
|
50
51
|
export function renderDeckHtml(
|
|
51
52
|
dir: string,
|
|
52
|
-
options: { playerScript: string; target: PageTarget },
|
|
53
|
+
options: { playerScript: string; target: PageTarget; plugins?: PluginPage[] },
|
|
53
54
|
): string {
|
|
54
55
|
return renderDeckDocument(resolveDeck(dir).deck, options);
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export function renderDeckDocument(
|
|
58
59
|
deck: ProjectDeck,
|
|
59
|
-
options: {
|
|
60
|
+
options: {
|
|
61
|
+
playerScript: string;
|
|
62
|
+
target: PageTarget;
|
|
63
|
+
/** Each plugin's page; only the player and the build carry them (ADR 33). */
|
|
64
|
+
plugins?: PluginPage[];
|
|
65
|
+
},
|
|
60
66
|
): string {
|
|
61
67
|
const { target } = options;
|
|
62
68
|
const mode: PageMode =
|
|
@@ -160,7 +166,8 @@ export function renderDeckDocument(
|
|
|
160
166
|
${slideScriptTags(slideScripts)}
|
|
161
167
|
<script>${options.playerScript}</script>
|
|
162
168
|
${target.kind === "dev" ? `<script>${target.liveReloadScript}</script>` : ""}
|
|
163
|
-
${target.kind === "dev" && includeNotes ? `<script>${target.annotateScript}</script>` : ""}
|
|
169
|
+
${target.kind === "dev" && includeNotes ? `<script>${target.annotateScript}</script>` : ""}
|
|
170
|
+
${pluginPagesHtml(target, options.plugins ?? [])}`,
|
|
164
171
|
});
|
|
165
172
|
}
|
|
166
173
|
|
|
@@ -225,6 +232,24 @@ function renderKeyHintHtml(lang: string): string {
|
|
|
225
232
|
return `<div id="${PAGE_ID.hint}" role="status"><span><kbd>s</kbd>${labels.rail}</span><span><kbd>p</kbd>${labels.presenter}</span></div>`;
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
/**
|
|
236
|
+
* The plugins' pages, after the player, which gives them `dek.plugin`: their keys and data as one
|
|
237
|
+
* JSON block, then each script in name order. Only the player and the build carry them; the
|
|
238
|
+
* presenter view and a video, like every still page, never do.
|
|
239
|
+
*/
|
|
240
|
+
function pluginPagesHtml(target: PageTarget, plugins: PluginPage[]): string {
|
|
241
|
+
const carried = target.kind === "build" || (target.kind === "dev" && target.mode === "player");
|
|
242
|
+
if (!carried || plugins.length === 0) {
|
|
243
|
+
return "";
|
|
244
|
+
}
|
|
245
|
+
const ordered = plugins.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
246
|
+
const config = Object.fromEntries(ordered.map(({ name, keys, data }) => [name, { keys, data }]));
|
|
247
|
+
return [
|
|
248
|
+
`<script type="application/json" id="${PAGE_ID.plugins}">${jsonForScript(config)}</script>`,
|
|
249
|
+
...ordered.map(({ script }) => `<script>${script.replace(/<\/(script)/gi, "<\\/$1")}</script>`),
|
|
250
|
+
].join("\n ");
|
|
251
|
+
}
|
|
252
|
+
|
|
228
253
|
function jsonForScript(value: unknown): string {
|
|
229
254
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
230
255
|
}
|