@hajimism/dek 0.5.0 → 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 +37 -0
- package/package.json +12 -5
- package/src/cli/address.ts +48 -11
- 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/keep-alive.ts +12 -6
- package/src/cli/lint.ts +53 -1
- package/src/cli/main.ts +2 -2
- package/src/cli/sync.ts +21 -6
- package/src/cli/text.ts +20 -2
- package/src/cli/theme.ts +1 -1
- 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/context.ts +2 -0
- package/src/core/lint/shared-styles.ts +26 -1
- package/src/core/lint/theme-css.ts +64 -3
- package/src/core/lint.ts +15 -2
- 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/sandbox.ts +1 -1
- package/src/core/sarif.ts +47 -5
- package/src/core/slide-measure.ts +1 -0
- package/src/core/spawn.ts +9 -7
- 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 +9 -3
- package/src/server/lock.ts +39 -9
- package/src/server/pages.ts +9 -1
- package/tsconfig.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
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
|
+
|
|
23
|
+
## [0.5.1](https://github.com/hajimism/dek/compare/v0.5.0...v0.5.1) (2026-10-30)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* **cli:** stop a dev server whose parent died before the server was up ([80b533f](https://github.com/hajimism/dek/commit/80b533fc691cfc104b6c83f7d99be4d73fc75c8d))
|
|
29
|
+
* **dev:** say when the running server serves another deck only ([333ae06](https://github.com/hajimism/dek/commit/333ae06818fa8ad09f5ed492ae33148cce9254fd))
|
|
30
|
+
* judge a command that skips its stdin by how it exits, not as one that cannot start ([103152c](https://github.com/hajimism/dek/commit/103152cd2132ba9c430ede4d7020844f7a8dcf9a))
|
|
31
|
+
* **lint:** free no DEK013 class a skeleton or a slide script may use ([e32638b](https://github.com/hajimism/dek/commit/e32638b12220055d8a5a329b9e93f9eda47371e8))
|
|
32
|
+
* **lint:** name the files a hint sends you to as paths from where the command ran ([5943dbe](https://github.com/hajimism/dek/commit/5943dbe1b11ac4529faae9cc1b98f22b4c63d214))
|
|
33
|
+
* **lint:** name the files a hint sends you to as paths from where the command ran ([c5fcce3](https://github.com/hajimism/dek/commit/c5fcce3101061a6eaad55b27d94019f5b028e762))
|
|
34
|
+
* **lint:** say how to stay within max_classes in DEK013 and DEK026 ([9c3078b](https://github.com/hajimism/dek/commit/9c3078bad4d93e5f381c99f5b6e0dd2c059f32dc))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Documentation
|
|
38
|
+
|
|
39
|
+
* say that DEK035 leaves a deck that breaks by phrase to Chromium ([c21b428](https://github.com/hajimism/dek/commit/c21b4284db4a46a231778a4fa8d42aac38c46b0d))
|
|
40
|
+
* say that DEK035 leaves a deck that breaks by phrase to Chromium ([dfef085](https://github.com/hajimism/dek/commit/dfef085bedf2d120c62a0f7418fd3d69f0a77001))
|
|
41
|
+
|
|
5
42
|
## [0.5.0](https://github.com/hajimism/dek/compare/v0.4.0...v0.5.0) (2026-10-27)
|
|
6
43
|
|
|
7
44
|
|
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.
|
|
65
|
-
"@happy-dom/global-registrator": "21.0.
|
|
71
|
+
"@biomejs/biome": "2.6.4",
|
|
72
|
+
"@happy-dom/global-registrator": "21.0.4",
|
|
66
73
|
"@types/bun": "^1.4.2",
|
|
67
74
|
"fast-check": "4.10.2",
|
|
68
|
-
"happy-dom": "21.0.
|
|
69
|
-
"knip": "6.
|
|
75
|
+
"happy-dom": "21.0.4",
|
|
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/address.ts
CHANGED
|
@@ -1,30 +1,49 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join } from "node:path";
|
|
3
|
-
import { shellName } from "../core/escape.ts";
|
|
2
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
3
|
+
import { isPlainWord, shellName } from "../core/escape.ts";
|
|
4
4
|
import { isInside } from "../core/path.ts";
|
|
5
5
|
import { snapshotName } from "../core/ref.ts";
|
|
6
|
+
import { isRefName } from "../core/ref-name.ts";
|
|
6
7
|
import { type AnySpec, COMMANDS, subcommandsOf } from "./commands.ts";
|
|
7
8
|
|
|
8
9
|
/** A deck a hint is about: the name a command takes, and the directory that makes it implicit. */
|
|
9
10
|
export type HintDeck = { name: string; dir: string };
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* A hint
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* where a first word names something in that deck instead, or where the name would read as a
|
|
16
|
-
* subcommand or a flag. The name is one shell word, however it is spelled, or `<deck>` when no
|
|
17
|
-
* word holds it alike in every shell, with where to find it unless `note` is false, for a hint
|
|
18
|
-
* that joins several and says it once. A command that already names a deck, or takes none, is
|
|
19
|
-
* left as written.
|
|
13
|
+
* A hint as it reads from `cwd`. Hints are written as if read inside the deck they are about:
|
|
14
|
+
* its commands are addressed as `addressCommands` says, and the deck files it names become paths
|
|
15
|
+
* from `cwd`, as the path of the diagnostic beside it is, except in a ref.
|
|
20
16
|
*/
|
|
21
17
|
export function addressHint(
|
|
22
18
|
hint: string,
|
|
23
19
|
deck: HintDeck | undefined,
|
|
24
20
|
cwd: string,
|
|
21
|
+
options: { note?: boolean } = {},
|
|
22
|
+
): string {
|
|
23
|
+
if (deck === undefined) {
|
|
24
|
+
return hint;
|
|
25
|
+
}
|
|
26
|
+
const addressed = addressCommands(hint, deck, cwd, options);
|
|
27
|
+
// A ref's files are its author's to fix, so a hint about one names them as the ref does.
|
|
28
|
+
return isRefName(deck.name) ? addressed : addressFiles(addressed, deck.dir, cwd);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A hint's commands as they run from `cwd`. Inside the deck they are about, a command finds its
|
|
33
|
+
* deck by itself. Run anywhere else, a command that takes a deck needs its name: as the first
|
|
34
|
+
* word outside every deck, and as `--deck` inside another deck, where a first word names
|
|
35
|
+
* something in that deck instead, or where the name would read as a subcommand or a flag. The
|
|
36
|
+
* name is one shell word, however it is spelled, or `<deck>` when no word holds it alike in every
|
|
37
|
+
* shell, with where to find it unless `note` is false, for a hint that joins several and says it
|
|
38
|
+
* once. A command that already names a deck, or takes none, is left as written.
|
|
39
|
+
*/
|
|
40
|
+
export function addressCommands(
|
|
41
|
+
hint: string,
|
|
42
|
+
deck: HintDeck,
|
|
43
|
+
cwd: string,
|
|
25
44
|
{ note: noted = true }: { note?: boolean } = {},
|
|
26
45
|
): string {
|
|
27
|
-
if (
|
|
46
|
+
if (isInside(cwd, deck.dir)) {
|
|
28
47
|
return hint;
|
|
29
48
|
}
|
|
30
49
|
const word = shellName(deck.name) ?? DECK_PLACEHOLDER;
|
|
@@ -80,6 +99,24 @@ export function deckFlag(name: string): string {
|
|
|
80
99
|
return name.startsWith("-") ? `--deck=${word}` : `--deck ${word}`;
|
|
81
100
|
}
|
|
82
101
|
|
|
102
|
+
/**
|
|
103
|
+
* A deck file as a hint names it from the deck: a slide's own file, a voice file, theme.css, or
|
|
104
|
+
* script.md, or a project file reached with `../../`. Not part of a longer path or word. A path
|
|
105
|
+
* from `cwd` that is no plain word, through a folder a hint may not name, is left as written.
|
|
106
|
+
*/
|
|
107
|
+
const DECK_FILE_RE =
|
|
108
|
+
/(?<![\w./-])(\.\.\/\.\.\/[\w./-]*[\w-]|(?:slides|voice)\/[\w.-]*[\w-]|theme\.css|script\.md)(?![\w/-])/g;
|
|
109
|
+
|
|
110
|
+
function addressFiles(hint: string, dir: string, cwd: string): string {
|
|
111
|
+
if (cwd === dir) {
|
|
112
|
+
return hint;
|
|
113
|
+
}
|
|
114
|
+
return hint.replace(DECK_FILE_RE, (file) => {
|
|
115
|
+
const path = relative(cwd, join(dir, file)) || ".";
|
|
116
|
+
return isPlainWord(path) ? path : file;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
83
120
|
/** A backquoted command: `dekc`, a command word, and what follows it up to the closing quote. */
|
|
84
121
|
const COMMAND_RE = /`dekc ([a-z]+)((?: [^`]*)?)`/g;
|
|
85
122
|
|
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/keep-alive.ts
CHANGED
|
@@ -7,6 +7,13 @@ const PARENT_POLL_MS = 1_000;
|
|
|
7
7
|
/** How long a stop waits for the server to close before the process exits anyway. */
|
|
8
8
|
const SHUTDOWN_GRACE_MS = 2_000;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The process that started this one, read as the CLI loads. A parent read once the server is up
|
|
12
|
+
* would come too late: whoever waits for the URL can be killed before the next line runs, and the
|
|
13
|
+
* parent read then is already init, which never goes away.
|
|
14
|
+
*/
|
|
15
|
+
const startedBy = process.ppid;
|
|
16
|
+
|
|
10
17
|
/**
|
|
11
18
|
* Serve until told to stop: Ctrl-C, a kill, a closed terminal, or the process that started the
|
|
12
19
|
* server going away, so a test run or an editor that dies without cleaning up leaves no server
|
|
@@ -51,16 +58,15 @@ export function stopOnce(
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/**
|
|
54
|
-
* Call `fn` once this process is
|
|
55
|
-
* leaves it no chance to stop its children. A process started
|
|
56
|
-
* (already under init) is never orphaned.
|
|
61
|
+
* Call `fn` once this process is no longer under `parent`, the one that started it, which is how
|
|
62
|
+
* Unix says the parent died: a SIGKILL leaves it no chance to stop its children. A process started
|
|
63
|
+
* with no parent of its own to lose (already under init) is never orphaned.
|
|
57
64
|
*/
|
|
58
65
|
export function onOrphaned(
|
|
59
66
|
fn: () => void,
|
|
60
|
-
options: { ppid?: () => number; intervalMs?: number } = {},
|
|
67
|
+
options: { parent?: number; ppid?: () => number; intervalMs?: number } = {},
|
|
61
68
|
): () => void {
|
|
62
|
-
const
|
|
63
|
-
const parent = ppid();
|
|
69
|
+
const { parent = startedBy, ppid = () => process.ppid } = options;
|
|
64
70
|
const timer = setInterval(() => {
|
|
65
71
|
if (ppid() !== parent) {
|
|
66
72
|
clearInterval(timer);
|
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/main.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { isRefName } from "../core/ref-name.ts";
|
|
|
7
7
|
import { type ProjectDeck, readDeckFile, resolveProject } from "../core/resolve.ts";
|
|
8
8
|
import { takeUnsandboxed } from "../core/sandbox.ts";
|
|
9
9
|
import { themeFacts } from "../core/theme-facts.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { addressCommands, deckWord, type HintDeck, withPlaceholderNote } from "./address.ts";
|
|
11
11
|
import {
|
|
12
12
|
type AnySpec,
|
|
13
13
|
type CliResult,
|
|
@@ -207,7 +207,7 @@ function retypedWithDeck<T>(line: CommandLine, cwd: string, call: Call, resolve:
|
|
|
207
207
|
const decks = error.decks.filter((deck) => runsIn(call, deck));
|
|
208
208
|
const [first] = decks;
|
|
209
209
|
// Each command is retyped for one deck; where to find a name left as <deck> is said once.
|
|
210
|
-
const retyped = (deck: HintDeck) =>
|
|
210
|
+
const retyped = (deck: HintDeck) => addressCommands(typed, deck, cwd, { note: false });
|
|
211
211
|
const hint =
|
|
212
212
|
error.decks.length === 0
|
|
213
213
|
? "run `dekc new <name>` to make a deck"
|
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/cli/theme.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function themeCommand({ deck, ref }: ReadableDeck, name?: string): ThemeR
|
|
|
36
36
|
if (!existsSync(path)) {
|
|
37
37
|
throw new DekError("theme.css not found", {
|
|
38
38
|
path,
|
|
39
|
-
hint: "copy the project theme.css into the deck, or run `dekc new <name>` for a fresh deck",
|
|
39
|
+
hint: "copy the project's template, ../../theme.css, into the deck, or run `dekc new <name>` for a fresh deck",
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
const css = readDeckFile(deck.dir, path) ?? "";
|
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. */
|